android-midscene-automation 0.1.32 → 0.1.34
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 +8 -0
- package/README.md +9 -0
- package/package.json +1 -1
- package/remote-agent/index.ts +5 -1
- package/server/appium-recorder/appium-runner.ts +65 -18
- package/server/appium-recorder/repository.ts +32 -1
- package/server/appium-recorder/routes.ts +21 -2
- package/server/appium-recorder/script-export.ts +11 -3
- package/server/appium-recorder/variable-store.ts +22 -0
- package/server/appium-recorder/variables.ts +89 -0
- package/src/appium-recorder/AppiumPage.vue +59 -18
- package/src/appium-recorder/api.ts +23 -2
- package/src/appium-recorder/components/FlowStepEditor.vue +28 -2
- package/src/appium-recorder/components/NestedConditionBranches.vue +2 -2
- package/src/appium-recorder/components/PresetVariables.vue +109 -0
- package/src/appium-recorder/components/RecordedSteps.vue +1 -2
- package/src/appium-recorder/components/ScriptParameterSettings.vue +31 -0
- package/src/appium-recorder/components/VariableExtractionSettings.vue +17 -0
- package/src/appium-recorder/components/VariableTable.vue +43 -0
- package/src/appium-recorder/flow-graph.ts +23 -10
- package/src/appium-recorder/flow-labels.ts +4 -1
- package/src/appium-recorder/types.ts +7 -0
- package/src/appium-recorder/variables.ts +34 -0
|
@@ -9,6 +9,10 @@ import DevicePreviewPanel from '../components/device/DevicePreviewPanel.vue';
|
|
|
9
9
|
import LoopSettings from './components/LoopSettings.vue';
|
|
10
10
|
import BreakLoopSettings from './components/BreakLoopSettings.vue';
|
|
11
11
|
import VisualChangeEndPicker from './components/VisualChangeEndPicker.vue';
|
|
12
|
+
import PresetVariables from './components/PresetVariables.vue';
|
|
13
|
+
import VariableExtractionSettings from './components/VariableExtractionSettings.vue';
|
|
14
|
+
import ScriptParameterSettings from './components/ScriptParameterSettings.vue';
|
|
15
|
+
import { validateVariables, validateExtraction, validateReturns, type TestVariable } from './variables';
|
|
12
16
|
import { defaultLoopConfig, enclosingLoops, breakLoopTarget, validateLoop, validateLoopSteps } from './bounded-loop';
|
|
13
17
|
import {
|
|
14
18
|
clearAppiumDeviceAppData,
|
|
@@ -20,6 +24,7 @@ import {
|
|
|
20
24
|
launchAppiumDeviceApp,
|
|
21
25
|
replayAppiumScript,
|
|
22
26
|
saveAppiumScript,
|
|
27
|
+
saveVariables,
|
|
23
28
|
stopAppiumReplay,
|
|
24
29
|
tapAppiumDevice,
|
|
25
30
|
} from './api';
|
|
@@ -66,7 +71,7 @@ import type {
|
|
|
66
71
|
AppiumVisualChangeRegion,
|
|
67
72
|
} from './types';
|
|
68
73
|
|
|
69
|
-
type NodeStepType = 'tap' | 'input' | '
|
|
74
|
+
type NodeStepType = 'tap' | 'input' | 'waitFor';
|
|
70
75
|
type BranchName = 'yes' | 'no';
|
|
71
76
|
type LegacyFlow = NonNullable<AppiumRecordedStep['flow']> & { scope?: string };
|
|
72
77
|
type SwipeGesture = NonNullable<AppiumRecordedStep['swipe']>;
|
|
@@ -86,10 +91,10 @@ const historyScript = shallowRef<{ id: string; name: string } | null>(null);
|
|
|
86
91
|
const AUTO_TREE_REFRESH_INTERVAL_MS = 1800;
|
|
87
92
|
|
|
88
93
|
const insertableRecorderActions: RecorderAction[] = [
|
|
94
|
+
'extractVariable',
|
|
89
95
|
'delay',
|
|
90
96
|
'tap',
|
|
91
97
|
'input',
|
|
92
|
-
'assertExists',
|
|
93
98
|
'waitFor',
|
|
94
99
|
'tapIfExists',
|
|
95
100
|
'inputIfExists',
|
|
@@ -117,7 +122,6 @@ const insertableRecorderActions: RecorderAction[] = [
|
|
|
117
122
|
'breakLoop',
|
|
118
123
|
'clearAppData',
|
|
119
124
|
'waitDisappear',
|
|
120
|
-
'assertText',
|
|
121
125
|
'longPress',
|
|
122
126
|
'pinch',
|
|
123
127
|
'noop',
|
|
@@ -164,7 +168,7 @@ const selectedScriptId = shallowRef('');
|
|
|
164
168
|
const newScriptDialogVisible = shallowRef(false);
|
|
165
169
|
const newScriptRevision = shallowRef(0);
|
|
166
170
|
const storedWorkbenchTab = window.localStorage.getItem(WORKBENCH_TAB_STORAGE_KEY);
|
|
167
|
-
const activeWorkbenchTab = shallowRef<'recording' | 'scripts'>(storedWorkbenchTab === 'scripts' ? 'scripts' : 'recording');
|
|
171
|
+
const activeWorkbenchTab = shallowRef<'recording' | 'scripts' | 'variables'>(storedWorkbenchTab === 'scripts' ? 'scripts' : storedWorkbenchTab === 'variables' ? 'variables' : 'recording');
|
|
168
172
|
const steps = shallowRef<AppiumRecordedStep[]>([]);
|
|
169
173
|
const aiRecognitionTestStep = shallowRef<AppiumRecordedStep | null>(null);
|
|
170
174
|
const flowClipboard = shallowRef<FlowClipboard | null>(null);
|
|
@@ -174,6 +178,7 @@ const replayOutput = shallowRef('');
|
|
|
174
178
|
const replayOutputDialogVisible = shallowRef(false);
|
|
175
179
|
const linkedScriptDialogVisible = shallowRef(false);
|
|
176
180
|
const linkedScriptTargetId = shallowRef('');
|
|
181
|
+
const linkedParameters = reactive<AppiumRecordedStep>({ id: 'link-parameters', type: 'runScript', label: '', parameters: [], returns: [] });
|
|
177
182
|
const linkedScriptInsertIndex = shallowRef<number | undefined>();
|
|
178
183
|
const linkedScriptBranchTarget = shallowRef<BranchTarget | null>(null);
|
|
179
184
|
const linkedScriptExpectedActivity = shallowRef('');
|
|
@@ -214,6 +219,7 @@ const form = reactive({
|
|
|
214
219
|
appPackage: '',
|
|
215
220
|
appActivity: '',
|
|
216
221
|
});
|
|
222
|
+
const scriptVariables = shallowRef<TestVariable[]>([]);
|
|
217
223
|
|
|
218
224
|
const savedDraftSnapshot = shallowRef('');
|
|
219
225
|
const currentDraftSnapshot = computed(() => JSON.stringify({
|
|
@@ -221,6 +227,7 @@ const currentDraftSnapshot = computed(() => JSON.stringify({
|
|
|
221
227
|
appPackage: form.appPackage,
|
|
222
228
|
appActivity: form.appActivity,
|
|
223
229
|
steps: steps.value,
|
|
230
|
+
variables: scriptVariables.value,
|
|
224
231
|
}));
|
|
225
232
|
const flowClipboardCount = computed(() => flowClipboard.value?.steps.length || 0);
|
|
226
233
|
const hasUnsavedChanges = computed(() => currentDraftSnapshot.value !== savedDraftSnapshot.value);
|
|
@@ -229,6 +236,16 @@ function markDraftSaved() {
|
|
|
229
236
|
savedDraftSnapshot.value = currentDraftSnapshot.value;
|
|
230
237
|
}
|
|
231
238
|
|
|
239
|
+
function onVariablesSaved(id: string, variables: TestVariable[]) {
|
|
240
|
+
const script = scripts.value.find(item => item.id === id);
|
|
241
|
+
if (script) script.variables = variables;
|
|
242
|
+
if (id && selectedScriptId.value === id) {
|
|
243
|
+
// 仅更新变量的已保存基线,不把尚未保存的节点标记为已保存。
|
|
244
|
+
const snapshot = JSON.parse(savedDraftSnapshot.value);
|
|
245
|
+
savedDraftSnapshot.value = JSON.stringify({ ...snapshot, variables });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
232
249
|
function warnBeforeUnload(event: BeforeUnloadEvent) {
|
|
233
250
|
if (!props.active || !hasUnsavedChanges.value) return;
|
|
234
251
|
event.preventDefault();
|
|
@@ -462,7 +479,6 @@ function createStep(type: NodeStepType, node: AppiumNode): AppiumRecordedStep {
|
|
|
462
479
|
const labelMap = {
|
|
463
480
|
tap: '点击',
|
|
464
481
|
input: '输入',
|
|
465
|
-
assertExists: '断言存在',
|
|
466
482
|
waitFor: '等待出现',
|
|
467
483
|
};
|
|
468
484
|
return {
|
|
@@ -475,7 +491,7 @@ function createStep(type: NodeStepType, node: AppiumNode): AppiumRecordedStep {
|
|
|
475
491
|
? [{ strategy: 'xpath', value: node.xpath, unique: true, matchCount: 1 }]
|
|
476
492
|
: undefined,
|
|
477
493
|
fallback: node.bounds ? { strategy: 'bounds', centerX: node.bounds.centerX, centerY: node.bounds.centerY } : undefined,
|
|
478
|
-
timeoutMs: type === '
|
|
494
|
+
timeoutMs: type === 'waitFor' ? 10000 : undefined,
|
|
479
495
|
pageBefore: currentPageSnapshot(node),
|
|
480
496
|
snapshot: {
|
|
481
497
|
text: node.text,
|
|
@@ -1126,11 +1142,15 @@ function openLinkedScriptDialog(index?: number, branchTarget?: BranchTarget) {
|
|
|
1126
1142
|
return;
|
|
1127
1143
|
}
|
|
1128
1144
|
linkedScriptTargetId.value = compatibleLinkableScripts.value[0]?.id || '';
|
|
1145
|
+
linkedParameters.parameters = [];
|
|
1146
|
+
linkedParameters.returns = [];
|
|
1129
1147
|
linkedScriptInsertIndex.value = index;
|
|
1130
1148
|
linkedScriptDialogVisible.value = true;
|
|
1131
1149
|
}
|
|
1132
1150
|
|
|
1133
1151
|
function addLinkedScriptStep() {
|
|
1152
|
+
try { validateVariables(linkedParameters.parameters); validateReturns(linkedParameters.returns); }
|
|
1153
|
+
catch (error) { ElMessage.error(String(error)); return; }
|
|
1134
1154
|
const script = scripts.value.find((item) => item.id === linkedScriptTargetId.value);
|
|
1135
1155
|
if (!script) {
|
|
1136
1156
|
ElMessage.warning('请选择要连接的脚本');
|
|
@@ -1151,7 +1171,7 @@ function addLinkedScriptStep() {
|
|
|
1151
1171
|
return;
|
|
1152
1172
|
}
|
|
1153
1173
|
insertStep(
|
|
1154
|
-
createRunScriptStep(script),
|
|
1174
|
+
{ ...createRunScriptStep(script), parameters: JSON.parse(JSON.stringify(linkedParameters.parameters || [])), returns: JSON.parse(JSON.stringify(linkedParameters.returns || [])) },
|
|
1155
1175
|
linkedScriptInsertIndex.value,
|
|
1156
1176
|
linkedScriptBranchTarget.value || undefined,
|
|
1157
1177
|
);
|
|
@@ -1368,7 +1388,7 @@ async function addAction(
|
|
|
1368
1388
|
if (result) return insertStep({ ...step }, index, branchTarget);
|
|
1369
1389
|
return;
|
|
1370
1390
|
}
|
|
1371
|
-
if (action === 'tap' || action === 'input' || action === '
|
|
1391
|
+
if (action === 'tap' || action === 'input' || action === 'waitFor') {
|
|
1372
1392
|
return addStep(action, index, branchTarget);
|
|
1373
1393
|
}
|
|
1374
1394
|
if (action === 'keyBack') {
|
|
@@ -1491,6 +1511,22 @@ async function addAction(
|
|
|
1491
1511
|
}
|
|
1492
1512
|
const node = getSelectedNode();
|
|
1493
1513
|
if (!node) return;
|
|
1514
|
+
if (action === 'extractVariable') {
|
|
1515
|
+
const step = reactive<AppiumRecordedStep>({ ...createNodeActionStep('extractVariable', '提取变量', node), extractVariable: { name: '', attribute: 'text', sensitive: false }, flow: { nodeKind: 'action' } });
|
|
1516
|
+
const result = await ElMessageBox({
|
|
1517
|
+
title: '提取组件到变量',
|
|
1518
|
+
message: h(ElForm, { labelPosition: 'top' }, () => h(VariableExtractionSettings, { step, onUpdate: patch => Object.assign(step, patch) })),
|
|
1519
|
+
showCancelButton: true, confirmButtonText: '添加', cancelButtonText: '取消',
|
|
1520
|
+
beforeClose: (action, _instance, done) => {
|
|
1521
|
+
if (action === 'confirm') {
|
|
1522
|
+
try { validateExtraction(step.extractVariable); } catch (error) { ElMessage.error(String(error)); return; }
|
|
1523
|
+
}
|
|
1524
|
+
done();
|
|
1525
|
+
},
|
|
1526
|
+
}).catch(() => null);
|
|
1527
|
+
if (result) return insertStep(JSON.parse(JSON.stringify(step)), index, branchTarget);
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1494
1530
|
if (action === 'checkboxState' || action === 'radioButtonState' || action === 'checkedState') {
|
|
1495
1531
|
const controlName = nativeControlName(action);
|
|
1496
1532
|
if (!matchesNativeControl(action, node.className) || node.checkable !== true) {
|
|
@@ -1588,14 +1624,6 @@ async function addAction(
|
|
|
1588
1624
|
if (action === 'waitDisappear') {
|
|
1589
1625
|
return insertStep(createNodeActionStep('waitDisappear', '等待元素消失', node, { timeoutMs: 10000 }), index, branchTarget);
|
|
1590
1626
|
}
|
|
1591
|
-
if (action === 'assertText') {
|
|
1592
|
-
const input = await ElMessageBox.prompt('请输入要断言的文本', '断言文本', {
|
|
1593
|
-
inputValue: node.text,
|
|
1594
|
-
confirmButtonText: '添加',
|
|
1595
|
-
cancelButtonText: '取消',
|
|
1596
|
-
}).catch(() => null);
|
|
1597
|
-
if (input?.value) return insertStep(createNodeActionStep('assertText', '断言文本', node, { value: input.value }), index, branchTarget);
|
|
1598
|
-
}
|
|
1599
1627
|
}
|
|
1600
1628
|
|
|
1601
1629
|
async function addBranchAction(index: number, branch: BranchName, action: RecorderAction) {
|
|
@@ -1643,6 +1671,7 @@ async function saveOnActivityChange() {
|
|
|
1643
1671
|
appActivity: pending.beforeActivity || form.appActivity,
|
|
1644
1672
|
deviceId: selectedDeviceId.value,
|
|
1645
1673
|
steps: steps.value,
|
|
1674
|
+
variables: scriptVariables.value,
|
|
1646
1675
|
});
|
|
1647
1676
|
await loadScripts();
|
|
1648
1677
|
pendingNavigation.value = null;
|
|
@@ -1714,7 +1743,7 @@ async function editInputStep(index: number) {
|
|
|
1714
1743
|
}
|
|
1715
1744
|
|
|
1716
1745
|
function setActiveWorkbenchTab(tab: string | number) {
|
|
1717
|
-
const nextTab = tab === 'scripts' ? 'scripts' : 'recording';
|
|
1746
|
+
const nextTab = tab === 'scripts' ? 'scripts' : tab === 'variables' ? 'variables' : 'recording';
|
|
1718
1747
|
activeWorkbenchTab.value = nextTab;
|
|
1719
1748
|
window.localStorage.setItem(WORKBENCH_TAB_STORAGE_KEY, nextTab);
|
|
1720
1749
|
}
|
|
@@ -1726,6 +1755,7 @@ function loadScript(script: AppiumRecordedScript) {
|
|
|
1726
1755
|
form.name = script.name;
|
|
1727
1756
|
form.appPackage = script.appPackage;
|
|
1728
1757
|
form.appActivity = script.appActivity;
|
|
1758
|
+
scriptVariables.value = JSON.parse(JSON.stringify(script.variables || []));
|
|
1729
1759
|
if (script.deviceId && script.deviceId !== selectedDeviceId.value) {
|
|
1730
1760
|
void switchDevice(script.deviceId);
|
|
1731
1761
|
}
|
|
@@ -1742,6 +1772,8 @@ function loadSelectedScript() {
|
|
|
1742
1772
|
}
|
|
1743
1773
|
|
|
1744
1774
|
function resetCurrentScript() {
|
|
1775
|
+
void saveVariables('/api/appium-recorder/variables?draft=1', []).catch(() => ElMessage.error('变量草稿清理失败'));
|
|
1776
|
+
scriptVariables.value = [];
|
|
1745
1777
|
flowClipboard.value = null;
|
|
1746
1778
|
selectedScriptId.value = '';
|
|
1747
1779
|
form.name = '';
|
|
@@ -1753,7 +1785,7 @@ function resetCurrentScript() {
|
|
|
1753
1785
|
|
|
1754
1786
|
function requestNewScript() {
|
|
1755
1787
|
if (newScriptDisabled.value) return;
|
|
1756
|
-
if (selectedScriptId.value || form.name.trim() || steps.value.length) {
|
|
1788
|
+
if (selectedScriptId.value || form.name.trim() || steps.value.length || scriptVariables.value.length) {
|
|
1757
1789
|
newScriptDialogVisible.value = true;
|
|
1758
1790
|
return;
|
|
1759
1791
|
}
|
|
@@ -1808,6 +1840,7 @@ async function duplicateScript(script: AppiumRecordedScript) {
|
|
|
1808
1840
|
try {
|
|
1809
1841
|
const payload = await saveAppiumScript({
|
|
1810
1842
|
name: createDuplicateScriptName(script.name),
|
|
1843
|
+
variables: script.variables || [],
|
|
1811
1844
|
appPackage: script.appPackage,
|
|
1812
1845
|
appActivity: script.appActivity,
|
|
1813
1846
|
deviceId: script.deviceId,
|
|
@@ -1939,8 +1972,10 @@ async function importScriptFile(event: Event) {
|
|
|
1939
1972
|
|
|
1940
1973
|
async function saveScript() {
|
|
1941
1974
|
if (!ensureAppPackageSelected()) return false;
|
|
1975
|
+
const wasNew = !selectedScriptId.value;
|
|
1942
1976
|
saving.value = true;
|
|
1943
1977
|
try {
|
|
1978
|
+
validateVariables(scriptVariables.value);
|
|
1944
1979
|
const payload = await saveAppiumScript({
|
|
1945
1980
|
id: selectedScriptId.value || undefined,
|
|
1946
1981
|
name: form.name,
|
|
@@ -1948,9 +1983,11 @@ async function saveScript() {
|
|
|
1948
1983
|
appActivity: form.appActivity,
|
|
1949
1984
|
deviceId: selectedDeviceId.value,
|
|
1950
1985
|
steps: steps.value,
|
|
1986
|
+
variables: scriptVariables.value,
|
|
1951
1987
|
});
|
|
1952
1988
|
await loadScripts();
|
|
1953
1989
|
selectedScriptId.value = payload.script.id;
|
|
1990
|
+
if (wasNew) void saveVariables('/api/appium-recorder/variables?draft=1', []).catch(() => ElMessage.error('变量草稿清理失败'));
|
|
1954
1991
|
markDraftSaved();
|
|
1955
1992
|
ElMessage.success('Appium 脚本已保存');
|
|
1956
1993
|
return true;
|
|
@@ -2192,6 +2229,9 @@ watch(
|
|
|
2192
2229
|
</div>
|
|
2193
2230
|
</el-tab-pane>
|
|
2194
2231
|
|
|
2232
|
+
<el-tab-pane label="预设变量" name="variables" lazy>
|
|
2233
|
+
<PresetVariables :key="`${selectedScriptId}:${newScriptRevision}`" v-model:variables="scriptVariables" :script-id="selectedScriptId" :script-name="form.name" :disabled="recordingBusy || saving" @saved="onVariablesSaved" />
|
|
2234
|
+
</el-tab-pane>
|
|
2195
2235
|
<el-tab-pane label="脚本列表" name="scripts">
|
|
2196
2236
|
<div class="appium-script-list">
|
|
2197
2237
|
<div class="appium-script-list__toolbar">
|
|
@@ -2365,6 +2405,7 @@ watch(
|
|
|
2365
2405
|
</el-button>
|
|
2366
2406
|
</div>
|
|
2367
2407
|
</el-form-item>
|
|
2408
|
+
<ScriptParameterSettings :step="linkedParameters" @update="Object.assign(linkedParameters, $event)" />
|
|
2368
2409
|
</el-form>
|
|
2369
2410
|
<template #footer>
|
|
2370
2411
|
<el-button @click="closeLinkedScriptDialog">取消</el-button>
|
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import { APP_BASE } from '../api';
|
|
2
2
|
import type { AppiumRecordedScript, AppiumRecordedStep } from './types';
|
|
3
3
|
import type { AiRecognitionResult } from './ai-recognition';
|
|
4
|
+
import type { TestVariable } from './variables';
|
|
5
|
+
|
|
6
|
+
// 队列跨组件挂载保留,切换脚本或清空草稿时仍按编辑顺序写入。
|
|
7
|
+
const variableWrites = new Map<string, Promise<unknown>>();
|
|
8
|
+
export function saveVariables(url: string, variables: TestVariable[]) {
|
|
9
|
+
const body = JSON.stringify({ variables });
|
|
10
|
+
const task = (variableWrites.get(url) || Promise.resolve()).catch(() => {}).then(async () => {
|
|
11
|
+
return readJson<{ variables: TestVariable[] }>(await fetch(url, {
|
|
12
|
+
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body, keepalive: true,
|
|
13
|
+
}));
|
|
14
|
+
});
|
|
15
|
+
variableWrites.set(url, task);
|
|
16
|
+
void task.finally(() => { if (variableWrites.get(url) === task) variableWrites.delete(url); }).catch(() => {});
|
|
17
|
+
return task;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function loadVariables(url: string) {
|
|
21
|
+
await variableWrites.get(url)?.catch(() => {});
|
|
22
|
+
return readJson<{ variables: TestVariable[] }>(await fetch(url));
|
|
23
|
+
}
|
|
4
24
|
|
|
5
25
|
async function readJson<T>(response: Response) {
|
|
6
26
|
const payload = (await response.json().catch(() => ({}))) as T & {
|
|
@@ -56,6 +76,7 @@ export async function testAiRecognition(input: { deviceId: string; prompt: strin
|
|
|
56
76
|
}
|
|
57
77
|
|
|
58
78
|
export function saveAppiumScript(input: {
|
|
79
|
+
variables?: import('./variables').TestVariable[];
|
|
59
80
|
id?: string;
|
|
60
81
|
name: string;
|
|
61
82
|
appPackage: string;
|
|
@@ -105,7 +126,7 @@ type ReplayStreamEvent =
|
|
|
105
126
|
| { type: 'error'; message: string };
|
|
106
127
|
|
|
107
128
|
export async function replayAppiumScript(
|
|
108
|
-
input: { id: string; deviceId?: string },
|
|
129
|
+
input: { id: string; deviceId?: string; parameters?: import('./variables').TestVariable[] },
|
|
109
130
|
onOutput?: (line: string) => void,
|
|
110
131
|
) {
|
|
111
132
|
const response = await fetch(`${APP_BASE}/api/appium-recorder/scripts/${encodeURIComponent(input.id)}/replay`, {
|
|
@@ -114,7 +135,7 @@ export async function replayAppiumScript(
|
|
|
114
135
|
'Content-Type': 'application/json',
|
|
115
136
|
Accept: 'application/x-ndjson',
|
|
116
137
|
},
|
|
117
|
-
body: JSON.stringify({ deviceId: input.deviceId }),
|
|
138
|
+
body: JSON.stringify({ deviceId: input.deviceId, parameters: input.parameters }),
|
|
118
139
|
});
|
|
119
140
|
if (!response.ok || !response.body) return readJson<ReplayResult>(response);
|
|
120
141
|
|
|
@@ -10,6 +10,8 @@ import LongPressSettings from './LongPressSettings.vue';
|
|
|
10
10
|
import StageLogSettings from './StageLogSettings.vue';
|
|
11
11
|
import LoopSettings from './LoopSettings.vue';
|
|
12
12
|
import BreakLoopSettings from './BreakLoopSettings.vue';
|
|
13
|
+
import VariableExtractionSettings from './VariableExtractionSettings.vue';
|
|
14
|
+
import ScriptParameterSettings from './ScriptParameterSettings.vue';
|
|
13
15
|
|
|
14
16
|
type FlowKind = 'action' | 'condition' | 'assertion';
|
|
15
17
|
type SwipeGesture = NonNullable<AppiumRecordedStep['swipe']>;
|
|
@@ -77,6 +79,20 @@ function patchSwipe(patch: Partial<SwipeGesture>) {
|
|
|
77
79
|
});
|
|
78
80
|
}
|
|
79
81
|
|
|
82
|
+
function patchTapCoordinate(axis: 'centerX' | 'centerY', value: number | undefined) {
|
|
83
|
+
if (value === undefined || !Number.isFinite(value)) return;
|
|
84
|
+
const fallback = {
|
|
85
|
+
...props.step.fallback,
|
|
86
|
+
strategy: 'bounds' as const,
|
|
87
|
+
centerX: props.step.fallback?.centerX ?? 0,
|
|
88
|
+
centerY: props.step.fallback?.centerY ?? 0,
|
|
89
|
+
[axis]: Math.max(0, Math.round(value)),
|
|
90
|
+
};
|
|
91
|
+
// 仅同步自动生成的名称,保留用户自定义的节点名称。
|
|
92
|
+
patchStep({ fallback, ...(/^点击坐标 \d+,\d+$/.test(props.step.label)
|
|
93
|
+
? { label: `点击坐标 ${fallback.centerX},${fallback.centerY}` } : {}) });
|
|
94
|
+
}
|
|
95
|
+
|
|
80
96
|
function patchVisualChange(patch: Partial<VisualChangeConfig>) {
|
|
81
97
|
patchStep({
|
|
82
98
|
visualChange: normalizeVisualChangeConfig({
|
|
@@ -109,7 +125,7 @@ function patchSelector(patch: Partial<AppiumSelector>) {
|
|
|
109
125
|
patchStep({
|
|
110
126
|
selector: { ...props.step.selector!, ...patch },
|
|
111
127
|
// 修改元素目标后,不能继续优先使用录制时的备用 XPath。
|
|
112
|
-
...(props.step.type === 'longPress' || isNativeStateCondition(props.step) ? { selectorChain: undefined } : {}),
|
|
128
|
+
...(props.step.type === 'longPress' || props.step.type === 'extractVariable' || isNativeStateCondition(props.step) ? { selectorChain: undefined } : {}),
|
|
113
129
|
});
|
|
114
130
|
}
|
|
115
131
|
</script>
|
|
@@ -137,7 +153,7 @@ function patchSelector(patch: Partial<AppiumSelector>) {
|
|
|
137
153
|
<el-form-item label="节点类型">
|
|
138
154
|
<el-select
|
|
139
155
|
:model-value="defaultKind()"
|
|
140
|
-
:disabled="disabled || isBooleanCondition(step) || step.type === 'log' || step.type === 'openGallery' || step.type === 'endFlow' || step.type === 'loop' || step.type === 'breakLoop'"
|
|
156
|
+
:disabled="disabled || isBooleanCondition(step) || step.type === 'extractVariable' || step.type === 'log' || step.type === 'openGallery' || step.type === 'endFlow' || step.type === 'loop' || step.type === 'breakLoop'"
|
|
141
157
|
@update:model-value="patchFlow({ nodeKind: $event as FlowKind })"
|
|
142
158
|
>
|
|
143
159
|
<el-option label="操作" value="action" />
|
|
@@ -157,7 +173,17 @@ function patchSelector(patch: Partial<AppiumSelector>) {
|
|
|
157
173
|
</el-form-item>
|
|
158
174
|
</div>
|
|
159
175
|
<LongPressSettings v-if="step.type === 'longPress'" :step="step" :disabled="disabled" @update="patchStep" />
|
|
176
|
+
<div v-if="step.type === 'coordinateTap'" class="appium-flow-editor__grid">
|
|
177
|
+
<el-form-item label="坐标 X">
|
|
178
|
+
<el-input-number :model-value="step.fallback?.centerX ?? 0" :disabled="disabled" :min="0" :max="99999" :precision="0" controls-position="right" aria-label="坐标 X" @update:model-value="patchTapCoordinate('centerX', $event)" />
|
|
179
|
+
</el-form-item>
|
|
180
|
+
<el-form-item label="坐标 Y">
|
|
181
|
+
<el-input-number :model-value="step.fallback?.centerY ?? 0" :disabled="disabled" :min="0" :max="99999" :precision="0" controls-position="right" aria-label="坐标 Y" @update:model-value="patchTapCoordinate('centerY', $event)" />
|
|
182
|
+
</el-form-item>
|
|
183
|
+
</div>
|
|
160
184
|
<StageLogSettings v-if="step.type === 'log'" :step="step" :disabled="disabled" @update="patchStep" />
|
|
185
|
+
<VariableExtractionSettings v-if="step.type === 'extractVariable'" :step="step" :disabled="disabled" @update="patchStep" />
|
|
186
|
+
<ScriptParameterSettings v-if="step.type === 'runScript'" :step="step" :disabled="disabled" @update="patchStep" />
|
|
161
187
|
<LoopSettings v-if="step.type === 'loop'" :step="step" :disabled="disabled" @update="patchStep" />
|
|
162
188
|
<BreakLoopSettings v-if="step.type === 'breakLoop'" :step="step" :steps="steps" :disabled="disabled" @update="patchStep" />
|
|
163
189
|
<TextClickSettings v-if="step.type === 'textClick'" :step="step" :disabled="disabled" @update="patchStep" />
|
|
@@ -44,6 +44,7 @@ type InsertAction =
|
|
|
44
44
|
| 'waitDisappear'
|
|
45
45
|
| 'waitActivity'
|
|
46
46
|
| 'runScript'
|
|
47
|
+
| 'extractVariable'
|
|
47
48
|
| 'noop'
|
|
48
49
|
| 'log'
|
|
49
50
|
| 'visualChangeStart'
|
|
@@ -89,6 +90,7 @@ const actionGroups: Array<{ title: string; actions: Array<{ type: InsertAction;
|
|
|
89
90
|
actions: [
|
|
90
91
|
{ type: 'tap', label: '录制点击' },
|
|
91
92
|
{ type: 'input', label: '录制输入' },
|
|
93
|
+
{ type: 'extractVariable', label: '提取变量' },
|
|
92
94
|
{ type: 'clearInput', label: '清空输入' },
|
|
93
95
|
{ type: 'coordinateTap', label: '点击坐标' },
|
|
94
96
|
{ type: 'longPress', label: '长按' },
|
|
@@ -120,8 +122,6 @@ const actionGroups: Array<{ title: string; actions: Array<{ type: InsertAction;
|
|
|
120
122
|
{ type: 'clearIfExists', label: '存在则清空' },
|
|
121
123
|
{ type: 'backIfExists', label: '存在则返回' },
|
|
122
124
|
{ type: 'waitFor', label: '等待出现' },
|
|
123
|
-
{ type: 'assertExists', label: '断言存在' },
|
|
124
|
-
{ type: 'assertText', label: '断言文本' },
|
|
125
125
|
{ type: 'waitDisappear', label: '等待元素消失' },
|
|
126
126
|
{ type: 'waitActivity', label: '等待 Activity' },
|
|
127
127
|
{ type: 'visualChangeStart', label: '检测画面变化开始' },
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
|
3
|
+
import { QuestionFilled } from '@element-plus/icons-vue';
|
|
4
|
+
import { loadVariables, saveVariables } from '../api';
|
|
5
|
+
import VariableTable from './VariableTable.vue';
|
|
6
|
+
import { validateVariables, type TestVariable } from '../variables';
|
|
7
|
+
const props = defineProps<{ variables: TestVariable[]; disabled?: boolean; scriptName: string; scriptId: string }>();
|
|
8
|
+
const emit = defineEmits<{ 'update:variables': [TestVariable[]]; saved: [string, TestVariable[]] }>();
|
|
9
|
+
const globals = ref<TestVariable[]>([]);
|
|
10
|
+
const busy = ref(false);
|
|
11
|
+
const loaded = ref(false);
|
|
12
|
+
const error = ref('');
|
|
13
|
+
const status = ref({ global: '', private: '' });
|
|
14
|
+
const invalid = ref({ global: false, private: false });
|
|
15
|
+
const revisions = { global: 0, private: 0 };
|
|
16
|
+
let mounted = true;
|
|
17
|
+
onBeforeUnmount(() => { mounted = false; });
|
|
18
|
+
const privateUrl = props.scriptId ? `/api/appium-recorder/variables?scriptId=${encodeURIComponent(props.scriptId)}` : '/api/appium-recorder/variables?draft=1';
|
|
19
|
+
async function load() {
|
|
20
|
+
busy.value = true; error.value = '';
|
|
21
|
+
try {
|
|
22
|
+
globals.value = (await loadVariables('/api/appium-recorder/variables')).variables; loaded.value = true;
|
|
23
|
+
if (props.scriptId || !props.variables.length) {
|
|
24
|
+
const variables = (await loadVariables(privateUrl)).variables;
|
|
25
|
+
if (mounted) emit('update:variables', variables);
|
|
26
|
+
}
|
|
27
|
+
} catch (cause) { error.value = String(cause); }
|
|
28
|
+
finally { busy.value = false; }
|
|
29
|
+
}
|
|
30
|
+
function update(scope: 'global' | 'private', values: TestVariable[]) {
|
|
31
|
+
if (scope === 'global') globals.value = values;
|
|
32
|
+
else emit('update:variables', values);
|
|
33
|
+
const revision = ++revisions[scope];
|
|
34
|
+
let variables: TestVariable[];
|
|
35
|
+
try {
|
|
36
|
+
variables = validateVariables(values);
|
|
37
|
+
} catch (cause) {
|
|
38
|
+
status.value[scope] = cause instanceof Error ? cause.message : String(cause);
|
|
39
|
+
invalid.value[scope] = true;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
invalid.value[scope] = false;
|
|
43
|
+
status.value[scope] = '正在保存…';
|
|
44
|
+
// 同一作用域串行写入,慢响应不能覆盖后一次编辑;不锁定输入框。
|
|
45
|
+
void (async () => {
|
|
46
|
+
try {
|
|
47
|
+
await saveVariables(scope === 'global' ? '/api/appium-recorder/variables' : privateUrl, variables);
|
|
48
|
+
if (scope === 'private') emit('saved', props.scriptId, variables);
|
|
49
|
+
if (revision === revisions[scope]) status.value[scope] = scope === 'private' && !props.scriptId ? '草稿已自动保存' : '已自动保存';
|
|
50
|
+
} catch (cause) {
|
|
51
|
+
if (revision === revisions[scope]) {
|
|
52
|
+
status.value[scope] = cause instanceof Error ? cause.message : String(cause);
|
|
53
|
+
invalid.value[scope] = true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
})();
|
|
57
|
+
}
|
|
58
|
+
onMounted(load);
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<template>
|
|
62
|
+
<div class="preset-variables">
|
|
63
|
+
<section class="preset-variables__section" aria-label="全局变量">
|
|
64
|
+
<div class="preset-variables__title">
|
|
65
|
+
<h3>全局变量</h3>
|
|
66
|
+
<el-tooltip placement="bottom-end" effect="light" :show-after="150">
|
|
67
|
+
<template #content>
|
|
68
|
+
<div class="preset-variables__help">
|
|
69
|
+
<strong>如何使用预设变量</strong>
|
|
70
|
+
<p>全局变量可在所有脚本中使用;私有变量仅在当前脚本中使用,同名时优先使用私有变量。</p>
|
|
71
|
+
<p>编辑后自动保存,变量值可以为空字符串。新脚本的私有变量先保存为草稿,首次保存脚本时归入该脚本。</p>
|
|
72
|
+
<p>使用 <code v-pre>{{变量名}}</code> 在输入、判断、定位、日志中引用,例如 <code v-pre>{{account}}</code>。变量名使用字母、数字和下划线,不能以数字开头。</p>
|
|
73
|
+
<p>敏感值在日志和报告中脱敏,导出时清空敏感值;密码、令牌等名称自动标记为敏感,也可手动勾选。</p>
|
|
74
|
+
<p>含敏感变量的运行不保存截图或 Appium 原始日志。本地变量非加密存储。</p>
|
|
75
|
+
</div>
|
|
76
|
+
</template>
|
|
77
|
+
<el-button class="preset-variables__help-button" :icon="QuestionFilled" text aria-label="预设变量使用说明" />
|
|
78
|
+
</el-tooltip>
|
|
79
|
+
</div>
|
|
80
|
+
<el-alert v-if="error" :title="error" type="error" :closable="false" /><el-button v-if="error" @click="load">重新加载</el-button>
|
|
81
|
+
<VariableTable :model-value="globals" :disabled="disabled || busy || !loaded" @update:model-value="update('global', $event)">
|
|
82
|
+
<span class="preset-variables__status" :class="{ 'is-error': invalid.global }" role="status">{{ status.global }}</span>
|
|
83
|
+
</VariableTable>
|
|
84
|
+
</section>
|
|
85
|
+
<section class="preset-variables__section" aria-label="当前脚本私有变量">
|
|
86
|
+
<div class="preset-variables__title"><h3>当前脚本私有变量</h3></div>
|
|
87
|
+
<div class="preset-variables__script">{{ scriptName || '未命名脚本' }}</div>
|
|
88
|
+
<VariableTable :model-value="variables" :disabled="disabled || busy" @update:model-value="update('private', $event)">
|
|
89
|
+
<span class="preset-variables__status" :class="{ 'is-error': invalid.private }" role="status">{{ status.private }}</span>
|
|
90
|
+
</VariableTable>
|
|
91
|
+
</section>
|
|
92
|
+
</div>
|
|
93
|
+
</template>
|
|
94
|
+
|
|
95
|
+
<style scoped>
|
|
96
|
+
.preset-variables { padding: 12px 16px; overflow: auto; }
|
|
97
|
+
.preset-variables__help-button { width: 28px; height: 28px; padding: 0; font-size: 16px; }
|
|
98
|
+
.preset-variables__section { padding: 16px 0 22px; }
|
|
99
|
+
.preset-variables__section:first-child { padding-top: 0; }
|
|
100
|
+
.preset-variables__section + .preset-variables__section { border-top: 1px solid var(--el-border-color-lighter); }
|
|
101
|
+
.preset-variables__title { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
|
|
102
|
+
.preset-variables__title h3 { margin: 0; font-size: 14px; font-weight: 600; }
|
|
103
|
+
.preset-variables__script { font-size: 12px; color: var(--el-text-color-secondary); margin: -5px 0 12px; overflow-wrap: anywhere; }
|
|
104
|
+
.preset-variables__help { width: min(340px, calc(100vw - 64px)); font-size: 13px; line-height: 1.7; overflow-wrap: anywhere; }
|
|
105
|
+
.preset-variables__help p { margin: 8px 0 0; }
|
|
106
|
+
.preset-variables__help code { color: var(--el-color-primary); }
|
|
107
|
+
.preset-variables__status { font-size: 12px; color: var(--el-text-color-secondary); overflow-wrap: anywhere; }
|
|
108
|
+
.preset-variables__status.is-error { color: var(--el-color-danger); }
|
|
109
|
+
</style>
|
|
@@ -61,6 +61,7 @@ const insertActionGroups: FlowActionGroup[] = [
|
|
|
61
61
|
actions: [
|
|
62
62
|
{ type: 'tap', label: '录制点击' },
|
|
63
63
|
{ type: 'input', label: '录制输入' },
|
|
64
|
+
{ type: 'extractVariable', label: '提取变量' },
|
|
64
65
|
{ type: 'clearInput', label: '清空输入' },
|
|
65
66
|
{ type: 'coordinateTap', label: '点击坐标' },
|
|
66
67
|
{ type: 'longPress', label: '长按' },
|
|
@@ -93,8 +94,6 @@ const insertActionGroups: FlowActionGroup[] = [
|
|
|
93
94
|
{ type: 'clearIfExists', label: '存在则清空' },
|
|
94
95
|
{ type: 'backIfExists', label: '存在则返回' },
|
|
95
96
|
{ type: 'waitFor', label: '等待出现' },
|
|
96
|
-
{ type: 'assertExists', label: '断言存在' },
|
|
97
|
-
{ type: 'assertText', label: '断言文本' },
|
|
98
97
|
{ type: 'waitDisappear', label: '等待元素消失' },
|
|
99
98
|
{ type: 'waitActivity', label: '等待 Activity' },
|
|
100
99
|
{ type: 'visualChangeStart', label: '检测画面变化开始节点' },
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { Delete, Plus } from '@element-plus/icons-vue';
|
|
3
|
+
import VariableTable from './VariableTable.vue';
|
|
4
|
+
import type { AppiumRecordedStep } from '../types';
|
|
5
|
+
import type { ScriptReturn } from '../variables';
|
|
6
|
+
const props = defineProps<{ step: AppiumRecordedStep; disabled?: boolean }>();
|
|
7
|
+
const emit = defineEmits<{ update: [Partial<AppiumRecordedStep>] }>();
|
|
8
|
+
function update(index: number, patch: Partial<ScriptReturn>) {
|
|
9
|
+
emit('update', { returns: (props.step.returns || []).map((item, i) => i === index ? { ...item, ...patch } : item) });
|
|
10
|
+
}
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<el-form-item label="传入子脚本参数"><VariableTable :model-value="step.parameters || []" :disabled="disabled" @update:model-value="emit('update', { parameters: $event })" /></el-form-item>
|
|
15
|
+
<el-form-item label="返回值映射">
|
|
16
|
+
<div class="return-mappings">
|
|
17
|
+
<div v-for="(item, index) in step.returns || []" :key="index" class="return-row">
|
|
18
|
+
<el-input :model-value="item.name" placeholder="子脚本变量名" aria-label="子脚本返回变量" :disabled="disabled" @update:model-value="update(index, { name: String($event) })" />
|
|
19
|
+
<span>→</span>
|
|
20
|
+
<el-input :model-value="item.target" placeholder="当前脚本变量名" aria-label="返回目标变量" :disabled="disabled" @update:model-value="update(index, { target: String($event) })" />
|
|
21
|
+
<el-tooltip content="删除映射"><el-button :icon="Delete" text :disabled="disabled" aria-label="删除返回映射" @click="emit('update', { returns: step.returns!.filter((_, i) => i !== index) })" /></el-tooltip>
|
|
22
|
+
</div>
|
|
23
|
+
<el-button :icon="Plus" :disabled="disabled" size="small" @click="emit('update', { returns: [...(step.returns || []), { name: '', target: '' }] })">新增返回映射</el-button>
|
|
24
|
+
</div>
|
|
25
|
+
</el-form-item>
|
|
26
|
+
</template>
|
|
27
|
+
|
|
28
|
+
<style scoped>
|
|
29
|
+
.return-mappings { width: 100%; }
|
|
30
|
+
.return-row { display: grid; grid-template-columns: minmax(0, 1fr) 15px minmax(0, 1fr) 30px; gap: 5px; align-items: center; margin-bottom: 8px; }
|
|
31
|
+
</style>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { AppiumRecordedStep } from '../types';
|
|
3
|
+
import { sensitiveVariableName, type VariableExtraction } from '../variables';
|
|
4
|
+
const props = defineProps<{ step: AppiumRecordedStep; disabled?: boolean }>();
|
|
5
|
+
const emit = defineEmits<{ update: [Partial<AppiumRecordedStep>] }>();
|
|
6
|
+
function update(patch: Partial<VariableExtraction>) {
|
|
7
|
+
emit('update', { extractVariable: { name: '', attribute: 'text', ...props.step.extractVariable, ...patch } });
|
|
8
|
+
}
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<el-form-item label="保存到当前脚本变量"><el-input :model-value="step.extractVariable?.name || ''" :disabled="disabled" aria-label="提取变量名" @update:model-value="update({ name: String($event) })" /></el-form-item>
|
|
13
|
+
<el-form-item label="组件属性"><el-select :model-value="step.extractVariable?.attribute || 'text'" :disabled="disabled" filterable allow-create default-first-option aria-label="提取组件属性" @update:model-value="update({ attribute: String($event) })">
|
|
14
|
+
<el-option v-for="name in ['text', 'content-desc', 'checked', 'selected', 'enabled', 'resource-id', 'class']" :key="name" :label="name" :value="name" />
|
|
15
|
+
</el-select></el-form-item>
|
|
16
|
+
<el-form-item label="敏感变量"><el-checkbox :model-value="step.extractVariable?.sensitive || sensitiveVariableName(step.extractVariable?.name || '')" :disabled="disabled || sensitiveVariableName(step.extractVariable?.name || '')" @update:model-value="update({ sensitive: Boolean($event) })" /></el-form-item>
|
|
17
|
+
</template>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { Delete, Plus, CopyDocument } from '@element-plus/icons-vue';
|
|
3
|
+
import { ElMessage } from 'element-plus';
|
|
4
|
+
import { sensitiveVariableName, type TestVariable } from '../variables';
|
|
5
|
+
const props = defineProps<{ modelValue: TestVariable[]; disabled?: boolean }>();
|
|
6
|
+
const emit = defineEmits<{ 'update:modelValue': [TestVariable[]] }>();
|
|
7
|
+
function update(index: number, patch: Partial<TestVariable>) {
|
|
8
|
+
emit('update:modelValue', props.modelValue.map((item, i) => i === index ? { ...item, ...patch } : item));
|
|
9
|
+
}
|
|
10
|
+
async function copy(name: string) {
|
|
11
|
+
try { await navigator.clipboard.writeText(`{{${name}}}`); ElMessage.success('已复制变量引用'); }
|
|
12
|
+
catch { ElMessage.error('复制失败'); }
|
|
13
|
+
}
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<div class="variable-table">
|
|
18
|
+
<el-table :data="modelValue" empty-text="暂无变量" size="small">
|
|
19
|
+
<el-table-column label="变量名" min-width="110"><template #default="{ row, $index }"><el-input :model-value="row.name" :disabled="disabled" aria-label="变量名" @update:model-value="update($index, { name: String($event) })" /></template></el-table-column>
|
|
20
|
+
<el-table-column label="值" min-width="125"><template #default="{ row, $index }"><el-input :model-value="row.value" :type="row.sensitive || sensitiveVariableName(row.name) ? 'password' : 'text'" :show-password="row.sensitive || sensitiveVariableName(row.name)" :disabled="disabled" aria-label="变量值" @update:model-value="update($index, { value: String($event) })" /></template></el-table-column>
|
|
21
|
+
<el-table-column label="敏感" width="65"><template #default="{ row, $index }"><el-checkbox :model-value="row.sensitive || sensitiveVariableName(row.name)" :disabled="disabled || sensitiveVariableName(row.name)" aria-label="敏感变量" @update:model-value="update($index, { sensitive: Boolean($event) })" /></template></el-table-column>
|
|
22
|
+
<el-table-column label="操作" width="76"><template #default="{ row, $index }"><div class="variable-table__actions">
|
|
23
|
+
<el-tooltip content="复制引用"><el-button :icon="CopyDocument" text size="small" :disabled="!row.name" aria-label="复制变量引用" @click="copy(row.name)" /></el-tooltip>
|
|
24
|
+
<el-tooltip content="删除变量"><el-button :icon="Delete" text size="small" :disabled="disabled" aria-label="删除变量" @click="emit('update:modelValue', modelValue.filter((_, i) => i !== $index))" /></el-tooltip>
|
|
25
|
+
</div></template></el-table-column>
|
|
26
|
+
</el-table>
|
|
27
|
+
<div class="variable-table__footer">
|
|
28
|
+
<el-button :icon="Plus" :disabled="disabled" size="small" @click="emit('update:modelValue', [...modelValue, { name: '', value: '', sensitive: false }])">新增变量</el-button>
|
|
29
|
+
<slot />
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</template>
|
|
33
|
+
|
|
34
|
+
<style scoped>
|
|
35
|
+
.variable-table { min-width: 0; }
|
|
36
|
+
.variable-table__footer { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; margin-top: 12px; }
|
|
37
|
+
.variable-table__actions { display: flex; align-items: center; gap: 2px; }
|
|
38
|
+
.variable-table__actions :deep(.el-button) { width: 26px; height: 28px; padding: 0; }
|
|
39
|
+
.variable-table :deep(.el-table th.el-table__cell) { background: var(--el-fill-color-light); color: var(--el-text-color-secondary); font-size: 12px; padding: 7px 0; }
|
|
40
|
+
.variable-table :deep(.el-table td.el-table__cell) { padding: 8px 0; }
|
|
41
|
+
.variable-table :deep(.el-table .cell) { padding: 0 8px; }
|
|
42
|
+
.variable-table :deep(.el-button + .el-button) { margin-left: 0; }
|
|
43
|
+
</style>
|