Browse Source
- 为CreateAdbOperationCommandHandler添加IUnitOfWork依赖和SaveChangesAsync调用 - 为UpdateAdbOperationCommandHandler添加IUnitOfWork依赖和SaveChangesAsync调用 - 为DeleteAdbOperationCommandHandler添加IUnitOfWork依赖和SaveChangesAsync调用 - 为CreateAtOperationCommandHandler添加IUnitOfWork依赖和SaveChangesAsync调用 - 为UpdateAtOperationCommandHandler添加IUnitOfWork依赖和SaveChangesAsync调用 - 为DeleteAtOperationCommandHandler添加IUnitOfWork依赖和SaveChangesAsync调用 修复DDD设计原则违反问题,确保数据被正确持久化到数据库,支持事务管理和异常处理。feature/x1-web-request
21 changed files with 3511 additions and 1552 deletions
File diff suppressed because it is too large
@ -0,0 +1,44 @@ |
|||
using Microsoft.EntityFrameworkCore.Migrations; |
|||
|
|||
#nullable disable |
|||
|
|||
namespace X1.Infrastructure.Migrations |
|||
{ |
|||
/// <inheritdoc />
|
|||
public partial class UpdateAdbOperationPathNullable : Migration |
|||
{ |
|||
/// <inheritdoc />
|
|||
protected override void Up(MigrationBuilder migrationBuilder) |
|||
{ |
|||
migrationBuilder.AlterColumn<string>( |
|||
name: "Path", |
|||
table: "tb_adboperations", |
|||
type: "character varying(500)", |
|||
maxLength: 500, |
|||
nullable: true, |
|||
comment: "命令执行时所依赖的路径(当启用绝对路径时必填)", |
|||
oldClrType: typeof(string), |
|||
oldType: "character varying(500)", |
|||
oldMaxLength: 500, |
|||
oldComment: "命令执行时所依赖的路径"); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
protected override void Down(MigrationBuilder migrationBuilder) |
|||
{ |
|||
migrationBuilder.AlterColumn<string>( |
|||
name: "Path", |
|||
table: "tb_adboperations", |
|||
type: "character varying(500)", |
|||
maxLength: 500, |
|||
nullable: false, |
|||
defaultValue: "", |
|||
comment: "命令执行时所依赖的路径", |
|||
oldClrType: typeof(string), |
|||
oldType: "character varying(500)", |
|||
oldMaxLength: 500, |
|||
oldNullable: true, |
|||
oldComment: "命令执行时所依赖的路径(当启用绝对路径时必填)"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,442 @@ |
|||
import React, { useEffect, useState } from 'react'; |
|||
import { AdbOperation, CreateAdbOperationRequest, UpdateAdbOperationRequest } from '@/services/adbOperationsService'; |
|||
import { Drawer, DrawerHeader, DrawerContent, DrawerFooter } from '@/components/ui/drawer'; |
|||
import { Button } from '@/components/ui/button'; |
|||
import { Input } from '@/components/ui/input'; |
|||
import { Label } from '@/components/ui/label'; |
|||
import { Textarea } from '@/components/ui/textarea'; |
|||
import { Checkbox } from '@/components/ui/checkbox'; |
|||
import { getTerminalDevices, TerminalDevice } from '@/services/terminalDeviceService'; |
|||
import { Plus, Trash2, X } from 'lucide-react'; |
|||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; |
|||
|
|||
interface CommandItem { |
|||
command: string; |
|||
waitTimeMs: number; |
|||
} |
|||
|
|||
interface AdbOperationDrawerProps { |
|||
open: boolean; |
|||
onOpenChange: (open: boolean) => void; |
|||
onSubmit: (data: CreateAdbOperationRequest | UpdateAdbOperationRequest) => Promise<void>; |
|||
initialData?: AdbOperation; |
|||
isEdit?: boolean; |
|||
isSubmitting?: boolean; |
|||
} |
|||
|
|||
export default function AdbOperationDrawer({ |
|||
open, |
|||
onOpenChange, |
|||
onSubmit, |
|||
initialData, |
|||
isEdit = false, |
|||
isSubmitting = false |
|||
}: AdbOperationDrawerProps) { |
|||
// 解析初始命令数据
|
|||
const parseInitialCommands = (): CommandItem[] => { |
|||
if (initialData?.command) { |
|||
try { |
|||
return JSON.parse(initialData.command); |
|||
} catch { |
|||
// 如果不是JSON格式,当作单条命令处理
|
|||
return [{ command: initialData.command, waitTimeMs: initialData.waitTimeMs || 0 }]; |
|||
} |
|||
} |
|||
return [{ command: '', waitTimeMs: 0 }]; |
|||
}; |
|||
|
|||
const [commands, setCommands] = useState<CommandItem[]>(parseInitialCommands()); |
|||
const [formData, setFormData] = useState<CreateAdbOperationRequest>({ |
|||
command: '', |
|||
description: '', |
|||
path: '', |
|||
useAbsolutePath: false, |
|||
isEnabled: true, |
|||
waitTimeMs: 0, |
|||
deviceId: '' |
|||
}); |
|||
|
|||
const [errors, setErrors] = useState<Record<string, string>>({}); |
|||
const [devices, setDevices] = useState<TerminalDevice[]>([]); |
|||
const [loadingDevices, setLoadingDevices] = useState(false); |
|||
|
|||
// 当抽屉打开时,初始化表单数据
|
|||
useEffect(() => { |
|||
if (open) { |
|||
if (initialData && isEdit) { |
|||
const parsedCommands = parseInitialCommands(); |
|||
setCommands(parsedCommands); |
|||
setFormData({ |
|||
command: initialData.command || '', |
|||
description: initialData.description || '', |
|||
path: initialData.path || '', |
|||
useAbsolutePath: initialData.useAbsolutePath ?? false, |
|||
isEnabled: initialData.isEnabled ?? true, |
|||
waitTimeMs: initialData.waitTimeMs || 0, |
|||
deviceId: initialData.deviceId || '' |
|||
}); |
|||
} else { |
|||
// 重置表单
|
|||
setCommands([{ command: '', waitTimeMs: 0 }]); |
|||
setFormData({ |
|||
command: '', |
|||
description: '', |
|||
path: '', |
|||
useAbsolutePath: false, |
|||
isEnabled: true, |
|||
waitTimeMs: 0, |
|||
deviceId: '' |
|||
}); |
|||
} |
|||
setErrors({}); |
|||
} |
|||
}, [open, initialData, isEdit]); |
|||
|
|||
// 加载设备列表
|
|||
useEffect(() => { |
|||
if (open) { |
|||
const loadDevices = async () => { |
|||
setLoadingDevices(true); |
|||
try { |
|||
const result = await getTerminalDevices({ pageSize: 100 }); // 获取所有设备
|
|||
if (result.isSuccess && result.data) { |
|||
setDevices(result.data.terminalDevices || []); |
|||
} |
|||
} catch (error) { |
|||
console.error('加载设备列表失败:', error); |
|||
} finally { |
|||
setLoadingDevices(false); |
|||
} |
|||
}; |
|||
|
|||
loadDevices(); |
|||
} |
|||
}, [open]); |
|||
|
|||
// 更新命令列表
|
|||
const updateCommands = (newCommands: CommandItem[]) => { |
|||
setCommands(newCommands); |
|||
// 将命令列表转换为JSON字符串
|
|||
const commandJson = JSON.stringify(newCommands); |
|||
setFormData(prev => ({ ...prev, command: commandJson })); |
|||
}; |
|||
|
|||
// 添加新命令
|
|||
const addCommand = () => { |
|||
updateCommands([...commands, { command: '', waitTimeMs: 0 }]); |
|||
}; |
|||
|
|||
// 删除命令
|
|||
const removeCommand = (index: number) => { |
|||
if (commands.length > 1) { |
|||
const newCommands = commands.filter((_, i) => i !== index); |
|||
updateCommands(newCommands); |
|||
} |
|||
}; |
|||
|
|||
// 更新单个命令
|
|||
const updateCommand = (index: number, field: keyof CommandItem, value: string | number) => { |
|||
const newCommands = [...commands]; |
|||
newCommands[index] = { ...newCommands[index], [field]: value }; |
|||
updateCommands(newCommands); |
|||
}; |
|||
|
|||
// 表单验证
|
|||
const validateForm = (): boolean => { |
|||
const newErrors: Record<string, string> = {}; |
|||
|
|||
// 验证设备ID
|
|||
if (!formData.deviceId.trim()) { |
|||
newErrors.deviceId = '请选择设备'; |
|||
} |
|||
|
|||
// 验证命令列表
|
|||
if (commands.length === 0) { |
|||
newErrors.commands = '至少需要一条ADB命令'; |
|||
} else { |
|||
for (let i = 0; i < commands.length; i++) { |
|||
if (!commands[i].command.trim()) { |
|||
newErrors[`command_${i}`] = `第${i + 1}条命令不能为空`; |
|||
} |
|||
if (commands[i].waitTimeMs < 0) { |
|||
newErrors[`waitTime_${i}`] = `第${i + 1}条命令的等待时间不能为负数`; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 验证ADB路径 - 只有在使用绝对路径时才必填
|
|||
if (formData.useAbsolutePath && !formData.path.trim()) { |
|||
newErrors.path = '使用绝对路径时必须指定ADB路径'; |
|||
} |
|||
|
|||
setErrors(newErrors); |
|||
return Object.keys(newErrors).length === 0; |
|||
}; |
|||
|
|||
const handleSubmit = async (e: React.FormEvent) => { |
|||
e.preventDefault(); |
|||
if (isSubmitting) return; |
|||
|
|||
if (!validateForm()) { |
|||
return; |
|||
} |
|||
|
|||
// 提交前清理数据
|
|||
const submitData = { |
|||
...formData, |
|||
command: formData.command, // 已经是JSON字符串
|
|||
description: formData.description.trim(), |
|||
path: formData.path.trim() |
|||
}; |
|||
|
|||
await onSubmit(submitData); |
|||
}; |
|||
|
|||
const handleInputChange = (field: keyof CreateAdbOperationRequest, value: string | number | boolean) => { |
|||
setFormData(prev => ({ ...prev, [field]: value })); |
|||
|
|||
// 清除对应字段的错误
|
|||
if (errors[field]) { |
|||
setErrors(prev => ({ ...prev, [field]: '' })); |
|||
} |
|||
}; |
|||
|
|||
return ( |
|||
<Drawer open={open} onOpenChange={onOpenChange}> |
|||
<div className="flex flex-col h-full"> |
|||
<DrawerHeader className="flex-shrink-0"> |
|||
<div className="flex items-center justify-between w-full"> |
|||
<h2 className="text-lg font-semibold"> |
|||
{isEdit ? '编辑ADB操作' : '创建ADB操作'} |
|||
</h2> |
|||
<Button |
|||
variant="ghost" |
|||
size="sm" |
|||
onClick={() => onOpenChange(false)} |
|||
className="h-8 w-8 p-0" |
|||
> |
|||
<X className="h-4 w-4" /> |
|||
</Button> |
|||
</div> |
|||
</DrawerHeader> |
|||
|
|||
<form onSubmit={handleSubmit} className="flex flex-col flex-1 min-h-0"> |
|||
<DrawerContent className="flex flex-col flex-1 min-h-0 p-0 overflow-hidden"> |
|||
{/* 固定区域:设备选择 */} |
|||
<div className="space-y-2 flex-shrink-0 p-4 pb-2"> |
|||
<Label className="text-sm font-medium"> |
|||
选择设备 <span className="text-red-500">*</span> |
|||
</Label> |
|||
<Select |
|||
value={formData.deviceId} |
|||
onValueChange={(value) => handleInputChange('deviceId', value)} |
|||
disabled={isSubmitting || loadingDevices} |
|||
> |
|||
<SelectTrigger className={errors.deviceId ? 'border-red-500 focus:border-red-500' : ''}> |
|||
<SelectValue placeholder="请选择设备" /> |
|||
</SelectTrigger> |
|||
<SelectContent> |
|||
{devices.map((device) => ( |
|||
<SelectItem key={device.serial} value={device.serial}> |
|||
{device.alias || device.name} - {device.brand} ({device.serial}) |
|||
</SelectItem> |
|||
))} |
|||
</SelectContent> |
|||
</Select> |
|||
{errors.deviceId && ( |
|||
<p className="text-sm text-red-500">{errors.deviceId}</p> |
|||
)} |
|||
<p className="text-xs text-muted-foreground"> |
|||
选择要执行ADB操作的设备,设备ID将与TerminalDevice的Serial字段关联 |
|||
</p> |
|||
</div> |
|||
|
|||
{/* 命令列表区域 */} |
|||
<div className={`px-4 ${commands.length > 2 ? 'flex-1 min-h-0 flex flex-col' : ''}`}> |
|||
<div className="flex items-center justify-between flex-shrink-0 mb-2"> |
|||
<Label className="text-sm font-medium"> |
|||
ADB命令列表 <span className="text-red-500">*</span> |
|||
</Label> |
|||
<Button |
|||
type="button" |
|||
variant="outline" |
|||
size="sm" |
|||
onClick={addCommand} |
|||
disabled={isSubmitting} |
|||
className="flex items-center gap-1" |
|||
> |
|||
<Plus className="h-4 w-4" /> |
|||
添加命令 |
|||
</Button> |
|||
</div> |
|||
|
|||
{errors.commands && ( |
|||
<p className="text-sm text-red-500 flex-shrink-0 mb-2">{errors.commands}</p> |
|||
)} |
|||
|
|||
{/* 命令列表区域 - 根据命令数量决定是否可滚动 */} |
|||
<div className={commands.length > 2 ? 'flex-1 overflow-y-auto' : ''}> |
|||
<div className={`space-y-3 ${commands.length > 2 ? 'pr-2' : ''}`}> |
|||
{commands.map((cmd, index) => ( |
|||
<div key={index} className="flex gap-3 items-start p-3 border rounded-lg bg-background"> |
|||
<div className="flex-1 space-y-3"> |
|||
<div> |
|||
<Label className="text-xs text-muted-foreground"> |
|||
命令 {index + 1} |
|||
</Label> |
|||
<Input |
|||
value={cmd.command} |
|||
onChange={e => updateCommand(index, 'command', e.target.value)} |
|||
placeholder="例如: adb devices, adb shell ls, adb install app.apk" |
|||
disabled={isSubmitting} |
|||
className={errors[`command_${index}`] ? 'border-red-500 focus:border-red-500' : ''} |
|||
/> |
|||
{errors[`command_${index}`] && ( |
|||
<p className="text-sm text-red-500">{errors[`command_${index}`]}</p> |
|||
)} |
|||
</div> |
|||
|
|||
<div> |
|||
<Label className="text-xs text-muted-foreground"> |
|||
等待时间(毫秒) |
|||
</Label> |
|||
<Input |
|||
type="number" |
|||
min="0" |
|||
step="100" |
|||
value={cmd.waitTimeMs} |
|||
onChange={e => updateCommand(index, 'waitTimeMs', parseInt(e.target.value) || 0)} |
|||
placeholder="例如: 1000" |
|||
disabled={isSubmitting} |
|||
className={errors[`waitTime_${index}`] ? 'border-red-500 focus:border-red-500' : ''} |
|||
/> |
|||
{errors[`waitTime_${index}`] && ( |
|||
<p className="text-sm text-red-500">{errors[`waitTime_${index}`]}</p> |
|||
)} |
|||
</div> |
|||
</div> |
|||
|
|||
{commands.length > 1 && ( |
|||
<Button |
|||
type="button" |
|||
variant="outline" |
|||
size="sm" |
|||
onClick={() => removeCommand(index)} |
|||
disabled={isSubmitting} |
|||
className="text-red-500 hover:text-red-700 flex-shrink-0" |
|||
> |
|||
<Trash2 className="h-4 w-4" /> |
|||
</Button> |
|||
)} |
|||
</div> |
|||
))} |
|||
</div> |
|||
</div> |
|||
|
|||
<p className="text-xs text-muted-foreground flex-shrink-0 mt-2"> |
|||
可以添加多条ADB命令,系统将按顺序执行。每条命令可以设置独立的等待时间。 |
|||
</p> |
|||
</div> |
|||
|
|||
{/* 固定区域:其他表单字段 */} |
|||
<div className="space-y-4 flex-shrink-0 p-4 pt-2"> |
|||
{/* 操作描述 */} |
|||
<div className="space-y-2"> |
|||
<Label htmlFor="description" className="text-sm font-medium"> |
|||
操作描述 |
|||
</Label> |
|||
<Textarea |
|||
id="description" |
|||
value={formData.description} |
|||
onChange={e => handleInputChange('description', e.target.value)} |
|||
placeholder="请描述此ADB操作的用途和功能(可选)" |
|||
rows={3} |
|||
disabled={isSubmitting} |
|||
/> |
|||
<p className="text-xs text-muted-foreground"> |
|||
详细描述此操作的用途,便于后续管理和维护 |
|||
</p> |
|||
</div> |
|||
|
|||
{/* ADB路径 */} |
|||
<div className="space-y-2"> |
|||
<Label htmlFor="path" className="text-sm font-medium"> |
|||
ADB路径 {formData.useAbsolutePath && <span className="text-red-500">*</span>} |
|||
</Label> |
|||
<Input |
|||
id="path" |
|||
value={formData.path} |
|||
onChange={e => handleInputChange('path', e.target.value)} |
|||
placeholder="例如: /usr/local/bin/adb, C:\adb\adb.exe(可选)" |
|||
disabled={isSubmitting} |
|||
className={errors.path ? 'border-red-500 focus:border-red-500' : ''} |
|||
/> |
|||
{errors.path && ( |
|||
<p className="text-xs text-red-500">{errors.path}</p> |
|||
)} |
|||
<p className="text-xs text-muted-foreground"> |
|||
指定ADB可执行文件的完整路径,留空则使用系统默认路径 |
|||
</p> |
|||
</div> |
|||
|
|||
{/* 选项设置 */} |
|||
<div className="space-y-4"> |
|||
<div className="flex items-center space-x-2"> |
|||
<Checkbox |
|||
id="useAbsolutePath" |
|||
checked={formData.useAbsolutePath} |
|||
onCheckedChange={(checked) => |
|||
handleInputChange('useAbsolutePath', checked as boolean) |
|||
} |
|||
disabled={isSubmitting} |
|||
/> |
|||
<Label htmlFor="useAbsolutePath" className="text-sm font-medium"> |
|||
使用绝对路径 |
|||
</Label> |
|||
</div> |
|||
<p className="text-xs text-muted-foreground ml-6"> |
|||
启用后将使用绝对路径执行命令,否则使用相对路径 |
|||
</p> |
|||
|
|||
<div className="flex items-center space-x-2"> |
|||
<Checkbox |
|||
id="isEnabled" |
|||
checked={formData.isEnabled} |
|||
onCheckedChange={(checked) => |
|||
handleInputChange('isEnabled', checked as boolean) |
|||
} |
|||
disabled={isSubmitting} |
|||
/> |
|||
<Label htmlFor="isEnabled" className="text-sm font-medium"> |
|||
启用ADB操作 |
|||
</Label> |
|||
</div> |
|||
<p className="text-xs text-muted-foreground ml-6"> |
|||
启用后此操作可以被执行,禁用后将跳过此操作 |
|||
</p> |
|||
</div> |
|||
</div> |
|||
</DrawerContent> |
|||
|
|||
<DrawerFooter className="flex-shrink-0"> |
|||
<Button |
|||
type="button" |
|||
variant="outline" |
|||
onClick={() => onOpenChange(false)} |
|||
disabled={isSubmitting} |
|||
> |
|||
取消 |
|||
</Button> |
|||
<Button |
|||
type="submit" |
|||
disabled={isSubmitting} |
|||
className="bg-primary text-primary-foreground hover:bg-primary/90" |
|||
> |
|||
{isSubmitting ? '保存中...' : (isEdit ? '更新' : '创建')} |
|||
</Button> |
|||
</DrawerFooter> |
|||
</form> |
|||
</div> |
|||
</Drawer> |
|||
); |
|||
} |
|||
@ -1,355 +0,0 @@ |
|||
import React from 'react'; |
|||
import { Button } from '@/components/ui/button'; |
|||
import { Input } from '@/components/ui/input'; |
|||
import { Label } from '@/components/ui/label'; |
|||
import { Textarea } from '@/components/ui/textarea'; |
|||
import { Checkbox } from '@/components/ui/checkbox'; |
|||
import { CreateAdbOperationRequest, UpdateAdbOperationRequest } from '@/services/adbOperationsService'; |
|||
import { getTerminalDevices, TerminalDevice } from '@/services/terminalDeviceService'; |
|||
import { Plus, Trash2 } from 'lucide-react'; |
|||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; |
|||
|
|||
interface CommandItem { |
|||
command: string; |
|||
waitTimeMs: number; |
|||
} |
|||
|
|||
interface AdbOperationFormProps { |
|||
onSubmit: (data: CreateAdbOperationRequest | UpdateAdbOperationRequest) => void; |
|||
initialData?: Partial<CreateAdbOperationRequest>; |
|||
isEdit?: boolean; |
|||
isSubmitting?: boolean; |
|||
} |
|||
|
|||
export default function AdbOperationForm({ |
|||
onSubmit, |
|||
initialData, |
|||
isEdit = false, |
|||
isSubmitting = false |
|||
}: AdbOperationFormProps) { |
|||
// 解析初始命令数据
|
|||
const parseInitialCommands = (): CommandItem[] => { |
|||
if (initialData?.command) { |
|||
try { |
|||
return JSON.parse(initialData.command); |
|||
} catch { |
|||
// 如果不是JSON格式,当作单条命令处理
|
|||
return [{ command: initialData.command, waitTimeMs: initialData.waitTimeMs || 0 }]; |
|||
} |
|||
} |
|||
return [{ command: '', waitTimeMs: 0 }]; |
|||
}; |
|||
|
|||
const [commands, setCommands] = React.useState<CommandItem[]>(parseInitialCommands()); |
|||
const [formData, setFormData] = React.useState<CreateAdbOperationRequest>({ |
|||
command: initialData?.command || '', |
|||
description: initialData?.description || '', |
|||
path: initialData?.path || '', |
|||
useAbsolutePath: initialData?.useAbsolutePath ?? false, |
|||
isEnabled: initialData?.isEnabled ?? true, |
|||
waitTimeMs: initialData?.waitTimeMs || 0, |
|||
deviceId: initialData?.deviceId || '' |
|||
}); |
|||
|
|||
const [errors, setErrors] = React.useState<Record<string, string>>({}); |
|||
const [devices, setDevices] = React.useState<TerminalDevice[]>([]); |
|||
const [loadingDevices, setLoadingDevices] = React.useState(false); |
|||
|
|||
// 加载设备列表
|
|||
React.useEffect(() => { |
|||
const loadDevices = async () => { |
|||
setLoadingDevices(true); |
|||
try { |
|||
const result = await getTerminalDevices({ pageSize: 100 }); // 获取所有设备
|
|||
if (result.isSuccess && result.data) { |
|||
setDevices(result.data.terminalDevices || []); |
|||
} |
|||
} catch (error) { |
|||
console.error('加载设备列表失败:', error); |
|||
} finally { |
|||
setLoadingDevices(false); |
|||
} |
|||
}; |
|||
|
|||
loadDevices(); |
|||
}, []); |
|||
|
|||
// 更新命令列表
|
|||
const updateCommands = (newCommands: CommandItem[]) => { |
|||
setCommands(newCommands); |
|||
// 将命令列表转换为JSON字符串
|
|||
const commandJson = JSON.stringify(newCommands); |
|||
setFormData(prev => ({ ...prev, command: commandJson })); |
|||
}; |
|||
|
|||
// 添加新命令
|
|||
const addCommand = () => { |
|||
updateCommands([...commands, { command: '', waitTimeMs: 0 }]); |
|||
}; |
|||
|
|||
// 删除命令
|
|||
const removeCommand = (index: number) => { |
|||
if (commands.length > 1) { |
|||
const newCommands = commands.filter((_, i) => i !== index); |
|||
updateCommands(newCommands); |
|||
} |
|||
}; |
|||
|
|||
// 更新单个命令
|
|||
const updateCommand = (index: number, field: keyof CommandItem, value: string | number) => { |
|||
const newCommands = [...commands]; |
|||
newCommands[index] = { ...newCommands[index], [field]: value }; |
|||
updateCommands(newCommands); |
|||
}; |
|||
|
|||
// 表单验证
|
|||
const validateForm = (): boolean => { |
|||
const newErrors: Record<string, string> = {}; |
|||
|
|||
// 验证设备ID
|
|||
if (!formData.deviceId.trim()) { |
|||
newErrors.deviceId = '请选择设备'; |
|||
} |
|||
|
|||
// 验证命令列表
|
|||
if (commands.length === 0) { |
|||
newErrors.commands = '至少需要一条ADB命令'; |
|||
} else { |
|||
for (let i = 0; i < commands.length; i++) { |
|||
if (!commands[i].command.trim()) { |
|||
newErrors[`command_${i}`] = `第${i + 1}条命令不能为空`; |
|||
} |
|||
if (commands[i].waitTimeMs < 0) { |
|||
newErrors[`waitTime_${i}`] = `第${i + 1}条命令的等待时间不能为负数`; |
|||
} |
|||
} |
|||
} |
|||
|
|||
setErrors(newErrors); |
|||
return Object.keys(newErrors).length === 0; |
|||
}; |
|||
|
|||
const handleSubmit = (e: React.FormEvent) => { |
|||
e.preventDefault(); |
|||
if (isSubmitting) return; // 防止重复提交
|
|||
|
|||
if (!validateForm()) { |
|||
return; |
|||
} |
|||
|
|||
// 提交前清理数据
|
|||
const submitData = { |
|||
...formData, |
|||
command: formData.command, // 已经是JSON字符串
|
|||
description: formData.description.trim(), |
|||
path: formData.path.trim() |
|||
}; |
|||
|
|||
onSubmit(submitData); |
|||
}; |
|||
|
|||
const handleInputChange = (field: keyof CreateAdbOperationRequest, value: string | number | boolean) => { |
|||
setFormData(prev => ({ ...prev, [field]: value })); |
|||
|
|||
// 清除对应字段的错误
|
|||
if (errors[field]) { |
|||
setErrors(prev => ({ ...prev, [field]: '' })); |
|||
} |
|||
}; |
|||
|
|||
return ( |
|||
<form onSubmit={handleSubmit} className="space-y-6"> |
|||
{/* 设备选择 */} |
|||
<div className="space-y-2"> |
|||
<Label className="text-sm font-medium"> |
|||
选择设备 <span className="text-red-500">*</span> |
|||
</Label> |
|||
<Select |
|||
value={formData.deviceId} |
|||
onValueChange={(value) => handleInputChange('deviceId', value)} |
|||
disabled={isSubmitting || loadingDevices} |
|||
> |
|||
<SelectTrigger className={errors.deviceId ? 'border-red-500 focus:border-red-500' : ''}> |
|||
<SelectValue placeholder="请选择设备" /> |
|||
</SelectTrigger> |
|||
<SelectContent> |
|||
{devices.map((device) => ( |
|||
<SelectItem key={device.serial} value={device.serial}> |
|||
{device.alias || device.name} - {device.brand} ({device.serial}) |
|||
</SelectItem> |
|||
))} |
|||
</SelectContent> |
|||
</Select> |
|||
{errors.deviceId && ( |
|||
<p className="text-sm text-red-500">{errors.deviceId}</p> |
|||
)} |
|||
<p className="text-xs text-muted-foreground"> |
|||
选择要执行ADB操作的设备,设备ID将与TerminalDevice的Serial字段关联 |
|||
</p> |
|||
</div> |
|||
|
|||
{/* 命令列表 */} |
|||
<div className="space-y-4"> |
|||
<div className="flex items-center justify-between"> |
|||
<Label className="text-sm font-medium"> |
|||
ADB命令列表 <span className="text-red-500">*</span> |
|||
</Label> |
|||
<Button |
|||
type="button" |
|||
variant="outline" |
|||
size="sm" |
|||
onClick={addCommand} |
|||
disabled={isSubmitting} |
|||
className="flex items-center gap-1" |
|||
> |
|||
<Plus className="h-4 w-4" /> |
|||
添加命令 |
|||
</Button> |
|||
</div> |
|||
|
|||
{errors.commands && ( |
|||
<p className="text-sm text-red-500">{errors.commands}</p> |
|||
)} |
|||
|
|||
<div className="space-y-3"> |
|||
{commands.map((cmd, index) => ( |
|||
<div key={index} className="flex gap-3 items-start p-3 border rounded-lg"> |
|||
<div className="flex-1 space-y-3"> |
|||
<div> |
|||
<Label className="text-xs text-muted-foreground"> |
|||
命令 {index + 1} |
|||
</Label> |
|||
<Input |
|||
value={cmd.command} |
|||
onChange={e => updateCommand(index, 'command', e.target.value)} |
|||
placeholder="例如: adb devices, adb shell ls, adb install app.apk" |
|||
disabled={isSubmitting} |
|||
className={errors[`command_${index}`] ? 'border-red-500 focus:border-red-500' : ''} |
|||
/> |
|||
{errors[`command_${index}`] && ( |
|||
<p className="text-sm text-red-500">{errors[`command_${index}`]}</p> |
|||
)} |
|||
</div> |
|||
|
|||
<div> |
|||
<Label className="text-xs text-muted-foreground"> |
|||
等待时间(毫秒) |
|||
</Label> |
|||
<Input |
|||
type="number" |
|||
min="0" |
|||
step="100" |
|||
value={cmd.waitTimeMs} |
|||
onChange={e => updateCommand(index, 'waitTimeMs', parseInt(e.target.value) || 0)} |
|||
placeholder="例如: 1000" |
|||
disabled={isSubmitting} |
|||
className={errors[`waitTime_${index}`] ? 'border-red-500 focus:border-red-500' : ''} |
|||
/> |
|||
{errors[`waitTime_${index}`] && ( |
|||
<p className="text-sm text-red-500">{errors[`waitTime_${index}`]}</p> |
|||
)} |
|||
</div> |
|||
</div> |
|||
|
|||
{commands.length > 1 && ( |
|||
<Button |
|||
type="button" |
|||
variant="outline" |
|||
size="sm" |
|||
onClick={() => removeCommand(index)} |
|||
disabled={isSubmitting} |
|||
className="text-red-500 hover:text-red-700" |
|||
> |
|||
<Trash2 className="h-4 w-4" /> |
|||
</Button> |
|||
)} |
|||
</div> |
|||
))} |
|||
</div> |
|||
|
|||
<p className="text-xs text-muted-foreground"> |
|||
可以添加多条ADB命令,系统将按顺序执行。每条命令可以设置独立的等待时间。 |
|||
</p> |
|||
</div> |
|||
|
|||
<div className="space-y-2"> |
|||
<Label htmlFor="description" className="text-sm font-medium"> |
|||
操作描述 |
|||
</Label> |
|||
<Textarea |
|||
id="description" |
|||
value={formData.description} |
|||
onChange={e => handleInputChange('description', e.target.value)} |
|||
placeholder="请描述此ADB操作的用途和功能(可选)" |
|||
rows={3} |
|||
disabled={isSubmitting} |
|||
/> |
|||
<p className="text-xs text-muted-foreground"> |
|||
详细描述此操作的用途,便于后续管理和维护 |
|||
</p> |
|||
</div> |
|||
|
|||
<div className="space-y-2"> |
|||
<Label htmlFor="path" className="text-sm font-medium"> |
|||
ADB路径 |
|||
</Label> |
|||
<Input |
|||
id="path" |
|||
value={formData.path} |
|||
onChange={e => handleInputChange('path', e.target.value)} |
|||
placeholder="例如: /usr/local/bin/adb, C:\adb\adb.exe(可选)" |
|||
disabled={isSubmitting} |
|||
/> |
|||
<p className="text-xs text-muted-foreground"> |
|||
指定ADB可执行文件的完整路径,留空则使用系统默认路径 |
|||
</p> |
|||
</div> |
|||
|
|||
<div className="space-y-4"> |
|||
<div className="flex items-center space-x-2"> |
|||
<Checkbox |
|||
id="useAbsolutePath" |
|||
checked={formData.useAbsolutePath} |
|||
onCheckedChange={(checked) => |
|||
handleInputChange('useAbsolutePath', checked as boolean) |
|||
} |
|||
disabled={isSubmitting} |
|||
/> |
|||
<Label htmlFor="useAbsolutePath" className="text-sm font-medium"> |
|||
使用绝对路径 |
|||
</Label> |
|||
</div> |
|||
<p className="text-xs text-muted-foreground ml-6"> |
|||
启用后将使用绝对路径执行命令,否则使用相对路径 |
|||
</p> |
|||
|
|||
<div className="flex items-center space-x-2"> |
|||
<Checkbox |
|||
id="isEnabled" |
|||
checked={formData.isEnabled} |
|||
onCheckedChange={(checked) => |
|||
handleInputChange('isEnabled', checked as boolean) |
|||
} |
|||
disabled={isSubmitting} |
|||
/> |
|||
<Label htmlFor="isEnabled" className="text-sm font-medium"> |
|||
启用ADB操作 |
|||
</Label> |
|||
</div> |
|||
<p className="text-xs text-muted-foreground ml-6"> |
|||
启用后此操作可以被执行,禁用后将跳过此操作 |
|||
</p> |
|||
</div> |
|||
|
|||
<div className="flex gap-3 pt-4"> |
|||
<Button |
|||
type="submit" |
|||
className="flex-1" |
|||
disabled={isSubmitting} |
|||
> |
|||
{isSubmitting ? '提交中...' : (isEdit ? '更新ADB操作' : '创建ADB操作')} |
|||
</Button> |
|||
</div> |
|||
</form> |
|||
); |
|||
} |
|||
File diff suppressed because it is too large
Loading…
Reference in new issue