新增大哥池弹窗

This commit is contained in:
2026-04-17 17:15:52 +08:00
parent 5bdf5722a1
commit 5f45cde984
3 changed files with 383 additions and 0 deletions

View File

@@ -0,0 +1,337 @@
<template>
<Teleport to="body">
<div v-if="visible" class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50">
<div class="mx-4 flex max-h-[84vh] w-full max-w-6xl flex-col overflow-hidden rounded-2xl bg-white shadow-2xl">
<div class="flex items-center justify-between border-b border-gray-200 px-5 py-4">
<div>
<h3 class="text-lg font-semibold text-gray-900">大哥池</h3>
<p class="mt-1 text-sm text-gray-500">读取本地 sqlite `brother_info` 数据</p>
</div>
<div class="flex items-center gap-2">
<button @click="handleDeleteAll" :disabled="loading || total === 0"
class="rounded-lg bg-red-50 px-3 py-2 text-sm text-red-600 transition-colors hover:bg-red-100 disabled:cursor-not-allowed disabled:opacity-50">全部删除</button>
<button @click="handleClose"
class="rounded-lg px-3 py-2 text-sm text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700">关闭</button>
</div>
</div>
<div class="border-b border-gray-100 bg-gray-50 px-5 py-4">
<div class="grid grid-cols-1 gap-3 md:grid-cols-4">
<input v-model.trim="filters.keyword" type="text" placeholder="关键词displayId / 昵称 / userIdStr"
class="h-10 rounded-lg border border-gray-300 bg-white px-3 text-sm outline-none focus:border-blue-500"
@keyup.enter="handleSearch" />
<input v-model.trim="filters.region" type="text" placeholder="地区"
class="h-10 rounded-lg border border-gray-300 bg-white px-3 text-sm outline-none focus:border-blue-500"
@keyup.enter="handleSearch" />
<input v-model.trim="filters.hostDisplayId" type="text" placeholder="主播 displayId"
class="h-10 rounded-lg border border-gray-300 bg-white px-3 text-sm outline-none focus:border-blue-500"
@keyup.enter="handleSearch" />
<input v-model.trim="filters.displayId" type="text" placeholder="大哥 displayId"
class="h-10 rounded-lg border border-gray-300 bg-white px-3 text-sm outline-none focus:border-blue-500"
@keyup.enter="handleSearch" />
</div>
<div class="mt-3 grid grid-cols-2 gap-3 md:grid-cols-6">
<input v-model.number="filters.minLevel" type="number" min="0" placeholder="最低等级"
class="h-10 rounded-lg border border-gray-300 bg-white px-3 text-sm outline-none focus:border-blue-500" />
<input v-model.number="filters.maxLevel" type="number" min="0" placeholder="最高等级"
class="h-10 rounded-lg border border-gray-300 bg-white px-3 text-sm outline-none focus:border-blue-500" />
<input v-model.number="filters.minCoins" type="number" min="0" placeholder="最低金币"
class="h-10 rounded-lg border border-gray-300 bg-white px-3 text-sm outline-none focus:border-blue-500" />
<input v-model.number="filters.maxCoins" type="number" min="0" placeholder="最高金币"
class="h-10 rounded-lg border border-gray-300 bg-white px-3 text-sm outline-none focus:border-blue-500" />
<select v-model.number="pageSize"
class="h-10 rounded-lg border border-gray-300 bg-white px-3 text-sm outline-none focus:border-blue-500">
<option :value="10">10 / </option>
<option :value="20">20 / </option>
<option :value="50">50 / </option>
<option :value="100">100 / </option>
</select>
<div class="flex gap-2">
<button @click="handleSearch" :disabled="loading"
class="flex-1 rounded-lg bg-blue-600 px-3 py-2 text-sm text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60">查询</button>
<button @click="handleReset" :disabled="loading"
class="flex-1 rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700 transition-colors hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-60">重置</button>
</div>
</div>
</div>
<div class="flex items-center justify-between border-b border-gray-100 px-5 py-3 text-sm text-gray-500">
<div> {{ total }} 本页 {{ rows.length }} </div>
<div v-if="loading" class="text-blue-600">加载中...</div>
</div>
<div class="min-h-0 flex-1 overflow-auto">
<table class="min-w-full border-collapse text-left text-sm">
<thead class="sticky top-0 bg-slate-900 text-slate-100">
<tr>
<th class="px-4 py-3 font-medium">ID</th>
<th class="px-4 py-3 font-medium">displayId</th>
<th class="px-4 py-3 font-medium">昵称</th>
<th class="px-4 py-3 font-medium">地区</th>
<th class="px-4 py-3 font-medium">等级</th>
<th class="px-4 py-3 font-medium">打赏金币</th>
<th class="px-4 py-3 font-medium">粉丝数</th>
<th class="px-4 py-3 font-medium">主播ID</th>
<th class="px-4 py-3 font-medium">创建时间</th>
</tr>
</thead>
<tbody>
<tr v-for="row in rows" :key="row.id" class="border-b border-gray-100 hover:bg-blue-50/50">
<td class="px-4 py-3 text-gray-700">{{ row.id ?? '-' }}</td>
<td class="px-4 py-3 font-medium text-gray-900">{{ row.displayId || '-' }}</td>
<td class="px-4 py-3 text-gray-700">{{ row.nickname || '-' }}</td>
<td class="px-4 py-3 text-gray-700">{{ row.region || '-' }}</td>
<td class="px-4 py-3 text-gray-700">{{ row.level ?? '-' }}</td>
<td class="px-4 py-3 text-gray-700">{{ formatNumber(row.hostcoins) }}</td>
<td class="px-4 py-3 text-gray-700">{{ formatNumber(row.followerCount) }}</td>
<td class="px-4 py-3 text-gray-700">{{ row.hostDisplayId || '-' }}</td>
<td class="px-4 py-3 text-gray-700">{{ formatTime(row.createTime) }}</td>
</tr>
<tr v-if="!loading && rows.length === 0">
<td colspan="9" class="px-4 py-12 text-center text-gray-400">暂无大哥池数据</td>
</tr>
</tbody>
</table>
</div>
<div class="flex items-center justify-between border-t border-gray-200 px-5 py-4">
<div class="text-sm text-gray-500"> {{ page }} / {{ totalPages || 1 }} </div>
<div class="flex items-center gap-2">
<button @click="goPrev" :disabled="loading || page <= 1"
class="rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700 transition-colors hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50">上一页</button>
<button @click="goNext" :disabled="loading || page >= totalPages"
class="rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700 transition-colors hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50">下一页</button>
</div>
</div>
</div>
<div v-if="showDeleteConfirm" class="absolute inset-0 z-[10001] flex items-center justify-center bg-black/45 px-4">
<div class="w-full max-w-md rounded-2xl bg-white p-6 shadow-2xl">
<div class="text-lg font-semibold text-gray-900">确认全部删除</div>
<p class="mt-3 text-sm leading-6 text-gray-600">
将删除本地 sqlite <code class="rounded bg-gray-100 px-1.5 py-0.5 text-xs text-gray-700">brother_info</code>
的全部数据删除后不可恢复
</p>
<div class="mt-6 flex justify-end gap-3">
<button
@click="showDeleteConfirm = false"
:disabled="loading"
class="rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm text-gray-700 transition-colors hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50"
>
取消
</button>
<button
@click="confirmDeleteAll"
:disabled="loading"
class="rounded-lg bg-red-600 px-4 py-2 text-sm text-white transition-colors hover:bg-red-700 disabled:cursor-not-allowed disabled:opacity-50"
>
{{ loading ? '删除中...' : '确认删除' }}
</button>
</div>
</div>
</div>
</div>
</Teleport>
</template>
<script setup>
import { reactive, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
const props = defineProps({
visible: {
type: Boolean,
required: true
}
})
const emit = defineEmits(['close'])
const loading = ref(false)
const rows = ref([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(20)
const totalPages = ref(0)
const showDeleteConfirm = ref(false)
const filters = reactive({
keyword: '',
region: '',
hostDisplayId: '',
displayId: '',
minLevel: undefined,
maxLevel: undefined,
minCoins: undefined,
maxCoins: undefined
})
function getTkBridge() {
return window?.electronAPI?.tk || null
}
function buildPayload() {
const nextFilters = {}
for (const [key, value] of Object.entries(filters)) {
if (value === '' || value === null || value === undefined) continue
nextFilters[key] = value
}
return {
page: page.value,
pageSize: pageSize.value,
filters: nextFilters
}
}
async function loadData() {
const tkBridge = getTkBridge()
if (!tkBridge?.queryBrotherInfo) {
rows.value = []
total.value = 0
totalPages.value = 0
ElMessage.error('当前客户端未加载大哥池查询桥接,请重启客户端后再试')
return
}
loading.value = true
try {
const raw = await tkBridge.queryBrotherInfo(JSON.stringify(buildPayload()))
const result = typeof raw === 'string' ? JSON.parse(raw) : raw
if (result?.status !== 'success') {
throw new Error(result?.message || '查询大哥池失败')
}
rows.value = Array.isArray(result?.list) ? result.list : []
total.value = Number(result?.total || 0)
totalPages.value = Math.max(1, Number(result?.totalPages || 0))
page.value = Number(result?.page || page.value || 1)
} catch (error) {
rows.value = []
total.value = 0
totalPages.value = 0
ElMessage.error(error instanceof Error ? error.message : String(error))
} finally {
loading.value = false
}
}
async function handleDeleteAll() {
const tkBridge = getTkBridge()
if (!tkBridge?.getAllBrotherInfo || !tkBridge?.deleteBrotherInfo) {
ElMessage.error('当前客户端未加载大哥池删除桥接,请重启客户端后再试')
return
}
showDeleteConfirm.value = true
}
async function confirmDeleteAll() {
const tkBridge = getTkBridge()
if (!tkBridge?.getAllBrotherInfo || !tkBridge?.deleteBrotherInfo) {
showDeleteConfirm.value = false
ElMessage.error('当前客户端未加载大哥池删除桥接,请重启客户端后再试')
return
}
loading.value = true
try {
const rawList = await tkBridge.getAllBrotherInfo()
const listResult = typeof rawList === 'string' ? JSON.parse(rawList) : rawList
if (listResult?.status !== 'success') {
throw new Error(listResult?.message || '读取大哥池失败')
}
const ids = (Array.isArray(listResult?.list) ? listResult.list : [])
.map(item => Number(item?.id))
.filter(id => Number.isFinite(id))
if (ids.length === 0) {
showDeleteConfirm.value = false
ElMessage.success('大哥池已经是空的')
rows.value = []
total.value = 0
totalPages.value = 1
page.value = 1
return
}
const rawDelete = await tkBridge.deleteBrotherInfo(JSON.stringify({ ids }))
const deleteResult = typeof rawDelete === 'string' ? JSON.parse(rawDelete) : rawDelete
if (deleteResult?.status !== 'success') {
throw new Error(deleteResult?.message || '删除大哥池失败')
}
ElMessage.success(`已删除 ${ids.length} 条大哥池数据`)
showDeleteConfirm.value = false
page.value = 1
await loadData()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : String(error))
} finally {
loading.value = false
}
}
function handleSearch() {
page.value = 1
void loadData()
}
function handleReset() {
filters.keyword = ''
filters.region = ''
filters.hostDisplayId = ''
filters.displayId = ''
filters.minLevel = undefined
filters.maxLevel = undefined
filters.minCoins = undefined
filters.maxCoins = undefined
page.value = 1
pageSize.value = 20
void loadData()
}
function handleClose() {
emit('close')
}
function goPrev() {
if (page.value <= 1) return
page.value -= 1
void loadData()
}
function goNext() {
if (page.value >= totalPages.value) return
page.value += 1
void loadData()
}
function formatNumber(value) {
if (value === null || value === undefined || value === '') return '-'
const numericValue = Number(value)
return Number.isFinite(numericValue) ? numericValue.toLocaleString() : String(value)
}
function formatTime(value) {
const timestamp = Number(value)
if (!Number.isFinite(timestamp) || timestamp <= 0) return '-'
const date = new Date(timestamp)
if (Number.isNaN(date.getTime())) return '-'
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hour = String(date.getHours()).padStart(2, '0')
const minute = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hour}:${minute}`
}
watch(() => props.visible, (visible) => {
if (!visible) return
void loadData()
})
watch(pageSize, () => {
if (!props.visible) return
page.value = 1
void loadData()
})
</script>

View File

@@ -189,6 +189,38 @@ export interface ElectronAPI {
onUpdateError: (callback: (error: { message: string }) => void) => () => void onUpdateError: (callback: (error: { message: string }) => void) => () => void
onUpdateManualInstall: (callback: (info: { path: string }) => void) => () => void onUpdateManualInstall: (callback: (info: { path: string }) => void) => () => void
onGreetingStatsChanged: (callback: (stats: GreetingStats) => void) => () => void onGreetingStatsChanged: (callback: (stats: GreetingStats) => void) => () => void
tk?: {
updateStartConfig: (config: string) => Promise<{ success: boolean; error?: string }>
getDataCount: () => Promise<string>
loginTikTok: () => Promise<void>
loginBigTikTok: () => Promise<void>
loginBackStage: (data: string) => Promise<void>
loginBackStageCopy: (data: string) => Promise<void>
checkBackStageLoginStatus: (account?: string) => Promise<string>
checkBackStageLoginStatusCopy: () => Promise<string>
stopCrawl: () => Promise<void>
getVersion: () => Promise<string>
checkTkLoginStatus: () => Promise<string>
visitAnchor: (id: string) => Promise<void>
exportData: (data: string) => Promise<void>
controlTask: (data: string) => Promise<{ success: boolean; message?: string; error?: string }>
getBrotherInfo: () => Promise<string>
queryBrotherInfo: (data: string) => Promise<string>
getAllBrotherInfo: () => Promise<string>
deleteBrotherInfo: (data: string) => Promise<string>
findBigBrother: (data: string) => Promise<{ success: boolean }>
storageSet: (data: string) => Promise<{ success: boolean; error?: string }>
storageRead: (data: string) => Promise<string>
openRoom: (id: string) => Promise<void>
storageAccount: (data: string) => Promise<{ success: boolean; error?: string }>
readAccount: (data: string) => Promise<string>
setClipboard: (text: string) => Promise<{ success: boolean; error?: string }>
startBrotherMonitor: () => Promise<{ success: boolean; error?: string }>
getBrotherLoginStatus: () => Promise<string>
visitGifter: (data: string) => Promise<{ success: boolean; error?: string }>
closeAllBrowsers: () => Promise<{ success: boolean; error?: string }>
}
} }
// 声明全局类型 // 声明全局类型

View File

@@ -141,6 +141,17 @@
<button @click="showHostDialog = true" class="mt-4 w-full rounded-lg border border-purple-200 bg-purple-50 px-4 py-3 text-sm font-medium text-purple-600 transition-colors hover:bg-purple-100">打开执行主播库</button> <button @click="showHostDialog = true" class="mt-4 w-full rounded-lg border border-purple-200 bg-purple-50 px-4 py-3 text-sm font-medium text-purple-600 transition-colors hover:bg-purple-100">打开执行主播库</button>
</section> </section>
<section class="rounded-xl border border-gray-200 bg-white p-5 shadow-sm">
<div class="flex items-start justify-between gap-3">
<div>
<div class="text-sm font-medium text-gray-900">大哥池</div>
<p class="mt-1 text-xs leading-5 text-gray-500">参考 brother_info 数据池维护大哥列表并支持切换为大哥池模式启动任务</p>
</div>
<span :class="['rounded-full px-3 py-1 text-xs font-medium', isBrotherInfoMode ? 'bg-fuchsia-100 text-fuchsia-700' : 'bg-gray-100 text-gray-600']">{{ isBrotherInfoMode ? '当前使用中' : '可切换' }}</span>
</div>
<button @click="showBrotherInfoDialog = true" class="mt-4 w-full rounded-lg border border-fuchsia-200 bg-fuchsia-50 px-4 py-3 text-sm font-medium text-fuchsia-600 transition-colors hover:bg-fuchsia-100">打开大哥池</button>
</section>
<section class="rounded-xl bg-slate-950 p-5 shadow-sm"> <section class="rounded-xl bg-slate-950 p-5 shadow-sm">
<div class="text-sm font-medium text-slate-100">本次提交参数</div> <div class="text-sm font-medium text-slate-100">本次提交参数</div>
<pre class="mt-3 overflow-x-auto text-xs leading-6 text-slate-300">{{ payloadPreview }}</pre> <pre class="mt-3 overflow-x-auto text-xs leading-6 text-slate-300">{{ payloadPreview }}</pre>
@@ -208,6 +219,7 @@
<AIConfigDialog :visible="showAIDialog" :config="aiConfig" @close="showAIDialog = false" @save="handleSaveAIConfig" @change="(key, value) => aiConfig[key] = value" /> <AIConfigDialog :visible="showAIDialog" :config="aiConfig" @close="showAIDialog = false" @save="handleSaveAIConfig" @change="(key, value) => aiConfig[key] = value" />
<HostListDialog :visible="showHostDialog" @close="showHostDialog = false" @save="() => {}" /> <HostListDialog :visible="showHostDialog" @close="showHostDialog = false" @save="() => {}" />
<BrotherInfoDialog :visible="showBrotherInfoDialog" @close="showBrotherInfoDialog = false" />
<GreetingDialog :visible="showGreetingDialog" @close="showGreetingDialog = false" @confirm="handleGreetingConfirm" /> <GreetingDialog :visible="showGreetingDialog" @close="showGreetingDialog = false" @confirm="handleGreetingConfirm" />
</div> </div>
</template> </template>
@@ -220,6 +232,7 @@ import ViewPlaceholder from '@/components/ViewPlaceholder.vue'
import HostListDialog from '@/components/HostListDialog.vue' import HostListDialog from '@/components/HostListDialog.vue'
import AIConfigDialog from '@/components/AIConfigDialog.vue' import AIConfigDialog from '@/components/AIConfigDialog.vue'
import GreetingDialog from '@/components/GreetingDialog.vue' import GreetingDialog from '@/components/GreetingDialog.vue'
import BrotherInfoDialog from '@/components/BrotherInfoDialog.vue'
const props = defineProps({ navSidebarWidth: { type: Number, default: 144 } }) const props = defineProps({ navSidebarWidth: { type: Number, default: 144 } })
const TIKTOK_VIEW_IDS = Array.from({ length: 9 }, (_, index) => index + 10) const TIKTOK_VIEW_IDS = Array.from({ length: 9 }, (_, index) => index + 10)
@@ -231,6 +244,7 @@ const statusText = ref('待启动')
const showAIDialog = ref(false) const showAIDialog = ref(false)
const showHostDialog = ref(false) const showHostDialog = ref(false)
const showGreetingDialog = ref(false) const showGreetingDialog = ref(false)
const showBrotherInfoDialog = ref(false)
const aiConfigured = ref(false) const aiConfigured = ref(false)
const loginConfirmed = ref(false) const loginConfirmed = ref(false)
const preparedConfigKey = ref('') const preparedConfigKey = ref('')