Files
web-fusion/src/views/tk/Workbenches.vue

945 lines
34 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>
<div class="h-full w-full overflow-y-auto bg-gray-50 p-6">
<div class="container mx-auto">
<div class="grid grid-cols-1 lg:grid-cols-12 gap-4 mb-4">
<!-- Stat Cards -->
<!-- 总数量 (较小) -->
<div class="lg:col-span-2 bg-white p-4 rounded-xl shadow-sm border border-slate-100">
<div class="flex items-center justify-between mb-1">
<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 class="text-xl font-bold text-slate-900">{{ hostData.totalCount }}</div>
</div>
<!-- 新建主播 (较小) -->
<div class="lg:col-span-2 bg-white p-4 rounded-xl shadow-sm border border-slate-100">
<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 class="text-xl font-bold text-slate-900">{{ hostData.validAnchorsCount }}</div>
</div>
<!-- 查询 (较小) -->
<div class="lg:col-span-2 bg-white p-4 rounded-xl shadow-sm border border-slate-100">
<div class="flex items-center justify-between mb-1">
<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 class="text-xl font-bold text-slate-900">{{ hostData.checkedDataCount }}</div>
</div>
<!-- 邀请 (较大突出显示) -->
<div
class="lg:col-span-3 bg-gradient-to-br from-blue-600 to-blue-500 p-5 rounded-xl shadow-lg shadow-blue-500/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 p-5 rounded-xl shadow-sm border border-slate-100 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-blue-600">{{ formattedTime }}</div>
</div>
<div class="flex items-center gap-2">
<span class="w-4 h-4 rounded-full" :class="isTkLoggedIn ? 'bg-emerald-500' : 'bg-red-500'"></span>
<button @click="openTK"
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm font-semibold shadow-lg shadow-blue-600/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-4 h-4 rounded-full"
:class="tkData[index].code == 1 ? 'bg-emerald-500' : 'bg-red-500'"></span>
<h3 class="font-bold text-slate-800">{{ $t('workbenches.guildAccount') }} {{ index === 0
? 'A'
: 'B' }}</h3>
</div>
<span class=" text-slate-500" style="font-size: 17px;">{{ $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 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 rounded-2xl shadow-sm border border-slate-100 overflow-hidden">
<div class="p-6 border-b border-slate-100 flex justify-between items-center">
<div class="flex items-center gap-3">
<div class="w-8 h-8 bg-slate-100 rounded-lg flex items-center justify-center">
<span class="material-icons-round text-slate-600 text-lg">settings</span>
</div>
<h2 class="font-bold text-slate-800">{{ $t('workbenchesSetup.workbenches') }}</h2>
</div>
<div class="flex items-center gap-4 text-sm">
<div class="flex items-center gap-2 text-slate-500">
<span>{{ $t('workbenchesSetup.network') }}:</span>
<span class="text-blue-600 font-bold">{{ locale == 'zh' ? countryData : countryDataEN }}</span>
<button @click="editCountry" :disabled="isRefreshingCountry"
class="ml-1 p-1 rounded-md hover:bg-slate-100 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
:title="$t('workbenchesSetup.editCountry') || '编辑国家'">
<span class="material-icons-round text-slate-500 text-base">edit</span>
</button>
<button @click="refreshCountryFn" :disabled="isRefreshingCountry"
class="p-1 rounded-md hover:bg-slate-100 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
:title="$t('workbenchesSetup.refreshCountry') || '刷新国家'">
<span class="material-icons-round text-slate-500 text-base"
:class="{ 'animate-spin': isRefreshingCountry }">refresh</span>
</button>
</div>
<div class="flex items-center gap-2">
<span class="text-slate-500">指定国家:</span>
<select v-model="country_info"
class="bg-slate-50 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 mb-4 flex items-center gap-2">
<span class="w-1 h-4 bg-blue-600 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">
<span
class="bg-slate-50 px-3 py-2 text-xs font-medium text-slate-500 w-28 flex items-center border-r border-slate-200">{{
$t('workbenchesSetup.minCoinsNum') }}</span>
<input
class="flex-1 px-4 py-2 text-sm bg-white 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">
<span
class="bg-slate-50 px-3 py-2 text-xs font-medium text-slate-500 w-28 flex items-center border-r border-slate-200">{{
$t('workbenchesSetup.maxCoinsNum') }}</span>
<input
class="flex-1 px-4 py-2 text-sm bg-white 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 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">
<span
class="bg-slate-50 px-3 py-2 text-xs font-medium text-slate-500 w-28 flex items-center border-r border-slate-200">{{
$t('workbenchesSetup.minFansNum') }}</span>
<input
class="flex-1 px-4 py-2 text-sm bg-white 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">
<span
class="bg-slate-50 px-3 py-2 text-xs font-medium text-slate-500 w-28 flex items-center border-r border-slate-200">{{
$t('workbenchesSetup.maxFansNum') }}</span>
<input
class="flex-1 px-4 py-2 text-sm bg-white 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 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">
<input
class="flex-1 px-4 py-2 text-sm bg-white 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 px-3 py-2 text-xs font-medium text-slate-500 w-24 flex items-center justify-center border-l border-slate-200">{{
$t('workbenchesSetup.hour') }}</span>
</div>
<div class="flex shadow-sm rounded-lg overflow-hidden border border-slate-200">
<input
class="flex-1 px-4 py-2 text-sm bg-white 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 px-3 py-2 text-xs font-medium text-slate-500 w-24 flex items-center justify-center border-l border-slate-200">{{
$t('workbenchesSetup.hour24') }}</span>
</div>
</div>
</div>
<!-- Quantity Limit -->
<div>
<h4 class="text-sm font-bold text-slate-800 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-blue-600 text-white border-blue-600' : 'bg-white text-slate-600 border-slate-200 hover:border-blue-600/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 border-none outline-none focus:ring-0 disabled:bg-slate-100"
type="number" v-model="hostNum" :disabled="!pyData.isStart" />
<span
class="bg-slate-100 px-3 py-2 text-xs font-medium text-slate-500 w-16 flex items-center justify-center border-l border-slate-200">{{
$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">
<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-blue-600 border-blue-600' : '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 group-hover:text-blue-600 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-blue-600 border-blue-600' : '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 group-hover:text-blue-600 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-blue-600 border-blue-600' : '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 group-hover:text-blue-600 transition-colors">过滤排行榜单</span>
</div>
</div>
</div>
<div class="mt-6 text-center">
<div class="flex flex-col md:flex-row gap-4 justify-center items-center">
<button v-if="pyData.isStart" @click="submit"
class="bg-slate-900 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 transition-all flex items-center gap-2">
<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-3 rounded-xl font-bold text-lg shadow-xl shadow-red-500/20 transition-all flex items-center gap-3">
<span class="material-icons-round">stop</span>
{{ $t('workbenchesSetup.stop') }}
</button>
<button @click="resetTask"
class="bg-amber-500 hover:bg-amber-600 hover:scale-[1.02] active:scale-[0.98] text-white px-10 py-3 rounded-xl font-bold text-lg shadow-xl shadow-amber-500/20 transition-all flex items-center gap-2">
<span class="material-icons-round">refresh</span>
重置任务
</button>
</div>
<p class="mt-4 text-xs font-medium text-emerald-600">
到期时间: {{ timestampToTime(expiredTime) }}
</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed, onUnmounted } from 'vue';
import { useRouter } from 'vue-router';
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'
import { useCountryInfo } from '@/composables/useCountryInfo'
// 获取路由实例
const router = useRouter();
// 重启 Python 服务的方法
const restartPythonService = async () => {
return new Promise((resolve, reject) => {
if (window.electronAPI && window.electronAPI.restartPythonService) {
window.electronAPI.restartPythonService().then(result => {
resolve(result)
}).catch(err => {
reject(err)
})
} else {
reject(new Error('重启 Python 服务的方法不可用'))
}
})
}
const { t, locale } = useI18n()
// 使用独立的国家信息管理(不与其他页面共享)
const {
countryData,
countryDataEN,
isLoading: isRefreshingCountry,
initCountryInfo,
refreshCountry,
showEditCountryDialog
} = useCountryInfo()
//导入python交互方法
const { fetchDataConfig, fetchDataCount, loginBackStage, loginTikTok, backStageloginStatus, backStageloginStatusCopy, getTkLoginStatus } = usePythonBridge();
//ip国家 - 使用独立的国家信息
const countryDataRef = countryData;
const countryDataENRef = countryDataEN;
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);
//账号是否登陆中
let isLogin = ref([false, false]);
//TK登录状态
let isTkLoggedIn = ref(false);
//TK状态轮询定时器
let tkStatusTimer = ref(null);
//设置状态轮询定时器
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)
// 初始化独立的国家信息
initCountryInfo(t)
setTimeout(() => {
// Check if user exists before calling getExpiredTime
if (getUser()?.tenantId) {
getExpiredTime(getUser().tenantId).then((res) => {
console.log('time:', res);
expiredTime.value = res.expiredTime
})
}
}, 1000);
checkVPN()
setInterval(async () => {
await checkVPN()
}, 1000 * 20)
})
// 编辑国家信息
const editCountry = async () => {
try {
const newCountry = await showEditCountryDialog(t);
// 确认后获取该国家的列表
if (newCountry) {
fetchCountryList(newCountry);
}
} catch {
// 用户取消编辑
}
};
// 刷新国家信息
const refreshCountryFn = async () => {
await refreshCountry(t);
// 刷新成功后获取国家列表
if (countryData.value) {
fetchCountryList(countryData.value);
}
};
// 获取国家列表
const fetchCountryList = async (countryName) => {
try {
const url = `https://datasave.api.yolozs.com/api/save_data/country_info?countryName=${countryName}`;
const res = await fetch(url);
const countryres = await res.json();
if (countryres.data) {
country_Lst.value = countryres.data;
}
} catch (error) {
console.error('获取国家列表失败:', error);
}
};
//提交数据到py
const submit = () => {
pyData.value.country = countryData.value;
console.log('提交的区间值:', pyData.value);
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('停止失败')
})
};
// 重置 (Unused but kept for port completeness)
// 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];
};
// 重置任务 - 重启 Python 服务
const resetTask = async () => {
try {
ElMessageBox.confirm(
'确定要重置任务吗?这将重启服务并中断当前正在进行的获取主播和大哥的任务。',
'重置任务',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(async () => {
ElMessage.info('正在重启 Python 服务...');
const result = await restartPythonService();
await restartPythonService();
if (result.success) {
ElMessage.success('Python 服务已重启,正在重新加载工作台页面...');
// 重新加载 TK 工作台页面(相当于重新进入 tk 工作台)
setTimeout(() => {
// 调用 window.reloadTkWorkbench() 方法触发 TK 工作台重新加载
if (window.reloadTkWorkbench) {
window.reloadTkWorkbench();
}
}, 1000);
} else {
ElMessage.error(`重启失败: ${result.error}`);
}
}).catch(() => {
// 用户取消
});
} catch (error) {
console.error('重置任务失败:', error);
ElMessage.error('重置任务失败,请稍后重试');
}
};
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(tkData.value[index].account);
}, 2000)
} else if (index == 1) {
isLogin.value[0] = true;
statusTimerCopy = setInterval(() => {
getloginStatus(tkData.value[index].account);
}, 2000)
}
}
const openTK = () => {
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';
if (isTkLoggedIn.value) {
clearInterval(tkStatusTimer.value);
tkStatusTimer.value = null;
}
}).catch(() => {
isTkLoggedIn.value = false;
});
}
function getloginStatus(account) {
backStageloginStatus(account).then((res) => {
console.log("登录状态", res)
if (!res) return;
const data = res;
// 根据 account 找到对应的 index
const index = tkData.value.findIndex(item => item.account === account);
if (index === -1) return;
// 兼容新旧接口返回格式
// 新格式: { status, account, loginStatus, message }
// 旧格式: { code, index, message }
const isSuccess = data.message === "登录成功"
tkData.value[index].code = isSuccess ? 1 : 0;
if (isSuccess) {
clearInterval(statusTimer);
clearInterval(statusTimerCopy);
statusTimer = null;
statusTimerCopy = null;
submitting.value = false
isLogin.value[0] = 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)
}).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(':');
});
// 清理所有定时器
const clearAllTimers = () => {
// 清理获取主播数量的定时器
if (getHostTimer.value) {
clearInterval(getHostTimer.value);
getHostTimer.value = null;
}
// 清理获取查询次数的定时器
if (getNumTimer.value) {
clearInterval(getNumTimer.value);
getNumTimer.value = null;
}
// 清理 TK 状态轮询定时器
if (tkStatusTimer.value) {
clearInterval(tkStatusTimer.value);
tkStatusTimer.value = null;
}
// 清理设置状态轮询定时器
if (statusTimer) {
clearInterval(statusTimer);
statusTimer = null;
}
// 清理设置状态轮询定时器(副本)
if (statusTimerCopy) {
clearInterval(statusTimerCopy);
statusTimerCopy = null;
}
// 清理运行时间的定时器
if (timerCrawl) {
clearInterval(timerCrawl);
timerCrawl = null;
}
console.log('所有定时器已清理');
};
// 在组件卸载时清理定时器
onUnmounted(() => {
clearAllTimers();
});
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>
/* 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>