feat: 设备列表功能完成

This commit is contained in:
许永平
2025-07-04 17:36:14 +08:00
parent 034a306077
commit c5c559f4ef

View File

@@ -1,6 +1,6 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { ChevronLeft, Plus, Filter, Search, RefreshCw, QrCode, Smartphone, Loader2, AlertTriangle, Trash2 } from 'lucide-react';
import { ChevronLeft, Plus, Filter, Search, RefreshCw, QrCode, Smartphone, Loader2, AlertTriangle, Trash2, X } from 'lucide-react';
import { devicesApi } from '../../api';
import { useToast } from '../../components/ui/toast';
@@ -247,6 +247,11 @@ export default function Devices() {
messageType: 'default',
showAnimation: false
});
// 自动获取二维码
setTimeout(() => {
fetchDeviceQRCode();
}, 100);
};
const handleCloseAddDeviceModal = () => {
@@ -369,62 +374,62 @@ export default function Devices() {
});
return (
<div className="flex-1 flex flex-col bg-gray-50">
<div className="min-h-screen bg-gray-50">
{/* 固定header */}
<header className="fixed top-0 left-0 right-0 z-20 bg-white border-b">
<div className="flex items-center justify-between p-4">
<div className="flex items-center space-x-3">
<header className="fixed top-0 left-0 right-0 z-20 bg-white border-b border-gray-200">
<div className="flex items-center justify-between px-4 py-3">
<div className="flex items-center gap-3">
<button
className="p-2 hover:bg-gray-100 rounded-full"
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
onClick={() => navigate(-1)}
>
<ChevronLeft className="h-5 w-5" />
</button>
<h1 className="text-lg font-medium"></h1>
<h1 className="text-lg font-semibold"></h1>
</div>
<button
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center"
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center gap-2 transition-colors"
onClick={handleOpenAddDeviceModal}
>
<Plus className="h-4 w-4 mr-2" />
<Plus className="h-4 w-4" />
</button>
</div>
</header>
{/* 可滚动的内容区域 */}
<div className="flex-1 overflow-y-auto pt-16 pb-24">
{/* 内容区域 */}
<div className="pt-16 pb-20">
<div className="p-4 space-y-4">
{/* 统计卡片 */}
<div className="grid grid-cols-2 gap-3">
<div className="p-3 bg-white rounded-lg shadow">
<div className="text-sm text-gray-500"></div>
<div className="text-xl font-bold text-blue-600">{stats.totalDevices}</div>
<div className="bg-white p-4 rounded-xl shadow-sm border border-gray-100">
<div className="text-sm text-gray-500 mb-1"></div>
<div className="text-2xl font-bold text-blue-600">{stats.totalDevices}</div>
</div>
<div className="p-3 bg-white rounded-lg shadow">
<div className="text-sm text-gray-500">线</div>
<div className="text-xl font-bold text-green-600">{stats.onlineDevices}</div>
<div className="bg-white p-4 rounded-xl shadow-sm border border-gray-100">
<div className="text-sm text-gray-500 mb-1">线</div>
<div className="text-2xl font-bold text-green-600">{stats.onlineDevices}</div>
</div>
</div>
{/* 搜索和过滤 */}
<div className="bg-white p-4 rounded-lg shadow space-y-3">
<div className="flex items-center space-x-2">
<div className="bg-white p-4 rounded-xl shadow-sm border border-gray-100 space-y-4">
<div className="flex items-center gap-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<input
type="text"
placeholder="搜索设备IMEI/备注"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-9 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
className="w-full pl-10 pr-4 py-2.5 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
/>
</div>
<button className="p-2 border border-gray-300 rounded-lg hover:bg-gray-50">
<button className="p-2.5 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors">
<Filter className="h-4 w-4" />
</button>
<button
className="p-2 border border-gray-300 rounded-lg hover:bg-gray-50"
className="p-2.5 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
onClick={handleRefresh}
>
<RefreshCw className="h-4 w-4" />
@@ -432,19 +437,17 @@ export default function Devices() {
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-lg bg-white"
>
<option value="all"></option>
<option value="online">线</option>
<option value="offline">线</option>
</select>
</div>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="px-3 py-2 border border-gray-200 rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
>
<option value="all"></option>
<option value="online">线</option>
<option value="offline">线</option>
</select>
<button
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg text-sm disabled:bg-gray-300 disabled:cursor-not-allowed"
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg text-sm disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors"
onClick={handleDeleteClick}
disabled={!selectedDeviceId}
>
@@ -454,14 +457,14 @@ export default function Devices() {
</div>
{/* 设备列表 */}
<div className="space-y-2">
<div className="space-y-3">
{filteredDevices.map((device) => (
<div
key={device.id}
className="bg-white p-3 rounded-lg shadow hover:shadow-md transition-shadow cursor-pointer relative"
className="bg-white p-4 rounded-xl shadow-sm border border-gray-100 hover:shadow-md transition-all cursor-pointer"
onClick={(e) => handleDeviceClick(device.id, e)}
>
<div className="flex items-center space-x-3">
<div className="flex items-start gap-3">
<input
type="checkbox"
checked={selectedDeviceId === device.id}
@@ -473,32 +476,30 @@ export default function Devices() {
}
}}
onClick={(e) => e.stopPropagation()}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded mt-0.5"
/>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-1">
<div className="font-medium truncate">{device.memo || "未命名设备"}</div>
<span className={`ml-2 px-2 py-1 text-xs rounded-full ${
<div className="flex items-center justify-between mb-2">
<div className="font-semibold text-gray-900 truncate">{device.memo || "未命名设备"}</div>
<span className={`px-2.5 py-1 text-xs rounded-full font-medium ${
device.status === "online"
? "bg-green-100 text-green-800"
: "bg-gray-100 text-gray-800"
? "bg-green-100 text-green-700"
: "bg-gray-100 text-gray-600"
}`}>
{device.status === "online" ? "在线" : "离线"}
</span>
</div>
<div className="text-sm text-gray-500">
<span className="mr-1">IMEI: {device.imei}</span>
</div>
<div className="text-sm text-gray-500">: {device.wechatId || "未绑定或微信离线"}</div>
<div className="flex items-center justify-between mt-1 text-sm">
<span className="text-gray-500">: {device.totalFriend}</span>
<div className="space-y-1 text-sm text-gray-600">
<div>IMEI: {device.imei}</div>
<div>: {device.wechatId || "未绑定或微信离线"}</div>
<div>: {device.totalFriend}</div>
</div>
</div>
</div>
</div>
))}
<div ref={observerTarget} className="h-10 flex items-center justify-center">
<div ref={observerTarget} className="py-4 flex items-center justify-center">
{isLoading && <div className="text-sm text-gray-500">...</div>}
{!hasMore && devices.length > 0 && <div className="text-sm text-gray-500"></div>}
{!hasMore && devices.length === 0 && <div className="text-sm text-gray-500"></div>}
@@ -509,29 +510,43 @@ export default function Devices() {
{/* 添加设备弹窗 */}
{isAddDeviceOpen && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg max-w-md w-full max-h-[90vh] overflow-y-auto">
<div className="p-6">
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl max-w-md w-full max-h-[90vh] overflow-y-auto">
<div className="p-5">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold"></h2>
<h2 className="text-xl font-semibold text-gray-900"></h2>
<button
onClick={handleCloseAddDeviceModal}
className="text-gray-400 hover:text-gray-600"
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
>
<X className="h-5 w-5 text-gray-400" />
</button>
</div>
<div className="space-y-4">
<div className="flex space-x-2 border-b">
<div className="flex border-b border-gray-200">
<button
className={`pb-2 px-4 ${activeTab === "scan" ? "border-b-2 border-blue-500 text-blue-500" : "text-gray-500"}`}
onClick={() => setActiveTab("scan")}
className={`flex-1 py-2.5 px-4 text-sm font-medium transition-colors ${
activeTab === "scan"
? "text-blue-600 border-b-2 border-blue-600"
: "text-gray-500 hover:text-gray-700"
}`}
onClick={() => {
setActiveTab("scan");
// 切换到扫码添加时自动获取二维码
setTimeout(() => {
fetchDeviceQRCode();
}, 100);
}}
>
</button>
<button
className={`pb-2 px-4 ${activeTab === "manual" ? "border-b-2 border-blue-500 text-blue-500" : "text-gray-500"}`}
className={`flex-1 py-2.5 px-4 text-sm font-medium transition-colors ${
activeTab === "manual"
? "text-blue-600 border-b-2 border-blue-600"
: "text-gray-500 hover:text-gray-700"
}`}
onClick={() => setActiveTab("manual")}
>
@@ -539,33 +554,34 @@ export default function Devices() {
</div>
{activeTab === "scan" && (
<div className="space-y-4 py-4">
<div className="flex flex-col items-center justify-center p-6 space-y-4 relative">
<div className="absolute left-1/2 top-0 -translate-x-1/2 -translate-y-1/2 z-10">
<div className="flex flex-col items-center w-full py-2">
{pollingStatus.isPolling || pollingStatus.showAnimation ? (
<>
<span className="text-sm text-gray-800"></span>
<div className="flex space-x-1 mt-1">
<div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce" style={{ animationDelay: '0ms' }}></div>
<div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce" style={{ animationDelay: '150ms' }}></div>
<div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce" style={{ animationDelay: '300ms' }}></div>
</div>
</>
) : (
<span className="text-sm text-gray-800">5</span>
)}
</div>
<div className="py-3">
<div className="flex flex-col items-center space-y-4">
{/* 状态提示 */}
<div className="text-center">
{pollingStatus.isPolling || pollingStatus.showAnimation ? (
<div className="space-y-1">
<span className="text-sm text-gray-700"></span>
<div className="flex justify-center space-x-1">
<div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce" style={{ animationDelay: '0ms' }}></div>
<div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce" style={{ animationDelay: '150ms' }}></div>
<div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce" style={{ animationDelay: '300ms' }}></div>
</div>
</div>
) : (
<span className="text-sm text-gray-600">5</span>
)}
</div>
<div className="bg-white p-4 rounded-lg shadow-md border border-gray-200 w-full max-w-[280px] min-h-[280px] flex flex-col items-center justify-center">
{/* 二维码区域 */}
<div className="bg-gray-50 p-3 rounded-xl w-full max-w-[220px] min-h-[220px] flex flex-col items-center justify-center">
{isLoadingQRCode ? (
<div className="flex flex-col items-center justify-center space-y-3">
<Loader2 className="h-12 w-12 animate-spin text-blue-500" />
<div className="flex flex-col items-center space-y-2">
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
<p className="text-sm text-gray-500">...</p>
</div>
) : qrCodeImage ? (
<div id="qrcode-container" className="flex flex-col items-center space-y-3">
<div className="relative w-64 h-64 flex items-center justify-center">
<div id="qrcode-container" className="flex flex-col items-center space-y-2">
<div className="relative w-44 h-44 flex items-center justify-center">
<img
src={qrCodeImage}
alt="设备添加二维码"
@@ -582,36 +598,38 @@ export default function Devices() {
}
}}
/>
<div className="qrcode-error hidden absolute inset-0 flex flex-col items-center justify-center text-center text-red-500 bg-white">
<AlertTriangle className="h-10 w-10 mb-2" />
<p></p>
<div className="qrcode-error hidden absolute inset-0 flex flex-col items-center justify-center text-center text-red-500 bg-white rounded-lg">
<AlertTriangle className="h-6 w-6 mb-1" />
<p className="text-xs"></p>
</div>
</div>
<p className="text-sm text-center text-gray-600 mt-2">
<p className="text-sm text-center text-gray-600">
使
</p>
</div>
) : (
<div className="text-center text-gray-500">
<QrCode className="h-12 w-12 mx-auto mb-3 opacity-50" />
<p></p>
<QrCode className="h-8 w-8 mx-auto mb-2 opacity-50" />
<p className="text-sm"></p>
</div>
)}
</div>
{/* 操作按钮 */}
<button
type="button"
onClick={fetchDeviceQRCode}
disabled={isLoadingQRCode}
className="w-48 mt-8 bg-blue-600 hover:bg-blue-700 text-white py-2 rounded-lg disabled:bg-gray-300"
className="w-full bg-blue-600 hover:bg-blue-700 text-white py-2.5 rounded-xl disabled:bg-gray-300 transition-colors flex items-center justify-center gap-2"
>
{isLoadingQRCode ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin inline" />
<Loader2 className="h-4 w-4 animate-spin" />
...
</>
) : (
<>
<RefreshCw className="mr-2 h-4 w-4 inline" />
<RefreshCw className="h-4 w-4" />
</>
)}
@@ -621,49 +639,50 @@ export default function Devices() {
)}
{activeTab === "manual" && (
<div className="space-y-4 py-4">
<div className="space-y-4">
<div className="py-3 space-y-4">
<div className="space-y-3">
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<label className="text-sm font-medium text-gray-700"></label>
<input
type="text"
placeholder="请输入设备名称"
value={deviceName}
onChange={(e) => setDeviceName(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
className="w-full px-4 py-2.5 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
/>
<p className="text-xs text-gray-500">
便
</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">IMEI</label>
<label className="text-sm font-medium text-gray-700">IMEI</label>
<input
type="text"
placeholder="请输入设备IMEI"
value={deviceImei}
onChange={(e) => setDeviceImei(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
className="w-full px-4 py-2.5 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
/>
<p className="text-xs text-gray-500">
IMEI码
</p>
</div>
<div className="flex justify-end space-x-2">
<button
className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
onClick={() => setIsAddDeviceOpen(false)}
>
</button>
<button
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg disabled:bg-gray-300"
onClick={handleAddDevice}
disabled={!deviceImei.trim() || !deviceName.trim() || isSubmittingImei}
>
{isSubmittingImei ? "添加中..." : "添加"}
</button>
</div>
</div>
<div className="flex gap-3">
<button
className="flex-1 px-4 py-2.5 border border-gray-200 rounded-xl hover:bg-gray-50 transition-colors"
onClick={() => setIsAddDeviceOpen(false)}
>
</button>
<button
className="flex-1 px-4 py-2.5 bg-blue-600 hover:bg-blue-700 text-white rounded-xl disabled:bg-gray-300 transition-colors"
onClick={handleAddDevice}
disabled={!deviceImei.trim() || !deviceName.trim() || isSubmittingImei}
>
{isSubmittingImei ? "添加中..." : "添加"}
</button>
</div>
</div>
)}
@@ -675,21 +694,24 @@ export default function Devices() {
{/* 删除确认弹窗 */}
{isDeleteDialogOpen && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg max-w-md w-full p-6">
<h3 className="text-lg font-semibold mb-2"></h3>
<p className="text-gray-600 mb-6">
</p>
<div className="flex justify-end space-x-2">
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl max-w-md w-full p-6">
<div className="text-center mb-6">
<AlertTriangle className="h-12 w-12 text-red-500 mx-auto mb-4" />
<h3 className="text-xl font-semibold text-gray-900 mb-2"></h3>
<p className="text-gray-600">
</p>
</div>
<div className="flex gap-3">
<button
className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
className="flex-1 px-4 py-3 border border-gray-200 rounded-xl hover:bg-gray-50 transition-colors"
onClick={handleCancelDelete}
>
</button>
<button
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg"
className="flex-1 px-4 py-3 bg-red-600 hover:bg-red-700 text-white rounded-xl transition-colors"
onClick={handleConfirmDelete}
>