imetting/frontend/src/pages/admin/TerminalManagement.jsx

469 lines
15 KiB
JavaScript

import React, { useState, useEffect } from 'react';
import {
Plus,
Edit,
Trash2,
Search,
X,
Monitor,
Smartphone,
Tablet,
Power,
RefreshCw
} from 'lucide-react';
import apiClient from '../../utils/apiClient';
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
import ConfirmDialog from '../../components/ConfirmDialog';
import Toast from '../../components/Toast';
import PageLoading from '../../components/PageLoading';
import FormModal from '../../components/FormModal';
import './TerminalManagement.css';
const TerminalManagement = () => {
const [terminals, setTerminals] = useState([]); // All terminals
const [terminalTypes, setTerminalTypes] = useState([]);
const [loading, setLoading] = useState(true);
// Local state for filtering/pagination
const [keyword, setKeyword] = useState('');
const [filterType, setFilterType] = useState('');
const [filterStatus, setFilterStatus] = useState('');
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(20);
const [showModal, setShowModal] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const [selectedTerminal, setSelectedTerminal] = useState(null);
const [deleteConfirmInfo, setDeleteConfirmInfo] = useState(null);
const [toasts, setToasts] = useState([]);
const [formData, setFormData] = useState({
imei: '',
terminal_name: '',
terminal_type: '',
description: '',
status: 1
});
// Toast helper
const showToast = (message, type = 'info') => {
const id = Date.now();
setToasts(prev => [...prev, { id, message, type }]);
};
const removeToast = (id) => {
setToasts(prev => prev.filter(toast => toast.id !== id));
};
useEffect(() => {
fetchTerminalTypes();
fetchTerminals();
}, []);
const fetchTerminalTypes = async () => {
try {
const response = await apiClient.get(
buildApiUrl(API_ENDPOINTS.DICT_DATA.BY_TYPE('client_platform')),
{ params: { parent_code: 'TERMINAL' } }
);
if (response.code === '200') {
setTerminalTypes(response.data.items || []);
}
} catch (error) {
console.error('Failed to fetch terminal types:', error);
}
};
const fetchTerminals = async () => {
setLoading(true);
try {
// Fetch all terminals (using a large size)
const params = {
page: 1,
size: 10000
};
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.TERMINALS.LIST), { params });
if (response.code === '200') {
setTerminals(response.data.items);
}
} catch (error) {
console.error('Failed to fetch terminals:', error);
showToast('获取终端列表失败', 'error');
} finally {
setLoading(false);
}
};
// Local Filtering
const filteredTerminals = terminals.filter(terminal => {
const matchesKeyword = !keyword ||
terminal.imei.toLowerCase().includes(keyword.toLowerCase()) ||
(terminal.terminal_name && terminal.terminal_name.toLowerCase().includes(keyword.toLowerCase()));
const matchesType = !filterType || terminal.terminal_type === filterType;
const matchesStatus = filterStatus === '' || terminal.status === parseInt(filterStatus);
return matchesKeyword && matchesType && matchesStatus;
});
// Local Pagination
const total = filteredTerminals.length;
const paginatedTerminals = filteredTerminals.slice((page - 1) * pageSize, page * pageSize);
const handleReset = () => {
setKeyword('');
setFilterType('');
setFilterStatus('');
setPage(1);
};
const handleOpenCreate = () => {
setIsEditing(false);
setSelectedTerminal(null);
setFormData({
imei: '',
terminal_name: '',
terminal_type: terminalTypes.length > 0 ? terminalTypes[0].dict_code : '',
description: '',
status: 1
});
setShowModal(true);
};
const handleOpenEdit = (terminal) => {
setIsEditing(true);
setSelectedTerminal(terminal);
setFormData({
imei: terminal.imei,
terminal_name: terminal.terminal_name || '',
terminal_type: terminal.terminal_type,
description: terminal.description || '',
status: terminal.status
});
setShowModal(true);
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!formData.imei) {
showToast('请输入IMEI号', 'error');
return;
}
if (!formData.terminal_type) {
showToast('请选择终端类型', 'error');
return;
}
try {
if (isEditing) {
await apiClient.put(
buildApiUrl(API_ENDPOINTS.TERMINALS.UPDATE(selectedTerminal.id)),
formData
);
showToast('更新成功', 'success');
} else {
await apiClient.post(buildApiUrl(API_ENDPOINTS.TERMINALS.CREATE), formData);
showToast('创建成功', 'success');
}
setShowModal(false);
fetchTerminals();
} catch (error) {
const msg = error.response?.data?.message || '操作失败';
showToast(msg, 'error');
}
};
const handleDelete = async () => {
if (!deleteConfirmInfo) return;
try {
await apiClient.delete(buildApiUrl(API_ENDPOINTS.TERMINALS.DELETE(deleteConfirmInfo.id)));
showToast('删除成功', 'success');
fetchTerminals();
} catch (error) {
const msg = error.response?.data?.message || '删除失败';
showToast(msg, 'error');
} finally {
setDeleteConfirmInfo(null);
}
};
const handleToggleStatus = async (terminal) => {
try {
const newStatus = terminal.status === 1 ? 0 : 1;
await apiClient.post(
buildApiUrl(API_ENDPOINTS.TERMINALS.STATUS(terminal.id)),
null,
{ params: { status: newStatus } }
);
showToast(`${newStatus === 1 ? '启用' : '停用'}终端`, 'success');
// Update local state directly for better UX
setTerminals(prev => prev.map(t =>
t.id === terminal.id ? { ...t, status: newStatus } : t
));
} catch (error) {
showToast('状态更新失败', 'error');
}
};
const getTerminalTypeLabel = (code) => {
const type = terminalTypes.find(t => t.dict_code === code);
return type ? type.label_cn : code;
};
if (loading && terminals.length === 0) {
return <PageLoading message="加载终端数据..." />;
}
return (
<div className="terminal-management">
<div className="page-header">
<div className="header-left">
<h1>终端管理</h1>
<p className="subtitle">管理专用终端设备的接入与状态</p>
</div>
<button className="btn-primary" onClick={handleOpenCreate}>
<Plus size={20} />
添加终端
</button>
</div>
<div className="filter-bar">
<div className="search-group">
<div className="search-box">
<Search size={18} />
<input
type="text"
placeholder="搜索IMEI或终端名称"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
/>
{keyword && (
<button className="clear-search" onClick={() => setKeyword('')}>
<X size={16} />
</button>
)}
</div>
</div>
<div className="filter-group">
<select
value={filterType}
onChange={(e) => { setFilterType(e.target.value); setPage(1); }}
className="filter-select"
>
<option value="">所有类型</option>
{terminalTypes.map(t => (
<option key={t.dict_code} value={t.dict_code}>{t.label_cn}</option>
))}
</select>
<select
value={filterStatus}
onChange={(e) => { setFilterStatus(e.target.value); setPage(1); }}
className="filter-select"
>
<option value="">所有状态</option>
<option value="1">启用</option>
<option value="0">停用</option>
</select>
<button className="btn-icon" onClick={handleReset} title="重置筛选">
<RefreshCw size={18} />
</button>
</div>
</div>
<div className="table-container">
<table className="data-table">
<thead>
<tr>
<th>IMEI</th>
<th>终端名称</th>
<th>类型</th>
<th>当前绑定账号</th>
<th>状态</th>
<th>激活状态</th>
<th>最后在线</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{paginatedTerminals.length === 0 ? (
<tr>
<td colSpan="9" className="empty-state">暂无数据</td>
</tr>
) : (
paginatedTerminals.map(terminal => (
<tr key={terminal.id}>
<td className="font-mono truncate-cell" title={terminal.imei}>{terminal.imei}</td>
<td className="truncate-cell" title={terminal.terminal_name}>{terminal.terminal_name || '-'}</td>
<td>
<span className="type-badge">
{getTerminalTypeLabel(terminal.terminal_type)}
</span>
</td>
<td>
{terminal.current_user_caption ? (
<div className="user-info truncate-cell" title={`${terminal.current_user_caption} (${terminal.current_username || ''})`}>
<span className="user-name">{terminal.current_user_caption}</span>
{terminal.current_username && <span className="user-sub text-xs text-gray-500">({terminal.current_username})</span>}
</div>
) : (
<span className="text-gray-400">-</span>
)}
</td>
<td>
<div className="status-toggle" onClick={() => handleToggleStatus(terminal)}>
<div className={`toggle-switch ${terminal.status === 1 ? 'on' : 'off'}`}>
<div className="toggle-slider"></div>
</div>
<span className="status-text">{terminal.status === 1 ? '启用' : '停用'}</span>
</div>
</td>
<td>
<span className={`status-dot ${terminal.is_activated === 1 ? 'active' : 'inactive'}`}></span>
{terminal.is_activated === 1 ? '已激活' : '未激活'}
</td>
<td>{terminal.last_online_at ? new Date(terminal.last_online_at).toLocaleString() : '-'}</td>
<td>{new Date(terminal.created_at).toLocaleDateString()}</td>
<td>
<div className="action-buttons">
<button
className="btn-icon btn-edit"
onClick={() => handleOpenEdit(terminal)}
title="编辑"
>
<Edit size={16} />
</button>
<button
className="btn-icon btn-delete"
onClick={() => setDeleteConfirmInfo({ id: terminal.id, name: terminal.imei })}
title="删除"
>
<Trash2 size={16} />
</button>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{/* 分页 */}
{total > 0 && (
<div className="pagination">
<div className="pagination-info">
{total} 条记录
</div>
<div className="pagination-controls">
<button
disabled={page === 1}
onClick={() => setPage(p => p - 1)}
>
上一页
</button>
<span className="page-number">{page}</span>
<button
disabled={page * pageSize >= total}
onClick={() => setPage(p => p + 1)}
>
下一页
</button>
</div>
</div>
)}
{/* 弹窗 */}
<FormModal
isOpen={showModal}
onClose={() => setShowModal(false)}
title={isEditing ? '编辑终端' : '添加终端'}
actions={
<>
<button type="button" className="btn-secondary" onClick={() => setShowModal(false)}>取消</button>
<button type="button" className="btn-primary" onClick={handleSubmit}>保存</button>
</>
}
>
<div className="form-group">
<label>IMEI *</label>
<input
type="text"
value={formData.imei}
onChange={e => setFormData({...formData, imei: e.target.value})}
placeholder="请输入设备IMEI号"
disabled={isEditing} // IMEI通常不可修改
required
/>
</div>
<div className="form-group">
<label>终端名称</label>
<input
type="text"
value={formData.terminal_name}
onChange={e => setFormData({...formData, terminal_name: e.target.value})}
placeholder="给设备起个名字"
/>
</div>
<div className="form-group">
<label>终端类型 *</label>
<select
value={formData.terminal_type}
onChange={e => setFormData({...formData, terminal_type: e.target.value})}
required
>
<option value="" disabled>请选择类型</option>
{terminalTypes.map(t => (
<option key={t.dict_code} value={t.dict_code}>{t.label_cn}</option>
))}
</select>
</div>
<div className="form-group">
<label>备注</label>
<textarea
value={formData.description}
onChange={e => setFormData({...formData, description: e.target.value})}
placeholder="设备说明..."
rows={3}
/>
</div>
<div className="form-group checkbox-group">
<label>
<input
type="checkbox"
checked={formData.status === 1}
onChange={e => setFormData({...formData, status: e.target.checked ? 1 : 0})}
/>
立即启用
</label>
</div>
</FormModal>
{/* 删除确认 */}
{deleteConfirmInfo && (
<ConfirmDialog
isOpen={true}
title="删除终端"
message={`确定要删除IMEI为 ${deleteConfirmInfo.name} 的终端吗?`}
onConfirm={handleDelete}
onClose={() => setDeleteConfirmInfo(null)}
type="danger"
/>
)}
{/* Toasts */}
{toasts.map(t => (
<Toast key={t.id} message={t.message} type={t.type} onClose={() => removeToast(t.id)} />
))}
</div>
);
};
export default TerminalManagement;