2026-02-04 19:56:19 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<Teleport to="body">
|
|
|
|
|
|
<div v-if="visible" class="fixed inset-0 bg-black/50 flex items-center justify-center" style="z-index: 9999">
|
|
|
|
|
|
<div class="bg-white rounded-xl shadow-2xl w-full max-w-4xl max-h-[80vh] flex flex-col mx-4">
|
|
|
|
|
|
<!-- 头部 -->
|
|
|
|
|
|
<div class="p-4 border-b border-gray-200 flex items-center justify-between">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h3 class="text-lg font-semibold text-gray-600">主播管理</h3>
|
|
|
|
|
|
<span class="text-sm text-gray-700">
|
|
|
|
|
|
已选 {{ selectedCount }} / 共 {{ filteredHosts.length }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button @click="onClose" class="text-gray-700 hover:text-gray-700 text-xl">✕</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 工具栏 -->
|
|
|
|
|
|
<div class="p-4 border-b border-gray-100 space-y-3">
|
|
|
|
|
|
<div class="flex flex-wrap gap-2">
|
|
|
|
|
|
<button @click="selectAll"
|
|
|
|
|
|
class="px-3 py-1.5 text-sm bg-blue-100 text-blue-700 hover:bg-blue-200 rounded border border-blue-300">全选</button>
|
|
|
|
|
|
<button @click="selectNone"
|
|
|
|
|
|
class="px-3 py-1.5 text-sm bg-slate-100 text-slate-700 hover:bg-slate-200 rounded border border-slate-300">全不选</button>
|
|
|
|
|
|
<button @click="invertSelect"
|
|
|
|
|
|
class="px-3 py-1.5 text-sm bg-slate-100 text-slate-700 hover:bg-slate-200 rounded border border-slate-300">反选</button>
|
|
|
|
|
|
<button @click="deleteSelected" :disabled="!selectedCount"
|
|
|
|
|
|
class="px-3 py-1.5 text-sm bg-red-100 text-red-600 hover:bg-red-200 rounded disabled:opacity-50">
|
|
|
|
|
|
删除选中
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 筛选 -->
|
|
|
|
|
|
<div class="flex flex-wrap items-center gap-4 text-sm">
|
|
|
|
|
|
<label class="flex items-center gap-2 cursor-pointer">
|
|
|
|
|
|
<input type="checkbox" v-model="filters.gold" class="w-4 h-4" />
|
|
|
|
|
|
<span class="text-yellow-600">金票</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label class="flex items-center gap-2 cursor-pointer">
|
|
|
|
|
|
<input type="checkbox" v-model="filters.ordinary" class="w-4 h-4" />
|
|
|
|
|
|
<span>普票</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
|
|
|
|
<span class="text-gray-700">在线人数</span>
|
|
|
|
|
|
<input type="number" placeholder="最小" v-model="filters.minOnlineFans"
|
|
|
|
|
|
class="w-20 px-2 py-1 border border-gray-300 rounded text-sm" />
|
|
|
|
|
|
<span>~</span>
|
|
|
|
|
|
<input type="number" placeholder="最大" v-model="filters.maxOnlineFans"
|
|
|
|
|
|
class="w-20 px-2 py-1 border border-gray-300 rounded text-sm" />
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 等级过滤 -->
|
|
|
|
|
|
<div class="relative border-l border-gray-200 pl-4 ml-2">
|
|
|
|
|
|
<button @click="showLevelDropdown = !showLevelDropdown"
|
|
|
|
|
|
class="flex items-center gap-2 px-3 py-1 border border-gray-300 rounded text-sm hover:bg-gray-50">
|
|
|
|
|
|
<span class="text-gray-700 font-medium">等级过滤</span>
|
|
|
|
|
|
<span class="text-xs text-blue-600">
|
|
|
|
|
|
{{ selectedLevels.size > 0 ? `已选 ${selectedLevels.size}` : '全部' }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<svg :class="['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>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 下拉菜单 -->
|
|
|
|
|
|
<div v-if="showLevelDropdown"
|
|
|
|
|
|
class="absolute top-full left-4 mt-1 bg-white border border-gray-200 rounded-lg shadow-lg z-50 p-3 w-72">
|
|
|
|
|
|
<div class="text-xs text-gray-500 mb-2">选择接收的主播等级(不选则接收全部)</div>
|
|
|
|
|
|
<div class="space-y-2 max-h-60 overflow-auto">
|
|
|
|
|
|
<div v-for="parent in LEVEL_OPTIONS" :key="parent.value"
|
|
|
|
|
|
class="border border-gray-100 rounded p-2">
|
|
|
|
|
|
<label
|
|
|
|
|
|
class="flex items-center gap-2 cursor-pointer font-medium text-gray-700">
|
|
|
|
|
|
<input type="checkbox"
|
|
|
|
|
|
:checked="isParentSelected(parent).allSelected"
|
|
|
|
|
|
:indeterminate="isParentSelected(parent).partialSelected"
|
|
|
|
|
|
@change="toggleParentLevel(parent.value)" class="w-4 h-4" />
|
|
|
|
|
|
{{ parent.label }} 级
|
|
|
|
|
|
<span class="text-xs text-gray-400">({{
|
|
|
|
|
|
isParentSelected(parent).selectedChildCount }}/{{
|
|
|
|
|
|
parent.children.length
|
|
|
|
|
|
}})</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<div class="flex flex-wrap gap-2 mt-1 ml-6">
|
|
|
|
|
|
<label v-for="child in parent.children" :key="child.value"
|
|
|
|
|
|
class="flex items-center gap-1 cursor-pointer text-gray-600">
|
|
|
|
|
|
<input type="checkbox" :checked="selectedLevels.has(child.value)"
|
|
|
|
|
|
@change="toggleLevel(child.value)" class="w-3 h-3" />
|
|
|
|
|
|
<span class="text-xs">{{ child.label }}</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-2 pt-2 border-t border-gray-100 flex justify-between">
|
|
|
|
|
|
<button @click="updateLevelFilter(new Set())"
|
|
|
|
|
|
class="text-xs text-gray-500 hover:text-gray-700">
|
|
|
|
|
|
清空选择
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button @click="showLevelDropdown = false"
|
|
|
|
|
|
class="text-xs text-blue-600 hover:text-blue-700">
|
|
|
|
|
|
完成
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 接收上限 - 紧凑布局 -->
|
|
|
|
|
|
<div class="flex items-center gap-2 border-l border-gray-200 pl-4 ml-2">
|
|
|
|
|
|
<span class="text-gray-700 font-medium whitespace-nowrap">接收上限</span>
|
|
|
|
|
|
<input type="number" min="0" placeholder="无限制" v-model.number="maxCount"
|
|
|
|
|
|
@change="updateMaxCount(maxCount)"
|
|
|
|
|
|
class="w-20 px-2 py-1 border border-gray-300 rounded text-sm focus:border-blue-500 focus:outline-none" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 主播列表 -->
|
|
|
|
|
|
<div class="flex-1 overflow-auto p-4">
|
|
|
|
|
|
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
|
|
|
|
|
|
<div v-for="host in filteredHosts" :key="host.anchorId" @click="toggleSelect(host.anchorId)"
|
|
|
|
|
|
:class="[
|
|
|
|
|
|
'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 class="flex items-center justify-between mb-1">
|
|
|
|
|
|
<span class="font-medium text-sm truncate flex-1" :title="host.anchorId">
|
|
|
|
|
|
{{ host.anchorId }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span :class="host.state ? 'text-green-500' : 'text-red-500'">
|
|
|
|
|
|
{{ host.state ? '✓' : '✗' }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="flex items-center justify-between text-xs text-gray-700">
|
|
|
|
|
|
<span>{{ host.country || '—' }}</span>
|
|
|
|
|
|
<div class="flex items-center gap-1">
|
|
|
|
|
|
<span v-if="host.hostsLevel"
|
|
|
|
|
|
class="px-1.5 py-0.5 rounded bg-purple-100 text-purple-600 text-xs">
|
|
|
|
|
|
{{ host.hostsLevel }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span :class="[
|
|
|
|
|
|
'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>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="filteredHosts.length === 0" class="text-center text-gray-700 py-12">
|
|
|
|
|
|
暂无主播数据
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 底部 -->
|
|
|
|
|
|
<div class="p-4 border-t border-gray-200 flex justify-end gap-3">
|
|
|
|
|
|
<button @click="onClose" class="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg">
|
|
|
|
|
|
关闭
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button @click="handleSave"
|
|
|
|
|
|
class="px-4 py-2 text-sm bg-blue-500 text-white rounded-lg hover:bg-blue-600">
|
|
|
|
|
|
保存
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Teleport>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { ref, computed, watch, onMounted } from 'vue'
|
|
|
|
|
|
import { isElectron } from '../utils/electronBridge'
|
|
|
|
|
|
|
|
|
|
|
|
const props = defineProps({
|
|
|
|
|
|
visible: { type: Boolean, required: true }
|
|
|
|
|
|
})
|
|
|
|
|
|
const emit = defineEmits(['close', 'save'])
|
|
|
|
|
|
|
|
|
|
|
|
// Level Options
|
|
|
|
|
|
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' },
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
// State
|
|
|
|
|
|
const hosts = ref([])
|
|
|
|
|
|
const selected = ref(new Set())
|
|
|
|
|
|
const filters = ref({
|
|
|
|
|
|
gold: true,
|
|
|
|
|
|
ordinary: true,
|
|
|
|
|
|
minOnlineFans: '',
|
|
|
|
|
|
maxOnlineFans: '',
|
|
|
|
|
|
})
|
|
|
|
|
|
const maxCount = ref(100)
|
|
|
|
|
|
const selectedLevels = ref(new Set())
|
|
|
|
|
|
const showLevelDropdown = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
// Lifecycle
|
|
|
|
|
|
watch(() => props.visible, (newVal) => {
|
|
|
|
|
|
if (newVal) {
|
|
|
|
|
|
document.body.style.overflow = 'hidden'
|
|
|
|
|
|
loadHosts()
|
|
|
|
|
|
loadConfig()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
document.body.style.overflow = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
if (props.visible) {
|
|
|
|
|
|
document.body.style.overflow = 'hidden'
|
|
|
|
|
|
loadHosts()
|
|
|
|
|
|
loadConfig()
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-02-05 18:46:35 +08:00
|
|
|
|
// 监听过滤器变化,同步到后端配置
|
|
|
|
|
|
watch(() => filters.value, async (newFilters) => {
|
|
|
|
|
|
if (!isElectron()) return
|
|
|
|
|
|
try {
|
|
|
|
|
|
await window.electronAPI.updateAutomationConfig({
|
|
|
|
|
|
filters: {
|
|
|
|
|
|
gold: newFilters.gold,
|
|
|
|
|
|
ordinary: newFilters.ordinary,
|
|
|
|
|
|
minOnlineFans: newFilters.minOnlineFans ? parseInt(newFilters.minOnlineFans) : 0,
|
|
|
|
|
|
maxOnlineFans: newFilters.maxOnlineFans ? parseInt(newFilters.maxOnlineFans) : 0,
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
console.log('[HostListDialog] 过滤配置已同步:', newFilters)
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('同步过滤配置失败:', e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}, { deep: true })
|
|
|
|
|
|
|
2026-02-04 19:56:19 +08:00
|
|
|
|
// Loading
|
|
|
|
|
|
const loadHosts = async () => {
|
|
|
|
|
|
if (!isElectron()) return
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = await window.electronAPI.loadAnchorData()
|
|
|
|
|
|
hosts.value = data
|
|
|
|
|
|
selected.value = new Set()
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('加载主播数据失败:', e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const loadConfig = async () => {
|
|
|
|
|
|
if (!isElectron()) return
|
|
|
|
|
|
try {
|
|
|
|
|
|
const config = await window.electronAPI.getAutomationConfig()
|
|
|
|
|
|
if (config?.maxAnchorCount !== undefined) {
|
|
|
|
|
|
maxCount.value = config.maxAnchorCount
|
|
|
|
|
|
}
|
|
|
|
|
|
if (config?.filters?.hostsLevelList) {
|
|
|
|
|
|
selectedLevels.value = new Set(config.filters.hostsLevelList)
|
|
|
|
|
|
}
|
2026-02-05 18:46:35 +08:00
|
|
|
|
// 加载金票/普票过滤配置
|
|
|
|
|
|
if (config?.filters?.gold !== undefined) {
|
|
|
|
|
|
filters.value.gold = config.filters.gold
|
|
|
|
|
|
}
|
|
|
|
|
|
if (config?.filters?.ordinary !== undefined) {
|
|
|
|
|
|
filters.value.ordinary = config.filters.ordinary
|
|
|
|
|
|
}
|
|
|
|
|
|
// 加载在线人数过滤配置
|
|
|
|
|
|
if (config?.filters?.minOnlineFans !== undefined && config.filters.minOnlineFans > 0) {
|
|
|
|
|
|
filters.value.minOnlineFans = config.filters.minOnlineFans
|
|
|
|
|
|
}
|
|
|
|
|
|
if (config?.filters?.maxOnlineFans !== undefined && config.filters.maxOnlineFans > 0 && config.filters.maxOnlineFans < 9999999999999) {
|
|
|
|
|
|
filters.value.maxOnlineFans = config.filters.maxOnlineFans
|
|
|
|
|
|
}
|
2026-02-04 19:56:19 +08:00
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('加载配置失败:', e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Helpers
|
|
|
|
|
|
const getAllChildLevels = (parentValue) => {
|
|
|
|
|
|
const parent = LEVEL_OPTIONS.find(p => p.value === parentValue)
|
|
|
|
|
|
return parent ? parent.children.map(c => c.value) : []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Filtering
|
|
|
|
|
|
const isParentSelected = (parent) => {
|
|
|
|
|
|
const childLevels = parent.children.map(c => c.value)
|
|
|
|
|
|
const selectedChildCount = childLevels.filter(l => selectedLevels.value.has(l)).length
|
|
|
|
|
|
const allSelected = selectedChildCount === childLevels.length
|
|
|
|
|
|
const partialSelected = selectedChildCount > 0 && !allSelected
|
|
|
|
|
|
return { allSelected, partialSelected, selectedChildCount }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const updateLevelFilter = async (levels) => {
|
|
|
|
|
|
selectedLevels.value = levels
|
|
|
|
|
|
if (!isElectron()) return
|
|
|
|
|
|
try {
|
|
|
|
|
|
await window.electronAPI.updateAutomationConfig({
|
|
|
|
|
|
filters: { hostsLevelList: Array.from(levels) }
|
|
|
|
|
|
})
|
|
|
|
|
|
console.log('[HostListDialog] 等级过滤已更新:', Array.from(levels))
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('更新等级配置失败:', e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const toggleLevel = (level) => {
|
|
|
|
|
|
const newSet = new Set(selectedLevels.value)
|
|
|
|
|
|
if (newSet.has(level)) {
|
|
|
|
|
|
newSet.delete(level)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
newSet.add(level)
|
|
|
|
|
|
}
|
|
|
|
|
|
updateLevelFilter(newSet)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const toggleParentLevel = (parentValue) => {
|
|
|
|
|
|
const childLevels = getAllChildLevels(parentValue)
|
|
|
|
|
|
const allSelected = childLevels.every(l => selectedLevels.value.has(l))
|
|
|
|
|
|
const newSet = new Set(selectedLevels.value)
|
|
|
|
|
|
|
|
|
|
|
|
if (allSelected) {
|
|
|
|
|
|
childLevels.forEach(l => newSet.delete(l))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
childLevels.forEach(l => newSet.add(l))
|
|
|
|
|
|
}
|
|
|
|
|
|
updateLevelFilter(newSet)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const updateMaxCount = async (value) => {
|
|
|
|
|
|
// value is already updated via v-model
|
|
|
|
|
|
if (!isElectron()) return
|
|
|
|
|
|
try {
|
|
|
|
|
|
await window.electronAPI.updateAutomationConfig({ maxAnchorCount: value })
|
|
|
|
|
|
console.log('[HostListDialog] 主播数据上限已更新:', value)
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('更新配置失败:', e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const filteredHosts = computed(() => {
|
|
|
|
|
|
return hosts.value.filter(h => {
|
|
|
|
|
|
if (!filters.value.gold && h.invitationType === 2) return false
|
|
|
|
|
|
if (!filters.value.ordinary && h.invitationType === 1) return false
|
|
|
|
|
|
if (filters.value.minOnlineFans && h.onlineFans !== undefined) {
|
|
|
|
|
|
if (h.onlineFans < parseInt(filters.value.minOnlineFans)) return false
|
|
|
|
|
|
}
|
|
|
|
|
|
if (filters.value.maxOnlineFans && h.onlineFans !== undefined) {
|
|
|
|
|
|
if (h.onlineFans > parseInt(filters.value.maxOnlineFans)) return false
|
|
|
|
|
|
}
|
|
|
|
|
|
if (selectedLevels.value.size > 0 && h.hostsLevel) {
|
|
|
|
|
|
if (!selectedLevels.value.has(h.hostsLevel)) return false
|
|
|
|
|
|
}
|
|
|
|
|
|
return true
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const selectedCount = computed(() => selected.value.size)
|
|
|
|
|
|
|
|
|
|
|
|
const toggleSelect = (id) => {
|
|
|
|
|
|
const next = new Set(selected.value)
|
|
|
|
|
|
if (next.has(id)) next.delete(id)
|
|
|
|
|
|
else next.add(id)
|
|
|
|
|
|
selected.value = next
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const selectAll = () => {
|
|
|
|
|
|
selected.value = new Set(filteredHosts.value.map(h => h.anchorId))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const selectNone = () => {
|
|
|
|
|
|
selected.value = new Set()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const invertSelect = () => {
|
|
|
|
|
|
const next = new Set()
|
|
|
|
|
|
filteredHosts.value.forEach(h => {
|
|
|
|
|
|
if (!selected.value.has(h.anchorId)) next.add(h.anchorId)
|
|
|
|
|
|
})
|
|
|
|
|
|
selected.value = next
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const deleteSelected = () => {
|
|
|
|
|
|
if (!selected.value.size) return
|
|
|
|
|
|
if (!confirm(`确认删除选中的 ${selected.value.size} 项吗?`)) return
|
|
|
|
|
|
|
|
|
|
|
|
const remaining = hosts.value.filter(h => !selected.value.has(h.anchorId))
|
|
|
|
|
|
hosts.value = remaining
|
|
|
|
|
|
selected.value = new Set()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const onClose = () => emit('close')
|
|
|
|
|
|
|
|
|
|
|
|
const handleSave = async () => {
|
|
|
|
|
|
if (isElectron()) {
|
|
|
|
|
|
await window.electronAPI.saveAnchorData(JSON.parse(JSON.stringify(hosts.value)))
|
|
|
|
|
|
}
|
|
|
|
|
|
emit('save', hosts.value)
|
|
|
|
|
|
onClose()
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|