开启tk 状态灯

This commit is contained in:
2026-01-23 16:12:37 +08:00
parent 1576cadf8c
commit 21eec1085a
4 changed files with 1141 additions and 200 deletions

View File

@@ -38,7 +38,7 @@ const initBridge = () => {
const val = target[prop]; const val = target[prop];
if (typeof val === 'function') return val; if (typeof val === 'function') return val;
// 返回空函数,确保 handleResponse 可调用 // 返回空函数,确保 handleResponse 可调用
return () => {}; return () => { };
}, },
set(target, prop, value) { set(target, prop, value) {
target[prop] = value; target[prop] = value;
@@ -129,6 +129,16 @@ export function usePythonBridge() {
}); });
}; };
// 查询TK登录状态
const getTkLoginStatus = () => {
return new Promise((resolve) => {
if (!bridge.value) return resolve(false);
callBridge('getTkLoginStatus', (result) => {
resolve(result);
});
});
};
// 在组件挂载时初始化桥接 // 在组件挂载时初始化桥接
onMounted(initBridge); onMounted(initBridge);
@@ -143,5 +153,6 @@ export function usePythonBridge() {
exportToExcel, exportToExcel,
stopScript, stopScript,
getVersion, getVersion,
getTkLoginStatus,
}; };
} }

View File

@@ -4,47 +4,36 @@
<header class="px-8 py-6 border-b border-slate-100 dark:border-slate-200/60 bg-white"> <header class="px-8 py-6 border-b border-slate-100 dark:border-slate-200/60 bg-white">
<div class="flex flex-wrap items-center gap-4"> <div class="flex flex-wrap items-center gap-4">
<div class="relative flex-1 min-w-[200px]"> <div class="relative flex-1 min-w-[200px]">
<select <select v-model="searchForm.country"
v-model="searchForm.country" class="w-full bg-slate-50 border-none rounded-xl py-3 pl-4 pr-10 text-sm text-slate-600 appearance-none focus:ring-2 focus:ring-primary/20 shadow-soft-inner outline-none">
class="w-full bg-slate-50 border-none rounded-xl py-3 pl-4 pr-10 text-sm text-slate-600 appearance-none focus:ring-2 focus:ring-primary/20 shadow-soft-inner outline-none"
>
<option value="">{{ $t('hostList.selectAll') }}</option> <option value="">{{ $t('hostList.selectAll') }}</option>
<option v-for="item in options" :key="item.value" :value="item.value">{{ item.label }}</option> <option v-for="item in options" :key="item.value" :value="item.value">{{ item.label }}</option>
</select> </select>
<span class="material-icons-round absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none">expand_more</span> <span
class="material-icons-round absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none">expand_more</span>
</div> </div>
<div class="relative flex-1 min-w-[200px]"> <div class="relative flex-1 min-w-[200px]">
<span class="material-icons-round absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 text-sm pointer-events-none">calendar_today</span> <span
<input class="material-icons-round absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 text-sm pointer-events-none">calendar_today</span>
ref="dateInput" <input ref="dateInput" v-model="searchForm.createTime" type="date" @click="openDatePicker"
v-model="searchForm.createTime" class="w-full bg-slate-50 border-none rounded-xl py-3 pl-10 pr-4 text-sm text-slate-600 focus:ring-2 focus:ring-primary/20 shadow-soft-inner outline-none cursor-pointer" />
type="date"
@click="openDatePicker"
class="w-full bg-slate-50 border-none rounded-xl py-3 pl-10 pr-4 text-sm text-slate-600 focus:ring-2 focus:ring-primary/20 shadow-soft-inner outline-none cursor-pointer"
/>
</div> </div>
<div class="relative flex-[1.5] min-w-[240px]"> <div class="relative flex-[1.5] min-w-[240px]">
<input <input v-model="searchForm.hostsId"
v-model="searchForm.hostsId" class="w-full bg-slate-50 border-none rounded-xl py-3 px-4 text-sm text-slate-600 focus:ring-2 focus:ring-primary/20 shadow-soft-inner outline-none"
class="w-full bg-slate-50 border-none rounded-xl py-3 px-4 text-sm text-slate-600 focus:ring-2 focus:ring-primary/20 shadow-soft-inner outline-none" :placeholder="$t('hostList.placeHostId')" />
:placeholder="$t('hostList.placeHostId')"
/>
</div> </div>
<div class="flex items-center gap-2 ml-auto"> <div class="flex items-center gap-2 ml-auto">
<button <button @click="serch"
@click="serch" class="bg-primary hover:bg-blue-700 text-white px-8 py-3 rounded-xl font-medium text-sm transition-all shadow-lg shadow-primary/20 flex items-center gap-2">
class="bg-primary hover:bg-blue-700 text-white px-8 py-3 rounded-xl font-medium text-sm transition-all shadow-lg shadow-primary/20 flex items-center gap-2"
>
<span class="material-icons-round text-sm">search</span> <span class="material-icons-round text-sm">search</span>
{{ $t('hostList.query') }} {{ $t('hostList.query') }}
</button> </button>
<button <button @click="filterdialogVisible = true"
@click="filterdialogVisible = true" class="bg-slate-100 hover:bg-slate-200 text-slate-600 p-3 rounded-xl transition-all">
class="bg-slate-100 hover:bg-slate-200 text-slate-600 p-3 rounded-xl transition-all"
>
<span class="material-icons-round">tune</span> <span class="material-icons-round">tune</span>
</button> </button>
</div> </div>
@@ -54,9 +43,9 @@
<!-- Table --> <!-- Table -->
<div class="flex-1 flex flex-col overflow-hidden relative" style="padding-left: 20px;"> <div class="flex-1 flex flex-col overflow-hidden relative" style="padding-left: 20px;">
<div v-if="loading" class="absolute inset-0 bg-white/50 z-30 flex items-center justify-center"> <div v-if="loading" class="absolute inset-0 bg-white/50 z-30 flex items-center justify-center">
<span class="material-icons-round animate-spin text-primary text-4xl">sync</span> <span class="material-icons-round animate-spin text-primary text-4xl">sync</span>
</div> </div>
<!-- Table with Fixed Layout --> <!-- Table with Fixed Layout -->
<div class="flex-1 overflow-auto"> <div class="flex-1 overflow-auto">
<table class="w-full text-left" style="table-layout: fixed; min-width: 1000px;"> <table class="w-full text-left" style="table-layout: fixed; min-width: 1000px;">
@@ -64,7 +53,7 @@
<col style="width: 12%;"> <col style="width: 12%;">
<col style="width: 5%;"> <col style="width: 5%;">
<col style="width: 8%;"> <col style="width: 8%;">
<col style="width: 15%;"> <col style="width: 8%;">
<col style="width: 6%;"> <col style="width: 6%;">
<col style="width: 10%;"> <col style="width: 10%;">
<col style="width: 8%;"> <col style="width: 8%;">
@@ -79,7 +68,8 @@
<th class="py-3 px-2 font-semibold bg-white">{{ $t('hostList.hostId') }}</th> <th class="py-3 px-2 font-semibold bg-white">{{ $t('hostList.hostId') }}</th>
<th class="py-3 px-2 font-semibold bg-white">{{ $t('hostList.grade') }}</th> <th class="py-3 px-2 font-semibold bg-white">{{ $t('hostList.grade') }}</th>
<th class="py-3 px-2 font-semibold bg-white">{{ $t('hostList.invitationType') }}</th> <th class="py-3 px-2 font-semibold bg-white">{{ $t('hostList.invitationType') }}</th>
<th class="py-3 px-2 font-semibold bg-white">{{ $t('hostList.liveSessions') }}/{{ $t('hostList.liveRevenue') }}</th> <!-- <th class="py-3 px-2 font-semibold bg-white">{{ $t('hostList.liveSessions') }}/{{ $t('hostList.liveRevenue') }}</th> -->
<th class="py-3 px-2 font-semibold bg-white">{{ $t('hostList.liveSessions') }}</th>
<th class="py-3 px-2 font-semibold bg-white">{{ $t('hostList.country') }}</th> <th class="py-3 px-2 font-semibold bg-white">{{ $t('hostList.country') }}</th>
<th class="py-3 px-2 font-semibold bg-white">{{ $t('hostList.creationTime') }}</th> <th class="py-3 px-2 font-semibold bg-white">{{ $t('hostList.creationTime') }}</th>
<th class="py-3 px-2 font-semibold bg-white">{{ $t('hostList.anchorcoins') }}</th> <th class="py-3 px-2 font-semibold bg-white">{{ $t('hostList.anchorcoins') }}</th>
@@ -90,40 +80,36 @@
<th class="py-3 px-2 font-semibold bg-white">{{ $t('hostList.anchorType') }}</th> <th class="py-3 px-2 font-semibold bg-white">{{ $t('hostList.anchorType') }}</th>
</tr> </tr>
</thead> </thead>
<tbody class="text-sm text-slate-600"> <tbody class="text-sm text-slate-600">
<tr v-if="tableData.length === 0" class="text-center"> <tr v-if="tableData.length === 0" class="text-center">
<td colspan="14" class="py-10 text-slate-400">暂无数据</td> <td colspan="14" class="py-10 text-slate-400">暂无数据</td>
</tr> </tr>
<tr v-for="row in tableData" :key="row.hostId" class="group hover:bg-slate-50/80 transition-colors"> <tr v-for="row in tableData" :key="row.hostId" class="group hover:bg-slate-50/80 transition-colors">
<!-- Host ID --> <!-- Host ID -->
<td class="py-4 px-2 border-b border-slate-50 group-last:border-none"> <td class="py-4 px-2 border-b border-slate-50 group-last:border-none">
<span <span @click="openHTML(row.hostId)"
@click="openHTML(row.hostId)" class="font-medium text-slate-900 border-b border-transparent group-hover:border-primary/30 transition-all cursor-pointer hover:text-primary">
class="font-medium text-slate-900 border-b border-transparent group-hover:border-primary/30 transition-all cursor-pointer hover:text-primary" {{ row.hostId }}
> </span>
{{ row.hostId }} </td>
</span>
</td> <!-- Level -->
<td class="py-4 px-2 border-b border-slate-50 group-last:border-none">
<!-- Level --> {{ row.hostlevel }}
<td class="py-4 px-2 border-b border-slate-50 group-last:border-none"> </td>
{{ row.hostlevel }}
</td> <!-- Invitation Type -->
<td class="py-4 px-2 border-b border-slate-50 group-last:border-none">
<!-- Invitation Type --> <span class="px-3 py-1 text-[10px] font-bold uppercase rounded-full"
<td class="py-4 px-2 border-b border-slate-50 group-last:border-none"> :class="row.invitationType == 1 ? 'bg-green-50 text-green-600' : 'bg-amber-50 text-amber-600'">
<span {{ row.invitationType == 1 ? $t('hostList.invitationType1') : $t('hostList.invitationType2') }}
class="px-3 py-1 text-[10px] font-bold uppercase rounded-full" </span>
:class="row.invitationType == 1 ? 'bg-green-50 text-green-600' : 'bg-amber-50 text-amber-600'" </td>
>
{{ row.invitationType == 1 ? $t('hostList.invitationType1') : $t('hostList.invitationType2') }} <!-- Data Buttons -->
</span> <td class="py-4 px-2 border-b border-slate-50 group-last:border-none">
</td>
<!-- Data Buttons -->
<td class="py-4 px-2 border-b border-slate-50 group-last:border-none">
<div class="flex gap-2"> <div class="flex gap-2">
<button <button
@click="getliveHost(row.hostId)" @click="getliveHost(row.hostId)"
@@ -131,111 +117,100 @@
> >
{{ $t('hostList.viewSessions') }} {{ $t('hostList.viewSessions') }}
</button> </button>
<button <!-- <button
@click="getRevenueStats(row.hostId)" @click="getRevenueStats(row.hostId)"
class="px-3 py-1.5 bg-sky-50 text-sky-600 hover:bg-sky-600 hover:text-white rounded-lg text-xs font-medium transition-all" class="px-3 py-1.5 bg-sky-50 text-sky-600 hover:bg-sky-600 hover:text-white rounded-lg text-xs font-medium transition-all"
> >
{{ $t('hostList.viewRevenue') }} {{ $t('hostList.viewRevenue') }}
</button> </button> -->
</div> </div>
</td> </td>
<!-- Country -->
<td class="py-4 px-2 border-b border-slate-50 group-last:border-none">
{{ row.country }}
</td>
<!-- Time -->
<td class="py-4 px-2 border-b border-slate-50 group-last:border-none">
<div class="flex flex-col">
<span>{{ formatTimeOnlyDate(row.createTime) }}</span>
<span class="text-[10px] text-slate-400">{{ formatTimeOnlyTime(row.createTime) }}</span>
</div>
</td>
<!-- Coins -->
<td class="py-4 px-2 border-b border-slate-50 group-last:border-none font-semibold text-slate-900">
{{ row.hostsCoins }}
</td>
<!-- Yesterday Coins -->
<td class="py-4 px-2 border-b border-slate-50 group-last:border-none">
{{ row.yesterdayCoins }}
</td>
<!-- Fans -->
<td class="py-4 px-2 border-b border-slate-50 group-last:border-none">
{{ row.fans }}
</td>
<!-- Followers -->
<td class="py-4 px-2 border-b border-slate-50 group-last:border-none">
{{ row.fllowernum }}
</td>
<!-- Online Fans -->
<td class="py-4 px-2 border-b border-slate-50 group-last:border-none">
{{ row.onlineFans }}
</td>
<!-- Host Kind -->
<td class="py-4 px-2 border-b border-slate-50 group-last:border-none">
{{ row.hostsKind }}
</td>
</tr> <!-- Country -->
</tbody> <td class="py-4 px-2 border-b border-slate-50 group-last:border-none">
</table> {{ row.country }}
</td>
<!-- Time -->
<td class="py-4 px-2 border-b border-slate-50 group-last:border-none">
<div class="flex flex-col">
<span>{{ formatTimeOnlyDate(row.createTime) }}</span>
<span class="text-[10px] text-slate-400">{{ formatTimeOnlyTime(row.createTime) }}</span>
</div>
</td>
<!-- Coins -->
<td class="py-4 px-2 border-b border-slate-50 group-last:border-none font-semibold text-slate-900">
{{ row.hostsCoins }}
</td>
<!-- Yesterday Coins -->
<td class="py-4 px-2 border-b border-slate-50 group-last:border-none">
{{ row.yesterdayCoins }}
</td>
<!-- Fans -->
<td class="py-4 px-2 border-b border-slate-50 group-last:border-none">
{{ row.fans }}
</td>
<!-- Followers -->
<td class="py-4 px-2 border-b border-slate-50 group-last:border-none">
{{ row.fllowernum }}
</td>
<!-- Online Fans -->
<td class="py-4 px-2 border-b border-slate-50 group-last:border-none">
{{ row.onlineFans }}
</td>
<!-- Host Kind -->
<td class="py-4 px-2 border-b border-slate-50 group-last:border-none">
{{ row.hostsKind }}
</td>
</tr>
</tbody>
</table>
</div> </div>
</div> </div>
<!-- Footer / Pagination --> <!-- Footer / Pagination -->
<footer class="px-8 py-6 border-t border-slate-100 dark:border-slate-200/60 bg-white flex flex-wrap items-center justify-between gap-4"> <footer
class="px-8 py-6 border-t border-slate-100 dark:border-slate-200/60 bg-white flex flex-wrap items-center justify-between gap-4">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<div class="relative"> <div class="relative">
<select <select v-model="pageSize" @change="handleSizeChange"
v-model="pageSize" class="bg-slate-50 border-none rounded-lg py-2 pl-4 pr-10 text-sm text-slate-600 appearance-none focus:ring-2 focus:ring-primary/20 shadow-soft-inner outline-none">
@change="handleSizeChange"
class="bg-slate-50 border-none rounded-lg py-2 pl-4 pr-10 text-sm text-slate-600 appearance-none focus:ring-2 focus:ring-primary/20 shadow-soft-inner outline-none"
>
<option :value="10">10/page</option> <option :value="10">10/page</option>
<option :value="20">20/page</option> <option :value="20">20/page</option>
<option :value="50">50/page</option> <option :value="50">50/page</option>
<option :value="100">100/page</option> <option :value="100">100/page</option>
</select> </select>
<span class="material-icons-round absolute right-2 top-1/2 -translate-y-1/2 text-slate-400 text-sm pointer-events-none">expand_more</span> <span
class="material-icons-round absolute right-2 top-1/2 -translate-y-1/2 text-slate-400 text-sm pointer-events-none">expand_more</span>
</div> </div>
<span class="text-xs text-slate-400 font-medium">总条数: <span class="text-primary">{{ total }}</span></span> <span class="text-xs text-slate-400 font-medium">总条数: <span class="text-primary">{{ total }}</span></span>
</div> </div>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<button <button @click="changePage(page - 1)" :disabled="page <= 1"
@click="changePage(page - 1)" class="p-2 text-slate-400 hover:bg-slate-100 rounded-lg transition-colors disabled:opacity-50">
:disabled="page <= 1"
class="p-2 text-slate-400 hover:bg-slate-100 rounded-lg transition-colors disabled:opacity-50"
>
<span class="material-icons-round text-lg">chevron_left</span> <span class="material-icons-round text-lg">chevron_left</span>
</button> </button>
<!-- Page Numbers --> <!-- Page Numbers -->
<template v-for="(p, index) in paginationPages" :key="index"> <template v-for="(p, index) in paginationPages" :key="index">
<span v-if="p === '...'" class="w-8 h-8 flex items-center justify-center text-slate-400 text-sm">...</span> <span v-if="p === '...'" class="w-8 h-8 flex items-center justify-center text-slate-400 text-sm">...</span>
<button <button v-else @click="changePage(p)" class="w-8 h-8 rounded-lg text-xs font-bold transition-all"
v-else :class="p === page ? 'bg-slate-900 text-white shadow-md' : 'text-slate-600 hover:bg-slate-100'">
@click="changePage(p)"
class="w-8 h-8 rounded-lg text-xs font-bold transition-all"
:class="p === page ? 'bg-slate-900 text-white shadow-md' : 'text-slate-600 hover:bg-slate-100'"
>
{{ p }} {{ p }}
</button> </button>
</template> </template>
<button <button @click="changePage(page + 1)" :disabled="page >= totalPages"
@click="changePage(page + 1)" class="p-2 text-slate-400 hover:bg-slate-100 rounded-lg transition-colors disabled:opacity-50">
:disabled="page >= totalPages"
class="p-2 text-slate-400 hover:bg-slate-100 rounded-lg transition-colors disabled:opacity-50"
>
<span class="material-icons-round text-lg">chevron_right</span> <span class="material-icons-round text-lg">chevron_right</span>
</button> </button>
</div> </div>
@@ -275,7 +250,9 @@
</el-select> </el-select>
</el-col> </el-col>
<el-col :span="10"> <el-col :span="10">
<div><label class="text-sm text-slate-500 mb-1 block">{{ $t('hostList.ascending') }}/{{ $t('hostList.descending') }}</label></div> <div><label class="text-sm text-slate-500 mb-1 block">{{ $t('hostList.ascending') }}/{{
$t('hostList.descending')
}}</label></div>
<el-select v-model="sortData.sortForm" filterable :placeholder="$t('hostList.selectPlaceholder')" <el-select v-model="sortData.sortForm" filterable :placeholder="$t('hostList.selectPlaceholder')"
class="w-full"> class="w-full">
<el-option <el-option
@@ -298,7 +275,8 @@
</el-dialog> </el-dialog>
<!-- Dialogs --> <!-- Dialogs -->
<LiveRecordDialog v-model:modelValue="liveDetailDialogVisible" :rows="liveDetailRecords" @select="handleLiveSelect" /> <LiveRecordDialog v-model:modelValue="liveDetailDialogVisible" :rows="liveDetailRecords"
@select="handleLiveSelect" />
<el-dialog v-model="revenueDialogVisible" :title="$t('hostList.liveRevenue')" width="80vw" top="6vh" <el-dialog v-model="revenueDialogVisible" :title="$t('hostList.liveRevenue')" width="80vw" top="6vh"
:close-on-click-modal="false" destroy-on-close> :close-on-click-modal="false" destroy-on-close>
@@ -448,7 +426,7 @@ const paginationPages = computed(() => {
const current = page.value const current = page.value
const totalP = totalPages.value const totalP = totalPages.value
const pages = [] const pages = []
if (totalP <= 7) { if (totalP <= 7) {
// 总页数小于等于7显示全部 // 总页数小于等于7显示全部
for (let i = 1; i <= totalP; i++) { for (let i = 1; i <= totalP; i++) {
@@ -457,15 +435,15 @@ const paginationPages = computed(() => {
} else { } else {
// 总是显示第一页 // 总是显示第一页
pages.push(1) pages.push(1)
if (current > 4) { if (current > 4) {
pages.push('...') pages.push('...')
} }
// 计算中间页码范围 // 计算中间页码范围
let start = Math.max(2, current - 2) let start = Math.max(2, current - 2)
let end = Math.min(totalP - 1, current + 2) let end = Math.min(totalP - 1, current + 2)
// 调整范围确保显示足够的页码 // 调整范围确保显示足够的页码
if (current <= 4) { if (current <= 4) {
end = Math.min(5, totalP - 1) end = Math.min(5, totalP - 1)
@@ -473,21 +451,21 @@ const paginationPages = computed(() => {
if (current >= totalP - 3) { if (current >= totalP - 3) {
start = Math.max(totalP - 4, 2) start = Math.max(totalP - 4, 2)
} }
for (let i = start; i <= end; i++) { for (let i = start; i <= end; i++) {
pages.push(i) pages.push(i)
} }
if (current < totalP - 3) { if (current < totalP - 3) {
pages.push('...') pages.push('...')
} }
// 总是显示最后一页 // 总是显示最后一页
if (totalP > 1) { if (totalP > 1) {
pages.push(totalP) pages.push(totalP)
} }
} }
return pages return pages
}) })
@@ -549,9 +527,9 @@ function handleClose(done) {
function getliveHost(hostId) { function getliveHost(hostId) {
liveHostDetail({ liveHostDetail({
"hostsId": hostId, "hostsId": hostId,
"tenantId": userInfo.value.tenantId "tenantId": userInfo.value.tenantId
}).then(res => { }).then(res => {
const detailList = Array.isArray(res) ? res : (res?.records || []) const detailList = Array.isArray(res) ? res : (res?.records || [])
liveDetailRecords.value = detailList liveDetailRecords.value = detailList
liveDetailDialogVisible.value = true liveDetailDialogVisible.value = true
@@ -588,13 +566,13 @@ function getCountry() {
} }
function formatTimeOnlyDate(val) { function formatTimeOnlyDate(val) {
if(!val) return '' if (!val) return ''
return val.split(' ')[0] || val return val.split(' ')[0] || val
} }
function formatTimeOnlyTime(val) { function formatTimeOnlyTime(val) {
if(!val) return '' if (!val) return ''
return val.split(' ')[1] || '' return val.split(' ')[1] || ''
} }
// History parsing helpers (preserved from original) // History parsing helpers (preserved from original)
@@ -606,9 +584,9 @@ function parseHistoryItems(history) {
} }
if (!Array.isArray(arr)) return [] if (!Array.isArray(arr)) return []
return arr.map((item, index) => { return arr.map((item, index) => {
if (typeof item === 'number') return { date: `Day ${index + 1}`, value: item } if (typeof item === 'number') return { date: `Day ${index + 1}`, value: item }
if (item && typeof item === 'object') return { date: item.date ? String(item.date) : '', value: Number(item.value ?? 0) } if (item && typeof item === 'object') return { date: item.date ? String(item.date) : '', value: Number(item.value ?? 0) }
return null return null
}).filter(Boolean) }).filter(Boolean)
} }
function hasHistory(history) { return parseHistoryItems(history).length > 0 } function hasHistory(history) { return parseHistoryItems(history).length > 0 }
@@ -663,6 +641,7 @@ function formatTimestamp(value) {
padding: 4px; padding: 4px;
width: 190px; width: 190px;
} }
.history-sparkline-top, .history-sparkline-top,
.history-sparkline-bottom { .history-sparkline-bottom {
display: flex; display: flex;
@@ -671,6 +650,7 @@ function formatTimestamp(value) {
color: #64748b; color: #64748b;
margin-bottom: 2px; margin-bottom: 2px;
} }
.history-sparkline { .history-sparkline {
background-color: #fff; background-color: #fff;
border-radius: 2px; border-radius: 2px;

View File

@@ -1,41 +1,59 @@
<template> <template>
<div class="grid grid-cols-1 lg:grid-cols-4 gap-4 mb-4"> <div class="grid grid-cols-1 lg:grid-cols-12 gap-4 mb-4">
<!-- Stat Cards --> <!-- Stat Cards -->
<div class="bg-white dark:bg-slate-900 p-6 rounded-xl shadow-sm border border-slate-100 dark:border-slate-800"> <!-- 总数量 (较小) -->
<div class="flex items-center justify-between mb-2"> <div class="lg:col-span-2 bg-white dark:bg-slate-900 p-4 rounded-xl shadow-sm border border-slate-100 dark:border-slate-800">
<span class="text-sm font-medium text-slate-500">{{ $t('workbenches.totalnumber') }}</span> <div class="flex items-center justify-between mb-1">
<span class="material-icons-round text-primary/40">analytics</span> <span class="text-xs font-medium text-slate-500">{{ $t('workbenches.totalnumber') }}</span>
<span class="material-icons-round text-primary/40 text-lg">analytics</span>
</div> </div>
<div class="text-3xl font-bold text-slate-900 dark:text-white">{{ hostData.totalCount }}</div> <div class="text-xl font-bold text-slate-900 dark:text-white">{{ hostData.totalCount }}</div>
</div> </div>
<div class="bg-white dark:bg-slate-900 p-6 rounded-xl shadow-sm border border-slate-100 dark:border-slate-800">
<div class="flex items-center justify-between mb-2"> <!-- 新建主播 (较小) -->
<span class="text-sm font-medium text-slate-500">{{ $t('workbenches.createHost') }}</span> <div class="lg:col-span-2 bg-white dark:bg-slate-900 p-4 rounded-xl shadow-sm border border-slate-100 dark:border-slate-800">
<span class="material-icons-round text-secondary/40">person_add</span> <div class="flex items-center justify-between mb-1">
<span class="text-xs font-medium text-slate-500">{{ $t('workbenches.createHost') }}</span>
<span class="material-icons-round text-secondary/40 text-lg">person_add</span>
</div> </div>
<div class="text-3xl font-bold text-slate-900 dark:text-white">{{ hostData.validAnchorsCount }}</div> <div class="text-xl font-bold text-slate-900 dark:text-white">{{ hostData.validAnchorsCount }}</div>
</div> </div>
<div class="bg-white dark:bg-slate-900 p-6 rounded-xl shadow-sm border border-slate-100 dark:border-slate-800">
<div class="flex items-center justify-between mb-2"> <!-- 查询 (较小) -->
<span class="text-sm font-medium text-slate-500">{{ $t('workbenches.query') }} / {{ $t('workbenches.invite') <div class="lg:col-span-2 bg-white dark:bg-slate-900 p-4 rounded-xl shadow-sm border border-slate-100 dark:border-slate-800">
}}</span> <div class="flex items-center justify-between mb-1">
<span class="material-icons-round text-slate-400">compare_arrows</span> <span class="text-xs font-medium text-slate-500">{{ $t('workbenches.query') }}</span>
<span class="material-icons-round text-amber-400/60 text-lg">search</span>
</div> </div>
<div class="text-3xl font-bold text-slate-900 dark:text-white">{{ hostData.checkedDataCount }} <span <div class="text-xl font-bold text-slate-900 dark:text-white">{{ hostData.checkedDataCount }}</div>
class="text-slate-300 text-lg mx-2">/</span> {{ hostData.canInvitationCount }}</div>
</div> </div>
<div
class="bg-white dark:bg-slate-900 p-6 rounded-xl shadow-sm border border-slate-100 dark:border-slate-800 flex flex-col justify-center"> <!-- 邀请 (较大突出显示) -->
<div class="lg:col-span-3 bg-gradient-to-br from-primary to-blue-600 p-5 rounded-xl shadow-lg shadow-primary/20 text-white relative overflow-hidden">
<div class="absolute top-0 right-0 w-24 h-24 bg-white/10 rounded-full -translate-y-1/2 translate-x-1/2"></div>
<div class="flex items-center justify-between mb-2 relative z-10">
<span class="text-sm font-medium text-white/80">{{ $t('workbenches.invite') }}</span>
<span class="material-icons-round text-white/60">mail_outline</span>
</div>
<div class="text-3xl font-bold text-white relative z-10">{{ hostData.canInvitationCount }}</div>
<div class="text-xs text-white/60 mt-1">可邀请主播</div>
</div>
<!-- 运行时间 (较大) -->
<div class="lg:col-span-3 bg-white dark:bg-slate-900 p-5 rounded-xl shadow-sm border border-slate-100 dark:border-slate-800 flex flex-col justify-center">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<span class="text-xs font-semibold text-slate-400 uppercase tracking-wider block mb-1">{{ <span class="text-xs font-semibold text-slate-400 uppercase tracking-wider block mb-1">{{
$t('workbenches.runTime') }}</span> $t('workbenches.runTime') }}</span>
<div class="text-2xl font-mono font-bold text-primary">{{ formattedTime }}</div> <div class="text-2xl font-mono font-bold text-primary">{{ formattedTime }}</div>
</div> </div>
<button @click="openTK" <div class="flex items-center gap-2">
class="bg-primary hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm font-semibold transition-all shadow-lg shadow-primary/25"> <span class="w-2 h-2 rounded-full" :class="isTkLoggedIn ? 'bg-emerald-500' : 'bg-red-500'"></span>
{{ $t('workbenches.openTK') }} <button @click="openTK"
</button> class="bg-primary hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm font-semibold shadow-lg shadow-primary/25">
{{ $t('workbenches.openTK') }}
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -55,19 +73,23 @@
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<div> <div>
<label class="text-xs font-semibold text-slate-500 mb-1 block">{{ $t('workbenches.guildAccount') }}</label> <label class="text-xs font-semibold text-slate-500 mb-1 block">{{ $t('workbenches.guildAccount') }}</label>
<input <el-input
class="w-full bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-primary/20 outline-none transition-all disabled:opacity-50" v-model="tkData[index].account"
type="text" v-model="tkData[index].account" :placeholder="$t('workbenches.guildAccountPlace')" :placeholder="$t('workbenches.guildAccountPlace')"
:disabled="!(tkData[index].code == 0 && !isLogin[index])" /> :disabled="!(tkData[index].code == 0 && !isLogin[index])"
class="el-input-custom"
/>
</div> </div>
<div> <div>
<label class="text-xs font-semibold text-slate-500 mb-1 block">{{ $t('workbenches.guildPass') }}</label> <label class="text-xs font-semibold text-slate-500 mb-1 block">{{ $t('workbenches.guildPass') }}</label>
<div class="relative"> <el-input
<input v-model="tkData[index].password"
class="w-full bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-primary/20 outline-none transition-all disabled:opacity-50" type="password"
type="password" v-model="tkData[index].password" :placeholder="$t('workbenches.guildPassPlace')" show-password
:disabled="!(tkData[index].code == 0 && !isLogin[index])" /> :placeholder="$t('workbenches.guildPassPlace')"
</div> :disabled="!(tkData[index].code == 0 && !isLogin[index])"
class="el-input-custom"
/>
</div> </div>
</div> </div>
<button @click="loginTK(index)" :disabled="!(tkData[index].code == 0 && !isLogin[index])" <button @click="loginTK(index)" :disabled="!(tkData[index].code == 0 && !isLogin[index])"
@@ -79,8 +101,7 @@
</div> </div>
<!-- Configuration Panel --> <!-- Configuration Panel -->
<div <div class="bg-white dark:bg-slate-900 rounded-2xl shadow-sm border border-slate-100 dark:border-slate-800 overflow-hidden">
class="bg-white dark:bg-slate-900 rounded-2xl shadow-sm border border-slate-100 dark:border-slate-800 overflow-hidden">
<div class="p-6 border-b border-slate-100 dark:border-slate-800 flex justify-between items-center"> <div class="p-6 border-b border-slate-100 dark:border-slate-800 flex justify-between items-center">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<div class="w-8 h-8 bg-slate-100 dark:bg-slate-800 rounded-lg flex items-center justify-center"> <div class="w-8 h-8 bg-slate-100 dark:bg-slate-800 rounded-lg flex items-center justify-center">
@@ -280,7 +301,7 @@ import { tkaccountuseinfo, getExpiredTime } from '@/api/account'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
const { locale } = useI18n() const { locale } = useI18n()
//导入python交互方法 //导入python交互方法
const { fetchDataConfig, fetchDataCount, loginBackStage, loginTikTok, backStageloginStatus, backStageloginStatusCopy } = usePythonBridge(); const { fetchDataConfig, fetchDataCount, loginBackStage, loginTikTok, backStageloginStatus, backStageloginStatusCopy, getTkLoginStatus } = usePythonBridge();
//ip国家 //ip国家
@@ -305,6 +326,10 @@ let hostData = ref({
//账号是否登陆中 //账号是否登陆中
let isLogin = ref([false, false]); let isLogin = ref([false, false]);
//TK登录状态
let isTkLoggedIn = ref(false);
//TK状态轮询定时器
let tkStatusTimer = ref(null);
//设置状态轮询定时器 //设置状态轮询定时器
let statusTimer = ref(null); let statusTimer = ref(null);
let statusTimerCopy = ref(null); let statusTimerCopy = ref(null);
@@ -609,7 +634,23 @@ const openTK = () => {
// isTk.value = true; // isTk.value = true;
// console.log(isTk.value) // console.log(isTk.value)
loginTikTok(); loginTikTok();
// 开始轮询TK登录状态
if (tkStatusTimer.value) {
clearInterval(tkStatusTimer.value);
}
tkStatusTimer.value = setInterval(() => {
checkTkLoginStatus();
}, 3000);
}
// 检查TK登录状态
const checkTkLoginStatus = () => {
getTkLoginStatus().then((res) => {
isTkLoggedIn.value = res === true || res === 'true';
}).catch(() => {
isTkLoggedIn.value = false;
});
} }
function getloginStatus() { function getloginStatus() {
@@ -804,4 +845,41 @@ const checkVPN = async () => {
Most styles are replaced by Tailwind utility classes. Most styles are replaced by Tailwind utility classes.
We can keep specific overrides or custom animations here if needed. We can keep specific overrides or custom animations here if needed.
*/ */
/* Element Plus 输入框统一样式 */
.el-input-custom :deep(.el-input__wrapper) {
background-color: white;
border: 1px solid rgb(226, 232, 240);
border-radius: 0.5rem;
padding: 0.5rem 1rem;
box-shadow: none;
transition: all 0.15s ease;
}
.el-input-custom :deep(.el-input__wrapper:hover) {
border-color: rgb(203, 213, 225);
}
.el-input-custom :deep(.el-input__wrapper.is-focus) {
border-color: var(--el-color-primary);
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
}
.el-input-custom :deep(.el-input__inner) {
font-size: 0.875rem;
}
.el-input-custom :deep(.el-input__wrapper.is-disabled) {
opacity: 0.5;
}
/* 暗色模式支持 */
.dark .el-input-custom :deep(.el-input__wrapper) {
background-color: rgb(30, 41, 59);
border-color: rgb(51, 65, 85);
}
.dark .el-input-custom :deep(.el-input__wrapper:hover) {
border-color: rgb(71, 85, 105);
}
</style> </style>

View File

@@ -0,0 +1,872 @@
<template>
<div class="grid grid-cols-1 lg:grid-cols-4 gap-4 mb-4">
<!-- Stat Cards -->
<div class="bg-white dark:bg-slate-900 p-6 rounded-xl shadow-sm border border-slate-100 dark:border-slate-800">
<div class="flex items-center justify-between mb-2">
<span class="text-sm font-medium text-slate-500">{{ $t('workbenches.totalnumber') }}</span>
<span class="material-icons-round text-primary/40">analytics</span>
</div>
<div class="text-3xl font-bold text-slate-900 dark:text-white">{{ hostData.totalCount }}</div>
</div>
<div class="bg-white dark:bg-slate-900 p-6 rounded-xl shadow-sm border border-slate-100 dark:border-slate-800">
<div class="flex items-center justify-between mb-2">
<span class="text-sm font-medium text-slate-500">{{ $t('workbenches.createHost') }}</span>
<span class="material-icons-round text-secondary/40">person_add</span>
</div>
<div class="text-3xl font-bold text-slate-900 dark:text-white">{{ hostData.validAnchorsCount }}</div>
</div>
<div class="bg-white dark:bg-slate-900 p-6 rounded-xl shadow-sm border border-slate-100 dark:border-slate-800">
<div class="flex items-center justify-between mb-2">
<span class="text-sm font-medium text-slate-500">{{ $t('workbenches.query') }} / {{ $t('workbenches.invite')
}}</span>
<span class="material-icons-round text-slate-400">compare_arrows</span>
</div>
<div class="text-3xl font-bold text-slate-900 dark:text-white">{{ hostData.checkedDataCount }} <span
class="text-slate-300 text-lg mx-2">/</span> {{ hostData.canInvitationCount }}</div>
</div>
<div
class="bg-white dark:bg-slate-900 p-6 rounded-xl shadow-sm border border-slate-100 dark:border-slate-800 flex flex-col justify-center">
<div class="flex items-center justify-between">
<div>
<span class="text-xs font-semibold text-slate-400 uppercase tracking-wider block mb-1">{{
$t('workbenches.runTime') }}</span>
<div class="text-2xl font-mono font-bold text-primary">{{ formattedTime }}</div>
</div>
<div class="flex items-center gap-2">
<span class="w-2 h-2 rounded-full" :class="isTkLoggedIn ? 'bg-emerald-500' : 'bg-red-500'"></span>
<button @click="openTK"
class="bg-primary hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm font-semibold transition-all shadow-lg shadow-primary/25">
{{ $t('workbenches.openTK') }}
</button>
</div>
</div>
</div>
</div>
<!-- Guild Accounts -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div v-for="(item, index) in 2" :key="index" class="bg-white border border-slate-100 p-5 rounded-xl shadow-sm">
<div class="flex justify-between items-center mb-6">
<div class="flex items-center gap-2">
<span class="w-2 h-2 rounded-full" :class="tkData[index].code == 1 ? 'bg-emerald-500' : 'bg-red-500'"></span>
<h3 class="font-bold text-slate-800 dark:text-white">{{ $t('workbenches.guildAccount') }} {{ index === 0 ? 'A'
: 'B' }}</h3>
</div>
<span class="text-xs text-slate-500">{{ $t('workbenches.queriedNum') }}: {{ tkData[index].num }}</span>
</div>
<div class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="text-xs font-semibold text-slate-500 mb-1 block">{{ $t('workbenches.guildAccount') }}</label>
<el-input
v-model="tkData[index].account"
:placeholder="$t('workbenches.guildAccountPlace')"
:disabled="!(tkData[index].code == 0 && !isLogin[index])"
class="el-input-custom"
/>
</div>
<div>
<label class="text-xs font-semibold text-slate-500 mb-1 block">{{ $t('workbenches.guildPass') }}</label>
<el-input
v-model="tkData[index].password"
type="password"
show-password
:placeholder="$t('workbenches.guildPassPlace')"
:disabled="!(tkData[index].code == 0 && !isLogin[index])"
class="el-input-custom"
/>
</div>
</div>
<button @click="loginTK(index)" :disabled="!(tkData[index].code == 0 && !isLogin[index])"
class="w-full bg-slate-900 dark:bg-slate-700 hover:bg-black text-white py-2.5 rounded-lg font-medium transition-all disabled:opacity-50 disabled:cursor-not-allowed">
{{ $t('workbenches.loginBackend') }}
</button>
</div>
</div>
</div>
<!-- Configuration Panel -->
<div
class="bg-white dark:bg-slate-900 rounded-2xl shadow-sm border border-slate-100 dark:border-slate-800 overflow-hidden">
<div class="p-6 border-b border-slate-100 dark:border-slate-800 flex justify-between items-center">
<div class="flex items-center gap-3">
<div class="w-8 h-8 bg-slate-100 dark:bg-slate-800 rounded-lg flex items-center justify-center">
<span class="material-icons-round text-slate-600 dark:text-slate-400 text-lg">settings</span>
</div>
<h2 class="font-bold text-slate-800 dark:text-white">{{ $t('workbenchesSetup.workbenches') }}</h2>
</div>
<div class="flex items-center gap-4 text-sm">
<div class="text-slate-500">{{ $t('workbenchesSetup.network') }}: <span class="text-primary font-bold">{{ locale
== 'zh' ? countryData : countryDataEN }}</span></div>
<div class="flex items-center gap-2">
<span class="text-slate-500">指定国家:</span>
<select v-model="country_info"
class="bg-slate-50 dark:bg-slate-800 border-none rounded-lg text-xs font-medium focus:ring-0">
<option value="全部">全部</option>
<option v-for="(item, index) in country_Lst" :key="index" :value="item">{{ item }}</option>
</select>
</div>
</div>
</div>
<div class="p-6">
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6 mb-6">
<!-- Coins -->
<div>
<h4 class="text-sm font-bold text-slate-800 dark:text-white mb-4 flex items-center gap-2">
<span class="w-1 h-4 bg-primary rounded-full"></span>
{{ $t('workbenchesSetup.setCoinsNum') }}
</h4>
<div class="space-y-3">
<div class="flex shadow-sm rounded-lg overflow-hidden border border-slate-200 dark:border-slate-700">
<span
class="bg-slate-50 dark:bg-slate-800 px-3 py-2 text-xs font-medium text-slate-500 w-28 flex items-center border-r border-slate-200 dark:border-slate-700">{{
$t('workbenchesSetup.minCoinsNum') }}</span>
<input
class="flex-1 px-4 py-2 text-sm bg-white dark:bg-slate-900 border-none outline-none focus:ring-0 disabled:bg-slate-100"
type="number" v-model="pyData.gold.min" :disabled="!pyData.isStart" />
</div>
<div class="flex shadow-sm rounded-lg overflow-hidden border border-slate-200 dark:border-slate-700">
<span
class="bg-slate-50 dark:bg-slate-800 px-3 py-2 text-xs font-medium text-slate-500 w-28 flex items-center border-r border-slate-200 dark:border-slate-700">{{
$t('workbenchesSetup.maxCoinsNum') }}</span>
<input
class="flex-1 px-4 py-2 text-sm bg-white dark:bg-slate-900 border-none outline-none focus:ring-0 disabled:bg-slate-100"
type="number" v-model="pyData.gold.max" :disabled="!pyData.isStart" />
</div>
</div>
</div>
<!-- Fans -->
<div>
<h4 class="text-sm font-bold text-slate-800 dark:text-white mb-4 flex items-center gap-2">
<span class="w-1 h-4 bg-secondary rounded-full"></span>
{{ $t('workbenchesSetup.setFansNum') }}
</h4>
<div class="space-y-3">
<div class="flex shadow-sm rounded-lg overflow-hidden border border-slate-200 dark:border-slate-700">
<span
class="bg-slate-50 dark:bg-slate-800 px-3 py-2 text-xs font-medium text-slate-500 w-28 flex items-center border-r border-slate-200 dark:border-slate-700">{{
$t('workbenchesSetup.minFansNum') }}</span>
<input
class="flex-1 px-4 py-2 text-sm bg-white dark:bg-slate-900 border-none outline-none focus:ring-0 disabled:bg-slate-100"
type="number" v-model="pyData.fans.min" :disabled="!pyData.isStart" />
</div>
<div class="flex shadow-sm rounded-lg overflow-hidden border border-slate-200 dark:border-slate-700">
<span
class="bg-slate-50 dark:bg-slate-800 px-3 py-2 text-xs font-medium text-slate-500 w-28 flex items-center border-r border-slate-200 dark:border-slate-700">{{
$t('workbenchesSetup.maxFansNum') }}</span>
<input
class="flex-1 px-4 py-2 text-sm bg-white dark:bg-slate-900 border-none outline-none focus:ring-0 disabled:bg-slate-100"
type="number" v-model="pyData.fans.max" :disabled="!pyData.isStart" />
</div>
</div>
</div>
<!-- Frequency -->
<div>
<h4 class="text-sm font-bold text-slate-800 dark:text-white mb-4 flex items-center gap-2">
<span class="w-1 h-4 bg-emerald-500 rounded-full"></span>
{{ $t('workbenchesSetup.setQuery') }}
</h4>
<div class="space-y-3">
<div class="flex shadow-sm rounded-lg overflow-hidden border border-slate-200 dark:border-slate-700">
<input
class="flex-1 px-4 py-2 text-sm bg-white dark:bg-slate-900 border-none outline-none focus:ring-0 disabled:bg-slate-100"
type="number" v-model="pyData.frequency.hour" :disabled="!pyData.isStart" />
<span
class="bg-slate-100 dark:bg-slate-800 px-3 py-2 text-xs font-medium text-slate-500 w-24 flex items-center justify-center border-l border-slate-200 dark:border-slate-700">{{
$t('workbenchesSetup.hour') }}</span>
</div>
<div class="flex shadow-sm rounded-lg overflow-hidden border border-slate-200 dark:border-slate-700">
<input
class="flex-1 px-4 py-2 text-sm bg-white dark:bg-slate-900 border-none outline-none focus:ring-0 disabled:bg-slate-100"
type="number" v-model="pyData.frequency.day" :disabled="!pyData.isStart" />
<span
class="bg-slate-100 dark:bg-slate-800 px-3 py-2 text-xs font-medium text-slate-500 w-24 flex items-center justify-center border-l border-slate-200 dark:border-slate-700">{{
$t('workbenchesSetup.hour24') }}</span>
</div>
</div>
</div>
<!-- Quantity Limit -->
<div>
<h4 class="text-sm font-bold text-slate-800 dark:text-white mb-4 flex items-center gap-2">
<span class="w-1 h-4 bg-orange-400 rounded-full"></span>
{{ $t('workbenchesSetup.setNum') }}
<span class="text-[10px] text-slate-400 font-normal ml-1">({{ $t('workbenchesSetup.prompt') }})</span>
</h4>
<div class="space-y-3">
<div class="flex gap-2">
<button @click="isLimit = true" :disabled="!pyData.isStart"
class="flex-1 px-3 py-2 text-xs font-semibold rounded-md border transition-colors"
:class="isLimit ? 'bg-primary text-white border-primary' : 'bg-white text-slate-600 border-slate-200 hover:border-primary/50'">
{{ $t('workbenchesSetup.setHostNum') }}
</button>
<button @click="isLimit = false" :disabled="!pyData.isStart"
class="flex-1 px-3 py-2 text-xs font-semibold rounded-md border transition-colors"
:class="!isLimit ? 'bg-slate-500 text-white border-slate-500' : 'bg-white text-slate-600 border-slate-200 hover:border-slate-400'">
{{ $t('workbenchesSetup.unlimitedQuantity') }}
</button>
</div>
<div v-if="isLimit"
class="flex shadow-sm rounded-lg overflow-hidden border border-slate-200 dark:border-slate-700">
<input
class="flex-1 px-4 py-2 text-sm bg-white dark:bg-slate-900 border-none outline-none focus:ring-0 disabled:bg-slate-100"
type="number" v-model="hostNum" :disabled="!pyData.isStart" />
<span
class="bg-slate-100 dark:bg-slate-800 px-3 py-2 text-xs font-medium text-slate-500 w-16 flex items-center justify-center border-l border-slate-200 dark:border-slate-700">{{
$t('workbenchesSetup.num') }}</span>
</div>
</div>
</div>
</div>
<div
class="flex flex-col lg:flex-row items-center justify-between gap-6 pt-4 border-t border-slate-100 dark:border-slate-800">
<div class="flex items-center gap-6">
<div class="flex items-center gap-2 cursor-pointer group" @click="toggleFilter('filterGame')">
<span
class="w-4 h-4 rounded border-2 flex items-center justify-center transition-all"
:class="pyData.filterGame ? 'bg-primary border-primary' : 'bg-white border-slate-300'"
>
<span v-if="pyData.filterGame" class="material-icons-round text-white text-xs">check</span>
</span>
<span
class="text-sm text-slate-600 dark:text-slate-400 group-hover:text-primary transition-colors">过滤游戏主播</span>
</div>
<div class="flex items-center gap-2 cursor-pointer group" @click="toggleFilter('filterSelling')">
<span
class="w-4 h-4 rounded border-2 flex items-center justify-center transition-all"
:class="pyData.filterSelling ? 'bg-primary border-primary' : 'bg-white border-slate-300'"
>
<span v-if="pyData.filterSelling" class="material-icons-round text-white text-xs">check</span>
</span>
<span
class="text-sm text-slate-600 dark:text-slate-400 group-hover:text-primary transition-colors">过滤带货主播</span>
</div>
<div class="flex items-center gap-2 cursor-pointer group" @click="toggleFilter('rankingList')">
<span
class="w-4 h-4 rounded border-2 flex items-center justify-center transition-all"
:class="pyData.rankingList ? 'bg-primary border-primary' : 'bg-white border-slate-300'"
>
<span v-if="pyData.rankingList" class="material-icons-round text-white text-xs">check</span>
</span>
<span
class="text-sm text-slate-600 dark:text-slate-400 group-hover:text-primary transition-colors">过滤排行榜单</span>
</div>
</div>
</div>
<div class="mt-6 text-center">
<button v-if="pyData.isStart" @click="submit"
class="bg-slate-900 dark:bg-primary hover:scale-[1.02] active:scale-[0.98] text-white px-10 py-3 rounded-xl font-bold text-lg shadow-xl shadow-slate-900/10 dark:shadow-primary/20 transition-all flex items-center gap-2 mx-auto">
<span class="material-icons-round">bolt</span>
{{ $t('workbenchesSetup.start') }}
</button>
<button v-else @click="unsubmit"
class="bg-red-500 hover:bg-red-600 hover:scale-[1.02] active:scale-[0.98] text-white px-12 py-4 rounded-xl font-bold text-lg shadow-xl shadow-red-500/20 transition-all flex items-center gap-3 mx-auto">
<span class="material-icons-round">stop</span>
{{ $t('workbenchesSetup.stop') }}
</button>
<p class="mt-4 text-xs font-medium text-emerald-600 dark:text-emerald-400">
到期时间: {{ timestampToTime(expiredTime) }}
</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue';
import { usePythonBridge, } from '@/utils/pythonBridge'
import { setNumData, getNumData, getUser, setTkUser, getTkUser } from '@/utils/storage'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getCountryName } from '@/utils/countryUtil'
import { tkaccountuseinfo, getExpiredTime } from '@/api/account'
import { useI18n } from 'vue-i18n'
const { locale } = useI18n()
//导入python交互方法
const { fetchDataConfig, fetchDataCount, loginBackStage, loginTikTok, backStageloginStatus, backStageloginStatusCopy, getTkLoginStatus } = usePythonBridge();
//ip国家
let countryData = ref('');
//英文国家
let countryDataEN = ref('');
let country_info = ref('全部');
let country_Lst = ref();
//获取主播数量的定时器
let getHostTimer = ref(null);
//获取查询次数定时器
let getNumTimer = ref(null);
//获取的主播信息
let hostData = ref({
totalCount: 0,
validAnchorsCount: 0,
canInvitationCount: 0,
checkedDataCount: 0,
});
//是否开启tk
// let isTk = ref(true);
//TK登录状态
let isTkLoggedIn = ref(false);
//TK状态轮询定时器
let tkStatusTimer = ref(null);
//账号是否登陆中
let isLogin = ref([false, false]);
//设置状态轮询定时器
let statusTimer = ref(null);
let statusTimerCopy = ref(null);
//设置次数最大值
let maxCount = ref([
{
hourMax: 50,
dayMax: 300,
},
{
hourMax: 100,
dayMax: 600,
},
]);
//tk账号信息
let tkData = ref([
{
account: '',
password: '',
index: 1,
code: 0,
num: 0
},
{
account: '',
password: '',
index: 2,
code: 0,
num: 0
},
]);
//python需要的数据
let pyData = ref({
gold: { min: 0, max: 0 },
fans: { min: 0, max: 0 },
frequency: { hour: 0, day: 0 },
isStart: true,
country: countryData.value,
filterSelling: false,
filterGame: false,
rankingList: false,
tenantId: getUser().tenantId,
userId: getUser().userId,
});
//是否限制查询数量
let isLimit = ref(false);
//需要查询的主播数
let hostNum = ref(0);
//按钮提交状态
let submitting = ref(true);
let expiredTime = ref(null);
onMounted(async () => {
//从缓存获取数据
if (getNumData()) {
pyData.value = getNumData();
}
if (getTkUser()) {
tkData.value = getTkUser();
tkData.value[0].code = 0;
tkData.value[1].code = 0;
}
tkaccountuse(tkData.value[0].account, 0)
tkaccountuse(tkData.value[1].account, 1)
getIpInfo()
setTimeout(() => {
getExpiredTime(getUser().tenantId).then((res) => {
console.log('time:', res);
expiredTime.value = res.expiredTime
})
}, 1000);
checkVPN()
setInterval(async () => {
await checkVPN()
}, 1000 * 20)
})
const getIpInfo = async () => {
try {
const response = await fetch('https://ipapi.co/json/');
if (!response.ok) {
throw new Error('请求失败');
}
const data = await response.json();
console.log('IP信息:', data.country);
countryDataEN.value = data.country_name
countryData.value = getCountryName(data.country);
const url = `https://datasave.api.yolozs.com/api/save_data/country_info?countryName=${countryData.value}`;
const res = await fetch(url);
const countryres = await res.json();
country_Lst.value = countryres.data
} catch (error) {
console.error('请求出错:', error);
ElMessageBox.prompt('请输入将要获取国家的中文名', '获取国家失败', {
confirmButtonText: '确认',
cancelButtonText: '取消',
showClose: false,
closeOnClickModal: false,
showCancelButton: false,
})
.then(async ({ value }) => {
countryData.value = value
const url = `https://datasave.api.yolozs.com/api/save_data/country_info?countryName=${countryData.value}`;
const res = await fetch(url);
const countryres = await res.json();
country_Lst.value = countryres.data
})
// .catch(() => {
// ElMessage({
// type: 'info',
// message: 'Input canceled',
// })
// })
}
};
//提交数据到py
const submit = () => {
pyData.value.country = countryData.value;
console.log('提交的区间值:', pyData.value);
// if (tkData.value[0].account == '' && tkData.value[1].account == '') {
// ElMessage.error('请输入账号密码');
// return;
// }
// if (tkData.value[0].password == '' && tkData.value[1].password == '') {
// ElMessage.error('请输入账号密码');
// return;
// }
if (((Number(pyData.value.gold.min) > Number(pyData.value.gold.max)) || (Number(pyData.value.fans.min) > Number(pyData.value.fans.max)))) {
ElMessage.error('请输入正确的区间值');
return;
}
if ((Number(pyData.value.gold.max) <= 0 || Number(pyData.value.fans.max <= 0)) || pyData.value.gold.max == '' || pyData.value.fans.max == '') {
ElMessage.error('请输入正确的区间值');
return;
}
if (Number(pyData.value.frequency.hour) <= 0 || Number(pyData.value.frequency.day) <= 0 || pyData.value.frequency.hour == '' || pyData.value.frequency.day == '') {
ElMessage.error('请输入正确的频率区间值');
return;
}
//是否限制爬取数量
if (isLimit.value) {
if (hostNum.value <= 0) {
ElMessage.error('请输入正确的可邀请数量');
return;
}
}
ElMessageBox.confirm(
'确认开始爬取数据?',
'开始',
{
confirmButtonText: '开始',
cancelButtonText: '取消',
type: 'success',
}
)
.then(() => {
// console.log('提交的区间值:', pyData.value.gold, pyData.value.fans, pyData.value.frequency);
//开始按钮的状态 改为禁用
submitting.value = true;
setNumData(pyData.value);
console.error('提交的区间值:', JSON.stringify(pyData.value));
fetchDataConfig(JSON.stringify({
gold: pyData.value.gold,
fans: pyData.value.fans,
frequency: pyData.value.frequency,
isStart: true,
filterSelling: pyData.value.filterSelling,
filterGame: pyData.value.filterGame,
rankingList: !pyData.value.rankingList,
country: countryData.value,
tenantId: getUser().tenantId,
userId: getUser().id,
crawl_single_nation: country_info.value == '全部' ? '' : country_info.value
})).then((res) => {
//开始计时器
startTimer();
//开启查询次数
getHostTimer.value = setInterval(() => {
fetchDataCount().then((res) => {
hostData.value = JSON.parse(res);
if (isLimit.value) {
if (hostData.value.canInvitationCount >= hostNum.value) {
unsubmit();
alert('爬取完毕')
}
}
})
}, 1000);
getNumTimer.value = setInterval(() => {
tkaccountuse(tkData.value[0].account, 0)
tkaccountuse(tkData.value[1].account, 1)
}, 5000);
}).finally(() => {
setTimeout(() => {
pyData.value.isStart = false;
submitting.value = false;
}, 2000)
})
})
.catch(() => {
})
};
//停止
const unsubmit = () => {
fetchDataConfig(JSON.stringify({
gold: pyData.value.gold,
fans: pyData.value.fans,
frequency: pyData.value.frequency,
isStart: false,
filterSelling: pyData.value.filterSelling,
filterGame: pyData.value.filterGame,
rankingList: !pyData.value.rankingList,
country: countryData.value,
tenantId: getUser().tenantId,
userId: getUser().id,
crawl_single_nation: country_info.value == '全部' ? '' : country_info.value
})).then((res) => {
pauseTimer();
pyData.value.isStart = true;
clearInterval(getHostTimer.value);
getHostTimer.value = null;
clearInterval(getNumTimer.value);
getNumTimer.value = null;
// ElMessage.sussec('已停止')
}).catch((err) => {
// ElMessage.error('停止失败')
})
};
//重置
const reset = () => {
pyData.value.gold = { min: 0, max: 0 };
pyData.value.fans = { min: 0, max: 0 };
pyData.value.frequency = { hour: 0, day: 0 };
};
// 切换过滤选项 (用于Electron环境下的即时响应)
const toggleFilter = (filterName) => {
if (!pyData.value.isStart) return; // 如果已启动则不允许修改
pyData.value[filterName] = !pyData.value[filterName];
};
const loginTK = (index) => {
setTkUser(tkData.value)
loginBackStage({
account: tkData.value[index].account,
password: tkData.value[index].password,
index: index
})
if (index == 0) {
isLogin.value[1] = true;
statusTimer = setInterval(() => {
getloginStatus();
}, 2000)
} else if (index == 1) {
isLogin.value[0] = true;
statusTimerCopy = setInterval(() => {
getloginStatusCopy();
}, 2000)
}
}
const openTK = () => {
// isTk.value = true;
// console.log(isTk.value)
loginTikTok();
// 开始轮询TK登录状态
if (tkStatusTimer.value) {
clearInterval(tkStatusTimer.value);
}
tkStatusTimer.value = setInterval(() => {
checkTkLoginStatus();
}, 3000);
}
// 检查TK登录状态
const checkTkLoginStatus = () => {
getTkLoginStatus().then((res) => {
isTkLoggedIn.value = res === true || res === 'true';
}).catch(() => {
isTkLoggedIn.value = false;
});
}
function getloginStatus() {
backStageloginStatus().then((res) => {
const data = JSON.parse(res);
tkData.value[data.index].code = data.code
if (data.code == 1) {
clearInterval(statusTimer);
statusTimer = null;
submitting.value = false
isLogin.value[1] = false;
}
})
}
function getloginStatusCopy() {
backStageloginStatusCopy().then((res) => {
const data = JSON.parse(res);
tkData.value[data.index].code = data.code
if (data.code == 1) {
clearInterval(statusTimer);
statusTimer = null;
submitting.value = false
isLogin.value[0] = false;
}
})
}
function tkaccountuse(id, index) {
let num = 0;
console.log(id, index, "查询次数")
if (!id || id == '') {
return
}
tkaccountuseinfo(id).then((res) => {
console.log("查询返回", res)
num = res
tkData.value[index].num = num
setTkUser(tkData.value)
console.log('账号使用次数', tkData.value[index].num)
// ElMessage.error('账号使用次数', tkData.value[index].num);
}).catch((err) => {
console.log('账号使用次数', err)
})
}
const isRunning = ref(false);
const totalSeconds = ref(0);
//定时器
let timerCrawl = null;
const startTimedata = ref(null);
//清空时间 并开始运行
const startTimer = () => {
resetTimer();
if (isRunning.value) return;
isRunning.value = true;
startTimedata.value = Date.now();
timerCrawl = setInterval(() => {
totalSeconds.value = Math.floor((Date.now() - startTimedata.value) / 1000);
}, 1000);
};
//结束运行 暂停
const pauseTimer = () => {
isRunning.value = false;
clearInterval(timerCrawl);
};
//清空时间
const resetTimer = () => {
isRunning.value = false;
clearInterval(timerCrawl);
totalSeconds.value = 0;
};
// 格式化时间为 HH:MM:SS
const formattedTime = computed(() => {
const hours = Math.floor(totalSeconds.value / 3600);
const minutes = Math.floor((totalSeconds.value % 3600) / 60);
const seconds = totalSeconds.value % 60;
return [
hours.toString().padStart(2, '0'),
minutes.toString().padStart(2, '0'),
seconds.toString().padStart(2, '0')
].join(':');
});
function handleInputHour(value) {
console.log(value)
// 替换非数字字符为空字符串
let num = value.replace(/[^\d]/g, '');
// 如果值小于等于0则设置为0
if (Number(num) <= 0) {
num = 0;
}
if ((tkData.value[0].code == 1) && (tkData.value[1].code == 1)) {
if (Number(num) > maxCount.value[1].hourMax) {
num = maxCount.value[1].hourMax;
}
} else if ((tkData.value[0].code == 1) || (tkData.value[1].code == 1)) {
// 如果值大于最大值,则设置为最大值
if (Number(num) > maxCount.value[0].hourMax) {
num = maxCount.value[0].hourMax;
}
} else {
ElMessage.error('请先登录tk后台');
num = 0;
}
// 更新模型
pyData.value.frequency.hour = num;
}
function handleInputDay(value) {
console.log(value)
// 替换非数字字符为空字符串
let num = value.replace(/[^\d]/g, '');
// 如果值小于等于0则设置为0
if (Number(num) <= 0) {
num = 0;
}
if ((tkData.value[0].code == 1) && (tkData.value[1].code == 1)) {
if (Number(num) > maxCount.value[1].dayMax) {
num = maxCount.value[1].dayMax;
}
} else if ((tkData.value[0].code == 1) || (tkData.value[1].code == 1)) {
// 如果值大于最大值,则设置为最大值
if (Number(num) > maxCount.value[0].dayMax) {
num = maxCount.value[0].dayMax;
}
} else {
ElMessage.error('请先登录tk后台');
num = 0;
}
// 更新模型
pyData.value.frequency.day = num;
}
function timestampToTime(timestamp_ms) {
const date = new Date(timestamp_ms);
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
const seconds = date.getSeconds().toString().padStart(2, '0');
const milliseconds = date.getMilliseconds().toString().padStart(3, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
let isWifi = ref(false);
const checkVPN = async () => {
try {
// 设置超时 5 秒钟
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('请求超时')), 10000) // 10秒超时
);
// 使用 Promise.race 来进行超时控制
const response = await Promise.race([
fetch('https://www.google.com', { method: 'HEAD', mode: 'no-cors' }),
timeout
]);
// 判断 fetch 请求是否成功
if (response && response.type === 'opaque') {
// ElMessage.success('VPN连接正常');
isWifi.value = false;
} else {
ElMessage.error('VPN连接失败无法访问网络。');
isWifi.value = true;
}
} catch (error) {
// 捕获超时错误或其他错误
ElMessage.error('VPN连接失败无法访问网络。');
isWifi.value = true;
}
};
</script>
<style scoped>
/*
Most styles are replaced by Tailwind utility classes.
We can keep specific overrides or custom animations here if needed.
*/
/* Element Plus 输入框统一样式 */
.el-input-custom :deep(.el-input__wrapper) {
background-color: white;
border: 1px solid rgb(226, 232, 240);
border-radius: 0.5rem;
padding: 0.5rem 1rem;
box-shadow: none;
transition: all 0.15s ease;
}
.el-input-custom :deep(.el-input__wrapper:hover) {
border-color: rgb(203, 213, 225);
}
.el-input-custom :deep(.el-input__wrapper.is-focus) {
border-color: var(--el-color-primary);
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
}
.el-input-custom :deep(.el-input__inner) {
font-size: 0.875rem;
}
.el-input-custom :deep(.el-input__wrapper.is-disabled) {
opacity: 0.5;
}
/* 暗色模式支持 */
.dark .el-input-custom :deep(.el-input__wrapper) {
background-color: rgb(30, 41, 59);
border-color: rgb(51, 65, 85);
}
.dark .el-input-custom :deep(.el-input__wrapper:hover) {
border-color: rgb(71, 85, 105);
}
</style>