feat: 🚀 Add a scheduled task list

This commit is contained in:
viarotel 2024-07-24 19:06:34 +08:00
parent 16f953538b
commit d72202b311
31 changed files with 780 additions and 203 deletions

View File

@ -80,6 +80,7 @@
"watchEffect": true, "watchEffect": true,
"watchPostEffect": true, "watchPostEffect": true,
"watchSyncEffect": true, "watchSyncEffect": true,
"ElMessage": true "ElMessage": true,
"ElButtonProps": true
} }
} }

View File

@ -13,10 +13,10 @@
## 特点 ## 特点
- 🏃 同步:得益于 Web 技术,将更快速的与 Scrcpy 保持同步 - 🏃 同步:得益于 Web 技术,将更快速的与 Scrcpy 保持同步
- 🤖 自动化:允许自动连接到历史设备并自动执行镜像。 - 🤖 自动化:自动连接设备、自动执行镜像、自定义脚本、定时任务
- 💡 定制化:支持对多个设备偏好进行独立配置,并且能够添加备注以及导入导出所有配置的功能 - 💡 定制化:多设备管理、独立配置、自定义备注、配置导入导出
- 🔗 反向供网:集成了 Gnirehtet 反向供网功能 - 🔗 反向供网Gnirehtet 反向供网
- 🎨 主题:支持浅色模式和深色模式,跟随系统切换 - 🎨 主题:浅色模式、深色模式、跟随系统切换
- 😎 轻巧度:本机支持,仅显示设备屏幕 - 😎 轻巧度:本机支持,仅显示设备屏幕
- ⚡️ 性能30~120 帧每秒,取决于设备 - ⚡️ 性能30~120 帧每秒,取决于设备
- 🌟 质量1920×1080 或更高 - 🌟 质量1920×1080 或更高
@ -78,6 +78,7 @@ Windows 及 Linux 端内部集成了 Gnirehtet 用于提供 PC 到安卓设
- 批量安装应用 - 批量安装应用
- 批量文件管理 - 批量文件管理
- 批量执行脚本 - 批量执行脚本
- 批量定时任务
### 控制模式 ### 控制模式
@ -101,6 +102,7 @@ Windows 及 Linux 端内部集成了 Gnirehtet 用于提供 PC 到安卓设
- 安装应用 - 安装应用
- 文件管理 - 文件管理
- 执行脚本 - 执行脚本
- 定时任务
- 反向供网Gnirehtet - 反向供网Gnirehtet
- 多屏协同 - 多屏协同
@ -174,12 +176,12 @@ Windows 及 Linux 端内部集成了 Gnirehtet 用于提供 PC 到安卓设
### 输入控制 ### 输入控制
- 鼠标模式 - 鼠标模式
- 鼠标绑定
- 键盘模式 - 键盘模式
- 键盘注入方式 - 键盘注入方式
### 摄像控制 ### 摄像控制
- 启用摄像
- 摄像源 - 摄像源
- 摄像尺寸 - 摄像尺寸
- 摄像比例 - 摄像比例
@ -189,29 +191,31 @@ Windows 及 Linux 端内部集成了 Gnirehtet 用于提供 PC 到安卓设
> 优先级从高到低 > 优先级从高到低
1. 用户界面进行优化,制作合适的 Logo 1. 更好的标志
2. 内置的软件更新功能 ✅ 2. 软件更新功能 ✅
3. 录制和保存音视频 ✅ 3. 录制和保存音视频 ✅
4. 添加设备快捷交互控制栏 ✅ 4. 设备快捷交互控制栏 ✅
5. 支持自定义 Adb 及 Scrcpy 依赖 ✅ 5. 自定义 Adb 及 Scrcpy 依赖 ✅
6. 支持自定义设备名称,以及偏好设置的导出及导入 ✅ 6. 自定义设备名称 ✅
7. 定制化,支持对单个设备进行独立配置 ✅ 7. 偏好设置的导出及导入 ✅
8. 添加 macOS 及 linux 操作系统的支持 ✅ 8. 对单个设备进行独立配置 ✅
9. 支持国际化 ✅ 9. 添加 macOS 及 linux 操作系统的支持 ✅
10. 对深色模式的支持 ✅ 10. 国际化 ✅
11. 添加 Gnirehtet 反向供网功能 ✅ 11. 深色模式 ✅
12. 添加新的相机镜像相关功能 ✅ 12. 反向供网Gnirehtet
13. 更好的多屏协同 ✅ 13. 相机镜像 ✅
14. 设备交互栏添加更多功能:文件推送、旋转屏幕、音频控制等功能 ✅ 14. 多屏协同 ✅
15. 支持批量连接历史设备功能 ✅ 15. 文件推送、旋转屏幕、音频控制 ✅
16. 支持使用内置终端执行自定义命令 ✅ 16. 批量连接历史设备 ✅
17. 支持设备自动执行镜像 ✅ 17. 内置终端 ✅
18. 支持灵活启动镜像 ✅ 18. 自动执行镜像 ✅
19. 支持常用批量功能 ✅ 19. 灵活启动镜像 ✅
20. 支持对设备进行分组 🚧 20. 批量处理 ✅
21. 添加文件传输助手功能 🚧 21. 定时任务 ✅
22. 支持通过界面从设备下载选中的文件 🚧 22. 对设备进行分组 🚧
23. 添加对游戏的增强功能,如游戏键位映射 🚧 23. 文件传输助手 🚧
24. 通过界面管理设备文件 🚧
25. 游戏键位映射 🚧
## 常见问题 ## 常见问题

View File

@ -13,10 +13,10 @@
## Features ## Features
- 🏃 Synchronous: Benefit from web technologies to synchronize with Scrcpy faster - 🏃 Synchronous: Benefit from web technologies to synchronize with Scrcpy faster
- 🤖 Automation: Enables automatic connection to historical devices and automatic execution of mirror. - 🤖 Automation: Auto-connect devices, auto-execute images, custom scripts, scheduled tasks
- 💡 Customizable: Support independent configuration for multiple devices and ability to add notes and import/export all configurations - 💡 Customization: Multi-device management, independent configurations, custom notes, config import/export
- 🎨 Theme: Supports light mode and dark mode, system-wide switching - 🔗 Reverse tethering: Gnirehtet reverse tethering
- 🔗 Gnirehtet: Integrated Gnirehtet's reverse tethering functionality - 🎨 Themes: Light mode, dark mode, system-based switching
- 😎 Lightweight: Native support, only display device screen - 😎 Lightweight: Native support, only display device screen
- ⚡️ Performance: 30-120 fps depending on device - ⚡️ Performance: 30-120 fps depending on device
- 🌟 Quality: 1920×1080 or higher - 🌟 Quality: 1920×1080 or higher
@ -76,6 +76,7 @@ Refer to [scrcpy/doc/shortcuts](https://github.com/Genymobile/scrcpy/blob/master
- Batch Installation Application - Batch Installation Application
- Batch File Management - Batch File Management
- Batch Execution Script - Batch Execution Script
- Batch Scheduled Task
### Control Model ### Control Model
@ -99,6 +100,7 @@ Refer to [scrcpy/doc/shortcuts](https://github.com/Genymobile/scrcpy/blob/master
- Install APP - Install APP
- File Manager - File Manager
- Execution Script - Execution Script
- Scheduled Task
- Gnirehtet - Gnirehtet
- Mirror Group - Mirror Group
@ -172,12 +174,12 @@ Refer to [scrcpy/doc/shortcuts](https://github.com/Genymobile/scrcpy/blob/master
### Input Control ### Input Control
- Mouse mode - Mouse mode
- Mouse binding
- Keyboard mode - Keyboard mode
- Keyboard injection method - Keyboard injection method
### Camera Control ### Camera Control
- Enable camera
- Camera source - Camera source
- Camera resolution - Camera resolution
- Camera aspect ratio - Camera aspect ratio
@ -187,29 +189,31 @@ Refer to [scrcpy/doc/shortcuts](https://github.com/Genymobile/scrcpy/blob/master
> Priority from high to low: > Priority from high to low:
1. Optimize user interface, design a suitable logo ✅ 1. Improved logo ✅
2. Built-in software update function 2. Software update feature
3. Record and save audio/video ✅ 3. Record and save audio/video ✅
4. Add device quick interaction control bar ✅ 4. Device quick interaction control bar ✅
5. Support customization of Adb and Scrcpy dependencies ✅ 5. Custom Adb and Scrcpy dependencies ✅
6. Support custom device name, and import/export of preference settings ✅ 6. Custom device names ✅
7. Customization, support independent configuration for individual devices ✅ 7. Export and import preferences ✅
8. Add support for macOS and linux operating systems ✅ 8. Individual device configuration ✅
9. Support internationalization ✅ 9. macOS and Linux support ✅
10. Support for dark mode ✅ 10. Internationalization ✅
11. Add Gnirehtet reverse network function ✅ 11. Dark mode ✅
12. Add new camera mirror related features ✅ 12. Reverse tethering (Gnirehtet) ✅
13. Better multi -screen collaboration ✅ 13. Camera mirroring ✅
14. Add more features to device interaction bar: file push, screen rotation, audio control etc ✅ 14. Multi-screen collaboration ✅
15. Support bulk connecting to historical devices ✅ 15. File push, screen rotation, audio control ✅
16. Support to use built-in terminals to execute custom commands ✅ 16. Batch connect historical devices ✅
17. Support automatic execution of mirror on devices ✅ 17. Built-in terminal ✅
18. Support for custom startup mirroring ✅ 18. Auto-execute mirroring ✅
19. Support common batch processing function ✅ 19. Flexible mirroring launch ✅
20. Support the device to group 🚧 20. Batch processing ✅
21. Add file transmission assistant function 🚧 21. Scheduled tasks ✅
22. Support GUI-based selective file downloads from devices 🚧 22. Device grouping 🚧
23. Add game enhancement features such as game keyboard mapping 🚧 23. File transfer assistant 🚧
24. Manage device files via interface 🚧
25. Game key mapping 🚧
## FAQ ## FAQ

1
auto-imports.d.ts vendored
View File

@ -6,7 +6,6 @@
export {} export {}
declare global { declare global {
const EffectScope: typeof import('vue')['EffectScope'] const EffectScope: typeof import('vue')['EffectScope']
const ElMessage: typeof import('element-plus/es')['ElMessage']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate'] const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const computed: typeof import('vue')['computed'] const computed: typeof import('vue')['computed']
const createApp: typeof import('vue')['createApp'] const createApp: typeof import('vue')['createApp']

3
components.d.ts vendored
View File

@ -9,7 +9,6 @@ declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
ElAutocomplete: typeof import('element-plus/es')['ElAutocomplete'] ElAutocomplete: typeof import('element-plus/es')['ElAutocomplete']
ElButton: typeof import('element-plus/es')['ElButton'] ElButton: typeof import('element-plus/es')['ElButton']
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
ElCol: typeof import('element-plus/es')['ElCol'] ElCol: typeof import('element-plus/es')['ElCol']
ElCollapse: typeof import('element-plus/es')['ElCollapse'] ElCollapse: typeof import('element-plus/es')['ElCollapse']
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem'] ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
@ -23,12 +22,12 @@ declare module 'vue' {
ElForm: typeof import('element-plus/es')['ElForm'] ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem'] ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElIcon: typeof import('element-plus/es')['ElIcon'] ElIcon: typeof import('element-plus/es')['ElIcon']
ElIconSearch: typeof import('@element-plus/icons-vue')['Search']
ElInput: typeof import('element-plus/es')['ElInput'] ElInput: typeof import('element-plus/es')['ElInput']
ElLink: typeof import('element-plus/es')['ElLink'] ElLink: typeof import('element-plus/es')['ElLink']
ElOption: typeof import('element-plus/es')['ElOption'] ElOption: typeof import('element-plus/es')['ElOption']
ElPopover: typeof import('element-plus/es')['ElPopover'] ElPopover: typeof import('element-plus/es')['ElPopover']
ElRadio: typeof import('element-plus/es')['ElRadio'] ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElRow: typeof import('element-plus/es')['ElRow'] ElRow: typeof import('element-plus/es')['ElRow']
ElSelect: typeof import('element-plus/es')['ElSelect'] ElSelect: typeof import('element-plus/es')['ElSelect']

View File

@ -24,6 +24,7 @@
"dependencies": { "dependencies": {
"electron-in-page-search": "^1.3.2", "electron-in-page-search": "^1.3.2",
"nanoid": "^5.0.7", "nanoid": "^5.0.7",
"pinia-plugin-persistedstate": "^3.2.1",
"vue": "^3.4.26" "vue": "^3.4.26"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,8 +1,8 @@
<template> <template>
<el-dialog <el-dialog
v-model="visible" v-model="visible"
title="定时任务" :title="$t('device.task.name')"
width="60%" width="70%"
class="el-dialog-beautify" class="el-dialog-beautify"
append-to-body append-to-body
destroy-on-close destroy-on-close
@ -12,13 +12,17 @@
ref="formRef" ref="formRef"
:model="model" :model="model"
:rules="rules" :rules="rules"
label-width="120px" label-width="180px"
class="!pr-[120px] !pt-4" class="!pr-24 !pt-4"
>
<ele-form-item-col
:label="$t('device.task.type')"
:span="24"
prop="taskType"
> >
<ele-form-item-col label="任务类型" :span="24" prop="taskType">
<el-select <el-select
v-model="model.taskType" v-model="model.taskType"
placeholder="请选择任务类型" :placeholder="$t('common.select.please')"
clearable clearable
filterable filterable
@change="onTaskChange" @change="onTaskChange"
@ -32,21 +36,25 @@
</el-option> </el-option>
</el-select> </el-select>
</ele-form-item-col> </ele-form-item-col>
<ele-form-item-col label="执行频率" :span="24" prop="timerType"> <ele-form-item-col
:label="$t('device.task.frequency')"
:span="24"
prop="timerType"
>
<el-radio-group v-model="model.timerType"> <el-radio-group v-model="model.timerType">
<el-radio <el-radio
v-for="(item, index) of timerModel" v-for="(item, index) of timerModel"
:key="index" :key="index"
:value="item.value" :value="item.value"
> >
{{ item.label }} {{ $t(item.label) }}
</el-radio> </el-radio>
</el-radio-group> </el-radio-group>
</ele-form-item-col> </ele-form-item-col>
<ele-form-item-col <ele-form-item-col
v-if="['timeout'].includes(model.timerType)" v-if="['timeout'].includes(model.timerType)"
label="执行时间" :label="$t('device.task.timeout')"
:span="24" :span="24"
prop="timeout" prop="timeout"
> >
@ -61,7 +69,7 @@
<ele-form-item-col <ele-form-item-col
v-if="['interval'].includes(model.timerType)" v-if="['interval'].includes(model.timerType)"
label="重复规则" :label="$t('device.task.interval')"
:span="24" :span="24"
prop="interval" prop="interval"
> >
@ -74,14 +82,14 @@
<template #append> <template #append>
<el-select <el-select
v-model="model.intervalType" v-model="model.intervalType"
placeholder="请选择时间单位" :placeholder="$t('common.select.please')"
filterable filterable
class="!w-24" class="!w-36"
> >
<el-option <el-option
v-for="(item, index) of intervalModel" v-for="(item, index) of intervalModel"
:key="index" :key="index"
:label="item.label" :label="$t(item.label)"
:value="item.value" :value="item.value"
/> />
</el-select> </el-select>
@ -91,7 +99,7 @@
<ele-form-item-col <ele-form-item-col
v-if="['install'].includes(model.taskType)" v-if="['install'].includes(model.taskType)"
label="选择应用" :label="$t('device.task.extra.app')"
:span="24" :span="24"
prop="extra" prop="extra"
> >
@ -112,7 +120,7 @@
<ele-form-item-col <ele-form-item-col
v-if="['shell'].includes(model.taskType)" v-if="['shell'].includes(model.taskType)"
label="选择脚本" :label="$t('device.task.extra.shell')"
:span="24" :span="24"
prop="extra" prop="extra"
> >
@ -130,14 +138,20 @@
}" }"
/> />
</ele-form-item-col> </ele-form-item-col>
<ele-form-item-col :span="24" label="">
<div class="text-red-200 hover:text-red-500 transition-colors">
{{ $t('device.task.tips') }}
</div>
</ele-form-item-col>
</ele-form-row> </ele-form-row>
<template #footer> <template #footer>
<el-button @click="close"> <el-button @click="close">
取消 {{ $t('common.cancel') }}
</el-button> </el-button>
<el-button :loading type="primary" @click="submit"> <el-button :loading type="primary" @click="submit">
确定 {{ $t('common.confirm') }}
</el-button> </el-button>
</template> </template>
</el-dialog> </el-dialog>
@ -173,14 +187,16 @@ const model = ref({
const rules = computed(() => const rules = computed(() =>
Object.keys(model.value).reduce((obj, item) => { Object.keys(model.value).reduce((obj, item) => {
obj[item] = [{ required: true, message: '该选项不能为空', trigger: 'blur' }] obj[item] = [
{ required: true, message: window.t('common.required'), trigger: 'blur' },
]
if (item === 'timeout') { if (item === 'timeout') {
obj[item].push({ obj[item].push({
trigger: 'blur', trigger: 'blur',
validator: (rule, value, callback) => { validator: (rule, value, callback) => {
if (value.getTime() <= Date.now()) { if (value.getTime() <= Date.now()) {
callback(new Error('不能小于当前时间')) callback(new Error(window.t('device.task.timeout.tips')))
} }
else { else {
callback() callback()

View File

@ -16,7 +16,6 @@
> >
<template #default="{ loading = false } = {}"> <template #default="{ loading = false } = {}">
<el-button <el-button
type="primary"
plain plain
:title="$t(item.tips || item.label)" :title="$t(item.tips || item.label)"
:loading="loading" :loading="loading"
@ -74,7 +73,7 @@ const actionModel = [
component: Shell, component: Shell,
}, },
{ {
label: 'device.control.task.name', label: 'device.task.name',
elIcon: 'Clock', elIcon: 'Clock',
component: Tasks, component: Tasks,
}, },

View File

@ -160,7 +160,7 @@ export default {
tips: 'device.control.shell.tips', tips: 'device.control.shell.tips',
}, },
{ {
label: 'device.control.task.name', label: 'device.task.name',
elIcon: 'Clock', elIcon: 'Clock',
component: 'Tasks', component: 'Tasks',
}, },

View File

@ -7,7 +7,7 @@
:icon="loading ? '' : 'Monitor'" :icon="loading ? '' : 'Monitor'"
@click="handleClick(row)" @click="handleClick(row)"
> >
{{ loading ? $t('common.progress') : $t('device.mirror.start') }} {{ loading ? $t('common.starting') : $t('device.mirror.start') }}
</el-button> </el-button>
</template> </template>

View File

@ -25,7 +25,7 @@
<el-icon class="is-loading"> <el-icon class="is-loading">
<Loading /> <Loading />
</el-icon> </el-icon>
{{ $t('common.progress') }} {{ $t('common.starting') }}
</template> </template>
<template v-else> <template v-else>
{{ $t(item.label) }} {{ $t(item.label) }}

View File

@ -172,7 +172,11 @@ export default {
handleSave() { handleSave() {
this.preferenceStore.setData(this.preferenceData) this.preferenceStore.setData(this.preferenceData)
this.$message.success(this.$t('preferences.config.save.placeholder')) this.$message({
message: this.$t('preferences.config.save.placeholder'),
type: 'success',
grouping: true,
})
}, },
}, },
} }

View File

@ -0,0 +1,201 @@
<template>
<el-dialog
v-model="visible"
:title="$t('device.task.list')"
width="98%"
class="el-dialog-beautify"
append-to-body
destroy-on-close
@closed="onClosed"
>
<div class="flex items-center justify-center absolute top-4 left-center">
<el-radio-group
v-model="taskStatus"
size="small"
@change="onTaskStatusChange"
>
<el-radio-button value="progress" :label="$t('common.progress')" />
<el-radio-button value="finished" :label="$t('common.finished')" />
</el-radio-group>
</div>
<div v-loading="loading" class="mt-4">
<el-table :data="tableData" stripe>
<template #empty>
<el-empty></el-empty>
</template>
<el-table-column
v-slot="{ row }"
prop="taskType"
:label="$t('device.task.type')"
align="center"
>
{{ $t(getDictLabel(taskModel, row.taskType)) }}
</el-table-column>
<el-table-column
v-slot="{ row }"
prop="timerType"
:label="$t('device.task.frequency')"
align="center"
>
{{ $t(getDictLabel('timerType', row.timerType)) }}
</el-table-column>
<el-table-column
v-slot="{ row }"
:label="$t('device.task.timeout')"
align="center"
>
{{ row.formatTimeout }}
</el-table-column>
<el-table-column
v-slot="{ row }"
:label="$t('device.task.interval')"
align="center"
>
<span v-if="['timeout'].includes(row.timerType)" class="">
{{ $t('device.task.noRepeat') }}
</span>
<span v-if="['interval'].includes(row.timerType)" class="">
{{ row.interval }}
{{ $t(getDictLabel('timeUnit', row.intervalType)) }}
</span>
</el-table-column>
<el-table-column
v-slot="{ row }"
prop="devices"
:label="$t('device.task.devices')"
align="center"
>
<EleTagCollapse
effect="light"
borderless
:value="row.devices"
:label="(item) => item.$remark || `${item.$name} (${item.id})`"
class="justify-center"
/>
</el-table-column>
<el-table-column
v-slot="{ row }"
:label="$t('device.control.name')"
align="center"
>
<EleTooltipButton
v-if="['progress'].includes(taskStatus)"
text
type="danger"
effect="light"
:content="$t('common.stop')"
icon="CircleClose"
circle
@click="handleStop(row)"
>
</EleTooltipButton>
<EleTooltipButton
v-if="['finished'].includes(taskStatus)"
text
type="primary"
effect="light"
:content="$t('device.task.restart')"
icon="RefreshLeft"
circle
@click="handleReStart(row)"
>
</EleTooltipButton>
<EleTooltipButton
text
type="info"
effect="light"
:content="$t('common.remove')"
icon="Remove"
circle
@click="handleRemove(row)"
>
</EleTooltipButton>
</el-table-column>
</el-table>
</div>
<template #footer>
<div class="h-4"></div>
</template>
</el-dialog>
</template>
<script setup>
import { ElMessage } from 'element-plus'
import {
timeUnit as intervalModel,
timerType as timerModel,
} from '$/dicts/index.js'
import { useTaskStore } from '$/store/index.js'
import { sleep } from '$/utils'
import { getDictLabel } from '$/dicts/helper'
const taskStore = useTaskStore()
const visible = ref(false)
const loading = ref(false)
const taskModel = computed(() => taskStore.model)
const taskStatus = ref('progress')
const tableData = computed(() => {
const value = taskStore.list.filter(
item => item.taskStatus === taskStatus.value,
)
return value
})
async function open(args) {
visible.value = true
}
function close() {
visible.value = false
}
async function onClosed() {
taskStatus.value = 'progress'
}
async function onTaskStatusChange() {
loading.value = true
await sleep()
loading.value = false
}
function handleStop(row) {
taskStore.stop(row)
}
function handleReStart(row) {
taskStatus.value = 'progress'
taskStore.restart(row)
}
function handleRemove(row) {
taskStore.remove(row)
}
defineExpose({
open,
close,
})
</script>
<style></style>

View File

@ -0,0 +1,19 @@
<template>
<div class="" @click="handleClick">
<slot />
<TaskListDialog ref="taskListDialogRef" />
</div>
</template>
<script setup>
import TaskListDialog from './components/TaskListDialog/index.vue'
const taskListDialogRef = ref(null)
function handleClick() {
taskListDialogRef.value.open()
}
</script>
<style lang="postcss"></style>

View File

@ -2,8 +2,8 @@
<div class="flex items-center space-x-4 relative z-10"> <div class="flex items-center space-x-4 relative z-10">
<component <component
:is="item.component || 'div'" :is="item.component || 'div'"
v-for="(item, index) in actionModel" v-for="item in actionModel"
:key="index" :key="item.label"
class="flex-none" class="flex-none"
v-bind="{ v-bind="{
...(item.command ...(item.command
@ -13,12 +13,16 @@
: {}), : {}),
}" }"
> >
<template #default="{ loading = false } = {}"> <template #default="{ ...slotProps } = {}">
<el-button <EleTooltipButton
circle v-bind="{
size="small" text: true,
:title="$t(item.tips || item.label)" content: $t(item.tips || item.label),
:loading="loading" circle: true,
size: 'small',
effect: 'light',
...slotProps,
}"
> >
<template #icon> <template #icon>
<svg-icon <svg-icon
@ -30,21 +34,27 @@
<component :is="item.elIcon" /> <component :is="item.elIcon" />
</el-icon> </el-icon>
</template> </template>
</el-button> </EleTooltipButton>
</template> </template>
</component> </component>
</div> </div>
</template> </template>
<script setup> <script setup>
import Search from './components/Search/index.vue' import Task from './components/Task/index.vue'
import Restart from './components/Restart/index.vue'
import Log from './components/Log/index.vue'
import Terminal from './components/Terminal/index.vue' import Terminal from './components/Terminal/index.vue'
import Log from './components/Log/index.vue'
import Restart from './components/Restart/index.vue'
import Search from './components/Search/index.vue'
const props = defineProps({}) const props = defineProps({})
const actionModel = [ const actionModel = [
{
label: 'device.task.list',
elIcon: 'Clock',
component: Task,
},
{ {
label: 'device.terminal.name', label: 'device.terminal.name',
svgIcon: 'command', svgIcon: 'command',

21
src/dicts/helper.js Normal file
View File

@ -0,0 +1,21 @@
import * as dicts from '$/dicts/index.js'
export function getDictLabel(dict, value) {
let label = ''
if (typeof dict === 'function') {
label = dict(value)
}
else if (typeof dict === 'string') {
label = dicts?.[dict]?.find(item => item.value == value)?.label
}
else {
label = dict?.find(item => item.value == value)?.label
}
if (!label) {
return ''
}
return label
}

View File

@ -1,41 +1,41 @@
export const timerType = [ export const timerType = [
{ {
label: '单次执行', label: 'device.task.frequency.timeout',
value: 'timeout', value: 'timeout',
}, },
{ {
label: '周期重复', label: 'device.task.frequency.interval',
value: 'interval', value: 'interval',
}, },
] ]
export const timeUnit = [ export const timeUnit = [
{ {
label: '', label: 'time.unit.month',
value: 'month', value: 'month',
}, },
{ {
label: '', label: 'time.unit.week',
value: 'week', value: 'week',
}, },
{ {
label: '', label: 'time.unit.day',
value: 'day', value: 'day',
}, },
{ {
label: '小时', label: 'time.unit.hour',
value: 'hour', value: 'hour',
}, },
{ {
label: '分钟', label: 'time.unit.minute',
value: 'minute', value: 'minute',
}, },
{ {
label: '', label: 'time.unit.second',
value: 'second', value: 'second',
}, },
{ {
label: '毫秒', label: 'time.unit.millisecond',
value: 'millisecond', value: 'millisecond',
}, },
] ]

View File

@ -8,11 +8,17 @@
"common.input.placeholder": "Please input", "common.input.placeholder": "Please input",
"common.success": "Operation successful", "common.success": "Operation successful",
"common.success.batch": "Batch operation success", "common.success.batch": "Batch operation success",
"common.progress": "Starting", "common.starting": "Starting",
"common.loading": "Loading", "common.loading": "Loading",
"common.search": "Search", "common.search": "Search",
"common.batch": "Batch", "common.batch": "Batch",
"common.device": "Device", "common.device": "Device",
"common.progress": "In Progress",
"common.finished": "Finished",
"common.stop": "Stop",
"common.remove": "Remove",
"common.select.please": "Please Select",
"common.required": "This field cannot be empty",
"common.language.name": "Language", "common.language.name": "Language",
"common.language.placeholder": "Select language", "common.language.placeholder": "Select language",
@ -20,6 +26,14 @@
"common.language.zh-TW": "繁體中文", "common.language.zh-TW": "繁體中文",
"common.language.en-US": "English", "common.language.en-US": "English",
"time.unit.month": "month",
"time.unit.week": "week",
"time.unit.day": "day",
"time.unit.hour": "hour",
"time.unit.minute": "minute",
"time.unit.second": "second",
"time.unit.millisecond": "millisecond",
"close.quit": "Quit", "close.quit": "Quit",
"close.quit.cancel": "Cancel quit", "close.quit.cancel": "Cancel quit",
"close.minimize": "Minimize to tray", "close.minimize": "Minimize to tray",
@ -37,6 +51,22 @@
"device.permission.error": "Device permission error, please reconnect device and allow USB debugging", "device.permission.error": "Device permission error, please reconnect device and allow USB debugging",
"device.terminal.name": "Terminal", "device.terminal.name": "Terminal",
"device.task.name": "Scheduled Task",
"device.task.tips": " Note: Please ensure that your computer stays awake, otherwise scheduled tasks will not be executed properly.",
"device.task.list": "Scheduled Task List",
"device.task.type": "Task Type",
"device.task.frequency": "Execution Frequency",
"device.task.frequency.timeout": "Single execution",
"device.task.frequency.interval": "Periodic repetition",
"device.task.timeout": "Execution Time",
"device.task.timeout.tips": "Cannot be earlier than the current time",
"device.task.interval": "Repeat Interval",
"device.task.devices": "Involved Devices",
"device.task.noRepeat": "No Repeat",
"device.task.restart": "Execute Again",
"device.task.extra.app": "Select Application",
"device.task.extra.shell": "Select Script",
"device.wireless.name": "Wireless", "device.wireless.name": "Wireless",
"device.wireless.mode": "Wireless Mode", "device.wireless.mode": "Wireless Mode",
"device.wireless.mode.error": "Do not get the local area network connection address, please check the network", "device.wireless.mode.error": "Do not get the local area network connection address, please check the network",
@ -113,7 +143,6 @@
"device.control.shell.push.success": "Push script success", "device.control.shell.push.success": "Push script success",
"device.control.shell.enter": "Please enter the Enter key to confirm the execution of the script", "device.control.shell.enter": "Please enter the Enter key to confirm the execution of the script",
"device.control.shell.success": "Script execution successfully", "device.control.shell.success": "Script execution successfully",
"device.control.task.name": "Timing Task",
"device.control.capture": "Screenshot", "device.control.capture": "Screenshot",
"device.control.capture.progress": "Capturing screenshot for {deviceName}...", "device.control.capture.progress": "Capturing screenshot for {deviceName}...",
"device.control.capture.success.message": "Open screenshot location?", "device.control.capture.success.message": "Open screenshot location?",

View File

@ -8,11 +8,17 @@
"common.input.placeholder": "请填写", "common.input.placeholder": "请填写",
"common.success": "操作成功", "common.success": "操作成功",
"common.success.batch": "批量操作成功", "common.success.batch": "批量操作成功",
"common.progress": "启动中", "common.starting": "启动中",
"common.loading": "加载中", "common.loading": "加载中",
"common.search": "搜索", "common.search": "搜索",
"common.batch": "批量", "common.batch": "批量",
"common.device": "设备", "common.device": "设备",
"common.progress": "进行中",
"common.finished": "已结束",
"common.stop": "终止",
"common.remove": "移除",
"common.select.please": "请选择",
"common.required": "该选项不能为空",
"common.language.name": "语言", "common.language.name": "语言",
"common.language.placeholder": "选择你需要的语言", "common.language.placeholder": "选择你需要的语言",
@ -20,6 +26,14 @@
"common.language.zh-TW": "繁體中文", "common.language.zh-TW": "繁體中文",
"common.language.en-US": "English", "common.language.en-US": "English",
"time.unit.month": "月",
"time.unit.week": "周",
"time.unit.day": "天",
"time.unit.hour": "小时",
"time.unit.minute": "分钟",
"time.unit.second": "秒",
"time.unit.millisecond": "毫秒",
"close.quit": "退出", "close.quit": "退出",
"close.quit.cancel": "取消退出", "close.quit.cancel": "取消退出",
"close.minimize": "最小化到托盘", "close.minimize": "最小化到托盘",
@ -37,6 +51,22 @@
"device.permission.error": "设备可能未授权成功请重新插拔设备并点击允许USB调试", "device.permission.error": "设备可能未授权成功请重新插拔设备并点击允许USB调试",
"device.terminal.name": "终端调试", "device.terminal.name": "终端调试",
"device.task.name": "定时任务",
"device.task.tips": " 注意:请确保你的计算机保持唤醒状态,否则定时任务将无法被正常执行。",
"device.task.list": "定时任务列表",
"device.task.type": "任务类型",
"device.task.frequency": "执行频率",
"device.task.frequency.timeout": "单次执行",
"device.task.frequency.interval": "周期重复",
"device.task.timeout": "执行时间",
"device.task.timeout.tips": "不能小于当前时间",
"device.task.interval": "重复间隔",
"device.task.devices": "涉及设备",
"device.task.noRepeat": "不重复",
"device.task.restart": "再次执行",
"device.task.extra.app": "选择应用",
"device.task.extra.shell": "选择脚本",
"device.wireless.name": "无线", "device.wireless.name": "无线",
"device.wireless.mode": "无线模式", "device.wireless.mode": "无线模式",
"device.wireless.mode.error": "没有获取到局域网连接地址,请检查网络", "device.wireless.mode.error": "没有获取到局域网连接地址,请检查网络",
@ -113,7 +143,6 @@
"device.control.shell.push.success": "推送脚本成功", "device.control.shell.push.success": "推送脚本成功",
"device.control.shell.enter": "请输入回车键确认执行该脚本", "device.control.shell.enter": "请输入回车键确认执行该脚本",
"device.control.shell.success": "脚本执行成功", "device.control.shell.success": "脚本执行成功",
"device.control.task.name": "定时任务",
"device.control.capture": "截取屏幕", "device.control.capture": "截取屏幕",
"device.control.capture.progress": "正在截取 {deviceName} 的屏幕快照...", "device.control.capture.progress": "正在截取 {deviceName} 的屏幕快照...",
"device.control.capture.success.message": "是否前往截屏位置进行查看?", "device.control.capture.success.message": "是否前往截屏位置进行查看?",

View File

@ -8,11 +8,17 @@
"common.input.placeholder": "請輸入", "common.input.placeholder": "請輸入",
"common.success": "操作成功", "common.success": "操作成功",
"common.success.batch": "批量操作成功", "common.success.batch": "批量操作成功",
"common.progress": "啟動中", "common.starting": "啟動中",
"common.loading": "載入中", "common.loading": "載入中",
"common.search": "搜尋", "common.search": "搜尋",
"common.batch": "批量", "common.batch": "批量",
"common.device": "裝置", "common.device": "裝置",
"common.progress": "進行中",
"common.finished": "已結束",
"common.stop": "終止",
"common.remove": "移除",
"common.select.please": "請選擇",
"common.required": "該選項不能為空",
"common.language.name": "語言", "common.language.name": "語言",
"common.language.placeholder": "選擇你要的語言", "common.language.placeholder": "選擇你要的語言",
@ -20,6 +26,14 @@
"common.language.zh-TW": "繁體中文", "common.language.zh-TW": "繁體中文",
"common.language.en-US": "English", "common.language.en-US": "English",
"time.unit.month": "月",
"time.unit.week": "週",
"time.unit.day": "天",
"time.unit.hour": "小時",
"time.unit.minute": "分鐘",
"time.unit.second": "秒",
"time.unit.millisecond": "毫秒",
"close.quit": "結束", "close.quit": "結束",
"close.quit.cancel": "取消結束", "close.quit.cancel": "取消結束",
"close.minimize": "最小化至系統工具列", "close.minimize": "最小化至系統工具列",
@ -37,6 +51,22 @@
"device.permission.error": "裝置權限錯誤,請重新連接裝置並允許 USB 偵錯", "device.permission.error": "裝置權限錯誤,請重新連接裝置並允許 USB 偵錯",
"device.terminal.name": "終端偵錯", "device.terminal.name": "終端偵錯",
"device.task.name": "定時任務",
"device.task.tips": "注意:請確保您的電腦保持唤醒状态,否則定時任務將無法正常執行。",
"device.task.list": "定時任務列表",
"device.task.type": "任務類型",
"device.task.frequency": "執行頻率",
"device.task.frequency.timeout": "單次執行",
"device.task.frequency.interval": "週期重複",
"device.task.timeout": "執行時間",
"device.task.timeout.tips": "不能小於當前時間",
"device.task.interval": "重複間隔",
"device.task.devices": "涉及設備",
"device.task.noRepeat": "不重複",
"device.task.restart": "再次執行",
"device.task.extra.app": "選擇應用",
"device.task.extra.shell": "選擇腳本",
"device.wireless.name": "無線", "device.wireless.name": "無線",
"device.wireless.mode": "無線模式", "device.wireless.mode": "無線模式",
"device.wireless.mode.error": "未取得區域網路連接位址,請檢查網路", "device.wireless.mode.error": "未取得區域網路連接位址,請檢查網路",
@ -113,7 +143,6 @@
"device.control.shell.push.success": "推送腳本成功", "device.control.shell.push.success": "推送腳本成功",
"device.control.shell.enter": "請輸入回車鍵確認執行該腳本", "device.control.shell.enter": "請輸入回車鍵確認執行該腳本",
"device.control.shell.success": "腳本執行成功", "device.control.shell.success": "腳本執行成功",
"device.control.task.name": "定時任務",
"device.control.capture": "擷取螢幕", "device.control.capture": "擷取螢幕",
"device.control.capture.progress": "正在擷取 {deviceName} 的螢幕快照...", "device.control.capture.progress": "正在擷取 {deviceName} 的螢幕快照...",
"device.control.capture.success.message": "是否前往截圖位置進行檢視?", "device.control.capture.success.message": "是否前往截圖位置進行檢視?",

View File

@ -0,0 +1,92 @@
<template>
<div class="flex items-center space-x-2">
<ElTag
v-for="(item, index) of visibleTags"
:key="index"
v-bind="$props"
:class="{
'!border-none': borderless,
}"
>
{{ showLabel(item) }}
</ElTag>
<el-dropdown v-if="collapseTags.length">
<ElTag v-bind="$props">
+ {{ collapseTags.length }}
</ElTag>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="(item, index) of collapseTags"
:key="index"
:class="{
'!border-none': borderless,
}"
>
{{ showLabel(item) }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<script setup>
import { ElTag } from 'element-plus'
import { computed } from 'vue'
const props = defineProps({
...ElTag.props,
value: {
type: Array,
default: () => [],
},
visibleNumber: {
type: Number,
default: 1,
},
label: {
type: [String, Function, Number],
default: '',
},
borderless: {
type: Boolean,
default: false,
},
})
const visibleTags = computed(() => {
const value = props.value.slice(0, props.visibleNumber)
return value
})
const collapseTags = computed(() => {
const value = props.value.slice(props.visibleNumber)
return value
})
function showLabel(item) {
if (!props.label) {
return item?.label || ''
}
let value = ''
if (['number', 'string'].includes(typeof props.label)) {
value = item[props.label]
}
else if (typeof props.label === 'function') {
value = props.label(item)
}
return value
}
</script>
<style></style>

View File

@ -0,0 +1,43 @@
<template>
<el-tag
:class="{
'!border-none': borderless,
}"
>
{{ label }}
</el-tag>
</template>
<script setup>
import { getDictLabel } from '$/dicts/helper'
const props = defineProps({
value: {
type: [Number, String],
default: '',
},
dict: {
type: [String, Array, Function],
},
i18n: {
type: Boolean,
default: true,
},
borderless: {
type: Boolean,
default: false,
},
})
const label = computed(() => {
const value = getDictLabel(props.dict, props.value)
if (props.i18n) {
return window.t(value)
}
return value
})
</script>
<style></style>

View File

@ -0,0 +1,21 @@
<template>
<el-tooltip>
<ElButton v-bind="{ ...$props }" @click="emit('click', $event)">
<slot name="icon"></slot>
<slot></slot>
</ElButton>
<slot name="content"></slot>
</el-tooltip>
</template>
<script setup>
import { ElButton } from 'element-plus'
const props = defineProps({
...ElButton.props,
})
const emit = defineEmits(['click'])
</script>
<style></style>

View File

@ -10,9 +10,12 @@ import * as ElementPlusIcons from '@element-plus/icons-vue'
import { ElLoading, ElMessage, ElMessageBox } from 'element-plus' import { ElLoading, ElMessage, ElMessageBox } from 'element-plus'
import EleIconLoading from './components/EleIconLoading/index.vue' import EleIconLoading from './expands/EleIconLoading/index.vue'
import EleFormRow from './components/EleFormRow/index.vue' import EleFormRow from './expands/EleFormRow/index.vue'
import EleFormItemCol from './components/EleFormItemCol/index.vue' import EleFormItemCol from './expands/EleFormItemCol/index.vue'
import EleTooltipButton from './expands/EleTooltipButton/index.vue'
import EleTagDict from './expands/EleTagDict/index.vue'
import EleTagCollapse from './expands/EleTagCollapse/index.vue'
export default { export default {
install(app) { install(app) {
@ -34,5 +37,8 @@ export default {
app.component('EleFormRow', EleFormRow) app.component('EleFormRow', EleFormRow)
app.component('EleFormItemCol', EleFormItemCol) app.component('EleFormItemCol', EleFormItemCol)
app.component('EleTooltipButton', EleTooltipButton)
app.component('EleTagDict', EleTagDict)
app.component('EleTagCollapse', EleTagCollapse)
}, },
} }

View File

@ -1,4 +1,5 @@
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import persistedState from 'pinia-plugin-persistedstate'
import { useDeviceStore } from './device/index.js' import { useDeviceStore } from './device/index.js'
import { usePreferenceStore } from './preference/index.js' import { usePreferenceStore } from './preference/index.js'
import { useThemeStore } from './theme/index.js' import { useThemeStore } from './theme/index.js'
@ -10,6 +11,8 @@ export default {
install(app) { install(app) {
const store = createPinia() const store = createPinia()
store.use(persistedState)
app.use(store) app.use(store)
app.config.globalProperties.$store = { app.config.globalProperties.$store = {

View File

@ -11,7 +11,9 @@ import { clearTimer, isIPWithPort, replaceIP, setTimer } from '$/utils/index.js'
dayjs.extend(duration) dayjs.extend(duration)
export const useTaskStore = defineStore('app-task', () => { export const useTaskStore = defineStore(
'app-task',
() => {
const event = useEventBus('app-task') const event = useEventBus('app-task')
const model = ref([ const model = ref([
@ -36,6 +38,8 @@ export const useTaskStore = defineStore('app-task', () => {
...form, ...form,
timerId: void 0, timerId: void 0,
id: nanoid(), id: nanoid(),
taskStatus: 'start',
formatTimeout: dayjs(form.timeout).format('YYYY-MM-DD HH:mm:ss'),
} }
event.emit(task) event.emit(task)
@ -52,7 +56,9 @@ export const useTaskStore = defineStore('app-task', () => {
value = dayjs(task.timeout).diff(dayjs()) value = dayjs(task.timeout).diff(dayjs())
} }
else if (timerType === 'interval') { else if (timerType === 'interval') {
value = dayjs.duration(task.interval, task.intervalType).asMilliseconds() value = dayjs
.duration(task.interval, task.intervalType)
.asMilliseconds()
} }
return value return value
@ -71,19 +77,42 @@ export const useTaskStore = defineStore('app-task', () => {
handler(devices, { files }) handler(devices, { files })
if (['timeout'].includes(timerType)) { if (['timeout'].includes(timerType)) {
clear(task) stop(task)
} }
}, },
timeout, timeout,
) )
Object.assign(task, {
taskStatus: 'progress',
})
} }
function clear(task) { function stop(task) {
const { timerType, timerId } = task const { timerType, timerId } = task
if (timerId) { if (timerId) {
clearTimer(timerType, timerId) clearTimer(timerType, timerId)
Object.assign(task, {
id: void 0,
taskStatus: 'finished',
})
}
}
function restart(task) {
event.emit(task)
}
function remove(task) {
stop(task)
list.value = list.value.filter(item => item.id !== task.id) list.value = list.value.filter(item => item.id !== task.id)
} }
function removeAll(tasks) {
for (let index = 0; index < tasks.length; index++) {
const item = tasks[index]
remove(item)
}
} }
function on(name, callback) { function on(name, callback) {
@ -105,5 +134,23 @@ export const useTaskStore = defineStore('app-task', () => {
return event return event
} }
return { event, on, emit, list, model, add, start, clear } return {
}) event,
on,
emit,
list,
model,
add,
start,
stop,
restart,
remove,
removeAll,
}
},
{
persist: {
paths: ['list'],
},
},
)

View File

@ -4,7 +4,7 @@ import { camelCase, cloneDeep, keyBy } from 'lodash-es'
* @desc 使用async await 进项进行延时操作 * @desc 使用async await 进项进行延时操作
* @param {*} time * @param {*} time
*/ */
export function sleep(time = 1000) { export function sleep(time = 500) {
return new Promise((resolve) => { return new Promise((resolve) => {
setTimeout(() => resolve(true), time) setTimeout(() => resolve(true), time)
}) })

View File

@ -27,7 +27,7 @@ export default defineConfig({
'inset-0': 'top-0 bottom-0 left-0 right-0', 'inset-0': 'top-0 bottom-0 left-0 right-0',
'inset-center': 'inset-center':
'top-1/2 left-1/2 transform -translate-y-1/2 -translate-x-1/2', 'top-1/2 left-1/2 transform -translate-y-1/2 -translate-x-1/2',
'inset-left-center': 'left-1/2 transform -translate-x-1/2', 'top-center': 'top-1/2 transform -translate-y-1/2',
'inset-top-center': 'top-1/2 transform -translate-y-1/2', 'left-center': 'left-1/2 transform -translate-x-1/2',
}, },
}) })