자잘한 통신 오류 수정 및 모바일용 디자인 변경, connect 메뉴 별도 추가
This commit is contained in:
73
App.tsx
73
App.tsx
@@ -18,7 +18,7 @@ const App: React.FC = () => {
|
|||||||
const [address, setAddress] = useState(0xFF); // Broadcast address default
|
const [address, setAddress] = useState(0xFF); // Broadcast address default
|
||||||
|
|
||||||
// App State - Set Quick Test as Default
|
// App State - Set Quick Test as Default
|
||||||
const [activeTab, setActiveTab] = useState<'inventory' | 'settings' | 'memory' | 'quicktest'>('quicktest');
|
const [activeTab, setActiveTab] = useState<'connect' | 'inventory' | 'settings' | 'memory' | 'quicktest'>('quicktest');
|
||||||
const [logs, setLogs] = useState<LogEntry[]>([]);
|
const [logs, setLogs] = useState<LogEntry[]>([]);
|
||||||
const [tags, setTags] = useState<TagData[]>([]);
|
const [tags, setTags] = useState<TagData[]>([]);
|
||||||
const [isScanning, setIsScanning] = useState(false);
|
const [isScanning, setIsScanning] = useState(false);
|
||||||
@@ -146,6 +146,7 @@ const App: React.FC = () => {
|
|||||||
setStatus(ConnectionStatus.CONNECTED);
|
setStatus(ConnectionStatus.CONNECTED);
|
||||||
|
|
||||||
addLog('INFO', `Connected to serial port at ${baudRate}`);
|
addLog('INFO', `Connected to serial port at ${baudRate}`);
|
||||||
|
setActiveTab('quicktest');
|
||||||
startReading(p);
|
startReading(p);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@@ -318,7 +319,7 @@ const App: React.FC = () => {
|
|||||||
addLog('INFO', 'VERIFICATION SUCCESS: Data matches written value.');
|
addLog('INFO', 'VERIFICATION SUCCESS: Data matches written value.');
|
||||||
performingQuickWriteRef.current = false;
|
performingQuickWriteRef.current = false;
|
||||||
verificationRetriesRef.current = 0;
|
verificationRetriesRef.current = 0;
|
||||||
alert("Quick Test Successful: Write verified.");
|
//alert("Quick Test Successful: Write verified.");
|
||||||
} else {
|
} else {
|
||||||
if (verificationRetriesRef.current < 5) {
|
if (verificationRetriesRef.current < 5) {
|
||||||
verificationRetriesRef.current += 1;
|
verificationRetriesRef.current += 1;
|
||||||
@@ -614,9 +615,8 @@ const App: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getLogHeightClass = () => {
|
const getLogHeightClass = () => {
|
||||||
if (isLogExpanded) return 'h-96';
|
if (isLogExpanded) return 'h-[30vh]';
|
||||||
if (activeTab === 'quicktest' || activeTab === 'settings') return 'h-12';
|
return 'h-12';
|
||||||
return 'h-48';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -632,6 +632,13 @@ const App: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-4 space-y-2 flex-1">
|
<div className="p-4 space-y-2 flex-1">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('connect')}
|
||||||
|
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors ${activeTab === 'connect' ? 'bg-emerald-50 text-emerald-700' : 'text-slate-600 hover:bg-slate-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Wifi className="w-5 h-5" /> Connection
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('quicktest')}
|
onClick={() => setActiveTab('quicktest')}
|
||||||
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors ${activeTab === 'quicktest' ? 'bg-indigo-50 text-indigo-700' : 'text-slate-600 hover:bg-slate-50'
|
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors ${activeTab === 'quicktest' ? 'bg-indigo-50 text-indigo-700' : 'text-slate-600 hover:bg-slate-50'
|
||||||
@@ -662,42 +669,26 @@ const App: React.FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-4 border-t border-slate-100">
|
{/* Connection Panel removed from here and moved to main content */}
|
||||||
<ConnectionPanel
|
|
||||||
status={status}
|
|
||||||
onConnect={connectSerial}
|
|
||||||
onDisconnect={disconnectSerial}
|
|
||||||
baudRate={baudRate}
|
|
||||||
setBaudRate={setBaudRate}
|
|
||||||
address={address}
|
|
||||||
setAddress={setAddress}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className="flex-1 flex flex-col overflow-hidden relative">
|
<div className="flex-1 flex flex-col overflow-hidden relative">
|
||||||
{/* Mobile Top Navigation Bar */}
|
{/* Mobile Top Navigation Bar */}
|
||||||
<div className="md:hidden bg-white border-b border-slate-200 p-2 flex items-center justify-between shrink-0 overflow-x-auto">
|
<div className="md:hidden bg-white border-b border-slate-200 p-2 flex items-center justify-between shrink-0 overflow-x-auto">
|
||||||
<div className="flex items-center gap-3 pr-4 border-r border-slate-100 mr-2 shrink-0">
|
<div className="flex items-center gap-1 pr-4 border-r border-slate-100 mr-2 shrink-0">
|
||||||
<div className="flex items-center gap-1">
|
<Database className="text-blue-600 w-5 h-5" />
|
||||||
<Database className="text-blue-600 w-5 h-5" />
|
<span className="font-bold text-slate-800 text-sm">UHF</span>
|
||||||
<span className="font-bold text-slate-800 text-sm">UHF</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Connection Status Indicator */}
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab('settings')}
|
|
||||||
className={`flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium transition-colors ${status === ConnectionStatus.CONNECTED
|
|
||||||
? 'bg-emerald-50 text-emerald-600 border border-emerald-100'
|
|
||||||
: 'bg-red-50 text-red-600 border border-red-100 animate-pulse'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{status === ConnectionStatus.CONNECTED ? <Wifi className="w-3 h-3" /> : <WifiOff className="w-3 h-3" />}
|
|
||||||
<span>{status === ConnectionStatus.CONNECTED ? 'On' : 'Connect'}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1 flex-1 justify-around">
|
<div className="flex items-center gap-1 flex-1 justify-around">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('connect')}
|
||||||
|
className={`p-2 rounded-lg transition-colors flex flex-col items-center justify-center gap-1 ${activeTab === 'connect' ? 'bg-emerald-50 text-emerald-700' : 'text-slate-600 hover:bg-slate-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Wifi className="w-5 h-5" />
|
||||||
|
<span className="text-[10px] font-medium leading-none">Connect</span>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('quicktest')}
|
onClick={() => setActiveTab('quicktest')}
|
||||||
className={`p-2 rounded-lg transition-colors flex flex-col items-center justify-center gap-1 ${activeTab === 'quicktest' ? 'bg-indigo-50 text-indigo-700' : 'text-slate-600 hover:bg-slate-50'
|
className={`p-2 rounded-lg transition-colors flex flex-col items-center justify-center gap-1 ${activeTab === 'quicktest' ? 'bg-indigo-50 text-indigo-700' : 'text-slate-600 hover:bg-slate-50'
|
||||||
@@ -733,6 +724,22 @@ const App: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<main className="flex-1 p-6 overflow-auto">
|
<main className="flex-1 p-6 overflow-auto">
|
||||||
|
{activeTab === 'connect' && (
|
||||||
|
<div className="space-y-4 max-w-2xl mx-auto">
|
||||||
|
<h2 className="text-lg font-bold text-slate-800">Connection Manager</h2>
|
||||||
|
<div className="bg-white rounded-xl border border-slate-200 shadow-sm p-6">
|
||||||
|
<ConnectionPanel
|
||||||
|
status={status}
|
||||||
|
onConnect={connectSerial}
|
||||||
|
onDisconnect={disconnectSerial}
|
||||||
|
baudRate={baudRate}
|
||||||
|
setBaudRate={setBaudRate}
|
||||||
|
address={address}
|
||||||
|
setAddress={setAddress}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{activeTab === 'inventory' && (
|
{activeTab === 'inventory' && (
|
||||||
<InventoryPanel
|
<InventoryPanel
|
||||||
tags={tags}
|
tags={tags}
|
||||||
|
|||||||
@@ -35,60 +35,54 @@ export const QuickTestPanel: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
<h1 className="text-3xl font-bold text-slate-800 mb-2">Quick Test Mode</h1>
|
<h1 className="text-3xl font-bold text-slate-800 mb-2">Quick Test Mode</h1>
|
||||||
<p className="text-slate-500">
|
<p className="text-slate-500">
|
||||||
Automatically detects the first tag in the field and performs a Read or Write operation.<br/>
|
필드 내의 첫 번째 태그를 자동으로 감지하여 읽기 또는 쓰기 작업을 수행합니다.
|
||||||
Target: <strong>EPC Bank</strong>, Start Address: <strong>2</strong><br/>
|
|
||||||
Length: <strong>{config.length} Words</strong>, Format: <strong>{config.format.toUpperCase()}</strong>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 w-full">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 w-full">
|
||||||
|
|
||||||
{/* Read Card */}
|
{/* Read/Write Card */}
|
||||||
<div className="bg-white p-8 rounded-2xl shadow-sm border border-slate-200 hover:shadow-md transition-shadow flex flex-col items-center text-center">
|
<div className="bg-white p-8 rounded-2xl shadow-sm border border-slate-200 hover:shadow-md transition-shadow flex flex-col items-center text-center">
|
||||||
<div className="mb-6 p-4 bg-blue-50 text-blue-600 rounded-full">
|
<div className="mb-6 p-4 bg-blue-50 text-blue-600 rounded-full">
|
||||||
<HardDriveDownload className="w-8 h-8" />
|
<HardDriveDownload className="w-8 h-8" />
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-xl font-bold text-slate-800 mb-2">Auto Read</h2>
|
|
||||||
|
|
||||||
|
{/* 1. Read Button */}
|
||||||
|
<div className="w-full mb-6">
|
||||||
|
{isPending ? (
|
||||||
|
<button
|
||||||
|
onClick={onCancel}
|
||||||
|
className="w-full py-4 bg-red-500 hover:bg-red-600 text-white rounded-xl font-bold text-lg shadow-lg shadow-red-500/30 transition-all flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<XCircle className="w-5 h-5" /> Cancel
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={onQuickRead}
|
||||||
|
disabled={isScanning && !isPending}
|
||||||
|
className="w-full py-4 bg-blue-600 hover:bg-blue-700 disabled:bg-slate-300 disabled:cursor-not-allowed text-white rounded-xl font-bold text-lg shadow-lg shadow-blue-500/30 transition-all flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
Start Read Test
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 2. Read Result Display */}
|
||||||
<div className="w-full bg-slate-50 border border-slate-200 rounded-xl p-4 mb-6 min-h-[80px] flex items-center justify-center font-mono text-lg text-slate-700 break-all">
|
<div className="w-full bg-slate-50 border border-slate-200 rounded-xl p-4 mb-6 min-h-[80px] flex items-center justify-center font-mono text-lg text-slate-700 break-all">
|
||||||
{isPending ? (
|
{isPending ? (
|
||||||
<div className="flex items-center gap-2 text-amber-500 animate-pulse">
|
<div className="flex items-center gap-2 text-amber-500 animate-pulse">
|
||||||
<Activity className="w-5 h-5" /> Scanning & Reading...
|
<Activity className="w-5 h-5" /> Scanning & Reading...
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
result ? (
|
result ? (
|
||||||
<span className="text-emerald-600 font-bold">{result}</span>
|
<span className="text-emerald-600 font-bold">{result}</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-slate-300">No Data Read</span>
|
<span className="text-slate-300">No Data Read</span>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isPending ? (
|
{/* 3. Write Input */}
|
||||||
<button
|
|
||||||
onClick={onCancel}
|
|
||||||
className="w-full py-4 bg-red-500 hover:bg-red-600 text-white rounded-xl font-bold text-lg shadow-lg shadow-red-500/30 transition-all flex items-center justify-center gap-2"
|
|
||||||
>
|
|
||||||
<XCircle className="w-5 h-5" /> Cancel
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
onClick={onQuickRead}
|
|
||||||
disabled={isScanning && !isPending}
|
|
||||||
className="w-full py-4 bg-blue-600 hover:bg-blue-700 disabled:bg-slate-300 disabled:cursor-not-allowed text-white rounded-xl font-bold text-lg shadow-lg shadow-blue-500/30 transition-all flex items-center justify-center gap-2"
|
|
||||||
>
|
|
||||||
Start Read Test
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Write Card */}
|
|
||||||
<div className="bg-white p-8 rounded-2xl shadow-sm border border-slate-200 hover:shadow-md transition-shadow flex flex-col items-center text-center">
|
|
||||||
<div className="mb-6 p-4 bg-purple-50 text-purple-600 rounded-full">
|
|
||||||
<HardDriveUpload className="w-8 h-8" />
|
|
||||||
</div>
|
|
||||||
<h2 className="text-xl font-bold text-slate-800 mb-2">Auto Write</h2>
|
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={writeInput}
|
value={writeInput}
|
||||||
@@ -97,30 +91,72 @@ export const QuickTestPanel: React.FC<Props> = ({
|
|||||||
className="w-full bg-slate-50 border border-slate-300 text-center text-lg rounded-xl p-4 mb-6 font-mono focus:ring-2 focus:ring-purple-500 outline-none"
|
className="w-full bg-slate-50 border border-slate-300 text-center text-lg rounded-xl p-4 mb-6 font-mono focus:ring-2 focus:ring-purple-500 outline-none"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isPending ? (
|
{/* 4. Write Button */}
|
||||||
<button
|
<div className="w-full">
|
||||||
onClick={onCancel}
|
{isPending ? (
|
||||||
className="w-full py-4 bg-red-500 hover:bg-red-600 text-white rounded-xl font-bold text-lg shadow-lg shadow-red-500/30 transition-all flex items-center justify-center gap-2"
|
<button
|
||||||
>
|
onClick={onCancel}
|
||||||
<XCircle className="w-5 h-5" /> Cancel
|
className="w-full py-4 bg-red-500 hover:bg-red-600 text-white rounded-xl font-bold text-lg shadow-lg shadow-red-500/30 transition-all flex items-center justify-center gap-2"
|
||||||
</button>
|
>
|
||||||
) : (
|
<XCircle className="w-5 h-5" /> Cancel
|
||||||
<button
|
</button>
|
||||||
onClick={() => onQuickWrite(writeInput)}
|
) : (
|
||||||
disabled={(isScanning && !isPending) || !writeInput}
|
<button
|
||||||
className="w-full py-4 bg-slate-800 hover:bg-slate-900 disabled:bg-slate-300 disabled:cursor-not-allowed text-white rounded-xl font-bold text-lg shadow-lg shadow-slate-500/30 transition-all flex items-center justify-center gap-2"
|
onClick={() => onQuickWrite(writeInput)}
|
||||||
>
|
disabled={(isScanning && !isPending) || !writeInput}
|
||||||
Start Write Test
|
className="w-full py-4 bg-slate-800 hover:bg-slate-900 disabled:bg-slate-300 disabled:cursor-not-allowed text-white rounded-xl font-bold text-lg shadow-lg shadow-slate-500/30 transition-all flex items-center justify-center gap-2"
|
||||||
</button>
|
>
|
||||||
)}
|
Start Write Test
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Info / Config Card */}
|
||||||
|
<div className="flex flex-col justify-center space-y-6">
|
||||||
|
<div className="bg-white p-6 rounded-2xl shadow-sm border border-slate-200 hover:shadow-md transition-shadow">
|
||||||
|
<h3 className="text-lg font-bold text-slate-800 mb-4 flex items-center gap-2">
|
||||||
|
<Zap className="w-5 h-5 text-indigo-500" />
|
||||||
|
Test Configuration
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex justify-between items-center p-3 bg-slate-50 rounded-lg">
|
||||||
|
<span className="text-slate-500 text-sm">Target Bank</span>
|
||||||
|
<span className="font-bold text-slate-700">EPC (01)</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center p-3 bg-slate-50 rounded-lg">
|
||||||
|
<span className="text-slate-500 text-sm">Start Address</span>
|
||||||
|
<span className="font-bold text-slate-700">2 (User Data)</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center p-3 bg-slate-50 rounded-lg">
|
||||||
|
<span className="text-slate-500 text-sm">Data Length</span>
|
||||||
|
<span className="font-bold text-slate-700">{config.length} Words</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center p-3 bg-slate-50 rounded-lg">
|
||||||
|
<span className="text-slate-500 text-sm">Format</span>
|
||||||
|
<span className="font-bold text-slate-700">{config.format.toUpperCase()}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-indigo-50 p-6 rounded-2xl border border-indigo-100 text-sm text-indigo-800">
|
||||||
|
<h4 className="font-bold mb-2 flex items-center gap-2">
|
||||||
|
<AlertCircle className="w-4 h-4" />
|
||||||
|
Note
|
||||||
|
</h4>
|
||||||
|
<p>
|
||||||
|
설정(길이, 포맷)을 변경하려면 <strong>Settings</strong> 탭으로 이동하세요.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isScanning && !isPending && (
|
{isScanning && !isPending && (
|
||||||
<div className="mt-8 flex items-center gap-2 px-4 py-2 bg-amber-50 text-amber-700 border border-amber-200 rounded-full text-sm animate-pulse">
|
<div className="mt-8 flex items-center gap-2 px-4 py-2 bg-amber-50 text-amber-700 border border-amber-200 rounded-full text-sm animate-pulse">
|
||||||
<Activity className="w-4 h-4" /> Background scanning is active in Inventory...
|
<Activity className="w-4 h-4" /> Background scanning is active in Inventory...
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -72,10 +72,10 @@ export const SettingsPanel: React.FC<Props> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveLocalSettings = () => {
|
const handleSaveLocalSettings = () => {
|
||||||
onSaveQuickTestConfig({
|
onSaveQuickTestConfig({
|
||||||
length: qtLength,
|
length: qtLength,
|
||||||
format: qtFormat
|
format: qtFormat
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const baudRates = [9600, 19200, 38400, 57600, 115200];
|
const baudRates = [9600, 19200, 38400, 57600, 115200];
|
||||||
@@ -88,284 +88,290 @@ export const SettingsPanel: React.FC<Props> = ({
|
|||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-8 pb-6 border-b border-slate-100">
|
<div className="mb-8 pb-6 border-b border-slate-100">
|
||||||
<h2 className="text-2xl font-bold text-slate-800">System Configuration</h2>
|
<h2 className="text-2xl font-bold text-slate-800">System Configuration</h2>
|
||||||
<p className="text-slate-500 mt-1">
|
<p className="text-slate-500 mt-1">
|
||||||
Manage device parameters and local browser settings.
|
장치 매개변수 및 로컬 브라우저 설정을 관리합니다.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 1. Quick Test Settings Card */}
|
|
||||||
<div className="mb-10">
|
{/* 2. Reader Configuration */}
|
||||||
<div className="flex items-center gap-2 mb-4">
|
<div className="flex-1">
|
||||||
<Settings2 className="w-5 h-5 text-indigo-600" />
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6">
|
||||||
<h3 className="text-lg font-bold text-slate-800">Quick Test Settings (Local)</h3>
|
<div className="flex items-center gap-2">
|
||||||
|
<HardDrive className="w-5 h-5 text-blue-600" />
|
||||||
|
<h3 className="text-lg font-bold text-slate-800">Reader Configuration (Device)</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Device Actions */}
|
||||||
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
|
<button
|
||||||
|
onClick={onGetInfo}
|
||||||
|
className="flex items-center justify-center gap-2 px-4 py-2 bg-slate-100 text-slate-700 border border-slate-200 rounded-lg hover:bg-slate-200 hover:text-slate-900 transition-all font-medium"
|
||||||
|
>
|
||||||
|
<RefreshCw className={`w-4 h-4 ${!isLoaded ? 'animate-pulse' : ''}`} />
|
||||||
|
Read
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleApplyReaderSettings}
|
||||||
|
className="flex items-center justify-center gap-2 px-5 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 shadow-sm hover:shadow transition-colors font-medium"
|
||||||
|
>
|
||||||
|
<Save className="w-4 h-4" />
|
||||||
|
Write
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={onFactoryReset}
|
||||||
|
className="flex items-center justify-center gap-2 px-4 py-2 bg-white border border-red-200 text-red-600 rounded-lg hover:bg-red-50 hover:text-red-700 hover:border-red-300 transition-colors font-medium"
|
||||||
|
>
|
||||||
|
<RotateCcw className="w-4 h-4" />
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-indigo-50/50 p-6 rounded-xl border border-indigo-100">
|
{/* Status Info */}
|
||||||
<p className="text-sm text-indigo-900 mb-6 border-l-4 border-indigo-400 pl-3">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||||
These settings are stored in your browser's local storage and control the behavior of the "Quick Test" tab.
|
<div className="bg-slate-50 border border-slate-100 rounded-lg p-4 flex items-center gap-3">
|
||||||
</p>
|
<div className="p-2 bg-white rounded-md shadow-sm text-blue-600"><Cpu className="w-5 h-5" /></div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 mb-6">
|
<div>
|
||||||
<div className="space-y-3">
|
<div className="text-xs text-slate-400 font-semibold uppercase">Model</div>
|
||||||
<label className="text-sm font-medium text-slate-600">Read/Write Length (Words)</label>
|
<div className="text-sm font-bold text-slate-700">{readerInfo?.model || '--'}</div>
|
||||||
<div className="flex items-center gap-4">
|
</div>
|
||||||
<label className="flex items-center gap-2 cursor-pointer bg-white px-4 py-2 rounded-lg border border-indigo-100 shadow-sm">
|
</div>
|
||||||
<input
|
<div className="bg-slate-50 border border-slate-100 rounded-lg p-4 flex items-center gap-3">
|
||||||
type="radio"
|
<div className="p-2 bg-white rounded-md shadow-sm text-emerald-600"><Activity className="w-5 h-5" /></div>
|
||||||
checked={qtLength === 3}
|
<div>
|
||||||
onChange={() => setQtLength(3)}
|
<div className="text-xs text-slate-400 font-semibold uppercase">Firmware</div>
|
||||||
className="w-4 h-4 text-indigo-600 focus:ring-indigo-500 border-gray-300"
|
<div className="text-sm font-bold text-slate-700">{readerInfo?.version || '--'}</div>
|
||||||
/>
|
</div>
|
||||||
<span className="text-sm text-slate-700">3 Words (6 Bytes)</span>
|
</div>
|
||||||
</label>
|
<div className="bg-slate-50 border border-slate-100 rounded-lg p-4 flex items-center gap-3">
|
||||||
<label className="flex items-center gap-2 cursor-pointer bg-white px-4 py-2 rounded-lg border border-indigo-100 shadow-sm">
|
<div className="p-2 bg-white rounded-md shadow-sm text-violet-600"><Radio className="w-5 h-5" /></div>
|
||||||
<input
|
<div>
|
||||||
type="radio"
|
<div className="text-xs text-slate-400 font-semibold uppercase">Protocol</div>
|
||||||
checked={qtLength === 4}
|
<div className="text-sm font-bold text-slate-700">{readerInfo?.protocol || 'EPCC1-G2'}</div>
|
||||||
onChange={() => setQtLength(4)}
|
</div>
|
||||||
className="w-4 h-4 text-indigo-600 focus:ring-indigo-500 border-gray-300"
|
</div>
|
||||||
/>
|
<div className="bg-slate-50 border border-slate-100 rounded-lg p-4 flex items-center gap-3">
|
||||||
<span className="text-sm text-slate-700">4 Words (8 Bytes)</span>
|
<div className="p-2 bg-white rounded-md shadow-sm text-amber-600"><Zap className="w-5 h-5" /></div>
|
||||||
</label>
|
<div>
|
||||||
</div>
|
<div className="text-xs text-slate-400 font-semibold uppercase">Power</div>
|
||||||
</div>
|
<div className="text-sm font-bold text-slate-700">{readerInfo ? `${readerInfo.power} dBm` : '--'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
{/* Device Settings Form */}
|
||||||
<label className="text-sm font-medium text-slate-600">Data Display Format</label>
|
<div className={`space-y-8 transition-all duration-300 ${isLoaded ? 'opacity-100' : 'opacity-50 pointer-events-none grayscale'}`}>
|
||||||
<div className="flex items-center gap-4">
|
{/* 1. Communication Settings */}
|
||||||
<label className="flex items-center gap-2 cursor-pointer bg-white px-4 py-2 rounded-lg border border-indigo-100 shadow-sm">
|
<div>
|
||||||
<input
|
<h3 className="text-sm font-bold text-slate-900 uppercase tracking-wider mb-4 flex items-center gap-2">
|
||||||
type="radio"
|
<span className="w-1 h-4 bg-blue-500 rounded-full"></span>
|
||||||
checked={qtFormat === 'hex'}
|
Communication Parameters
|
||||||
onChange={() => setQtFormat('hex')}
|
</h3>
|
||||||
className="w-4 h-4 text-indigo-600 focus:ring-indigo-500 border-gray-300"
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
/>
|
<div className="space-y-1">
|
||||||
<span className="text-sm text-slate-700">HEX</span>
|
<label className="text-sm font-medium text-slate-600">Reader Address (Hex)</label>
|
||||||
</label>
|
<input
|
||||||
<label className="flex items-center gap-2 cursor-pointer bg-white px-4 py-2 rounded-lg border border-indigo-100 shadow-sm">
|
type="text"
|
||||||
<input
|
value={address.toString(16).toUpperCase()}
|
||||||
type="radio"
|
onChange={e => {
|
||||||
checked={qtFormat === 'ascii'}
|
const val = parseInt(e.target.value, 16);
|
||||||
onChange={() => setQtFormat('ascii')}
|
if (!isNaN(val) && val >= 0 && val <= 0xFF) setAddress(val);
|
||||||
className="w-4 h-4 text-indigo-600 focus:ring-indigo-500 border-gray-300"
|
}}
|
||||||
/>
|
className="w-full bg-white border border-slate-300 rounded-lg px-3 py-2 text-slate-900 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all"
|
||||||
<span className="text-sm text-slate-700">ASCII</span>
|
maxLength={2}
|
||||||
</label>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-sm font-medium text-slate-600">Baud Rate</label>
|
||||||
|
<select
|
||||||
|
value={baudRate}
|
||||||
|
onChange={e => setBaudRate(Number(e.target.value))}
|
||||||
|
className="w-full bg-white border border-slate-300 rounded-lg px-3 py-2 text-slate-900 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all"
|
||||||
|
>
|
||||||
|
{baudRates.map(b => <option key={b} value={b}>{b} bps</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-sm font-medium text-slate-600">Max Response Time</label>
|
||||||
|
<select
|
||||||
|
value={maxResponseTime}
|
||||||
|
onChange={e => setMaxResponseTime(Number(e.target.value))}
|
||||||
|
className="w-full bg-white border border-slate-300 rounded-lg px-3 py-2 text-slate-900 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all"
|
||||||
|
>
|
||||||
|
<option value={0}>Auto</option>
|
||||||
|
<option value={10}>10ms</option>
|
||||||
|
<option value={20}>20ms</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 2. RF Settings */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-bold text-slate-900 uppercase tracking-wider mb-4 flex items-center gap-2">
|
||||||
|
<span className="w-1 h-4 bg-emerald-500 rounded-full"></span>
|
||||||
|
RF Configuration
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-sm font-medium text-slate-600">Power Output</label>
|
||||||
|
<select
|
||||||
|
value={power}
|
||||||
|
onChange={e => setPower(Number(e.target.value))}
|
||||||
|
className="w-full bg-white border border-slate-300 rounded-lg px-3 py-2 text-slate-900 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all"
|
||||||
|
>
|
||||||
|
{powerLevels.map(p => <option key={p} value={p}>{p} dBm</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-sm font-medium text-slate-600">Min Frequency</label>
|
||||||
|
<select
|
||||||
|
value={minFreq}
|
||||||
|
onChange={e => setMinFreq(Number(e.target.value))}
|
||||||
|
disabled={isSingleFreq}
|
||||||
|
className="w-full bg-white border border-slate-300 rounded-lg px-3 py-2 text-slate-900 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all disabled:bg-slate-50 disabled:text-slate-400"
|
||||||
|
>
|
||||||
|
<option value={0}>Default</option>
|
||||||
|
<option value={1}>Custom 1</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<label className="text-sm font-medium text-slate-600">Max Frequency</label>
|
||||||
|
<label className="flex items-center gap-1.5 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isSingleFreq}
|
||||||
|
onChange={e => setIsSingleFreq(e.target.checked)}
|
||||||
|
className="w-3.5 h-3.5 rounded text-blue-600 focus:ring-blue-500 border-slate-300"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-slate-500">Single Freq</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
value={maxFreq}
|
||||||
|
onChange={e => setMaxFreq(Number(e.target.value))}
|
||||||
|
disabled={isSingleFreq}
|
||||||
|
className="w-full bg-white border border-slate-300 rounded-lg px-3 py-2 text-slate-900 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all disabled:bg-slate-50 disabled:text-slate-400"
|
||||||
|
>
|
||||||
|
<option value={0}>Default</option>
|
||||||
|
<option value={1}>Custom 1</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<div className="bg-slate-50/80 rounded-lg p-5 border border-slate-200">
|
||||||
onClick={handleSaveLocalSettings}
|
<label className="text-xs font-bold text-slate-500 uppercase tracking-wide mb-3 block">Frequency Region / Band</label>
|
||||||
className="flex items-center justify-center gap-2 px-5 py-2.5 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 shadow-sm hover:shadow transition-colors font-medium"
|
<div className="flex flex-wrap gap-x-8 gap-y-4">
|
||||||
title="Save these settings to browser"
|
{[
|
||||||
>
|
{ label: 'User Defined', value: FrequencyBand.USER },
|
||||||
<Save className="w-4 h-4" />
|
{ label: 'Chinese Band', value: FrequencyBand.CHINESE },
|
||||||
Save Local Settings
|
{ label: 'US Band', value: FrequencyBand.US },
|
||||||
</button>
|
{ label: 'Korean Band', value: FrequencyBand.KOREAN },
|
||||||
|
{ label: 'EU Band', value: FrequencyBand.EU },
|
||||||
|
].map((item) => (
|
||||||
|
<label key={item.value} className="flex items-center gap-2 cursor-pointer group">
|
||||||
|
<div className={`w-4 h-4 rounded-full border flex items-center justify-center transition-colors ${band === item.value ? 'border-blue-600 bg-blue-600' : 'border-slate-300 bg-white group-hover:border-blue-400'}`}>
|
||||||
|
{band === item.value && <div className="w-1.5 h-1.5 rounded-full bg-white" />}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="band"
|
||||||
|
checked={band === item.value}
|
||||||
|
onChange={() => setBand(item.value)}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
<span className={`text-sm ${band === item.value ? 'text-slate-900 font-medium' : 'text-slate-600'}`}>{item.label}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr className="border-slate-100 mb-10" />
|
<hr className="border-slate-100 mb-10" />
|
||||||
|
|
||||||
{/* 2. Reader Configuration */}
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<HardDrive className="w-5 h-5 text-blue-600" />
|
|
||||||
<h3 className="text-lg font-bold text-slate-800">Reader Configuration (Device)</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Device Actions */}
|
{/* 1. Quick Test Settings Card */}
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="mb-10">
|
||||||
<button
|
<div className="flex items-center gap-2 mb-4">
|
||||||
onClick={onGetInfo}
|
<Settings2 className="w-5 h-5 text-indigo-600" />
|
||||||
className="flex items-center justify-center gap-2 px-4 py-2 bg-slate-100 text-slate-700 border border-slate-200 rounded-lg hover:bg-slate-200 hover:text-slate-900 transition-all font-medium"
|
<h3 className="text-lg font-bold text-slate-800">Quick Test Settings (Local)</h3>
|
||||||
>
|
</div>
|
||||||
<RefreshCw className={`w-4 h-4 ${!isLoaded ? 'animate-pulse' : ''}`} />
|
|
||||||
Read Config
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
<div className="bg-indigo-50/50 p-6 rounded-xl border border-indigo-100">
|
||||||
onClick={handleApplyReaderSettings}
|
<p className="text-sm text-indigo-900 mb-6 border-l-4 border-indigo-400 pl-3">
|
||||||
className="flex items-center justify-center gap-2 px-5 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 shadow-sm hover:shadow transition-colors font-medium"
|
이 설정은 브라우저의 로컬 저장소에 저장되며 "Quick Test" 탭의 동작을 제어합니다.
|
||||||
>
|
</p>
|
||||||
<Save className="w-4 h-4" />
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 mb-6">
|
||||||
Apply Config
|
<div className="space-y-3">
|
||||||
</button>
|
<label className="text-sm font-medium text-slate-600">Read/Write Length (Words)</label>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<label className="flex items-center gap-2 cursor-pointer bg-white px-4 py-2 rounded-lg border border-indigo-100 shadow-sm">
|
||||||
onClick={onFactoryReset}
|
<input
|
||||||
className="flex items-center justify-center gap-2 px-4 py-2 bg-white border border-red-200 text-red-600 rounded-lg hover:bg-red-50 hover:text-red-700 hover:border-red-300 transition-colors font-medium"
|
type="radio"
|
||||||
>
|
checked={qtLength === 3}
|
||||||
<RotateCcw className="w-4 h-4" />
|
onChange={() => setQtLength(3)}
|
||||||
Reset
|
className="w-4 h-4 text-indigo-600 focus:ring-indigo-500 border-gray-300"
|
||||||
</button>
|
/>
|
||||||
</div>
|
<span className="text-sm text-slate-700">3 Words</span>
|
||||||
</div>
|
</label>
|
||||||
|
<label className="flex items-center gap-2 cursor-pointer bg-white px-4 py-2 rounded-lg border border-indigo-100 shadow-sm">
|
||||||
{/* Status Info */}
|
<input
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
type="radio"
|
||||||
<div className="bg-slate-50 border border-slate-100 rounded-lg p-4 flex items-center gap-3">
|
checked={qtLength === 4}
|
||||||
<div className="p-2 bg-white rounded-md shadow-sm text-blue-600"><Cpu className="w-5 h-5" /></div>
|
onChange={() => setQtLength(4)}
|
||||||
<div>
|
className="w-4 h-4 text-indigo-600 focus:ring-indigo-500 border-gray-300"
|
||||||
<div className="text-xs text-slate-400 font-semibold uppercase">Model</div>
|
/>
|
||||||
<div className="text-sm font-bold text-slate-700">{readerInfo?.model || '--'}</div>
|
<span className="text-sm text-slate-700">4 Words</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-slate-50 border border-slate-100 rounded-lg p-4 flex items-center gap-3">
|
|
||||||
<div className="p-2 bg-white rounded-md shadow-sm text-emerald-600"><Activity className="w-5 h-5" /></div>
|
<div className="space-y-3">
|
||||||
<div>
|
<label className="text-sm font-medium text-slate-600">Data Display Format</label>
|
||||||
<div className="text-xs text-slate-400 font-semibold uppercase">Firmware</div>
|
<div className="flex items-center gap-4">
|
||||||
<div className="text-sm font-bold text-slate-700">{readerInfo?.version || '--'}</div>
|
<label className="flex items-center gap-2 cursor-pointer bg-white px-4 py-2 rounded-lg border border-indigo-100 shadow-sm">
|
||||||
</div>
|
<input
|
||||||
</div>
|
type="radio"
|
||||||
<div className="bg-slate-50 border border-slate-100 rounded-lg p-4 flex items-center gap-3">
|
checked={qtFormat === 'hex'}
|
||||||
<div className="p-2 bg-white rounded-md shadow-sm text-violet-600"><Radio className="w-5 h-5" /></div>
|
onChange={() => setQtFormat('hex')}
|
||||||
<div>
|
className="w-4 h-4 text-indigo-600 focus:ring-indigo-500 border-gray-300"
|
||||||
<div className="text-xs text-slate-400 font-semibold uppercase">Protocol</div>
|
/>
|
||||||
<div className="text-sm font-bold text-slate-700">{readerInfo?.protocol || 'EPCC1-G2'}</div>
|
<span className="text-sm text-slate-700">HEX</span>
|
||||||
</div>
|
</label>
|
||||||
</div>
|
<label className="flex items-center gap-2 cursor-pointer bg-white px-4 py-2 rounded-lg border border-indigo-100 shadow-sm">
|
||||||
<div className="bg-slate-50 border border-slate-100 rounded-lg p-4 flex items-center gap-3">
|
<input
|
||||||
<div className="p-2 bg-white rounded-md shadow-sm text-amber-600"><Zap className="w-5 h-5" /></div>
|
type="radio"
|
||||||
<div>
|
checked={qtFormat === 'ascii'}
|
||||||
<div className="text-xs text-slate-400 font-semibold uppercase">Power</div>
|
onChange={() => setQtFormat('ascii')}
|
||||||
<div className="text-sm font-bold text-slate-700">{readerInfo ? `${readerInfo.power} dBm` : '--'}</div>
|
className="w-4 h-4 text-indigo-600 focus:ring-indigo-500 border-gray-300"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-slate-700">ASCII</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Device Settings Form */}
|
<button
|
||||||
<div className={`space-y-8 transition-all duration-300 ${isLoaded ? 'opacity-100' : 'opacity-50 pointer-events-none grayscale'}`}>
|
onClick={handleSaveLocalSettings}
|
||||||
{/* 1. Communication Settings */}
|
className="flex items-center justify-center gap-2 px-5 py-2.5 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 shadow-sm hover:shadow transition-colors font-medium"
|
||||||
<div>
|
title="Save these settings to browser"
|
||||||
<h3 className="text-sm font-bold text-slate-900 uppercase tracking-wider mb-4 flex items-center gap-2">
|
>
|
||||||
<span className="w-1 h-4 bg-blue-500 rounded-full"></span>
|
<Save className="w-4 h-4" />
|
||||||
Communication Parameters
|
Save Local Settings
|
||||||
</h3>
|
</button>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
</div>
|
||||||
<div className="space-y-1">
|
|
||||||
<label className="text-sm font-medium text-slate-600">Reader Address (Hex)</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={address.toString(16).toUpperCase()}
|
|
||||||
onChange={e => {
|
|
||||||
const val = parseInt(e.target.value, 16);
|
|
||||||
if (!isNaN(val) && val >= 0 && val <= 0xFF) setAddress(val);
|
|
||||||
}}
|
|
||||||
className="w-full bg-white border border-slate-300 rounded-lg px-3 py-2 text-slate-900 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all"
|
|
||||||
maxLength={2}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<label className="text-sm font-medium text-slate-600">Baud Rate</label>
|
|
||||||
<select
|
|
||||||
value={baudRate}
|
|
||||||
onChange={e => setBaudRate(Number(e.target.value))}
|
|
||||||
className="w-full bg-white border border-slate-300 rounded-lg px-3 py-2 text-slate-900 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all"
|
|
||||||
>
|
|
||||||
{baudRates.map(b => <option key={b} value={b}>{b} bps</option>)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<label className="text-sm font-medium text-slate-600">Max Response Time</label>
|
|
||||||
<select
|
|
||||||
value={maxResponseTime}
|
|
||||||
onChange={e => setMaxResponseTime(Number(e.target.value))}
|
|
||||||
className="w-full bg-white border border-slate-300 rounded-lg px-3 py-2 text-slate-900 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all"
|
|
||||||
>
|
|
||||||
<option value={0}>Auto</option>
|
|
||||||
<option value={10}>10ms</option>
|
|
||||||
<option value={20}>20ms</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 2. RF Settings */}
|
|
||||||
<div>
|
|
||||||
<h3 className="text-sm font-bold text-slate-900 uppercase tracking-wider mb-4 flex items-center gap-2">
|
|
||||||
<span className="w-1 h-4 bg-emerald-500 rounded-full"></span>
|
|
||||||
RF Configuration
|
|
||||||
</h3>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<label className="text-sm font-medium text-slate-600">Power Output</label>
|
|
||||||
<select
|
|
||||||
value={power}
|
|
||||||
onChange={e => setPower(Number(e.target.value))}
|
|
||||||
className="w-full bg-white border border-slate-300 rounded-lg px-3 py-2 text-slate-900 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all"
|
|
||||||
>
|
|
||||||
{powerLevels.map(p => <option key={p} value={p}>{p} dBm</option>)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<label className="text-sm font-medium text-slate-600">Min Frequency</label>
|
|
||||||
<select
|
|
||||||
value={minFreq}
|
|
||||||
onChange={e => setMinFreq(Number(e.target.value))}
|
|
||||||
disabled={isSingleFreq}
|
|
||||||
className="w-full bg-white border border-slate-300 rounded-lg px-3 py-2 text-slate-900 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all disabled:bg-slate-50 disabled:text-slate-400"
|
|
||||||
>
|
|
||||||
<option value={0}>Default</option>
|
|
||||||
<option value={1}>Custom 1</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<label className="text-sm font-medium text-slate-600">Max Frequency</label>
|
|
||||||
<label className="flex items-center gap-1.5 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={isSingleFreq}
|
|
||||||
onChange={e => setIsSingleFreq(e.target.checked)}
|
|
||||||
className="w-3.5 h-3.5 rounded text-blue-600 focus:ring-blue-500 border-slate-300"
|
|
||||||
/>
|
|
||||||
<span className="text-xs text-slate-500">Single Freq</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<select
|
|
||||||
value={maxFreq}
|
|
||||||
onChange={e => setMaxFreq(Number(e.target.value))}
|
|
||||||
disabled={isSingleFreq}
|
|
||||||
className="w-full bg-white border border-slate-300 rounded-lg px-3 py-2 text-slate-900 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all disabled:bg-slate-50 disabled:text-slate-400"
|
|
||||||
>
|
|
||||||
<option value={0}>Default</option>
|
|
||||||
<option value={1}>Custom 1</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-slate-50/80 rounded-lg p-5 border border-slate-200">
|
|
||||||
<label className="text-xs font-bold text-slate-500 uppercase tracking-wide mb-3 block">Frequency Region / Band</label>
|
|
||||||
<div className="flex flex-wrap gap-x-8 gap-y-4">
|
|
||||||
{[
|
|
||||||
{ label: 'User Defined', value: FrequencyBand.USER },
|
|
||||||
{ label: 'Chinese Band', value: FrequencyBand.CHINESE },
|
|
||||||
{ label: 'US Band', value: FrequencyBand.US },
|
|
||||||
{ label: 'Korean Band', value: FrequencyBand.KOREAN },
|
|
||||||
{ label: 'EU Band', value: FrequencyBand.EU },
|
|
||||||
].map((item) => (
|
|
||||||
<label key={item.value} className="flex items-center gap-2 cursor-pointer group">
|
|
||||||
<div className={`w-4 h-4 rounded-full border flex items-center justify-center transition-colors ${band === item.value ? 'border-blue-600 bg-blue-600' : 'border-slate-300 bg-white group-hover:border-blue-400'}`}>
|
|
||||||
{band === item.value && <div className="w-1.5 h-1.5 rounded-full bg-white" />}
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="band"
|
|
||||||
checked={band === item.value}
|
|
||||||
onChange={() => setBand(item.value)}
|
|
||||||
className="hidden"
|
|
||||||
/>
|
|
||||||
<span className={`text-sm ${band === item.value ? 'text-slate-900 font-medium' : 'text-slate-600'}`}>{item.label}</span>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user