android-midscene-automation 1.1.2 → 1.1.4
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 +9 -0
- package/README.md +9 -0
- package/package.json +1 -1
- package/server/appium-recorder/appium-runner.ts +258 -86
- package/server/appium-recorder/element-tap.test.ts +251 -0
- package/server/appium-recorder/managed-cleanup.test.ts +7 -7
- package/server/appium-recorder/replay-lifecycle.test.ts +4 -4
- package/server/appium-recorder/repository.ts +39 -9
- package/server/appium-recorder/request-timeout.ts +5 -0
- package/server/appium-recorder/routes.ts +38 -15
- package/server/appium-recorder/script-import.test.ts +45 -0
- package/server/appium-recorder/tree-dump.ts +44 -0
- package/server/remote-agents/replay-integration.test.ts +4 -4
- package/src/App.vue +10 -10
- package/src/appium-recorder/AppiumPage.vue +58 -12
- package/src/appium-recorder/RunHistoryDialog.vue +91 -51
- package/src/appium-recorder/api.ts +4 -0
- package/src/appium-recorder/components/FlowStepEditor.vue +51 -9
- package/src/appium-recorder/components/RecordedSteps.vue +1 -0
- package/src/appium-recorder/components/ScriptParameterSettings.vue +44 -10
- package/src/appium-recorder/components/VariableTable.vue +44 -5
- package/src/appium-recorder/tree.ts +1 -0
- package/src/style.css +149 -53
package/src/App.vue
CHANGED
|
@@ -790,7 +790,15 @@ onUnmounted(() => {
|
|
|
790
790
|
width="760px"
|
|
791
791
|
class="code-dialog"
|
|
792
792
|
>
|
|
793
|
-
<
|
|
793
|
+
<el-input
|
|
794
|
+
v-if="codeDialog.editing"
|
|
795
|
+
v-model="codeDialog.draftCode"
|
|
796
|
+
type="textarea"
|
|
797
|
+
resize="none"
|
|
798
|
+
class="code-dialog__editor"
|
|
799
|
+
/>
|
|
800
|
+
<pre v-else class="code-block code-dialog__body"><code>{{ codeDialog.code || '暂无代码' }}</code></pre>
|
|
801
|
+
<template #footer>
|
|
794
802
|
<el-button :icon="CopyDocument" @click="copyDialogCode">复制</el-button>
|
|
795
803
|
<el-button v-if="!codeDialog.editing" type="primary" :icon="Edit" @click="editCodeDialog">
|
|
796
804
|
编辑
|
|
@@ -803,15 +811,7 @@ onUnmounted(() => {
|
|
|
803
811
|
保存
|
|
804
812
|
</el-button>
|
|
805
813
|
</template>
|
|
806
|
-
</
|
|
807
|
-
<el-input
|
|
808
|
-
v-if="codeDialog.editing"
|
|
809
|
-
v-model="codeDialog.draftCode"
|
|
810
|
-
type="textarea"
|
|
811
|
-
resize="none"
|
|
812
|
-
class="code-dialog__editor"
|
|
813
|
-
/>
|
|
814
|
-
<pre v-else class="code-block code-dialog__body"><code>{{ codeDialog.code || '暂无代码' }}</code></pre>
|
|
814
|
+
</template>
|
|
815
815
|
</el-dialog>
|
|
816
816
|
|
|
817
817
|
<AppSidebar :active-menu="activeMenu" :collapsed="sidebarCollapsed"
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
downloadAppiumScript,
|
|
27
27
|
getAppiumScripts,
|
|
28
28
|
getAppiumTree,
|
|
29
|
-
|
|
29
|
+
importAppiumScripts,
|
|
30
30
|
launchAppiumDeviceApp,
|
|
31
31
|
replayAppiumScript,
|
|
32
32
|
saveAppiumScript,
|
|
@@ -2053,23 +2053,67 @@ async function downloadScript(script: AppiumRecordedScript) {
|
|
|
2053
2053
|
}
|
|
2054
2054
|
|
|
2055
2055
|
function chooseScriptImportFile() {
|
|
2056
|
+
if (importingScript.value) return;
|
|
2056
2057
|
scriptImportInput.value?.click();
|
|
2057
2058
|
}
|
|
2058
2059
|
|
|
2059
2060
|
async function importScriptFile(event: Event) {
|
|
2060
2061
|
const input = event.currentTarget as HTMLInputElement;
|
|
2061
|
-
const
|
|
2062
|
+
const files = Array.from(input.files || []);
|
|
2062
2063
|
input.value = '';
|
|
2063
|
-
if (!
|
|
2064
|
+
if (!files.length) return;
|
|
2064
2065
|
importingScript.value = true;
|
|
2066
|
+
|
|
2067
|
+
const importedScripts: AppiumRecordedScript[] = [];
|
|
2068
|
+
const failedFiles: Array<{ name: string; reason: string }> = [];
|
|
2065
2069
|
try {
|
|
2066
|
-
const
|
|
2067
|
-
const
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2070
|
+
const parsedFiles: Array<{ name: string; payload: unknown }> = [];
|
|
2071
|
+
for (const file of files) {
|
|
2072
|
+
try {
|
|
2073
|
+
const payload = JSON.parse(await file.text()) as unknown;
|
|
2074
|
+
parsedFiles.push({ name: file.name, payload });
|
|
2075
|
+
} catch (error) {
|
|
2076
|
+
failedFiles.push({
|
|
2077
|
+
name: file.name,
|
|
2078
|
+
reason: error instanceof Error ? error.message : '文件格式无效',
|
|
2079
|
+
});
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
if (parsedFiles.length) {
|
|
2084
|
+
try {
|
|
2085
|
+
const result = await importAppiumScripts(parsedFiles.map(item => item.payload));
|
|
2086
|
+
importedScripts.push(...result.scripts);
|
|
2087
|
+
} catch (error) {
|
|
2088
|
+
const reason = error instanceof Error ? error.message : '导入失败';
|
|
2089
|
+
failedFiles.push(...parsedFiles.map(file => ({ name: file.name, reason })));
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
if (importedScripts.length) {
|
|
2094
|
+
try {
|
|
2095
|
+
await loadScripts();
|
|
2096
|
+
} catch (error) {
|
|
2097
|
+
ElMessage.warning(`脚本已导入,但刷新列表失败:${error instanceof Error ? error.message : '未知错误'}`);
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
if (!failedFiles.length) {
|
|
2103
|
+
ElMessage.success(files.length === 1
|
|
2104
|
+
? `脚本「${importedScripts[0].name}」已导入`
|
|
2105
|
+
: `已成功导入 ${importedScripts.length} 个脚本`);
|
|
2106
|
+
return;
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
const failedSummary = failedFiles
|
|
2110
|
+
.map(({ name, reason }) => `${name}(${reason})`)
|
|
2111
|
+
.join(';');
|
|
2112
|
+
if (importedScripts.length) {
|
|
2113
|
+
ElMessage.warning(`已导入 ${importedScripts.length} 个脚本,${failedFiles.length} 个失败:${failedSummary}`);
|
|
2114
|
+
} else {
|
|
2115
|
+
ElMessage.error(`导入失败:${failedSummary}`);
|
|
2116
|
+
}
|
|
2073
2117
|
} finally {
|
|
2074
2118
|
importingScript.value = false;
|
|
2075
2119
|
}
|
|
@@ -2374,7 +2418,7 @@ watch(
|
|
|
2374
2418
|
<el-tab-pane label="脚本列表" name="scripts">
|
|
2375
2419
|
<div class="appium-script-list">
|
|
2376
2420
|
<div class="appium-script-list__toolbar">
|
|
2377
|
-
<el-tooltip content="
|
|
2421
|
+
<el-tooltip content="导入脚本(支持多选)" placement="top" :show-after="200"><span class="appium-script-icon">
|
|
2378
2422
|
<el-button size="small" :icon="Upload" aria-label="导入脚本" :loading="importingScript" @click="chooseScriptImportFile" />
|
|
2379
2423
|
</span></el-tooltip>
|
|
2380
2424
|
<input
|
|
@@ -2382,6 +2426,7 @@ watch(
|
|
|
2382
2426
|
class="appium-script-list__file-input"
|
|
2383
2427
|
type="file"
|
|
2384
2428
|
accept="application/json,.json"
|
|
2429
|
+
multiple
|
|
2385
2430
|
@change="importScriptFile"
|
|
2386
2431
|
>
|
|
2387
2432
|
</div>
|
|
@@ -2520,7 +2565,8 @@ watch(
|
|
|
2520
2565
|
<el-dialog
|
|
2521
2566
|
v-model="linkedScriptDialogVisible"
|
|
2522
2567
|
title="连接脚本"
|
|
2523
|
-
width="
|
|
2568
|
+
width="640px"
|
|
2569
|
+
class="appium-linked-script-dialog"
|
|
2524
2570
|
align-center
|
|
2525
2571
|
@closed="closeLinkedScriptDialog"
|
|
2526
2572
|
>
|
|
@@ -72,63 +72,74 @@ onMounted(load);
|
|
|
72
72
|
</script>
|
|
73
73
|
|
|
74
74
|
<template>
|
|
75
|
-
<el-dialog :model-value="true" :title="`${scriptName} · 历史结果对比`" width="min(1200px,
|
|
75
|
+
<el-dialog :model-value="true" :title="`${scriptName} · 历史结果对比`" width="min(1200px, calc(100vw - 32px))" class="run-history-dialog" align-center append-to-body draggable overflow @closed="emit('close')">
|
|
76
76
|
<div v-loading="loading" class="run-history">
|
|
77
77
|
<div class="history-toolbar">
|
|
78
78
|
<el-select v-model="version" clearable placeholder="全部应用版本" aria-label="应用版本"><el-option v-for="item in versions" :key="item" :value="item" /></el-select>
|
|
79
79
|
<el-select v-model="device" clearable placeholder="全部设备" aria-label="设备"><el-option v-for="item in devices" :key="item" :value="item" /></el-select>
|
|
80
80
|
<el-tooltip content="刷新历史"><el-button :icon="Refresh" aria-label="刷新历史" @click="load" /></el-tooltip>
|
|
81
|
-
<el-button :disabled="selected.length !== 2" :loading="comparing" @click="compare">对比两次运行</el-button>
|
|
81
|
+
<el-button type="primary" :disabled="selected.length !== 2" :loading="comparing" @click="compare">对比两次运行</el-button>
|
|
82
82
|
<el-tooltip content="删除选中历史"><el-button :icon="Delete" type="danger" plain :disabled="!selected.length" aria-label="删除选中历史" @click="remove" /></el-tooltip>
|
|
83
83
|
</div>
|
|
84
84
|
<el-alert v-if="error" :title="error" type="error" :closable="false" />
|
|
85
85
|
<el-empty v-else-if="!runs.length && !loading" description="暂无历史结果,后续运行会自动记录" />
|
|
86
86
|
<template v-else>
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
<
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
<
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
87
|
+
<div class="history-summary">
|
|
88
|
+
<strong>{{ filtered.length }}</strong><span>次运行</span>
|
|
89
|
+
<i />
|
|
90
|
+
<strong>{{ stats.passRate === null ? '—' : `${(stats.passRate * 100).toFixed(1)}%` }}</strong><span>通过率(不含手动终止)</span>
|
|
91
|
+
</div>
|
|
92
|
+
<section class="history-panel">
|
|
93
|
+
<h3>耗时趋势</h3>
|
|
94
|
+
<svg class="history-trend" viewBox="0 0 800 160" role="img" aria-label="按时间从早到晚的运行耗时趋势">
|
|
95
|
+
<path d="M20 10V130H790" fill="none" stroke="#a0a8ad" />
|
|
96
|
+
<polyline :points="points" fill="none" stroke="#168a75" stroke-width="2" />
|
|
97
|
+
<circle v-for="(run, index) in trend" :key="run.id" :cx="20 + index * 760 / Math.max(1, trend.length - 1)" :cy="130 - run.durationMs / maxDuration * 110" r="4" :fill="run.status === 'failed' ? '#d14343' : '#168a75'">
|
|
98
|
+
<title>{{ time(run.startedAt) }} · {{ seconds(run.durationMs) }} · {{ run.appVersion || '未知版本' }}</title>
|
|
99
|
+
</circle>
|
|
100
|
+
<text x="24" y="14">{{ seconds(maxDuration) }}</text><text x="20" y="153">{{ trend[0] ? time(trend[0].startedAt) : '' }}</text><text x="780" y="153" text-anchor="end">{{ trend.length > 1 ? time(trend[trend.length - 1].startedAt) : '' }}</text>
|
|
101
|
+
</svg>
|
|
102
|
+
</section>
|
|
103
|
+
<section class="history-panel">
|
|
104
|
+
<h3>运行记录</h3>
|
|
105
|
+
<el-table :data="filtered" row-key="id" max-height="270" @selection-change="selected = $event">
|
|
106
|
+
<el-table-column type="selection" width="42" />
|
|
107
|
+
<el-table-column label="运行时间" min-width="180"><template #default="{ row }">{{ time(row.startedAt) }}</template></el-table-column>
|
|
108
|
+
<el-table-column prop="appVersion" label="应用版本" min-width="140"><template #default="{ row }">{{ row.appVersion || '未知' }}</template></el-table-column>
|
|
109
|
+
<el-table-column prop="deviceId" label="设备" min-width="140" />
|
|
110
|
+
<el-table-column label="结果" width="90"><template #default="{ row }"><el-tag :type="row.status === 'passed' ? 'success' : row.status === 'failed' ? 'danger' : 'info'" size="small">{{ labels[row.status as keyof typeof labels] }}</el-tag></template></el-table-column>
|
|
111
|
+
<el-table-column label="耗时" width="100"><template #default="{ row }">{{ seconds(row.durationMs) }}</template></el-table-column>
|
|
112
|
+
<el-table-column label="录像" width="70"><template #default="{ row }"><el-tooltip v-if="row.video" content="播放回放视频"><el-button :icon="VideoPlay" text aria-label="播放回放视频" @click="videoRun = row" /></el-tooltip></template></el-table-column>
|
|
113
|
+
</el-table>
|
|
114
|
+
</section>
|
|
115
|
+
<section class="history-panel">
|
|
116
|
+
<h3>失败节点分布</h3>
|
|
117
|
+
<el-table :data="stats.failures" max-height="220" empty-text="暂无节点失败记录">
|
|
118
|
+
<el-table-column prop="label" label="节点" min-width="220" />
|
|
119
|
+
<el-table-column label="失败 / 到达次数" width="145"><template #default="{ row }">{{ row.failures }} / {{ row.runs }}</template></el-table-column>
|
|
120
|
+
<el-table-column label="观察结果" width="130"><template #default="{ row }">{{ row.runs < 2 ? '样本不足' : row.failures === row.runs ? '持续失败' : '偶发失败' }}</template></el-table-column>
|
|
121
|
+
<el-table-column label="首次失败 / 版本" min-width="190"><template #default="{ row }">{{ time(row.firstFailure) }} · {{ row.version || '未知' }}</template></el-table-column>
|
|
122
|
+
</el-table>
|
|
123
|
+
</section>
|
|
113
124
|
</template>
|
|
114
|
-
<section v-if="details.length === 2" class="history-comparison">
|
|
115
|
-
<h3
|
|
125
|
+
<section v-if="details.length === 2" class="history-panel history-comparison">
|
|
126
|
+
<div class="history-comparison__header"><h3>运行对比</h3><el-tag type="info">总耗时差 {{ seconds(details[1].durationMs - details[0].durationMs) }}</el-tag></div>
|
|
116
127
|
<el-alert v-if="details[0].deviceId !== details[1].deviceId || details[0].appPackage !== details[1].appPackage" title="两次运行的设备或应用不同,耗时差不能直接归因于版本变化" type="warning" :closable="false" />
|
|
117
128
|
<el-select v-model="nodeKey" clearable filterable placeholder="全部节点截图" aria-label="对比节点"><el-option v-for="node in comparisonNodes" :key="node.key" :label="node.label" :value="node.key" /></el-select>
|
|
118
129
|
<div class="comparison-columns">
|
|
119
|
-
<section v-for="run in details" :key="run.id">
|
|
120
|
-
<h4>{{ time(run.startedAt) }}
|
|
121
|
-
<p>{{ run.appVersion || '未知版本' }} · {{ run.deviceId }} · {{ seconds(run.durationMs) }}</p>
|
|
122
|
-
<p v-if="nodeKey">节点累计耗时(含截图开销):{{ nodeDuration(run) }}</p>
|
|
123
|
-
<p v-if="!run.frames.some(frame => !nodeKey || frame.key === nodeKey)">未捕获到对应截图</p>
|
|
130
|
+
<section v-for="run in details" :key="run.id" class="comparison-run-card">
|
|
131
|
+
<div class="comparison-run-card__header"><h4>{{ time(run.startedAt) }}</h4><el-tag :type="run.status === 'passed' ? 'success' : run.status === 'failed' ? 'danger' : 'info'" size="small">{{ labels[run.status] }}</el-tag></div>
|
|
132
|
+
<p class="comparison-run-card__meta">{{ run.appVersion || '未知版本' }} · {{ run.deviceId }} · {{ seconds(run.durationMs) }}</p>
|
|
133
|
+
<p v-if="nodeKey" class="comparison-run-card__note">节点累计耗时(含截图开销):{{ nodeDuration(run) }}</p>
|
|
134
|
+
<p v-if="!run.frames.some(frame => !nodeKey || frame.key === nodeKey)" class="comparison-run-card__note">未捕获到对应截图</p>
|
|
124
135
|
<div class="history-frames">
|
|
125
136
|
<figure v-for="(frame, index) in run.frames.filter(frame => !nodeKey || frame.key === nodeKey)" :key="index">
|
|
126
137
|
<figcaption>{{ frame.label }} · {{ frame.phase === 'before' ? '执行前' : frame.phase === 'after' ? '执行后' : frame.phase === 'observation' ? '观察采样' : frame.phase }} · {{ time(frame.capturedAt) }}</figcaption>
|
|
127
138
|
<el-image :src="frame.imageUrl" :preview-src-list="[frame.imageUrl]" preview-teleported fit="contain" loading="lazy" />
|
|
128
139
|
</figure>
|
|
129
140
|
</div>
|
|
130
|
-
<h4>节点结果</h4><ul><li v-for="(node, index) in run.nodes.filter(node => !nodeKey || node.key === nodeKey)" :key="index">{{ node.label }} · {{ node.failed ? '失败' : '完成' }} · {{ node.durationMs === null ? '耗时未知' : seconds(node.durationMs) }}</li></ul>
|
|
131
|
-
<h4>关键日志 / 完整日志</h4><el-input v-model="logQuery" clearable placeholder="日志关键字,如 stageLog" aria-label="日志关键字" /><pre>{{ logQuery ? run.output.split('\n').filter(line => line.toLowerCase().includes(logQuery.toLowerCase())).join('\n') || '无匹配日志' : run.output }}</pre>
|
|
141
|
+
<h4>节点结果</h4><ul class="history-node-results"><li v-for="(node, index) in run.nodes.filter(node => !nodeKey || node.key === nodeKey)" :key="index">{{ node.label }} · {{ node.failed ? '失败' : '完成' }} · {{ node.durationMs === null ? '耗时未知' : seconds(node.durationMs) }}</li></ul>
|
|
142
|
+
<h4>关键日志 / 完整日志</h4><el-input v-model="logQuery" class="history-log-search" clearable placeholder="日志关键字,如 stageLog" aria-label="日志关键字" /><pre class="history-log-output">{{ logQuery ? run.output.split('\n').filter(line => line.toLowerCase().includes(logQuery.toLowerCase())).join('\n') || '无匹配日志' : run.output }}</pre>
|
|
132
143
|
</section>
|
|
133
144
|
</div>
|
|
134
145
|
</section>
|
|
@@ -139,25 +150,54 @@ onMounted(load);
|
|
|
139
150
|
</el-select>
|
|
140
151
|
<video v-if="videoRun" :src="`${base}/${encodeURIComponent(videoRun.id)}/video?segment=${videoSegment}`" controls preload="metadata" style="width:100%;max-height:70vh" />
|
|
141
152
|
</el-dialog>
|
|
153
|
+
<template #footer><el-button type="primary" @click="emit('close')">关闭</el-button></template>
|
|
142
154
|
</el-dialog>
|
|
143
155
|
</template>
|
|
144
156
|
|
|
145
157
|
<style scoped>
|
|
146
|
-
.run-history { max-height:
|
|
147
|
-
.history-
|
|
158
|
+
:global(.run-history-dialog.el-dialog) { display: flex; flex-direction: column; max-height: calc(100dvh - 32px); overflow: hidden; padding: 0; }
|
|
159
|
+
:global(.run-history-dialog > .el-dialog__body) { flex: 1; min-height: 0; overflow: hidden; padding: 20px; }
|
|
160
|
+
.run-history { display: flex; flex-direction: column; gap: 16px; height: 100%; min-height: 0; overflow: auto; padding: 1px 8px 12px 1px; scrollbar-gutter: stable; }
|
|
161
|
+
.history-toolbar { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; padding: 12px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-control); background: var(--ui-bg-soft); box-shadow: var(--ui-shadow-inset); }
|
|
148
162
|
.history-toolbar .el-select { width: 190px; }
|
|
149
|
-
.history-
|
|
163
|
+
.history-summary { display: flex; align-items: baseline; gap: 6px; padding: 12px 16px; border: 1px solid #dbeafe; border-radius: var(--ui-radius-control); background: #eff6ff; color: var(--ui-text-secondary); font-size: 13px; }
|
|
164
|
+
.history-summary strong { color: var(--ui-primary); font-size: 20px; line-height: 1; }
|
|
165
|
+
.history-summary i { align-self: stretch; width: 1px; margin: 0 8px; background: #bfdbfe; }
|
|
166
|
+
.history-panel { padding: 16px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); box-shadow: var(--ui-shadow-card); }
|
|
167
|
+
.history-trend { display: block; box-sizing: border-box; width: 100%; height: 160px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-control); background: linear-gradient(180deg, #f8fafc, #f3faf7); box-shadow: var(--ui-shadow-inset); }
|
|
150
168
|
.history-trend text { font-size: 12px; fill: #52615b; }
|
|
151
|
-
h3 {
|
|
152
|
-
h4 {
|
|
169
|
+
h3 { margin: 0 0 12px; color: var(--ui-text); font-size: 15px; }
|
|
170
|
+
h4 { margin: 16px 0 8px; color: var(--ui-text); font-size: 14px; }
|
|
171
|
+
.history-comparison { display: grid; gap: 14px; }
|
|
172
|
+
.history-comparison__header { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
|
173
|
+
.history-comparison__header h3 { margin: 0; }
|
|
153
174
|
.history-comparison > .el-select { width: 100%; }
|
|
154
|
-
.comparison-columns { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap:
|
|
155
|
-
.comparison-
|
|
156
|
-
.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
175
|
+
.comparison-columns { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 16px; }
|
|
176
|
+
.comparison-run-card { min-width: 0; padding: 16px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-control); background: #fbfcfe; box-shadow: 0 3px 10px rgb(15 23 42 / 6%); overflow-wrap: anywhere; }
|
|
177
|
+
.comparison-run-card__header { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
|
178
|
+
.comparison-run-card__header h4 { margin: 0; }
|
|
179
|
+
.comparison-run-card__meta { margin: 6px 0 0; color: var(--ui-text-muted); font-size: 12px; }
|
|
180
|
+
.comparison-run-card__note { margin: 10px 0 0; padding: 8px 10px; border-radius: 8px; background: var(--ui-bg-soft); color: var(--ui-text-secondary); font-size: 12px; }
|
|
181
|
+
.history-frames { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; max-height: 360px; overflow: auto; padding: 2px; }
|
|
182
|
+
figure { margin: 0; overflow: hidden; border: 1px solid var(--ui-border); border-radius: 10px; background: var(--ui-surface); box-shadow: 0 2px 6px rgb(15 23 42 / 6%); }
|
|
183
|
+
figcaption { min-height: 34px; padding: 8px 10px; border-bottom: 1px solid var(--ui-border); background: var(--ui-bg-soft); color: var(--ui-text-secondary); font-size: 12px; line-height: 1.5; }
|
|
184
|
+
.el-image { display: block; width: 100%; height: 150px; }
|
|
185
|
+
.history-node-results { box-sizing: border-box; max-height: 160px; margin: 0; overflow: auto; padding: 10px 12px 10px 30px; border: 1px solid var(--ui-border); border-radius: 10px; background: var(--ui-surface); color: var(--ui-text-secondary); font-size: 12px; line-height: 1.8; }
|
|
186
|
+
.history-log-search { margin-bottom: 10px; }
|
|
187
|
+
.history-log-output { box-sizing: border-box; white-space: pre-wrap; overflow-wrap: anywhere; max-height: 280px; overflow: auto; margin: 0; padding: 12px; border: 1px solid #3c3c3c; border-radius: 6px; background: #1e1e1e; box-shadow: inset 0 1px 0 rgb(255 255 255 / 6%), inset 0 4px 12px rgb(0 0 0 / 32%), 0 4px 10px rgb(15 23 42 / 12%), 0 18px 30px -20px rgb(15 23 42 / 45%); color: #d4d4d4; font: 12px/1.6 "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; scrollbar-color: #424242 #1e1e1e; }
|
|
188
|
+
.history-log-output::-webkit-scrollbar { width: 10px; height: 10px; }
|
|
189
|
+
.history-log-output::-webkit-scrollbar-track { background: #1e1e1e; }
|
|
190
|
+
.history-log-output::-webkit-scrollbar-thumb { border: 2px solid #1e1e1e; border-radius: 6px; background: #424242; }
|
|
191
|
+
.history-log-output::-webkit-scrollbar-thumb:hover { background: #4f4f4f; }
|
|
192
|
+
.history-log-output::selection { background: #264f78; color: #fff; }
|
|
193
|
+
@media (max-width: 760px) {
|
|
194
|
+
:global(.run-history-dialog > .el-dialog__body) { padding: 14px; }
|
|
195
|
+
.run-history { padding-right: 3px; }
|
|
196
|
+
.history-toolbar .el-select { width: 100%; }
|
|
197
|
+
.history-summary { flex-wrap: wrap; }
|
|
198
|
+
.history-summary i { display: none; }
|
|
199
|
+
.history-panel { padding: 12px; }
|
|
200
|
+
.comparison-columns { grid-template-columns: minmax(0, 1fr); }
|
|
201
|
+
.history-frames { grid-template-columns: minmax(0, 1fr); }
|
|
202
|
+
}
|
|
163
203
|
</style>
|
|
@@ -118,6 +118,10 @@ export function importAppiumScript(input: unknown) {
|
|
|
118
118
|
return postJson<{ script: AppiumRecordedScript }>(`${APP_BASE}/api/appium-recorder/scripts/import`, input);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
export function importAppiumScripts(inputs: unknown[]) {
|
|
122
|
+
return postJson<{ scripts: AppiumRecordedScript[] }>(`${APP_BASE}/api/appium-recorder/scripts/import`, { files: inputs });
|
|
123
|
+
}
|
|
124
|
+
|
|
121
125
|
export async function downloadAppiumScript(id: string) {
|
|
122
126
|
const response = await fetch(`${APP_BASE}/api/appium-recorder/scripts/${encodeURIComponent(id)}/export`);
|
|
123
127
|
if (!response.ok) await readJson(response);
|
|
@@ -42,6 +42,15 @@ const visualChangeRoleLabel = computed(() => (
|
|
|
42
42
|
const visualChangeConfigDisabled = computed(() => Boolean(
|
|
43
43
|
props.disabled || visualChangeConfig.value.role === 'end',
|
|
44
44
|
));
|
|
45
|
+
const showTapCoordinates = computed(() => (
|
|
46
|
+
props.step.type === 'coordinateTap'
|
|
47
|
+
|| (['tap', 'tapIfExists'].includes(props.step.type) && props.step.fallback?.strategy === 'bounds')
|
|
48
|
+
));
|
|
49
|
+
const invalidTapCoordinates = computed(() => (
|
|
50
|
+
showTapCoordinates.value
|
|
51
|
+
&& Number(props.step.fallback?.centerX) === 0
|
|
52
|
+
&& Number(props.step.fallback?.centerY) === 0
|
|
53
|
+
));
|
|
45
54
|
|
|
46
55
|
function defaultKind(): FlowKind {
|
|
47
56
|
return defaultFlowKind(props.step);
|
|
@@ -195,15 +204,6 @@ function patchSelector(patch: Partial<AppiumSelector>) {
|
|
|
195
204
|
<el-input-number :model-value="step.pinch?.percent ?? 0.5" :disabled="disabled" :min="0.01" :max="1" :step="0.05" controls-position="right" @update:model-value="patchPinch({ percent: $event ?? 0.5 })" />
|
|
196
205
|
</el-form-item>
|
|
197
206
|
</template>
|
|
198
|
-
<div v-if="step.type === 'coordinateTap'" class="appium-flow-editor__grid">
|
|
199
|
-
<el-form-item label="坐标 X">
|
|
200
|
-
<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)" />
|
|
201
|
-
</el-form-item>
|
|
202
|
-
<el-form-item label="坐标 Y">
|
|
203
|
-
<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)" />
|
|
204
|
-
</el-form-item>
|
|
205
|
-
</div>
|
|
206
|
-
<CoordinatePointPicker v-if="step.type === 'coordinateTap'" :disabled="disabled" @select="applyTapPoint" />
|
|
207
207
|
<StageLogSettings v-if="step.type === 'log'" :step="step" :disabled="disabled" @update="patchStep" />
|
|
208
208
|
<VariableExtractionSettings v-if="step.type === 'extractVariable'" :step="step" :disabled="disabled" @update="patchStep" />
|
|
209
209
|
<ScriptParameterSettings v-if="step.type === 'runScript'" :step="step" :disabled="disabled" @update="patchStep" />
|
|
@@ -428,6 +428,48 @@ function patchSelector(patch: Partial<AppiumSelector>) {
|
|
|
428
428
|
</el-select>
|
|
429
429
|
</el-form-item>
|
|
430
430
|
</div>
|
|
431
|
+
<section v-if="showTapCoordinates" class="appium-flow-editor__fallback">
|
|
432
|
+
<div class="appium-flow-editor__grid">
|
|
433
|
+
<el-form-item :label="step.type === 'coordinateTap' ? '坐标 X' : '备用坐标 X'">
|
|
434
|
+
<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)" />
|
|
435
|
+
</el-form-item>
|
|
436
|
+
<el-form-item :label="step.type === 'coordinateTap' ? '坐标 Y' : '备用坐标 Y'">
|
|
437
|
+
<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)" />
|
|
438
|
+
</el-form-item>
|
|
439
|
+
</div>
|
|
440
|
+
<el-alert
|
|
441
|
+
v-if="invalidTapCoordinates"
|
|
442
|
+
class="appium-flow-editor__fallback-alert"
|
|
443
|
+
type="warning"
|
|
444
|
+
show-icon
|
|
445
|
+
:closable="false"
|
|
446
|
+
title="备用坐标无效;组件定位失败时无法使用坐标兜底"
|
|
447
|
+
/>
|
|
448
|
+
<CoordinatePointPicker :disabled="disabled" @select="applyTapPoint" />
|
|
449
|
+
</section>
|
|
431
450
|
</el-form>
|
|
432
451
|
</div>
|
|
433
452
|
</template>
|
|
453
|
+
|
|
454
|
+
<style scoped>
|
|
455
|
+
.appium-flow-editor__fallback {
|
|
456
|
+
display: grid;
|
|
457
|
+
gap: 10px;
|
|
458
|
+
margin-top: 8px;
|
|
459
|
+
padding-top: 12px;
|
|
460
|
+
border-top: 1px solid var(--el-border-color-lighter);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.appium-flow-editor__fallback :deep(.el-form-item) {
|
|
464
|
+
margin-bottom: 0;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
.appium-flow-editor__fallback-alert {
|
|
468
|
+
padding: 8px 12px;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.appium-flow-editor__fallback-alert :deep(.el-alert__title) {
|
|
472
|
+
font-size: 13px;
|
|
473
|
+
line-height: 20px;
|
|
474
|
+
}
|
|
475
|
+
</style>
|
|
@@ -367,6 +367,7 @@ function updateStep(index: number, step: AppiumRecordedStep) {
|
|
|
367
367
|
class="appium-flow-dialog"
|
|
368
368
|
align-center
|
|
369
369
|
append-to-body
|
|
370
|
+
draggable
|
|
370
371
|
>
|
|
371
372
|
<div class="appium-flow-toolbar appium-flow-dialog__toolbar">
|
|
372
373
|
<FlowNodeSearch v-model:query="searchQuery" :results="searchResults" :selected-id="searchTargetId" @select="focusSearchResult" />
|
|
@@ -11,21 +11,55 @@ function update(index: number, patch: Partial<ScriptReturn>) {
|
|
|
11
11
|
</script>
|
|
12
12
|
|
|
13
13
|
<template>
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
<div class="script-parameter-settings">
|
|
15
|
+
<section class="script-parameter-settings__section">
|
|
16
|
+
<div class="script-parameter-settings__heading">
|
|
17
|
+
<strong>传入子脚本参数</strong>
|
|
18
|
+
<span>同名参数会覆盖子脚本中的变量值</span>
|
|
19
|
+
</div>
|
|
20
|
+
<VariableTable layout="cards" empty-text="暂无传入参数" :model-value="step.parameters || []" :disabled="disabled" @update:model-value="emit('update', { parameters: $event })" />
|
|
21
|
+
</section>
|
|
22
|
+
<section class="script-parameter-settings__section">
|
|
23
|
+
<div class="script-parameter-settings__heading">
|
|
24
|
+
<strong>返回值映射</strong>
|
|
25
|
+
<span>将子脚本执行后的变量保存到当前脚本</span>
|
|
26
|
+
</div>
|
|
27
|
+
<div class="return-mappings">
|
|
17
28
|
<div v-for="(item, index) in step.returns || []" :key="index" class="return-row">
|
|
18
|
-
<el-input :model-value="item.name" placeholder="
|
|
19
|
-
<span>→</span>
|
|
20
|
-
<el-input :model-value="item.target" placeholder="
|
|
29
|
+
<label><span>子脚本变量</span><el-input :model-value="item.name" placeholder="例如:result" aria-label="子脚本返回变量" :disabled="disabled" @update:model-value="update(index, { name: String($event) })" /></label>
|
|
30
|
+
<span class="return-row__arrow">→</span>
|
|
31
|
+
<label><span>当前脚本变量</span><el-input :model-value="item.target" placeholder="例如:deviceResult" aria-label="返回目标变量" :disabled="disabled" @update:model-value="update(index, { target: String($event) })" /></label>
|
|
21
32
|
<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
33
|
</div>
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
34
|
+
<div v-if="!(step.returns || []).length" class="return-mappings__empty">
|
|
35
|
+
<span>无需获取子脚本返回值</span>
|
|
36
|
+
<small>需要继续使用子脚本产生的变量时再添加映射</small>
|
|
37
|
+
</div>
|
|
38
|
+
<el-button class="return-mappings__add" :icon="Plus" :disabled="disabled" size="small" @click="emit('update', { returns: [...(step.returns || []), { name: '', target: '' }] })">新增返回映射</el-button>
|
|
39
|
+
</div>
|
|
40
|
+
</section>
|
|
41
|
+
</div>
|
|
26
42
|
</template>
|
|
27
43
|
|
|
28
44
|
<style scoped>
|
|
45
|
+
.script-parameter-settings { container-type: inline-size; display: grid; width: 100%; gap: 18px; }
|
|
46
|
+
.script-parameter-settings__section { min-width: 0; }
|
|
47
|
+
.script-parameter-settings__section + .script-parameter-settings__section { padding-top: 18px; border-top: 1px solid var(--ui-border); }
|
|
48
|
+
.script-parameter-settings__heading { display: grid; gap: 4px; margin-bottom: 10px; }
|
|
49
|
+
.script-parameter-settings__heading strong { color: var(--ui-text); font-size: 14px; font-weight: 600; }
|
|
50
|
+
.script-parameter-settings__heading span { color: var(--ui-text-muted); font-size: 12px; line-height: 1.5; }
|
|
29
51
|
.return-mappings { width: 100%; }
|
|
30
|
-
.return-row { display: grid; grid-template-columns: minmax(0, 1fr)
|
|
52
|
+
.return-row { display: grid; grid-template-columns: minmax(0, 1fr) 18px minmax(0, 1fr) 28px; gap: 8px; align-items: end; margin-bottom: 10px; padding: 12px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-control); background: var(--ui-surface); }
|
|
53
|
+
.return-row label { display: grid; min-width: 0; gap: 6px; }
|
|
54
|
+
.return-row label > span { color: var(--ui-text-secondary); font-size: 12px; line-height: 1; }
|
|
55
|
+
.return-row__arrow { align-self: end; padding-bottom: 7px; color: var(--ui-text-muted); text-align: center; }
|
|
56
|
+
.return-mappings__empty { display: grid; place-items: center; gap: 7px; min-height: 76px; padding: 14px; border: 1px dashed var(--ui-border-strong); border-radius: var(--ui-radius-control); background: var(--ui-bg-soft); color: var(--ui-text-secondary); text-align: center; }
|
|
57
|
+
.return-mappings__empty small { color: var(--ui-text-muted); font-size: 12px; }
|
|
58
|
+
.return-mappings__add { margin-top: 10px; }
|
|
59
|
+
@container (max-width: 390px) {
|
|
60
|
+
.return-row { grid-template-columns: minmax(0, 1fr) 28px; }
|
|
61
|
+
.return-row label { grid-column: 1; }
|
|
62
|
+
.return-row__arrow { display: none; }
|
|
63
|
+
.return-row > .el-button { grid-column: 2; grid-row: 1 / span 2; align-self: center; }
|
|
64
|
+
}
|
|
31
65
|
</style>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { Delete, Plus, CopyDocument } from '@element-plus/icons-vue';
|
|
3
3
|
import { ElMessage } from 'element-plus';
|
|
4
4
|
import { sensitiveVariableName, type TestVariable } from '../variables';
|
|
5
|
-
const props = defineProps<{ modelValue: TestVariable[]; disabled?: boolean }>();
|
|
5
|
+
const props = defineProps<{ modelValue: TestVariable[]; disabled?: boolean; layout?: 'table' | 'cards'; emptyText?: string }>();
|
|
6
6
|
const emit = defineEmits<{ 'update:modelValue': [TestVariable[]] }>();
|
|
7
7
|
function update(index: number, patch: Partial<TestVariable>) {
|
|
8
8
|
emit('update:modelValue', props.modelValue.map((item, i) => i === index ? { ...item, ...patch } : item));
|
|
@@ -14,8 +14,34 @@ async function copy(name: string) {
|
|
|
14
14
|
</script>
|
|
15
15
|
|
|
16
16
|
<template>
|
|
17
|
-
<div class="variable-table">
|
|
18
|
-
<
|
|
17
|
+
<div class="variable-table" :class="{ 'variable-table--cards': layout === 'cards' }">
|
|
18
|
+
<template v-if="layout === 'cards'">
|
|
19
|
+
<div v-if="modelValue.length" class="variable-table__cards">
|
|
20
|
+
<div v-for="(row, index) in modelValue" :key="index" class="variable-table__card">
|
|
21
|
+
<label class="variable-table__field">
|
|
22
|
+
<span>变量名</span>
|
|
23
|
+
<el-input :model-value="row.name" :disabled="disabled" placeholder="例如:account" aria-label="变量名" @update:model-value="update(index, { name: String($event) })" />
|
|
24
|
+
</label>
|
|
25
|
+
<label class="variable-table__field">
|
|
26
|
+
<span>传入值</span>
|
|
27
|
+
<el-input :model-value="row.value" :type="row.sensitive || sensitiveVariableName(row.name) ? 'password' : 'text'" :show-password="row.sensitive || sensitiveVariableName(row.name)" :disabled="disabled" placeholder="输入固定值或变量引用" aria-label="变量值" @update:model-value="update(index, { value: String($event) })" />
|
|
28
|
+
</label>
|
|
29
|
+
<div class="variable-table__card-footer">
|
|
30
|
+
<el-checkbox :model-value="row.sensitive || sensitiveVariableName(row.name)" :disabled="disabled || sensitiveVariableName(row.name)" @update:model-value="update(index, { sensitive: Boolean($event) })">敏感值</el-checkbox>
|
|
31
|
+
<div class="variable-table__actions">
|
|
32
|
+
<el-tooltip content="复制变量引用"><el-button :icon="CopyDocument" text size="small" :disabled="!row.name" aria-label="复制变量引用" @click="copy(row.name)" /></el-tooltip>
|
|
33
|
+
<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>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
<div v-else class="variable-table__empty">
|
|
39
|
+
<span>{{ emptyText || '暂无变量' }}</span>
|
|
40
|
+
<small>子脚本将使用自身已有的变量值</small>
|
|
41
|
+
<el-button :icon="Plus" :disabled="disabled" size="small" @click="emit('update:modelValue', [{ name: '', value: '', sensitive: false }])">新增参数</el-button>
|
|
42
|
+
</div>
|
|
43
|
+
</template>
|
|
44
|
+
<el-table v-else :data="modelValue" empty-text="暂无变量" size="small">
|
|
19
45
|
<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
46
|
<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
47
|
<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>
|
|
@@ -24,8 +50,8 @@ async function copy(name: string) {
|
|
|
24
50
|
<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
51
|
</div></template></el-table-column>
|
|
26
52
|
</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 }])"
|
|
53
|
+
<div v-if="layout !== 'cards' || modelValue.length" class="variable-table__footer">
|
|
54
|
+
<el-button :icon="Plus" :disabled="disabled" size="small" @click="emit('update:modelValue', [...modelValue, { name: '', value: '', sensitive: false }])">{{ layout === 'cards' ? '新增参数' : '新增变量' }}</el-button>
|
|
29
55
|
<slot />
|
|
30
56
|
</div>
|
|
31
57
|
</div>
|
|
@@ -33,6 +59,13 @@ async function copy(name: string) {
|
|
|
33
59
|
|
|
34
60
|
<style scoped>
|
|
35
61
|
.variable-table { min-width: 0; }
|
|
62
|
+
.variable-table__cards { display: grid; gap: 10px; }
|
|
63
|
+
.variable-table__card { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.35fr); gap: 10px 12px; padding: 12px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-control); background: var(--ui-surface); }
|
|
64
|
+
.variable-table__field { display: grid; min-width: 0; gap: 6px; }
|
|
65
|
+
.variable-table__field > span { color: var(--ui-text-secondary); font-size: 12px; line-height: 1; }
|
|
66
|
+
.variable-table__card-footer { display: flex; grid-column: 1 / -1; align-items: center; justify-content: space-between; min-height: 28px; padding-top: 2px; }
|
|
67
|
+
.variable-table__empty { display: grid; place-items: center; gap: 7px; min-height: 112px; padding: 16px; border: 1px dashed var(--ui-border-strong); border-radius: var(--ui-radius-control); background: var(--ui-bg-soft); color: var(--ui-text-secondary); text-align: center; }
|
|
68
|
+
.variable-table__empty small { color: var(--ui-text-muted); font-size: 12px; }
|
|
36
69
|
.variable-table__footer { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; margin-top: 12px; }
|
|
37
70
|
.variable-table__actions { display: flex; align-items: center; gap: 2px; }
|
|
38
71
|
.variable-table__actions :deep(.el-button) { width: 26px; height: 28px; padding: 0; }
|
|
@@ -40,4 +73,10 @@ async function copy(name: string) {
|
|
|
40
73
|
.variable-table :deep(.el-table td.el-table__cell) { padding: 8px 0; }
|
|
41
74
|
.variable-table :deep(.el-table .cell) { padding: 0 8px; }
|
|
42
75
|
.variable-table :deep(.el-button + .el-button) { margin-left: 0; }
|
|
76
|
+
.variable-table--cards .variable-table__footer :deep(.el-button) { margin-left: 0; }
|
|
77
|
+
@container (max-width: 390px) {
|
|
78
|
+
.variable-table__card { grid-template-columns: minmax(0, 1fr); }
|
|
79
|
+
.variable-table__card-footer { grid-column: 1; }
|
|
80
|
+
}
|
|
81
|
+
.variable-table--cards { container-type: inline-size; }
|
|
43
82
|
</style>
|