初始化

This commit is contained in:
2026-01-22 15:18:09 +08:00
commit 85e5d1ccb7
28 changed files with 7664 additions and 0 deletions

View File

@@ -0,0 +1,474 @@
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