332 lines
14 KiB
Vue
332 lines
14 KiB
Vue
<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>
|