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,
"watchPostEffect": true,
"watchSyncEffect": true,
"ElMessage": true
"ElMessage": true,
"ElButtonProps": true
}
}

View File

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

View File

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

1
auto-imports.d.ts vendored
View File

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

3
components.d.ts vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -172,7 +172,11 @@ export default {
handleSave() {
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">
<component
:is="item.component || 'div'"
v-for="(item, index) in actionModel"
:key="index"
v-for="item in actionModel"
:key="item.label"
class="flex-none"
v-bind="{
...(item.command
@ -13,12 +13,16 @@
: {}),
}"
>
<template #default="{ loading = false } = {}">
<el-button
circle
size="small"
:title="$t(item.tips || item.label)"
:loading="loading"
<template #default="{ ...slotProps } = {}">
<EleTooltipButton
v-bind="{
text: true,
content: $t(item.tips || item.label),
circle: true,
size: 'small',
effect: 'light',
...slotProps,
}"
>
<template #icon>
<svg-icon
@ -30,21 +34,27 @@
<component :is="item.elIcon" />
</el-icon>
</template>
</el-button>
</EleTooltipButton>
</template>
</component>
</div>
</template>
<script setup>
import Search from './components/Search/index.vue'
import Restart from './components/Restart/index.vue'
import Log from './components/Log/index.vue'
import Task from './components/Task/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 actionModel = [
{
label: 'device.task.list',
elIcon: 'Clock',
component: Task,
},
{
label: 'device.terminal.name',
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 = [
{
label: '单次执行',
label: 'device.task.frequency.timeout',
value: 'timeout',
},
{
label: '周期重复',
label: 'device.task.frequency.interval',
value: 'interval',
},
]
export const timeUnit = [
{
label: '',
label: 'time.unit.month',
value: 'month',
},
{
label: '',
label: 'time.unit.week',
value: 'week',
},
{
label: '',
label: 'time.unit.day',
value: 'day',
},
{
label: '小时',
label: 'time.unit.hour',
value: 'hour',
},
{
label: '分钟',
label: 'time.unit.minute',
value: 'minute',
},
{
label: '',
label: 'time.unit.second',
value: 'second',
},
{
label: '毫秒',
label: 'time.unit.millisecond',
value: 'millisecond',
},
]

View File

@ -8,11 +8,17 @@
"common.input.placeholder": "Please input",
"common.success": "Operation successful",
"common.success.batch": "Batch operation success",
"common.progress": "Starting",
"common.starting": "Starting",
"common.loading": "Loading",
"common.search": "Search",
"common.batch": "Batch",
"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.placeholder": "Select language",
@ -20,6 +26,14 @@
"common.language.zh-TW": "繁體中文",
"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.cancel": "Cancel quit",
"close.minimize": "Minimize to tray",
@ -37,6 +51,22 @@
"device.permission.error": "Device permission error, please reconnect device and allow USB debugging",
"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.mode": "Wireless Mode",
"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.enter": "Please enter the Enter key to confirm the execution of the script",
"device.control.shell.success": "Script execution successfully",
"device.control.task.name": "Timing Task",
"device.control.capture": "Screenshot",
"device.control.capture.progress": "Capturing screenshot for {deviceName}...",
"device.control.capture.success.message": "Open screenshot location?",

View File

@ -8,11 +8,17 @@
"common.input.placeholder": "请填写",
"common.success": "操作成功",
"common.success.batch": "批量操作成功",
"common.progress": "启动中",
"common.starting": "启动中",
"common.loading": "加载中",
"common.search": "搜索",
"common.batch": "批量",
"common.device": "设备",
"common.progress": "进行中",
"common.finished": "已结束",
"common.stop": "终止",
"common.remove": "移除",
"common.select.please": "请选择",
"common.required": "该选项不能为空",
"common.language.name": "语言",
"common.language.placeholder": "选择你需要的语言",
@ -20,6 +26,14 @@
"common.language.zh-TW": "繁體中文",
"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.cancel": "取消退出",
"close.minimize": "最小化到托盘",
@ -37,6 +51,22 @@
"device.permission.error": "设备可能未授权成功请重新插拔设备并点击允许USB调试",
"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.mode": "无线模式",
"device.wireless.mode.error": "没有获取到局域网连接地址,请检查网络",
@ -113,7 +143,6 @@
"device.control.shell.push.success": "推送脚本成功",
"device.control.shell.enter": "请输入回车键确认执行该脚本",
"device.control.shell.success": "脚本执行成功",
"device.control.task.name": "定时任务",
"device.control.capture": "截取屏幕",
"device.control.capture.progress": "正在截取 {deviceName} 的屏幕快照...",
"device.control.capture.success.message": "是否前往截屏位置进行查看?",

View File

@ -8,11 +8,17 @@
"common.input.placeholder": "請輸入",
"common.success": "操作成功",
"common.success.batch": "批量操作成功",
"common.progress": "啟動中",
"common.starting": "啟動中",
"common.loading": "載入中",
"common.search": "搜尋",
"common.batch": "批量",
"common.device": "裝置",
"common.progress": "進行中",
"common.finished": "已結束",
"common.stop": "終止",
"common.remove": "移除",
"common.select.please": "請選擇",
"common.required": "該選項不能為空",
"common.language.name": "語言",
"common.language.placeholder": "選擇你要的語言",
@ -20,6 +26,14 @@
"common.language.zh-TW": "繁體中文",
"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.cancel": "取消結束",
"close.minimize": "最小化至系統工具列",
@ -37,6 +51,22 @@
"device.permission.error": "裝置權限錯誤,請重新連接裝置並允許 USB 偵錯",
"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.mode": "無線模式",
"device.wireless.mode.error": "未取得區域網路連接位址,請檢查網路",
@ -113,7 +143,6 @@
"device.control.shell.push.success": "推送腳本成功",
"device.control.shell.enter": "請輸入回車鍵確認執行該腳本",
"device.control.shell.success": "腳本執行成功",
"device.control.task.name": "定時任務",
"device.control.capture": "擷取螢幕",
"device.control.capture.progress": "正在擷取 {deviceName} 的螢幕快照...",
"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 EleIconLoading from './components/EleIconLoading/index.vue'
import EleFormRow from './components/EleFormRow/index.vue'
import EleFormItemCol from './components/EleFormItemCol/index.vue'
import EleIconLoading from './expands/EleIconLoading/index.vue'
import EleFormRow from './expands/EleFormRow/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 {
install(app) {
@ -34,5 +37,8 @@ export default {
app.component('EleFormRow', EleFormRow)
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 persistedState from 'pinia-plugin-persistedstate'
import { useDeviceStore } from './device/index.js'
import { usePreferenceStore } from './preference/index.js'
import { useThemeStore } from './theme/index.js'
@ -10,6 +11,8 @@ export default {
install(app) {
const store = createPinia()
store.use(persistedState)
app.use(store)
app.config.globalProperties.$store = {

View File

@ -11,99 +11,146 @@ import { clearTimer, isIPWithPort, replaceIP, setTimer } from '$/utils/index.js'
dayjs.extend(duration)
export const useTaskStore = defineStore('app-task', () => {
const event = useEventBus('app-task')
export const useTaskStore = defineStore(
'app-task',
() => {
const event = useEventBus('app-task')
const model = ref([
{
label: 'device.control.install',
value: 'install',
},
{
label: 'device.control.capture',
value: 'screenshot',
},
{
label: 'device.control.shell.name',
value: 'shell',
},
])
const list = ref([])
function add(form) {
const task = {
...form,
timerId: void 0,
id: nanoid(),
}
event.emit(task)
list.value.push(task)
}
function getTimeout(task) {
let value = 0
const { timerType } = task
if (timerType === 'timeout') {
value = dayjs(task.timeout).diff(dayjs())
}
else if (timerType === 'interval') {
value = dayjs.duration(task.interval, task.intervalType).asMilliseconds()
}
return value
}
function start({ task, handler }) {
const { timerType, devices } = task
const files = task.extra ? task.extra.split(',') : void 0
const timeout = getTimeout(task)
task.timerId = setTimer(
timerType,
() => {
handler(devices, { files })
if (['timeout'].includes(timerType)) {
clear(task)
}
const model = ref([
{
label: 'device.control.install',
value: 'install',
},
timeout,
)
}
{
label: 'device.control.capture',
value: 'screenshot',
},
{
label: 'device.control.shell.name',
value: 'shell',
},
])
function clear(task) {
const { timerType, timerId } = task
if (timerId) {
clearTimer(timerType, timerId)
list.value = list.value.filter(item => item.id !== task.id)
}
}
const list = ref([])
function on(name, callback) {
event.on((...args) => {
const [{ taskType }] = args
if (taskType !== name) {
return false
function add(form) {
const task = {
...form,
timerId: void 0,
id: nanoid(),
taskStatus: 'start',
formatTimeout: dayjs(form.timeout).format('YYYY-MM-DD HH:mm:ss'),
}
callback(...args)
})
event.emit(task)
return event
}
list.value.push(task)
}
function emit(name, args) {
event.emit({ taskType: name, ...args })
return event
}
function getTimeout(task) {
let value = 0
return { event, on, emit, list, model, add, start, clear }
})
const { timerType } = task
if (timerType === 'timeout') {
value = dayjs(task.timeout).diff(dayjs())
}
else if (timerType === 'interval') {
value = dayjs
.duration(task.interval, task.intervalType)
.asMilliseconds()
}
return value
}
function start({ task, handler }) {
const { timerType, devices } = task
const files = task.extra ? task.extra.split(',') : void 0
const timeout = getTimeout(task)
task.timerId = setTimer(
timerType,
() => {
handler(devices, { files })
if (['timeout'].includes(timerType)) {
stop(task)
}
},
timeout,
)
Object.assign(task, {
taskStatus: 'progress',
})
}
function stop(task) {
const { timerType, timerId } = task
if (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)
}
function removeAll(tasks) {
for (let index = 0; index < tasks.length; index++) {
const item = tasks[index]
remove(item)
}
}
function on(name, callback) {
event.on((...args) => {
const [{ taskType }] = args
if (taskType !== name) {
return false
}
callback(...args)
})
return event
}
function emit(name, args) {
event.emit({ taskType: name, ...args })
return event
}
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 进项进行延时操作
* @param {*} time
*/
export function sleep(time = 1000) {
export function sleep(time = 500) {
return new Promise((resolve) => {
setTimeout(() => resolve(true), time)
})

View File

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