Files
web-fusion/src/components/BrotherInfoDialog.vue
2026-04-28 17:01:53 +08:00

332 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>
</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>