|
@@ -21,9 +21,9 @@
|
|
|
<div class="screenshot-stage">
|
|
<div class="screenshot-stage">
|
|
|
<div v-if="imageSrc" class="screenshot-canvas" :style="canvasStyle">
|
|
<div v-if="imageSrc" class="screenshot-canvas" :style="canvasStyle">
|
|
|
<img class="screenshot-image" :src="imageSrc" alt="当前 Windows 截图" />
|
|
<img class="screenshot-image" :src="imageSrc" alt="当前 Windows 截图" />
|
|
|
- <template v-if="currentScreen?.elements">
|
|
|
|
|
|
|
+ <template v-if="locatedElements.length">
|
|
|
<button
|
|
<button
|
|
|
- v-for="element in currentScreen.elements"
|
|
|
|
|
|
|
+ v-for="element in locatedElements"
|
|
|
:key="element.id || element.element_index"
|
|
:key="element.id || element.element_index"
|
|
|
class="element-marker"
|
|
class="element-marker"
|
|
|
:style="markerStyle(element)"
|
|
:style="markerStyle(element)"
|
|
@@ -62,11 +62,16 @@
|
|
|
<el-table :data="currentScreen?.elements || []" height="420" border stripe>
|
|
<el-table :data="currentScreen?.elements || []" height="420" border stripe>
|
|
|
<el-table-column prop="element_index" label="#" width="54" />
|
|
<el-table-column prop="element_index" label="#" width="54" />
|
|
|
<el-table-column prop="name" label="名称" min-width="130" show-overflow-tooltip />
|
|
<el-table-column prop="name" label="名称" min-width="130" show-overflow-tooltip />
|
|
|
|
|
+ <el-table-column prop="approximate_location" label="大致位置" min-width="130" show-overflow-tooltip />
|
|
|
<el-table-column label="坐标" width="110">
|
|
<el-table-column label="坐标" width="110">
|
|
|
- <template #default="{ row }">{{ row.x }}, {{ row.y }}</template>
|
|
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <span v-if="row.is_located">{{ row.x }}, {{ row.y }}</span>
|
|
|
|
|
+ <el-tag v-else type="info">未定位</el-tag>
|
|
|
|
|
+ </template>
|
|
|
</el-table-column>
|
|
</el-table-column>
|
|
|
- <el-table-column label="操作" width="100" fixed="right">
|
|
|
|
|
|
|
+ <el-table-column label="操作" width="160" fixed="right">
|
|
|
<template #default="{ row }">
|
|
<template #default="{ row }">
|
|
|
|
|
+ <el-button size="small" :loading="locatingElementId === row.id" @click="locateElement(row)">找位置</el-button>
|
|
|
<el-dropdown @command="(command) => runElementMouse(row, command)">
|
|
<el-dropdown @command="(command) => runElementMouse(row, command)">
|
|
|
<el-button size="small">点击</el-button>
|
|
<el-button size="small">点击</el-button>
|
|
|
<template #dropdown>
|
|
<template #dropdown>
|
|
@@ -84,10 +89,23 @@
|
|
|
</aside>
|
|
</aside>
|
|
|
|
|
|
|
|
<el-dialog v-model="keyboardDialog" title="执行键盘操作" width="420px" @opened="focusKeyCapture">
|
|
<el-dialog v-model="keyboardDialog" title="执行键盘操作" width="420px" @opened="focusKeyCapture">
|
|
|
|
|
+ <div class="keyboard-builder">
|
|
|
|
|
+ <div class="muted">组合键</div>
|
|
|
|
|
+ <el-checkbox-group v-model="modifierKeys">
|
|
|
|
|
+ <el-checkbox-button label="win">Win</el-checkbox-button>
|
|
|
|
|
+ <el-checkbox-button label="ctrl">Ctrl</el-checkbox-button>
|
|
|
|
|
+ <el-checkbox-button label="alt">Alt</el-checkbox-button>
|
|
|
|
|
+ <el-checkbox-button label="shift">Shift</el-checkbox-button>
|
|
|
|
|
+ </el-checkbox-group>
|
|
|
|
|
+ <div class="muted">主键</div>
|
|
|
|
|
+ <el-select v-model="mainKey" filterable allow-create default-first-option placeholder="选择或输入主键,如 up、enter、a">
|
|
|
|
|
+ <el-option v-for="key in commonKeys" :key="key.value" :label="key.label" :value="key.value" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </div>
|
|
|
<div ref="keyCaptureRef" class="key-capture" tabindex="0" @keydown.prevent="captureKey">
|
|
<div ref="keyCaptureRef" class="key-capture" tabindex="0" @keydown.prevent="captureKey">
|
|
|
- <div class="muted">点击此区域后按下单键或组合键</div>
|
|
|
|
|
|
|
+ <div class="muted">也可以点击此区域捕获普通按键;Win 键建议用上方按钮选择</div>
|
|
|
<div class="key-list">
|
|
<div class="key-list">
|
|
|
- <el-tag v-for="key in capturedKeys" :key="key">{{ key }}</el-tag>
|
|
|
|
|
|
|
+ <el-tag v-for="key in finalKeyboardKeys" :key="key">{{ key }}</el-tag>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
<template #footer>
|
|
<template #footer>
|
|
@@ -128,6 +146,7 @@ const providers = ref([])
|
|
|
const models = ref([])
|
|
const models = ref([])
|
|
|
const analyzing = ref(false)
|
|
const analyzing = ref(false)
|
|
|
const screenshotLoading = ref(false)
|
|
const screenshotLoading = ref(false)
|
|
|
|
|
+const locatingElementId = ref(null)
|
|
|
const savingWorkflow = ref(false)
|
|
const savingWorkflow = ref(false)
|
|
|
const currentScreen = ref(null)
|
|
const currentScreen = ref(null)
|
|
|
const recording = ref(false)
|
|
const recording = ref(false)
|
|
@@ -137,6 +156,8 @@ const keyboardDialog = ref(false)
|
|
|
const textDialog = ref(false)
|
|
const textDialog = ref(false)
|
|
|
const programDialog = ref(false)
|
|
const programDialog = ref(false)
|
|
|
const capturedKeys = ref([])
|
|
const capturedKeys = ref([])
|
|
|
|
|
+const modifierKeys = ref([])
|
|
|
|
|
+const mainKey = ref('')
|
|
|
const keyCaptureRef = ref(null)
|
|
const keyCaptureRef = ref(null)
|
|
|
const textInput = ref('')
|
|
const textInput = ref('')
|
|
|
const quickProgram = ref('')
|
|
const quickProgram = ref('')
|
|
@@ -159,6 +180,31 @@ const imageSrc = computed(() => {
|
|
|
if (!currentScreen.value?.image_base64) return ''
|
|
if (!currentScreen.value?.image_base64) return ''
|
|
|
return `data:${currentScreen.value.mime_type || 'image/png'};base64,${currentScreen.value.image_base64}`
|
|
return `data:${currentScreen.value.mime_type || 'image/png'};base64,${currentScreen.value.image_base64}`
|
|
|
})
|
|
})
|
|
|
|
|
+const locatedElements = computed(() => (currentScreen.value?.elements || []).filter((item) => item.is_located))
|
|
|
|
|
+const finalKeyboardKeys = computed(() => {
|
|
|
|
|
+ const keys = [...modifierKeys.value]
|
|
|
|
|
+ if (mainKey.value) keys.push(normalizeKey(mainKey.value))
|
|
|
|
|
+ for (const key of capturedKeys.value) {
|
|
|
|
|
+ if (!keys.includes(key)) keys.push(key)
|
|
|
|
|
+ }
|
|
|
|
|
+ return keys
|
|
|
|
|
+})
|
|
|
|
|
+const commonKeys = [
|
|
|
|
|
+ { label: '↑ 最大化 / 上', value: 'up' },
|
|
|
|
|
+ { label: '↓ 下', value: 'down' },
|
|
|
|
|
+ { label: '← 左', value: 'left' },
|
|
|
|
|
+ { label: '→ 右', value: 'right' },
|
|
|
|
|
+ { label: 'Enter', value: 'enter' },
|
|
|
|
|
+ { label: 'Esc', value: 'escape' },
|
|
|
|
|
+ { label: 'Tab', value: 'tab' },
|
|
|
|
|
+ { label: 'Space', value: 'space' },
|
|
|
|
|
+ { label: 'Delete', value: 'delete' },
|
|
|
|
|
+ { label: 'Backspace', value: 'backspace' },
|
|
|
|
|
+ { label: 'F4', value: 'f4' },
|
|
|
|
|
+ { label: 'D', value: 'd' },
|
|
|
|
|
+ { label: 'E', value: 'e' },
|
|
|
|
|
+ { label: 'R', value: 'r' },
|
|
|
|
|
+]
|
|
|
const canvasStyle = computed(() => {
|
|
const canvasStyle = computed(() => {
|
|
|
if (!currentScreen.value?.width || !currentScreen.value?.height) return {}
|
|
if (!currentScreen.value?.width || !currentScreen.value?.height) return {}
|
|
|
return { aspectRatio: `${currentScreen.value.width} / ${currentScreen.value.height}` }
|
|
return { aspectRatio: `${currentScreen.value.width} / ${currentScreen.value.height}` }
|
|
@@ -213,6 +259,32 @@ async function analyzeScreen() {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+async function locateElement(element) {
|
|
|
|
|
+ if (!ensureAiSelected()) return
|
|
|
|
|
+ if (!currentScreen.value?.id) {
|
|
|
|
|
+ ElMessage.warning('请先分析界面后再定位元素')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ locatingElementId.value = element.id
|
|
|
|
|
+ try {
|
|
|
|
|
+ const { data } = await api.post(`/api/automation/screens/${currentScreen.value.id}/elements/${element.id}/locate`, {
|
|
|
|
|
+ provider_id: ai.provider_id,
|
|
|
|
|
+ model_id: ai.model_id,
|
|
|
|
|
+ temperature: ai.temperature,
|
|
|
|
|
+ })
|
|
|
|
|
+ if (!data.located) {
|
|
|
|
|
+ ElMessage.warning(data.ai_result?.reason || 'AI 未找到该元素')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ currentScreen.value = data.screen
|
|
|
|
|
+ ElMessage.success(`已定位:${data.element.x}, ${data.element.y}`)
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ ElMessage.error(error.response?.data?.detail || '定位元素失败')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ locatingElementId.value = null
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
async function captureScreenshot(silent = false) {
|
|
async function captureScreenshot(silent = false) {
|
|
|
screenshotLoading.value = true
|
|
screenshotLoading.value = true
|
|
|
try {
|
|
try {
|
|
@@ -254,6 +326,10 @@ function addNode(node) {
|
|
|
|
|
|
|
|
async function runElementMouse(element, mouseAction) {
|
|
async function runElementMouse(element, mouseAction) {
|
|
|
if (!ensureAiSelected()) return
|
|
if (!ensureAiSelected()) return
|
|
|
|
|
+ if (!element.is_located) {
|
|
|
|
|
+ ElMessage.warning('请先点击“找位置”定位该元素')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
try {
|
|
try {
|
|
|
const { data } = await api.post('/api/automation/actions/mouse', {
|
|
const { data } = await api.post('/api/automation/actions/mouse', {
|
|
|
...actionBase(),
|
|
...actionBase(),
|
|
@@ -276,6 +352,8 @@ async function runElementMouse(element, mouseAction) {
|
|
|
|
|
|
|
|
function openKeyboardDialog() {
|
|
function openKeyboardDialog() {
|
|
|
capturedKeys.value = []
|
|
capturedKeys.value = []
|
|
|
|
|
+ modifierKeys.value = []
|
|
|
|
|
+ mainKey.value = ''
|
|
|
keyboardDialog.value = true
|
|
keyboardDialog.value = true
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -286,18 +364,38 @@ async function focusKeyCapture() {
|
|
|
|
|
|
|
|
function captureKey(event) {
|
|
function captureKey(event) {
|
|
|
const key = normalizeKey(event.key)
|
|
const key = normalizeKey(event.key)
|
|
|
- if (!capturedKeys.value.includes(key)) capturedKeys.value.push(key)
|
|
|
|
|
|
|
+ if (['ctrl', 'alt', 'shift', 'win'].includes(key)) {
|
|
|
|
|
+ if (!modifierKeys.value.includes(key)) modifierKeys.value.push(key)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ mainKey.value = key
|
|
|
|
|
+ capturedKeys.value = []
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function normalizeKey(key) {
|
|
function normalizeKey(key) {
|
|
|
- const map = { Control: 'ctrl', Shift: 'shift', Alt: 'alt', Meta: 'win', Escape: 'esc', ' ': 'space' }
|
|
|
|
|
|
|
+ const map = {
|
|
|
|
|
+ Control: 'ctrl',
|
|
|
|
|
+ Shift: 'shift',
|
|
|
|
|
+ Alt: 'alt',
|
|
|
|
|
+ Meta: 'win',
|
|
|
|
|
+ OS: 'win',
|
|
|
|
|
+ Win: 'win',
|
|
|
|
|
+ Escape: 'escape',
|
|
|
|
|
+ ' ': 'space',
|
|
|
|
|
+ ArrowUp: 'up',
|
|
|
|
|
+ ArrowDown: 'down',
|
|
|
|
|
+ ArrowLeft: 'left',
|
|
|
|
|
+ ArrowRight: 'right',
|
|
|
|
|
+ PageUp: 'pageup',
|
|
|
|
|
+ PageDown: 'pagedown',
|
|
|
|
|
+ }
|
|
|
return map[key] || key.toLowerCase()
|
|
return map[key] || key.toLowerCase()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async function runKeyboard() {
|
|
async function runKeyboard() {
|
|
|
- if (!capturedKeys.value.length || !ensureAiSelected()) return
|
|
|
|
|
|
|
+ if (!finalKeyboardKeys.value.length || !ensureAiSelected()) return
|
|
|
try {
|
|
try {
|
|
|
- const keys = [...capturedKeys.value]
|
|
|
|
|
|
|
+ const keys = [...finalKeyboardKeys.value]
|
|
|
const { data } = await api.post('/api/automation/actions/keyboard', { ...actionBase(), keys })
|
|
const { data } = await api.post('/api/automation/actions/keyboard', { ...actionBase(), keys })
|
|
|
rememberProcesses(data.new_processes)
|
|
rememberProcesses(data.new_processes)
|
|
|
addNode({ node_type: 'keyboard', screen_id: currentScreen.value?.id || null, title: keys.join('+'), config: { keys } })
|
|
addNode({ node_type: 'keyboard', screen_id: currentScreen.value?.id || null, title: keys.join('+'), config: { keys } })
|