Files
tkPage/src/views/hosts/workbenches.vue
2026-01-23 14:35:09 +08:00

713 lines
25 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="flex flex-col h-full gap-6">
<!-- 顶部统计区域 - 大数字展示 -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<!-- 总数量 -->
<div class="bg-gradient-to-br from-blue-500 to-blue-600 p-6 rounded-2xl shadow-lg shadow-blue-500/20 text-white relative overflow-hidden">
<div class="absolute top-0 right-0 w-32 h-32 bg-white/10 rounded-full -translate-y-1/2 translate-x-1/2"></div>
<div class="relative z-10">
<div class="flex items-center gap-2 mb-2">
<span class="material-icons-round text-white/80">analytics</span>
<span class="text-sm font-medium text-white/80">{{ $t('workbenches.totalnumber') }}</span>
</div>
<div class="text-4xl font-bold tracking-tight">{{ hostData.totalCount.toLocaleString() }}</div>
<div class="mt-2 text-xs text-white/60">累计获取主播</div>
</div>
</div>
<!-- 新建主播 -->
<div class="bg-gradient-to-br from-emerald-500 to-emerald-600 p-6 rounded-2xl shadow-lg shadow-emerald-500/20 text-white relative overflow-hidden">
<div class="absolute top-0 right-0 w-32 h-32 bg-white/10 rounded-full -translate-y-1/2 translate-x-1/2"></div>
<div class="relative z-10">
<div class="flex items-center gap-2 mb-2">
<span class="material-icons-round text-white/80">person_add</span>
<span class="text-sm font-medium text-white/80">{{ $t('workbenches.createHost') }}</span>
</div>
<div class="text-4xl font-bold tracking-tight">{{ hostData.validAnchorsCount.toLocaleString() }}</div>
<div class="mt-2 text-xs text-white/60">有效主播数量</div>
</div>
</div>
<!-- 网络状态 -->
<div class="bg-gradient-to-br from-violet-500 to-violet-600 p-6 rounded-2xl shadow-lg shadow-violet-500/20 text-white relative overflow-hidden">
<div class="absolute top-0 right-0 w-32 h-32 bg-white/10 rounded-full -translate-y-1/2 translate-x-1/2"></div>
<div class="relative z-10">
<div class="flex items-center gap-2 mb-2">
<span class="material-icons-round text-white/80">public</span>
<span class="text-sm font-medium text-white/80">{{ $t('workbenchesSetup.network') }}</span>
</div>
<div class="text-2xl font-bold tracking-tight">{{ locale == 'zh' ? countryData : countryDataEN }}</div>
<div class="mt-2 flex items-center gap-2">
<span class="w-2 h-2 rounded-full" :class="isWifi ? 'bg-red-400 animate-pulse' : 'bg-green-400'"></span>
<span class="text-xs text-white/60">{{ isWifi ? 'VPN 断开' : 'VPN 已连接' }}</span>
</div>
</div>
</div>
</div>
<!-- 中央核心操作区 -->
<div class="flex-1 bg-white dark:bg-slate-900 rounded-3xl shadow-xl border border-slate-100 dark:border-slate-800 overflow-hidden">
<div class="h-full flex flex-col">
<!-- 运行时间 - 中央突出展示 -->
<div class="bg-gradient-to-r from-slate-50 to-slate-100 dark:from-slate-800 dark:to-slate-900 p-8 text-center border-b border-slate-200 dark:border-slate-700">
<div class="inline-flex items-center gap-3 mb-4">
<span class="w-3 h-3 rounded-full animate-pulse" :class="!pyData.isStart ? 'bg-emerald-500' : 'bg-slate-300'"></span>
<span class="text-sm font-semibold text-slate-500 uppercase tracking-widest">{{ $t('workbenches.runTime') }}</span>
</div>
<div class="text-6xl font-mono font-bold text-slate-900 dark:text-white tracking-wider mb-6">
{{ formattedTime }}
</div>
<!-- 操作按钮组 -->
<div class="flex items-center justify-center gap-4">
<button @click="openTK"
class="bg-slate-200 dark:bg-slate-700 hover:bg-slate-300 dark:hover:bg-slate-600 text-slate-700 dark:text-slate-200 px-6 py-3 rounded-xl text-sm font-semibold transition-all flex items-center gap-2">
<span class="material-icons-round text-lg">play_arrow</span>
{{ $t('workbenches.openTK') }}
</button>
<button v-if="pyData.isStart" @click="submit"
class="bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white px-10 py-4 rounded-xl font-bold text-lg shadow-xl shadow-blue-500/30 transition-all flex items-center gap-3 hover:scale-[1.02] active:scale-[0.98]">
<span class="material-icons-round">bolt</span>
{{ $t('workbenchesSetup.start') }}
</button>
<button v-else @click="unsubmit"
class="bg-gradient-to-r from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 text-white px-10 py-4 rounded-xl font-bold text-lg shadow-xl shadow-red-500/30 transition-all flex items-center gap-3 hover:scale-[1.02] active:scale-[0.98] animate-pulse">
<span class="material-icons-round">stop</span>
{{ $t('workbenchesSetup.stop') }}
</button>
</div>
<p class="mt-4 text-xs font-medium text-slate-400">
到期时间: <span class="text-emerald-600 dark:text-emerald-400">{{ timestampToTime(expiredTime) }}</span>
</p>
</div>
<!-- 配置区域 - 左右两侧卡片 -->
<div class="flex-1 p-6">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 h-full">
<!-- 左侧金币设置 -->
<div class="bg-slate-50 dark:bg-slate-800/50 rounded-2xl p-5 border border-slate-200 dark:border-slate-700">
<div class="flex items-center gap-3 mb-5">
<div class="w-10 h-10 bg-gradient-to-br from-amber-400 to-orange-500 rounded-xl flex items-center justify-center shadow-lg shadow-amber-500/20">
<span class="material-icons-round text-white">monetization_on</span>
</div>
<div>
<h4 class="font-bold text-slate-800 dark:text-white">{{ $t('workbenchesSetup.setCoinsNum') }}</h4>
<p class="text-xs text-slate-400">设置金币筛选范围</p>
</div>
</div>
<div class="space-y-3">
<div>
<label class="text-xs text-slate-500 mb-1.5 block">{{ $t('workbenchesSetup.minCoinsNum') }}</label>
<input
class="w-full px-4 py-3 text-sm bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-600 rounded-xl outline-none focus:border-amber-500 focus:ring-2 focus:ring-amber-500/20 transition-all disabled:bg-slate-100 disabled:cursor-not-allowed"
type="number" v-model="pyData.gold.min" :disabled="!pyData.isStart" placeholder="0" />
</div>
<div>
<label class="text-xs text-slate-500 mb-1.5 block">{{ $t('workbenchesSetup.maxCoinsNum') }}</label>
<input
class="w-full px-4 py-3 text-sm bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-600 rounded-xl outline-none focus:border-amber-500 focus:ring-2 focus:ring-amber-500/20 transition-all disabled:bg-slate-100 disabled:cursor-not-allowed"
type="number" v-model="pyData.gold.max" :disabled="!pyData.isStart" placeholder="999999" />
</div>
</div>
</div>
<!-- 中间国家设置 -->
<div class="bg-slate-50 dark:bg-slate-800/50 rounded-2xl p-5 border border-slate-200 dark:border-slate-700">
<div class="flex items-center gap-3 mb-5">
<div class="w-10 h-10 bg-gradient-to-br from-violet-400 to-purple-500 rounded-xl flex items-center justify-center shadow-lg shadow-violet-500/20">
<span class="material-icons-round text-white">flag</span>
</div>
<div>
<h4 class="font-bold text-slate-800 dark:text-white">指定国家</h4>
<p class="text-xs text-slate-400">选择爬取的目标国家</p>
</div>
</div>
<div class="space-y-3">
<div>
<label class="text-xs text-slate-500 mb-1.5 block">目标国家</label>
<select v-model="country_info"
class="w-full px-4 py-3 text-sm bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-600 rounded-xl outline-none focus:border-violet-500 focus:ring-2 focus:ring-violet-500/20 transition-all disabled:bg-slate-100 appearance-none cursor-pointer"
:disabled="!pyData.isStart">
<option value="全部">全部国家</option>
<option v-for="(item, index) in country_Lst" :key="index" :value="item">{{ item }}</option>
</select>
</div>
<div class="pt-2">
<div class="flex items-center justify-between text-xs text-slate-500 mb-2">
<span>当前IP所在地</span>
<span class="font-semibold text-slate-700 dark:text-slate-300">{{ locale == 'zh' ? countryData : countryDataEN }}</span>
</div>
<div class="h-1 bg-slate-200 dark:bg-slate-700 rounded-full overflow-hidden">
<div class="h-full bg-gradient-to-r from-violet-500 to-purple-500 rounded-full" style="width: 100%"></div>
</div>
</div>
</div>
</div>
<!-- 右侧粉丝设置 -->
<div class="bg-slate-50 dark:bg-slate-800/50 rounded-2xl p-5 border border-slate-200 dark:border-slate-700">
<div class="flex items-center gap-3 mb-5">
<div class="w-10 h-10 bg-gradient-to-br from-pink-400 to-rose-500 rounded-xl flex items-center justify-center shadow-lg shadow-pink-500/20">
<span class="material-icons-round text-white">favorite</span>
</div>
<div>
<h4 class="font-bold text-slate-800 dark:text-white">{{ $t('workbenchesSetup.setFansNum') }}</h4>
<p class="text-xs text-slate-400">设置粉丝筛选范围</p>
</div>
</div>
<div class="space-y-3">
<div>
<label class="text-xs text-slate-500 mb-1.5 block">{{ $t('workbenchesSetup.minFansNum') }}</label>
<input
class="w-full px-4 py-3 text-sm bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-600 rounded-xl outline-none focus:border-pink-500 focus:ring-2 focus:ring-pink-500/20 transition-all disabled:bg-slate-100 disabled:cursor-not-allowed"
type="number" v-model="pyData.fans.min" :disabled="!pyData.isStart" placeholder="0" />
</div>
<div>
<label class="text-xs text-slate-500 mb-1.5 block">{{ $t('workbenchesSetup.maxFansNum') }}</label>
<input
class="w-full px-4 py-3 text-sm bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-600 rounded-xl outline-none focus:border-pink-500 focus:ring-2 focus:ring-pink-500/20 transition-all disabled:bg-slate-100 disabled:cursor-not-allowed"
type="number" v-model="pyData.fans.max" :disabled="!pyData.isStart" placeholder="999999" />
</div>
</div>
</div>
</div>
</div>
</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 } = 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,
});
//是否开启tk
// let isTk = ref(true);
//账号是否登陆中
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;
}
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);
})
}, 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];
console.log(pyData.value)
};
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();
}
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.
*/
.text-size {
text-size: 50px !important;
}
</style>