475 lines
21 KiB
TypeScript
475 lines
21 KiB
TypeScript
|
|
import { useState, useCallback, useEffect } from 'react'
|
|||
|
|
import { createPortal } from 'react-dom'
|
|||
|
|
import { isElectron } from '../utils/electronBridge'
|
|||
|
|
|
|||
|
|
interface Host {
|
|||
|
|
anchorId: string
|
|||
|
|
country: string
|
|||
|
|
invitationType: number // 1=普票, 2=金票
|
|||
|
|
state: number
|
|||
|
|
onlineFans?: number
|
|||
|
|
hostsLevel?: string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface HostListDialogProps {
|
|||
|
|
visible: boolean
|
|||
|
|
onClose: () => void
|
|||
|
|
onSave: (hosts: Host[]) => void
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 等级数据定义
|
|||
|
|
const LEVEL_OPTIONS = [
|
|||
|
|
{
|
|||
|
|
label: 'A', value: 'A',
|
|||
|
|
children: [
|
|||
|
|
{ label: 'A1', value: 'A1' },
|
|||
|
|
{ label: 'A2', value: 'A2' },
|
|||
|
|
{ label: 'A3', value: 'A3' },
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
label: 'B', value: 'B',
|
|||
|
|
children: [
|
|||
|
|
{ label: 'B1', value: 'B1' },
|
|||
|
|
{ label: 'B2', value: 'B2' },
|
|||
|
|
{ label: 'B3', value: 'B3' },
|
|||
|
|
{ label: 'B4', value: 'B4' },
|
|||
|
|
{ label: 'B5', value: 'B5' },
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
label: 'C', value: 'C',
|
|||
|
|
children: [
|
|||
|
|
{ label: 'C1', value: 'C1' },
|
|||
|
|
{ label: 'C2', value: 'C2' },
|
|||
|
|
{ label: 'C3', value: 'C3' },
|
|||
|
|
{ label: 'C4', value: 'C4' },
|
|||
|
|
{ label: 'C5', value: 'C5' },
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
label: 'D', value: 'D',
|
|||
|
|
children: [
|
|||
|
|
{ label: 'D1', value: 'D1' },
|
|||
|
|
{ label: 'D2', value: 'D2' },
|
|||
|
|
{ label: 'D3', value: 'D3' },
|
|||
|
|
{ label: 'D4', value: 'D4' },
|
|||
|
|
{ label: 'D5', value: 'D5' },
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
// 获取所有子级等级值
|
|||
|
|
const getAllChildLevels = (parentValue: string): string[] => {
|
|||
|
|
const parent = LEVEL_OPTIONS.find(p => p.value === parentValue)
|
|||
|
|
return parent ? parent.children.map(c => c.value) : []
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function HostListDialog({ visible, onClose, onSave }: HostListDialogProps) {
|
|||
|
|
const [hosts, setHosts] = useState<Host[]>([])
|
|||
|
|
const [selected, setSelected] = useState<Set<string>>(new Set())
|
|||
|
|
const [filters, setFilters] = useState({
|
|||
|
|
gold: true,
|
|||
|
|
ordinary: true,
|
|||
|
|
minOnlineFans: '',
|
|||
|
|
maxOnlineFans: '',
|
|||
|
|
})
|
|||
|
|
const [maxCount, setMaxCount] = useState<number>(100)
|
|||
|
|
const [selectedLevels, setSelectedLevels] = useState<Set<string>>(new Set()) // 选中的等级
|
|||
|
|
const [showLevelDropdown, setShowLevelDropdown] = useState(false)
|
|||
|
|
|
|||
|
|
// 锁定 Body 滚动
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (visible) {
|
|||
|
|
document.body.style.overflow = 'hidden'
|
|||
|
|
} else {
|
|||
|
|
document.body.style.overflow = ''
|
|||
|
|
}
|
|||
|
|
return () => {
|
|||
|
|
document.body.style.overflow = ''
|
|||
|
|
}
|
|||
|
|
}, [visible])
|
|||
|
|
|
|||
|
|
// 加载主播数据和配置
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (visible) {
|
|||
|
|
loadHosts()
|
|||
|
|
loadConfig()
|
|||
|
|
}
|
|||
|
|
}, [visible])
|
|||
|
|
|
|||
|
|
const loadHosts = async () => {
|
|||
|
|
if (!isElectron()) return
|
|||
|
|
try {
|
|||
|
|
const data = await window.electronAPI!.loadAnchorData()
|
|||
|
|
setHosts(data as Host[])
|
|||
|
|
setSelected(new Set())
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error('加载主播数据失败:', e)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 从后端加载配置(包括 maxAnchorCount 和 hostsLevelList)
|
|||
|
|
const loadConfig = async () => {
|
|||
|
|
if (!isElectron()) return
|
|||
|
|
try {
|
|||
|
|
const config = await window.electronAPI!.getAutomationConfig()
|
|||
|
|
if ((config as any)?.maxAnchorCount !== undefined) {
|
|||
|
|
setMaxCount((config as any).maxAnchorCount)
|
|||
|
|
}
|
|||
|
|
// 加载等级过滤配置
|
|||
|
|
if (config?.filters?.hostsLevelList) {
|
|||
|
|
setSelectedLevels(new Set(config.filters.hostsLevelList))
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error('加载配置失败:', e)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新等级过滤配置到后端
|
|||
|
|
const updateLevelFilter = async (levels: Set<string>) => {
|
|||
|
|
setSelectedLevels(levels)
|
|||
|
|
if (!isElectron()) return
|
|||
|
|
try {
|
|||
|
|
await window.electronAPI!.updateAutomationConfig({
|
|||
|
|
filters: { hostsLevelList: Array.from(levels) }
|
|||
|
|
} as any)
|
|||
|
|
console.log('[HostListDialog] 等级过滤已更新:', Array.from(levels))
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error('更新等级配置失败:', e)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 切换单个等级选中状态
|
|||
|
|
const toggleLevel = (level: string) => {
|
|||
|
|
const newSet = new Set(selectedLevels)
|
|||
|
|
if (newSet.has(level)) {
|
|||
|
|
newSet.delete(level)
|
|||
|
|
} else {
|
|||
|
|
newSet.add(level)
|
|||
|
|
}
|
|||
|
|
updateLevelFilter(newSet)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 切换整个大类
|
|||
|
|
const toggleParentLevel = (parentValue: string) => {
|
|||
|
|
const childLevels = getAllChildLevels(parentValue)
|
|||
|
|
const allSelected = childLevels.every(l => selectedLevels.has(l))
|
|||
|
|
const newSet = new Set(selectedLevels)
|
|||
|
|
|
|||
|
|
if (allSelected) {
|
|||
|
|
// 全部取消
|
|||
|
|
childLevels.forEach(l => newSet.delete(l))
|
|||
|
|
} else {
|
|||
|
|
// 全部选中
|
|||
|
|
childLevels.forEach(l => newSet.add(l))
|
|||
|
|
}
|
|||
|
|
updateLevelFilter(newSet)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新 maxAnchorCount 到后端
|
|||
|
|
const updateMaxCount = async (value: number) => {
|
|||
|
|
setMaxCount(value)
|
|||
|
|
if (!isElectron()) return
|
|||
|
|
try {
|
|||
|
|
await window.electronAPI!.updateAutomationConfig({ maxAnchorCount: value } as any)
|
|||
|
|
console.log('[HostListDialog] 主播数据上限已更新:', value)
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error('更新配置失败:', e)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 筛选后的主播列表
|
|||
|
|
const filteredHosts = hosts.filter(h => {
|
|||
|
|
if (!filters.gold && h.invitationType === 2) return false
|
|||
|
|
if (!filters.ordinary && h.invitationType === 1) return false
|
|||
|
|
if (filters.minOnlineFans && h.onlineFans !== undefined) {
|
|||
|
|
if (h.onlineFans < parseInt(filters.minOnlineFans)) return false
|
|||
|
|
}
|
|||
|
|
if (filters.maxOnlineFans && h.onlineFans !== undefined) {
|
|||
|
|
if (h.onlineFans > parseInt(filters.maxOnlineFans)) return false
|
|||
|
|
}
|
|||
|
|
// 等级过滤:如果选择了等级,则只显示选中等级的主播
|
|||
|
|
if (selectedLevels.size > 0 && h.hostsLevel) {
|
|||
|
|
if (!selectedLevels.has(h.hostsLevel)) return false
|
|||
|
|
}
|
|||
|
|
return true
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const selectedCount = selected.size
|
|||
|
|
|
|||
|
|
const toggleSelect = useCallback((id: string) => {
|
|||
|
|
setSelected(prev => {
|
|||
|
|
const next = new Set(prev)
|
|||
|
|
if (next.has(id)) next.delete(id)
|
|||
|
|
else next.add(id)
|
|||
|
|
return next
|
|||
|
|
})
|
|||
|
|
}, [])
|
|||
|
|
|
|||
|
|
const selectAll = () => {
|
|||
|
|
setSelected(new Set(filteredHosts.map(h => h.anchorId)))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const selectNone = () => {
|
|||
|
|
setSelected(new Set())
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const invertSelect = () => {
|
|||
|
|
setSelected(prev => {
|
|||
|
|
const next = new Set<string>()
|
|||
|
|
filteredHosts.forEach(h => {
|
|||
|
|
if (!prev.has(h.anchorId)) next.add(h.anchorId)
|
|||
|
|
})
|
|||
|
|
return next
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const deleteSelected = () => {
|
|||
|
|
if (!selected.size) return
|
|||
|
|
if (!confirm(`确认删除选中的 ${selected.size} 项吗?`)) return
|
|||
|
|
|
|||
|
|
const remaining = hosts.filter(h => !selected.has(h.anchorId))
|
|||
|
|
setHosts(remaining)
|
|||
|
|
setSelected(new Set())
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleSave = async () => {
|
|||
|
|
if (isElectron()) {
|
|||
|
|
await window.electronAPI!.saveAnchorData(hosts)
|
|||
|
|
}
|
|||
|
|
onSave(hosts)
|
|||
|
|
onClose()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!visible) return null
|
|||
|
|
|
|||
|
|
return createPortal(
|
|||
|
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center" style={{ zIndex: 9999 }}>
|
|||
|
|
<div className="bg-white rounded-xl shadow-2xl w-full max-w-4xl max-h-[80vh] flex flex-col mx-4">
|
|||
|
|
{/* 头部 */}
|
|||
|
|
<div className="p-4 border-b border-gray-200 flex items-center justify-between">
|
|||
|
|
<div>
|
|||
|
|
<h3 className="text-lg font-semibold text-gray-600">主播管理</h3>
|
|||
|
|
<span className="text-sm text-gray-700">
|
|||
|
|
已选 {selectedCount} / 共 {filteredHosts.length}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<button onClick={onClose} className="text-gray-700 hover:text-gray-700 text-xl">✕</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 工具栏 */}
|
|||
|
|
<div className="p-4 border-b border-gray-100 space-y-3">
|
|||
|
|
<div className="flex flex-wrap gap-2">
|
|||
|
|
<button onClick={selectAll} className="px-3 py-1.5 text-sm bg-blue-100 text-blue-700 hover:bg-blue-200 rounded border border-blue-300">全选</button>
|
|||
|
|
<button onClick={selectNone} className="px-3 py-1.5 text-sm bg-slate-100 text-slate-700 hover:bg-slate-200 rounded border border-slate-300">全不选</button>
|
|||
|
|
<button onClick={invertSelect} className="px-3 py-1.5 text-sm bg-slate-100 text-slate-700 hover:bg-slate-200 rounded border border-slate-300">反选</button>
|
|||
|
|
<button
|
|||
|
|
onClick={deleteSelected}
|
|||
|
|
disabled={!selectedCount}
|
|||
|
|
className="px-3 py-1.5 text-sm bg-red-100 text-red-600 hover:bg-red-200 rounded disabled:opacity-50"
|
|||
|
|
>
|
|||
|
|
删除选中
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 筛选 */}
|
|||
|
|
<div className="flex flex-wrap items-center gap-4 text-sm">
|
|||
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|||
|
|
<input
|
|||
|
|
type="checkbox"
|
|||
|
|
checked={filters.gold}
|
|||
|
|
onChange={(e) => setFilters(f => ({ ...f, gold: e.target.checked }))}
|
|||
|
|
className="w-4 h-4"
|
|||
|
|
/>
|
|||
|
|
<span className="text-yellow-600">金票</span>
|
|||
|
|
</label>
|
|||
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|||
|
|
<input
|
|||
|
|
type="checkbox"
|
|||
|
|
checked={filters.ordinary}
|
|||
|
|
onChange={(e) => setFilters(f => ({ ...f, ordinary: e.target.checked }))}
|
|||
|
|
className="w-4 h-4"
|
|||
|
|
/>
|
|||
|
|
<span>普票</span>
|
|||
|
|
</label>
|
|||
|
|
|
|||
|
|
<span className="text-gray-700">在线人数</span>
|
|||
|
|
<input
|
|||
|
|
type="number"
|
|||
|
|
placeholder="最小"
|
|||
|
|
value={filters.minOnlineFans}
|
|||
|
|
onChange={(e) => setFilters(f => ({ ...f, minOnlineFans: e.target.value }))}
|
|||
|
|
className="w-20 px-2 py-1 border border-gray-300 rounded text-sm"
|
|||
|
|
/>
|
|||
|
|
<span>~</span>
|
|||
|
|
<input
|
|||
|
|
type="number"
|
|||
|
|
placeholder="最大"
|
|||
|
|
value={filters.maxOnlineFans}
|
|||
|
|
onChange={(e) => setFilters(f => ({ ...f, maxOnlineFans: e.target.value }))}
|
|||
|
|
className="w-20 px-2 py-1 border border-gray-300 rounded text-sm"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
{/* 等级过滤 */}
|
|||
|
|
<div className="relative border-l border-gray-200 pl-4 ml-2">
|
|||
|
|
<button
|
|||
|
|
onClick={() => setShowLevelDropdown(!showLevelDropdown)}
|
|||
|
|
className="flex items-center gap-2 px-3 py-1 border border-gray-300 rounded text-sm hover:bg-gray-50"
|
|||
|
|
>
|
|||
|
|
<span className="text-gray-700 font-medium">等级过滤</span>
|
|||
|
|
<span className="text-xs text-blue-600">
|
|||
|
|
{selectedLevels.size > 0 ? `已选 ${selectedLevels.size}` : '全部'}
|
|||
|
|
</span>
|
|||
|
|
<svg className={`w-4 h-4 transition-transform ${showLevelDropdown ? 'rotate-180' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|||
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|||
|
|
</svg>
|
|||
|
|
</button>
|
|||
|
|
|
|||
|
|
{/* 下拉菜单 */}
|
|||
|
|
{showLevelDropdown && (
|
|||
|
|
<div className="absolute top-full left-4 mt-1 bg-white border border-gray-200 rounded-lg shadow-lg z-50 p-3 w-72">
|
|||
|
|
<div className="text-xs text-gray-500 mb-2">选择接收的主播等级(不选则接收全部)</div>
|
|||
|
|
<div className="space-y-2 max-h-60 overflow-auto">
|
|||
|
|
{LEVEL_OPTIONS.map(parent => {
|
|||
|
|
const childLevels = parent.children.map(c => c.value)
|
|||
|
|
const selectedChildCount = childLevels.filter(l => selectedLevels.has(l)).length
|
|||
|
|
const allSelected = selectedChildCount === childLevels.length
|
|||
|
|
const partialSelected = selectedChildCount > 0 && !allSelected
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div key={parent.value} className="border border-gray-100 rounded p-2">
|
|||
|
|
<label className="flex items-center gap-2 cursor-pointer font-medium text-gray-700">
|
|||
|
|
<input
|
|||
|
|
type="checkbox"
|
|||
|
|
checked={allSelected}
|
|||
|
|
ref={el => { if (el) el.indeterminate = partialSelected }}
|
|||
|
|
onChange={() => toggleParentLevel(parent.value)}
|
|||
|
|
className="w-4 h-4"
|
|||
|
|
/>
|
|||
|
|
{parent.label} 级
|
|||
|
|
<span className="text-xs text-gray-400">({selectedChildCount}/{childLevels.length})</span>
|
|||
|
|
</label>
|
|||
|
|
<div className="flex flex-wrap gap-2 mt-1 ml-6">
|
|||
|
|
{parent.children.map(child => (
|
|||
|
|
<label key={child.value} className="flex items-center gap-1 cursor-pointer text-gray-600">
|
|||
|
|
<input
|
|||
|
|
type="checkbox"
|
|||
|
|
checked={selectedLevels.has(child.value)}
|
|||
|
|
onChange={() => toggleLevel(child.value)}
|
|||
|
|
className="w-3 h-3"
|
|||
|
|
/>
|
|||
|
|
<span className="text-xs">{child.label}</span>
|
|||
|
|
</label>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
})}
|
|||
|
|
</div>
|
|||
|
|
<div className="mt-2 pt-2 border-t border-gray-100 flex justify-between">
|
|||
|
|
<button
|
|||
|
|
onClick={() => updateLevelFilter(new Set())}
|
|||
|
|
className="text-xs text-gray-500 hover:text-gray-700"
|
|||
|
|
>
|
|||
|
|
清空选择
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
onClick={() => setShowLevelDropdown(false)}
|
|||
|
|
className="text-xs text-blue-600 hover:text-blue-700"
|
|||
|
|
>
|
|||
|
|
完成
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 接收上限 - 紧凑布局 */}
|
|||
|
|
<div className="flex items-center gap-2 border-l border-gray-200 pl-4 ml-2">
|
|||
|
|
<span className="text-gray-700 font-medium whitespace-nowrap">接收上限</span>
|
|||
|
|
<input
|
|||
|
|
type="number"
|
|||
|
|
min={0}
|
|||
|
|
placeholder="无限制"
|
|||
|
|
value={maxCount || ''}
|
|||
|
|
onChange={(e) => {
|
|||
|
|
const val = parseInt(e.target.value)
|
|||
|
|
updateMaxCount(isNaN(val) ? 0 : val)
|
|||
|
|
}}
|
|||
|
|
className="w-20 px-2 py-1 border border-gray-300 rounded text-sm focus:border-blue-500 focus:outline-none"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 主播列表 */}
|
|||
|
|
<div className="flex-1 overflow-auto p-4">
|
|||
|
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
|
|||
|
|
{filteredHosts.map(host => (
|
|||
|
|
<div
|
|||
|
|
key={host.anchorId}
|
|||
|
|
onClick={() => toggleSelect(host.anchorId)}
|
|||
|
|
className={`p-3 rounded-lg border cursor-pointer transition-all ${selected.has(host.anchorId)
|
|||
|
|
? 'border-blue-500 bg-blue-50 shadow'
|
|||
|
|
: 'border-gray-200 hover:border-gray-300 hover:shadow-sm'
|
|||
|
|
}`}
|
|||
|
|
>
|
|||
|
|
<div className="flex items-center justify-between mb-1">
|
|||
|
|
<span className="font-medium text-sm truncate flex-1" title={host.anchorId}>
|
|||
|
|
{host.anchorId}
|
|||
|
|
</span>
|
|||
|
|
<span className={host.state ? 'text-green-500' : 'text-red-500'}>
|
|||
|
|
{host.state ? '✓' : '✗'}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex items-center justify-between text-xs text-gray-700">
|
|||
|
|
<span>{host.country || '—'}</span>
|
|||
|
|
<div className="flex items-center gap-1">
|
|||
|
|
{host.hostsLevel && (
|
|||
|
|
<span className="px-1.5 py-0.5 rounded bg-purple-100 text-purple-600 text-xs">
|
|||
|
|
{host.hostsLevel}
|
|||
|
|
</span>
|
|||
|
|
)}
|
|||
|
|
<span className={`px-1.5 py-0.5 rounded border ${host.invitationType === 2
|
|||
|
|
? 'text-yellow-600 border-yellow-400'
|
|||
|
|
: 'border-gray-300'
|
|||
|
|
}`}>
|
|||
|
|
{host.invitationType === 2 ? '金票' : '普票'}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{filteredHosts.length === 0 && (
|
|||
|
|
<div className="text-center text-gray-700 py-12">
|
|||
|
|
暂无主播数据
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 底部 */}
|
|||
|
|
<div className="p-4 border-t border-gray-200 flex justify-end gap-3">
|
|||
|
|
<button
|
|||
|
|
onClick={onClose}
|
|||
|
|
className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg"
|
|||
|
|
>
|
|||
|
|
关闭
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
onClick={handleSave}
|
|||
|
|
className="px-4 py-2 text-sm bg-blue-500 text-white rounded-lg hover:bg-blue-600"
|
|||
|
|
>
|
|||
|
|
保存
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>,
|
|||
|
|
document.body
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default HostListDialog
|