新增大哥池弹窗
This commit is contained in:
337
src/components/BrotherInfoDialog.vue
Normal file
337
src/components/BrotherInfoDialog.vue
Normal 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>
|
||||
32
src/types/electron.d.ts
vendored
32
src/types/electron.d.ts
vendored
@@ -189,6 +189,38 @@ export interface ElectronAPI {
|
||||
onUpdateError: (callback: (error: { message: string }) => void) => () => void
|
||||
onUpdateManualInstall: (callback: (info: { path: string }) => 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 }>
|
||||
}
|
||||
}
|
||||
|
||||
// 声明全局类型
|
||||
|
||||
@@ -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>
|
||||
</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">
|
||||
<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>
|
||||
@@ -208,6 +219,7 @@
|
||||
|
||||
<AIConfigDialog :visible="showAIDialog" :config="aiConfig" @close="showAIDialog = false" @save="handleSaveAIConfig" @change="(key, value) => aiConfig[key] = value" />
|
||||
<HostListDialog :visible="showHostDialog" @close="showHostDialog = false" @save="() => {}" />
|
||||
<BrotherInfoDialog :visible="showBrotherInfoDialog" @close="showBrotherInfoDialog = false" />
|
||||
<GreetingDialog :visible="showGreetingDialog" @close="showGreetingDialog = false" @confirm="handleGreetingConfirm" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -220,6 +232,7 @@ import ViewPlaceholder from '@/components/ViewPlaceholder.vue'
|
||||
import HostListDialog from '@/components/HostListDialog.vue'
|
||||
import AIConfigDialog from '@/components/AIConfigDialog.vue'
|
||||
import GreetingDialog from '@/components/GreetingDialog.vue'
|
||||
import BrotherInfoDialog from '@/components/BrotherInfoDialog.vue'
|
||||
|
||||
const props = defineProps({ navSidebarWidth: { type: Number, default: 144 } })
|
||||
const TIKTOK_VIEW_IDS = Array.from({ length: 9 }, (_, index) => index + 10)
|
||||
@@ -231,6 +244,7 @@ const statusText = ref('待启动')
|
||||
const showAIDialog = ref(false)
|
||||
const showHostDialog = ref(false)
|
||||
const showGreetingDialog = ref(false)
|
||||
const showBrotherInfoDialog = ref(false)
|
||||
const aiConfigured = ref(false)
|
||||
const loginConfirmed = ref(false)
|
||||
const preparedConfigKey = ref('')
|
||||
|
||||
Reference in New Issue
Block a user