8 Commits
small ... main

Author SHA1 Message Date
38f8fe9bd0 ui样式回滚修复 2026-01-23 19:53:38 +08:00
21eec1085a 开启tk 状态灯 2026-01-23 16:12:37 +08:00
1576cadf8c 导航页尺寸优化 2026-01-20 19:54:12 +08:00
1adb587edb 自适应 2026-01-20 15:09:36 +08:00
14abd6c7c0 工作台自适应 2026-01-19 20:20:55 +08:00
afaacbd2fa 新版ui 2026-01-16 20:12:21 +08:00
32f9c48c91 新版ui 2026-01-16 19:00:07 +08:00
74bf170490 登录页优化 2026-01-16 13:35:17 +08:00
14 changed files with 3040 additions and 1785 deletions

View File

@@ -9,7 +9,68 @@
<title> <title>
<%= webpackConfig.name %> <%= webpackConfig.name %>
</title> </title>
<!-- pywebview API 自动注入,无需手动引入脚本 -->
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&amp;display=swap"
rel="stylesheet" />
<link href="https://fonts.googleapis.com/icon?family=Material+Icons+Round" rel="stylesheet" />
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com?plugins=forms,typography"></script>
<script>
tailwind.config = {
darkMode: "class",
theme: {
extend: {
colors: {
primary: "#0066FF", // Cyber Blue
secondary: "#6366F1", // Electric Indigo
"background-light": "#F8FAFC",
"background-dark": "#0F172A",
},
fontFamily: {
display: ["Inter", "sans-serif"],
},
borderRadius: {
DEFAULT: "12px",
'xl': '20px',
},
boxShadow: {
'premium': '0 4px 20px -2px rgba(0, 0, 0, 0.05), 0 2px 10px -2px rgba(0, 0, 0, 0.02)',
'glass': 'inset 0 0 0 1px rgba(255, 255, 255, 0.4)',
}
},
},
};
</script>
<!-- Global Styles -->
<style>
body {
font-family: 'Inter', sans-serif;
}
.glass-card {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.3);
}
.sidebar-active {
background: linear-gradient(90deg, #F0F7FF 0%, #FFFFFF 100%);
border-right: 3px solid #0066FF;
box-shadow: 4px 0 15px -5px rgba(0, 102, 255, 0.15);
}
.subtle-grid {
background-image: radial-gradient(#E2E8F0 0.5px, transparent 0.5px);
background-size: 24px 24px;
}
/* QT WebChannel */
</style>
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
</head> </head>
<body> <body>
@@ -21,19 +82,11 @@
<!-- built files will be auto injected --> <!-- built files will be auto injected -->
</body> </body>
<style> <style>
html,
body { body {
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow: hidden; /* width: 1600px;
width: 100%; height: 900px; */
height: 100%;
}
#app {
width: 100%;
height: 100%;
overflow: hidden;
} }
</style> </style>

View File

@@ -76,3 +76,7 @@ export function accountName(str) {
export function liveHostDetail(data) { export function liveHostDetail(data) {
return postAxios({ url: 'api/save_data/live_host_detail', data }) return postAxios({ url: 'api/save_data/live_host_detail', data })
} }
export function revenueStats(hostId) {
return getAxios({ url: 'api/save_data/revenue_stats?displayId=' + hostId })
}

BIN
src/assets/logo2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
src/assets/logoBg2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 KiB

View File

@@ -1,148 +1,63 @@
<template> <template>
<div class="sidebar"> <aside class="w-64 bg-white dark:bg-slate-900 border-r border-slate-200 dark:border-slate-800 flex flex-col z-20 h-full">
<div class="logo"> <div class="p-8 flex items-center gap-3">
<!-- <img style="margin-right: 10px;" src="@/assets/logo.png"> --> <div class="w-10 h-10 bg-primary rounded-xl flex items-center justify-center shadow-lg shadow-primary/20">
<img src="@/assets/logotext.png"> <span class="text-white font-bold text-xl">TK
</span>
</div>
<h1 class="text-xl font-bold tracking-tight text-slate-900 dark:text-white">Yolo <span class="text-primary">助手</span>
</h1>
</div> </div>
<ul>
<li @click="updateActiveIndex(1)">
<div>
<img v-show="activeIndex == 1" src="@/assets/navAction.png" autoplay loop muted class="background-img">
<div style="display: flex;">
<img v-show="activeIndex == 1" src="@/assets/workAction.png" style="margin-right: 10px;">
<img v-show="activeIndex == 2" src="@/assets/workAction.png" style="margin-right: 10px;">
<div :style="activeIndex == 1 ? 'color: #000' : 'color: #fff'" class="center-justify">{{
$t('menu.workbenches')
}}</div>
</div> <nav class="flex-1 px-4 space-y-4">
</div> <a
</li> class="flex items-center gap-3 px-4 py-4 rounded-lg font-medium cursor-pointer transition-colors"
<li @click="updateActiveIndex(2)"> :class="activeIndex == 1 ? 'sidebar-active text-primary' : 'text-slate-500 hover:bg-slate-50 dark:hover:bg-slate-800'"
<div> @click="updateActiveIndex(1)"
<img v-show="activeIndex == 2" src="@/assets/navAction.png" autoplay loop muted class="background-img"> >
<span class="material-icons-round">grid_view</span>
{{ $t('menu.workbenches') }}
</a>
<div style="display: flex;"> <a
<img v-show="activeIndex == 2" src="@/assets/listAction.png" style="margin-right: 10px;"> class="flex items-center gap-3 px-4 py-4 rounded-lg font-medium cursor-pointer transition-colors"
<img v-show="activeIndex == 1" src="@/assets/listAction.png" style="margin-right: 10px;"> :class="activeIndex == 2 ? 'sidebar-active text-primary' : 'text-slate-500 hover:bg-slate-50 dark:hover:bg-slate-800'"
<div :style="activeIndex == 2 ? 'color: #000' : 'color: #fff'" class="center-justify">{{ @click="updateActiveIndex(2)"
$t('menu.hostList') >
}}</div> <span class="material-icons-round">person_search</span>
{{ $t('menu.hostList') }}
</a>
</nav>
</div> <div class="p-6 border-t border-slate-100 dark:border-slate-800">
</div> <button
</li> class="flex items-center gap-3 text-slate-500 hover:text-red-500 transition-colors w-full px-4 py-2"
</ul> @click="$router.push('/')"
>
<a @click="$router.push('/')" href="javascript:void(0);" <span class="material-icons-round text-lg">logout</span>
style="position: absolute; bottom: 30px; color: aliceblue; font-size: 20px; font-weight: 500;"> <span class="font-medium">{{ $t('menu.logout') }}</span>
{{ </button>
$t('menu.logout') </div>
}} </aside>
</a>
</div>
</template> </template>
<script setup> <script setup>
import { ref, reactive, onMounted } from 'vue'; import { ref } from 'vue';
import { getUser } from '@/utils/storage' import { getUser } from '@/utils/storage'
import { defineEmits } from 'vue'; import { defineEmits } from 'vue';
const userInfo = ref(getUser()) const userInfo = ref(getUser())
let activeIndex = ref(1); let activeIndex = ref(1);
const emit = defineEmits(['activeIndex']); const emit = defineEmits(['activeIndex']);
const updateActiveIndex = (index) => { const updateActiveIndex = (index) => {
activeIndex.value = index; activeIndex.value = index;
emit('activeIndex', index); emit('activeIndex', index);
}; };
</script> </script>
<style scoped lang="less"> <style scoped>
.sidebar { /* Scoped styles are minimal now as we use Tailwind */
position: fixed;
left: 0;
top: 0;
height: 900px;
width: 280px;
background-color: @bg-color;
padding: 20px;
box-sizing: border-box;
.logo {
border-bottom: 1px solid #fff;
padding-top: 20px;
img:nth-of-type(1) {
height: 66px;
}
img:nth-of-type(2) {
height: 29px;
}
}
}
.sidebar ul {
list-style: none;
padding: 0;
}
.sidebar li {
margin-top: 50px;
padding-top: 30px;
padding-left: 30px;
margin-bottom: 50px;
height: 64px;
position: relative;
display: flex;
align-items: center;
cursor: pointer;
}
.sidebar a {
text-decoration: none;
color: #000000;
display: block;
padding: 8px;
border-radius: 4px;
font-family: Source Han Sans SC;
font-weight: 400;
font-size: 22px;
}
/* .sidebar a:hover {
background-color: #e0e0e0;
} */
.background-img {
position: absolute;
top: 0;
left: 13px;
width: 247px;
height: 126px;
object-fit: cover;
z-index: -1;
/* 确保视频在内容之下 */
}
.center-justify {
display: flex;
justify-content: space-around;
align-items: center;
}
</style> </style>

View File

@@ -79,5 +79,19 @@ export default {
invitationType: 'invitationType', invitationType: 'invitationType',
invitationType1: 'Regular', invitationType1: 'Regular',
invitationType2: 'Golden', invitationType2: 'Golden',
liveSessions: 'Live Sessions',
viewSessions: 'View Sessions',
liveRevenue: 'Live Revenue',
viewRevenue: 'View Revenue',
revenueHost: 'Host',
todayRevenueUsd: 'Today Revenue (USD)',
totalRevenueUsd: 'Total Revenue (USD)',
liveDays: 'Live Days',
historyRevenueUsd: 'History Revenue (USD)',
revenueHigh: 'High',
revenueLow: 'Low',
revenueTime: 'Time',
close: 'Close',
selectPlaceholder: 'Please select',
} }
} }

View File

@@ -1,10 +1,10 @@
export default { export default {
login: { login: {
title: '账号登', title: '账号登',
version: '版本号', version: '版本号',
login: '登录', login: '登录',
tenantName: '租户名称', tenantName: '租户名称',
account: '账', account: '账',
password: '密码', password: '密码',
Language: '语言设置', Language: '语言设置',
network: '网络设置', network: '网络设置',
@@ -27,6 +27,7 @@ export default {
guildPassPlace: '请输入登录密码', guildPassPlace: '请输入登录密码',
queriedNum: '今日已查询次数', queriedNum: '今日已查询次数',
loginBackend: '登录后台', loginBackend: '登录后台',
workbenches: '工作台',
}, },
workbenchesSetup: { workbenchesSetup: {
workbenches: '工作台', workbenches: '工作台',
@@ -34,7 +35,7 @@ export default {
setCoinsNum: '设置金币数量', setCoinsNum: '设置金币数量',
setFansNum: '设置粉丝数量', setFansNum: '设置粉丝数量',
setQuery: '后台查询频率', setQuery: '后台查询频率',
setNum: ' 期望可邀请主播数量', setNum: '期望可邀请主播数量',
minCoinsNum: '最小金币数量', minCoinsNum: '最小金币数量',
maxCoinsNum: '最大金币数量', maxCoinsNum: '最大金币数量',
minFansNum: '最小粉丝数量', minFansNum: '最小粉丝数量',
@@ -44,11 +45,9 @@ export default {
num: '个', num: '个',
start: '开始获取数据', start: '开始获取数据',
stop: '停止', stop: '停止',
prompt: '达数量后停止爬取', prompt: '达数量后停止爬取',
setHostNum: '设置爬取数量', setHostNum: '设置爬取数量',
unlimitedQuantity: '不限爬取数量', unlimitedQuantity: '不限爬取数量',
}, },
hostList: { hostList: {
placeCountry: '选择国家', placeCountry: '选择国家',
@@ -80,5 +79,19 @@ export default {
invitationType: '邀请类型', invitationType: '邀请类型',
invitationType1: '普票', invitationType1: '普票',
invitationType2: '金票', invitationType2: '金票',
liveSessions: '直播场次',
viewSessions: '查看场次',
liveRevenue: '直播收益',
viewRevenue: '查看收益',
revenueHost: '主播',
todayRevenueUsd: '今日收益(美元)',
totalRevenueUsd: '总收益(美元)',
liveDays: '直播天数',
historyRevenueUsd: '历史收益(美元)',
revenueHigh: '高',
revenueLow: '低',
revenueTime: '时间',
close: '关闭',
selectPlaceholder: '请选择',
} }
} }

View File

@@ -1,100 +1,74 @@
// pythonBridge.js // pythonBridge.js
// 适配 pywebview API (替代原有的 QWebChannel)
import { ref, onMounted } from 'vue'; import { ref, onMounted } from 'vue';
const bridge = ref(null); const bridge = ref(null);
const isReady = ref(false);
/** // 统一安全调用,确保 Qt 响应有回调可执行
* 等待 pywebview API 准备就绪 const callBridge = (method, ...args) => {
* @returns {Promise<void>} if (!bridge.value || typeof bridge.value[method] !== 'function') return;
*/ const last = args[args.length - 1];
const waitForPywebview = () => { const hasCallback = typeof last === 'function';
return new Promise((resolve) => { const callback = hasCallback ? args.pop() : () => { };
// 如果已经存在,直接返回 bridge.value[method](...args, callback);
if (window.pywebview && window.pywebview.api) {
resolve();
return;
}
// 监听 pywebviewready 事件
window.addEventListener('pywebviewready', () => {
resolve();
}, { once: true });
});
}; };
/** // 防御:若 Qt 返回了未知 id忽略以免 execCallbacks 报错
* 统一安全调用 pywebview API const patchQWebChannel = () => {
* pywebview 的方法调用会返回 Promise if (!window.QWebChannel || QWebChannel.__patchedIgnoreMissing) return;
*/ const originalHandleResponse = QWebChannel.prototype.handleResponse;
const callBridge = async (method, ...args) => { QWebChannel.__patchedIgnoreMissing = true;
if (!bridge.value || typeof bridge.value[method] !== 'function') { QWebChannel.prototype.handleResponse = function (message) {
console.warn(`[pythonBridge] 方法不存在: ${method}`); const cb = this.execCallbacks && this.execCallbacks[message.id];
return null; if (message.id && typeof cb !== 'function') {
} console.warn('忽略未知的 WebChannel 响应', message);
try { return;
const result = await bridge.value[method](...args); }
return result; return originalHandleResponse.call(this, message);
} catch (error) { };
console.error(`[pythonBridge] 调用 ${method} 失败:`, error);
return null;
}
}; };
/** // 初始化 QWebChannel
* 初始化 pywebview 桥接 const initBridge = () => {
*/ if (/localhost/.test(window.location.href)) return;
const initBridge = async () => { patchQWebChannel();
// 监听 DevTools 快捷键 (Ctrl + Shift + P) new QWebChannel(qt.webChannelTransport, (channel) => {
window.addEventListener('keydown', (e) => { // 兜底:任何缺失的回调都返回空函数,避免 execCallbacks 报错
// 1. 自定义快捷键: Ctrl + Shift + P -> 调用后端 channel.execCallbacks = new Proxy(channel.execCallbacks || {}, {
if (e.ctrlKey && e.shiftKey && (e.key === 'p' || e.key === 'P')) { get(target, prop) {
e.preventDefault(); const val = target[prop];
callBridge('openDevTools'); if (typeof val === 'function') return val;
return; // 返回空函数,确保 handleResponse 可调用
} return () => { };
},
set(target, prop, value) {
target[prop] = value;
return true;
},
});
// 2. 屏蔽 F12 (防止用户按下 F12 打开) bridge.value = channel.objects.bridge;
if (e.key === 'F12') {
e.preventDefault();
return;
}
}); });
// 开发环境 (localhost) 不初始化
if (/localhost/.test(window.location.href)) {
console.log('[pythonBridge] 开发环境,跳过初始化');
return;
}
await waitForPywebview();
if (window.pywebview && window.pywebview.api) {
bridge.value = window.pywebview.api;
isReady.value = true;
console.log('[pythonBridge] 初始化成功');
} else {
console.error('[pythonBridge] pywebview API 初始化失败');
}
}; };
export function usePythonBridge() { export function usePythonBridge() {
// 调用 Python 方法 // 调用 Python 方法
const fetchDataConfig = async (data) => { const fetchDataConfig = (data) => {
if (!bridge.value) return null; return new Promise((resolve) => {
return await callBridge('fetchDataConfig', data); if (!bridge.value) return resolve(null);
callBridge('fetchDataConfig', data, (result) => {
resolve(result);
});
});
}; };
// 查询获取主播的数据 // 查询获取主播的数据
const fetchDataCount = async () => { const fetchDataCount = () => {
if (!bridge.value) return null; return new Promise((resolve) => {
const result = await callBridge('fetchDataCount'); if (!bridge.value) return resolve(null);
// pywebview 返回的是字符串,需要解析 callBridge('fetchDataCount', (result) => {
try { resolve(result);
return typeof result === 'string' ? JSON.parse(result) : result; });
} catch { });
return result;
}
}; };
// 打开 tk 后台 // 打开 tk 后台
@@ -117,25 +91,23 @@ export function usePythonBridge() {
}; };
// 查询登录状态 // 查询登录状态
const backStageloginStatus = async () => { const backStageloginStatus = () => {
if (!bridge.value) return null; return new Promise((resolve) => {
const result = await callBridge('backStageloginStatus'); if (!bridge.value) return resolve(null);
try { callBridge('backStageloginStatus', (result) => {
return typeof result === 'string' ? JSON.parse(result) : result; resolve(result);
} catch { });
return result; });
}
}; };
// 查询登录状态(副账号) // 查询登录状态(副账号)
const backStageloginStatusCopy = async () => { const backStageloginStatusCopy = () => {
if (!bridge.value) return null; return new Promise((resolve) => {
const result = await callBridge('backStageloginStatusCopy'); if (!bridge.value) return resolve(null);
try { callBridge('backStageloginStatusCopy', (result) => {
return typeof result === 'string' ? JSON.parse(result) : result; resolve(result);
} catch { });
return result; });
}
}; };
// 导出表格 // 导出表格
@@ -148,33 +120,29 @@ export function usePythonBridge() {
}; };
// 获取版本号 // 获取版本号
const getVersion = async () => { const getVersion = () => {
if (!bridge.value) return null; return new Promise((resolve) => {
return await callBridge('currentVersion'); if (!bridge.value) return resolve(null);
callBridge('currentVersion', (result) => {
resolve(result);
});
});
}; };
// 存储账号信息 // 查询TK登录状态
const storageAccountInfo = async (key, data) => { const getTkLoginStatus = () => {
if (!bridge.value) return false; return new Promise((resolve) => {
return await callBridge('storageAccountInfo', key, JSON.stringify(data)); if (!bridge.value) return resolve(false);
}; callBridge('getTkLoginStatus', (result) => {
resolve(result);
// 读取账号信息 });
const readAccountInfo = async (key) => { });
if (!bridge.value) return null;
const result = await callBridge('readAccountInfo', key);
try {
return typeof result === 'string' ? JSON.parse(result) : result;
} catch {
return result;
}
}; };
// 在组件挂载时初始化桥接 // 在组件挂载时初始化桥接
onMounted(initBridge); onMounted(initBridge);
return { return {
isReady,
fetchDataConfig, fetchDataConfig,
fetchDataCount, fetchDataCount,
loginBackStage, loginBackStage,
@@ -185,7 +153,6 @@ export function usePythonBridge() {
exportToExcel, exportToExcel,
stopScript, stopScript,
getVersion, getVersion,
storageAccountInfo, getTkLoginStatus,
readAccountInfo,
}; };
} }

View File

@@ -1,80 +1,55 @@
<template> <template>
<div class="main"> <div class="main">
<div class="container"> <div class="auth-shell">
<div class="right"> <div class="top-actions ">
<img src="../assets/logoBg.png" class="background-video" alt=""> <div class="setup-item center-justify">
<!-- 设置 --> <el-dropdown>
<div class="center-align"> <span class="el-dropdown-link ">
<div></div> <span class="setup-text">{{ $t('login.network') }}</span>
<div class="setup"> </span>
<div class="setup-item center-justify"> </el-dropdown>
<div></div>
<el-dropdown>
<span class="el-dropdown-link">
<span style="font-size:20px; color: #fff;">
{{ $t('login.network') }}
</span>
</span>
</el-dropdown>
</div>
<div class="setup-item center-justify">
<div></div>
<el-dropdown>
<span class="el-dropdown-link">
<span style="font-size:20px; color: #fff;">
{{ $t('login.Language') }}
</span>
<!-- <el-icon class="el-icon--right">
<arrow-down />
</el-icon> -->
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="switchLanguage('zh')">
中文
</el-dropdown-item>
<el-dropdown-item @click="switchLanguage('en')">
English
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- <el-radio-group v-model="localeData" size="large">
<el-radio-button @click="switchLanguage('zh')" label="中文" />
<el-radio-button @click="switchLanguage('en')" label="English" />
</el-radio-group> -->
</div>
</div>
</div> </div>
<div class="center-line" style="margin-top: 40px;"> <div class="setup-item center-justify">
<!-- logo --> <el-dropdown>
<div class="logo"> <span class="el-dropdown-link">
<div class="center-justify" style="height: 80px; width: 300px;"> <span class="setup-text">{{ $t('login.Language') }}</span>
<!-- <img style="margin-right: 20px;height: 100%;" src="@/assets/logo.png"> --> </span>
<img style="height: 100%;" src="@/assets/logotext.png"> <template #dropdown>
</div> <el-dropdown-menu>
<el-dropdown-item @click="switchLanguage('zh')">
中文
</el-dropdown-item>
<el-dropdown-item @click="switchLanguage('en')">
English
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
<div class="auth-card">
<div class="auth-left">
<div class="logo ">
<img class="logo-image" src="@/assets/logo2.png" alt="">
<span class="logo-title">TK主播数据助手</span>
</div> </div>
<div class="welcome">{{ $t('login.title') }}</div>
<!-- From -->
<div class="from"> <div class="from">
<div class="from-title center-justify">
<div>{{ $t('login.title') }}</div>
</div>
<div class="from-input"> <div class="from-input">
<el-form label-position="left" label-width="100px" :model="formData"> <el-form label-position="left" label-width="100px" :model="formData">
<div class="field-label">{{ $t('login.tenantName') }}</div>
<div class="from-input-item1"> <div class="from-input-item1">
<img src="@/assets/username.png" alt=""> <img src="@/assets/username.png" alt="">
<el-input style="height: 25px;" v-model="formData.tenantName" <el-input style="height: 25px;" v-model="formData.tenantName"
:placeholder="$t('login.tenantName')" clearable @keyup.enter="onSubmit" /> :placeholder="$t('login.tenantName')" clearable @keyup.enter="onSubmit" />
</div> </div>
<div class="field-label">{{ $t('login.account') }}</div>
<div class="from-input-item1"> <div class="from-input-item1">
<img src="@/assets/username.png" alt=""> <img src="@/assets/username.png" alt="">
<el-input style="height: 25px;" v-model="formData.userId" <el-input style="height: 25px;" v-model="formData.userId"
:placeholder="$t('login.account')" clearable @keyup.enter="onSubmit" /> :placeholder="$t('login.account')" clearable @keyup.enter="onSubmit" />
</div> </div>
<div class="field-label">{{ $t('login.password') }}</div>
<div class="from-input-item1"> <div class="from-input-item1">
<img src="@/assets/password.png" alt=""> <img src="@/assets/password.png" alt="">
<el-input style="height: 25px; " v-model="formData.password" type="password" <el-input style="height: 25px; " v-model="formData.password" type="password"
@@ -82,18 +57,19 @@
</div> </div>
<div class="from-input-item"> <div class="from-input-item">
<el-button class="loginButton" color="#8f7ee7" type="primary" @click="onSubmit">{{ <el-button class="loginButton" style="height: 3vw;" color="#4f6ef7" type="primary"
$t('login.login') }}</el-button> :loading="isSubmitting" :disabled="isSubmitting" @click="onSubmit">{{
$t('login.login') }}</el-button>
</div> </div>
</el-form> </el-form>
</div> </div>
</div> </div>
<div class="version">{{ $t('login.version') }}:{{ version }}</div>
</div>
<div class="auth-right">
<img src="@/assets/logoBg2.png" class="illustration" alt="">
</div> </div>
<div class="version center-justify ">{{ $t('login.version') }}{{ version }}</div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
@@ -103,7 +79,6 @@ import { ref, reactive, onMounted } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { login, getIdByName } from '@/api/account'; import { login, getIdByName } from '@/api/account';
import { getToken, setToken, setUser, setUserPass, getUserPass } from '@/utils/storage'; import { getToken, setToken, setUser, setUserPass, getUserPass } from '@/utils/storage';
import { ElLoading } from 'element-plus';
import { usePythonBridge } from '@/utils/pythonBridge' import { usePythonBridge } from '@/utils/pythonBridge'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
@@ -117,7 +92,7 @@ function switchLanguage(lang) {
} }
const { getVersion, stopScript } = usePythonBridge(); const { getVersion, stopScript } = usePythonBridge();
let version = ref('5.1.0'); let version = ref('5.9.2');
onMounted(() => { onMounted(() => {
stopScript(); stopScript();
@@ -132,6 +107,7 @@ onMounted(() => {
const router = useRouter(); const router = useRouter();
const isSubmitting = ref(false);
const formData = ref({ const formData = ref({
tenantName: getUserPass() == null ? '' : getUserPass().tenantName, tenantName: getUserPass() == null ? '' : getUserPass().tenantName,
@@ -142,11 +118,10 @@ const formData = ref({
const onSubmit = () => { const onSubmit = () => {
const loading = ElLoading.service({ if (isSubmitting.value) {
lock: true, return;
text: 'Loading', }
background: 'rgba(0, 0, 0, 0.7)', isSubmitting.value = true;
});
setUserPass(formData.value); setUserPass(formData.value);
getIdByName(formData.value.tenantName).then((tenantId) => { getIdByName(formData.value.tenantName).then((tenantId) => {
console.log(tenantId) console.log(tenantId)
@@ -155,14 +130,16 @@ const onSubmit = () => {
username: formData.value.userId, username: formData.value.userId,
password: formData.value.password, password: formData.value.password,
}).then((res) => { }).then((res) => {
loading.close(); isSubmitting.value = false;
// console.log(res) // console.log(res)
setToken(res.tokenValue); setToken(res.tokenValue);
setUser(res); setUser(res);
router.push('/nav'); router.push('/nav');
}).catch((err) => { }).catch((err) => {
loading.close(); isSubmitting.value = false;
}); });
}).catch(() => {
isSubmitting.value = false;
}) })
@@ -171,157 +148,27 @@ const onSubmit = () => {
<style lang="less"> <style lang="less">
.main { .main {
width: 100%; width: 1600px;
height: 100vh; height: 900px;
overflow: hidden; overflow: hidden;
box-sizing: border-box; box-sizing: border-box;
background: linear-gradient(180deg, #f3f5f9 0%, #eef1f5 100%);
display: flex;
align-items: center;
justify-content: center;
font-family: "Source Han Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
/* 页面无法选中 */
-webkit-user-select: none; -webkit-user-select: none;
-moz-user-select: none; -moz-user-select: none;
-ms-user-select: none; -ms-user-select: none;
user-select: none; user-select: none;
.container {
display: flex;
box-sizing: border-box;
width: 100%;
height: 100%;
.right {
box-sizing: border-box;
position: relative;
width: 100%;
height: 100%;
padding: 20px 40px 20px 50px;
border-left: 3px solid #23516e;
position: relative;
/* 添加 position: relative */
overflow: hidden;
/* 防止内容溢出 */
.version {
color: #fff;
position: absolute;
font-size: 20px;
bottom: 20px;
left: calc(50% - 50px);
// box-sizing: border-box;
// width: 1600px;
}
.background-video {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
/* 确保视频在内容之下 */
}
.setup {
display: flex;
color: #fff;
.setup-item {
padding: 10px 6px;
display: flex;
&>div:nth-child(1) {
width: 20px;
height: 20px;
border-radius: 50%;
background-color: rgb(255, 255, 255);
margin-right: 5px;
}
}
}
.logo {
padding: 20px 0;
}
.from {
width: 420px;
// height: 320px;
color: @bg-color;
background-color: #ffffff44;
border-radius: 20px;
border: 1px solid #fff;
padding: 32px;
box-sizing: border-box;
.from-title {
font-family: Source Han Sans SC;
font-weight: 500;
font-size: 24px;
color: @bg-color;
line-height: 37px;
div {
font-size: 20px;
font-weight: 600;
// border-bottom: 4px solid #1db97d;
}
}
.from-input {
width: 100%;
padding: 15px 0;
.from-input-item {
display: flex;
padding: 8px 0;
.from-input-item-title {
color: @bg-color;
font-size: 18px;
font-weight: 500;
width: 80px;
height: 50px;
}
.loginButton {
width: 359px;
height: 50px;
background: #FFFFFF;
border-radius: 24px;
border: 1px solid #FFFFFF;
font-family: Source Han Sans SC;
font-weight: 500;
font-size: 18px;
color: @bg-color;
line-height: 37px;
}
}
.from-input-item1 {
display: flex;
width: 359px;
height: 50px;
background: @bg-color-light-light;
border-radius: 24px;
border: 1px solid #FFFFFF;
padding: 12px 25px 13px 25px;
box-sizing: border-box;
margin-bottom: 16px;
}
}
}
}
}
} }
.center-line { .center-line {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
// justify-content: center;
} }
.center-justify { .center-justify {
@@ -330,37 +177,246 @@ const onSubmit = () => {
align-items: center; align-items: center;
} }
.center-align { .auth-shell {
width: 1320px;
height: 760px;
position: relative;
display: flex; display: flex;
justify-content: space-between; align-items: center;
justify-content: center;
} }
.center-flex { .auth-shell::before,
.auth-shell::after {
content: "";
position: absolute;
border-radius: 50%;
filter: blur(0);
}
.auth-shell::before {
width: 420px;
height: 420px;
background: radial-gradient(circle, rgba(214, 226, 255, 0.7), rgba(214, 226, 255, 0));
left: -80px;
top: -40px;
}
.auth-shell::after {
width: 360px;
height: 360px;
background: radial-gradient(circle, rgba(255, 236, 214, 0.6), rgba(255, 236, 214, 0));
right: -40px;
bottom: -60px;
}
.top-actions {
position: absolute;
top: 50px;
right: 80px;
display: flex; display: flex;
justify-content: center; gap: 16px;
z-index: 2;
}
.setup-item {
background: rgba(255, 255, 255, 0.95);
border: 1px solid #e1e6f2;
border-radius: 16px;
padding: 8px 12px 8px 14px;
box-shadow: 0 8px 18px rgba(40, 57, 108, 0.12);
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
display: inline-flex;
align-items: center; align-items: center;
gap: 6px;
}
.setup-text {
font-size: 13px;
font-weight: 600;
color: #2b3347;
letter-spacing: 0.2px;
}
.setup-item::after {
content: "";
width: 6px;
height: 6px;
border-right: 2px solid #9aa1b3;
border-bottom: 2px solid #9aa1b3;
transform: rotate(45deg);
margin-top: -2px;
}
.setup-item:hover {
transform: translateY(-1px);
border-color: #c9d2f4;
box-shadow: 0 12px 24px rgba(40, 57, 108, 0.16);
}
.setup-item:active {
transform: translateY(0);
}
.auth-card {
width: 1200px;
height: 680px;
display: flex;
background: #ffffff;
border-radius: 28px;
overflow: hidden;
box-shadow: 0 24px 60px rgba(40, 57, 108, 0.16);
position: relative;
z-index: 1;
}
.auth-left {
width: 42%;
padding: 48px 56px;
display: flex;
flex-direction: column;
justify-content: center;
}
.logo {
// margin-bottom: 24px;
display: flex;
align-items: center;
}
.logo-image {
height: 200px;
margin-bottom: -20px;
}
.logo-title {
font-size: 20px;
font-weight: 600;
color: #2b3347;
letter-spacing: 2px;
position: relative;
margin-top: 30px;
padding: 10px 0px 10px 14px;
}
.logo-title::before {
content: "";
position: absolute;
left: 0;
top: 4px;
bottom: 4px;
width: 4px;
border-radius: 2px;
background: linear-gradient(180deg, #4f6ef7, #5b7bff);
}
.welcome {
font-size: 24px;
font-weight: 600;
color: #2b3347;
margin-bottom: 8px;
}
.from {
width: 100%;
color: #2b3347;
background-color: transparent;
border-radius: 0;
border: none;
padding: 0;
box-sizing: border-box;
}
.from-input {
width: 100%;
padding: 8px 0 0 0;
}
.field-label {
font-size: 13px;
color: #6b7280;
margin: 12px 0 6px;
}
.from-input-item {
display: flex;
padding: 16px 0 0 0;
}
.loginButton {
width: 100%;
height: 48px;
background: linear-gradient(135deg, #4f6ef7, #5b7bff);
border-radius: 14px;
border: none;
font-size: 16px;
font-weight: 600;
color: #ffffff;
box-shadow: 0 12px 24px rgba(79, 110, 247, 0.28);
}
.from-input-item1 {
display: flex;
align-items: center;
width: 100%;
height: 48px;
background: #f6f8fc;
border-radius: 12px;
border: 1px solid #e6eaf2;
padding: 10px 14px;
box-sizing: border-box;
margin-bottom: 4px;
gap: 10px;
}
.from-input-item1 img {
width: 18px;
height: 18px;
opacity: 0.6;
}
.version {
margin-top: 24px;
font-size: 12px;
color: #9aa1b3;
}
.auth-right {
width: 58%;
background: linear-gradient(160deg, #f8f9fc 0%, #f0f3f9 100%);
position: relative;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.illustration {
width: 90%;
height: auto;
max-height: 88%;
object-fit: contain;
filter: drop-shadow(0 30px 60px rgba(40, 57, 108, 0.25));
} }
.el-input__wrapper { .el-input__wrapper {
--el-input-focus-border-color: rgba(255, 255, 0, 0); --el-input-focus-border-color: rgba(255, 255, 0, 0);
--el-menu-hover-bg-color: rgba(255, 255, 0, 0); --el-menu-hover-bg-color: rgba(255, 255, 0, 0);
} }
</style> </style>
<style scoped lang="less"> <style scoped lang="less">
::v-deep(.el-input__wrapper) { ::v-deep(.el-input__wrapper) {
background-color: rgba(255, 0, 0, 0); background-color: transparent;
box-shadow: none; box-shadow: none;
} }
::v-deep(.el-input__inner) { ::v-deep(.el-input__inner) {
color: #fff; color: #2b3347;
} }
::v-deep(.el-input__inner::placeholder) { ::v-deep(.el-input__inner::placeholder) {
color: @bg-color; color: #9aa1b3;
} }
</style> </style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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>

440
src/views/hosttest.html Normal file
View File

@@ -0,0 +1,440 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>Yolo Assistant - Modern Light-Tech Dashboard</title>
<link href="https://fonts.googleapis.com" rel="preconnect" />
<link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&amp;display=swap"
rel="stylesheet" />
<link href="https://fonts.googleapis.com/icon?family=Material+Icons+Round" rel="stylesheet" />
<script src="https://cdn.tailwindcss.com?plugins=forms,typography"></script>
<script>
tailwind.config = {
darkMode: "class",
theme: {
extend: {
colors: {
primary: "#0052CC",
"background-light": "#F4F7FA",
"background-dark": "#0B1120",
"tech-blue": "#E3F2FD",
"tech-green": "#E8F5E9",
"tech-orange": "#FFF3E0",
},
fontFamily: {
display: ["Inter", "system-ui", "sans-serif"],
},
borderRadius: {
DEFAULT: "12px",
'xl': '20px',
},
boxShadow: {
'soft-inner': 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.05)',
'premium': '0 10px 25px -5px rgba(0, 0, 0, 0.04), 0 8px 10px -6px rgba(0, 0, 0, 0.04)',
}
},
},
};
</script>
<style>
body {
font-family: 'Inter', sans-serif;
}
.glass-card {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.4);
}
.sidebar-active {
background: white;
color: #0052CC !important;
border-radius: 40px 0 0 40px;
position: relative;
}
.sidebar-active::after {
content: '';
position: absolute;
right: -20px;
top: 0;
bottom: 0;
width: 20px;
background: white;
}
.sidebar-active::before {
content: '';
position: absolute;
right: 0;
top: -40px;
width: 40px;
height: 40px;
border-radius: 50%;
box-shadow: 20px 20px 0 0 white;
z-index: 1;
}
.sidebar-active-footer::before {
content: '';
position: absolute;
right: 0;
bottom: -40px;
width: 40px;
height: 40px;
border-radius: 50%;
box-shadow: 20px -20px 0 0 white;
z-index: 1;
}
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #CBD5E1;
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: #94A3B8;
}
</style>
</head>
<body class="bg-slate-900 font-display text-slate-900 antialiased overflow-hidden">
<div class="flex h-screen">
<aside class="w-64 flex flex-col pt-8 pb-10 bg-slate-900 shrink-0">
<div class="px-8 mb-12">
<div class="flex items-center gap-2">
<span class="text-3xl font-bold tracking-tighter text-white">yolo</span>
<span
class="px-2 py-0.5 text-xs font-semibold tracking-wider text-slate-900 bg-white rounded uppercase">助手</span>
</div>
</div>
<nav class="flex-1 space-y-2 pl-4">
<a class="flex items-center gap-4 px-6 py-4 text-slate-400 hover:text-slate-200 transition-all duration-200 group"
href="#">
<span class="material-icons-round text-2xl">grid_view</span>
<span class="font-medium">工作台</span>
</a>
<div class="relative">
<a class="sidebar-active flex items-center gap-4 px-6 py-4 text-primary transition-all duration-200"
href="#">
<span class="material-icons-round text-2xl">person_search</span>
<span class="font-medium">主播列表</span>
</a>
<div class="sidebar-active-footer"></div>
</div>
</nav>
<div class="px-8 mt-auto">
<button
class="flex items-center gap-3 text-slate-400 hover:text-red-400 transition-colors duration-200 group">
<span class="material-icons-round text-xl group-hover:rotate-12 transition-transform">logout</span>
<span class="font-medium">退出登录</span>
</button>
</div>
</aside>
<main class="flex-1 bg-white dark:bg-slate-50 m-2 rounded-3xl shadow-2xl flex flex-col overflow-hidden">
<header class="px-8 py-6 border-b border-slate-100 dark:border-slate-200/60 bg-white/50 backdrop-blur-md">
<div class="flex flex-wrap items-center gap-4">
<div class="relative flex-1 min-w-[200px]">
<select
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">
<option>选择国家</option>
<option>哈萨克斯坦</option>
<option>土库曼斯坦</option>
<option>摩尔多瓦</option>
</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>
</div>
<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">calendar_today</span>
<input
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"
placeholder="选择查询时间" type="text" />
</div>
<div class="relative flex-[1.5] min-w-[240px]">
<input
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"
placeholder="请输入主播id" type="text" />
</div>
<div class="flex items-center gap-2 ml-auto">
<button
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>
查询
</button>
<button class="bg-slate-100 hover:bg-slate-200 text-slate-600 p-3 rounded-xl transition-all">
<span class="material-icons-round">tune</span>
</button>
</div>
</div>
</header>
<div class="flex-1 overflow-auto p-8">
<table class="w-full text-left border-separate border-spacing-0">
<thead>
<tr class="text-slate-400 text-xs font-semibold uppercase tracking-wider">
<th class="pb-4 pl-4 font-normal"><input
class="rounded border-slate-300 text-primary focus:ring-primary/20"
type="checkbox" /></th>
<th class="pb-4 font-semibold">主播ID</th>
<th class="pb-4 font-semibold">等级</th>
<th class="pb-4 font-semibold">邀请类型</th>
<th class="pb-4 font-semibold">数据查看</th>
<th class="pb-4 font-semibold">国家</th>
<th class="pb-4 font-semibold">创建时间</th>
<th class="pb-4 font-semibold">主播金币</th>
<th class="pb-4 font-semibold text-right pr-4">操作</th>
</tr>
</thead>
<tbody class="text-sm text-slate-600">
<tr class="group hover:bg-slate-50/80 transition-colors">
<td class="py-5 pl-4 border-b border-slate-50 group-last:border-none">
<input class="rounded border-slate-300 text-primary focus:ring-primary/20"
type="checkbox" />
</td>
<td class="py-5 border-b border-slate-50 group-last:border-none">
<span
class="font-medium text-slate-900 border-b border-transparent group-hover:border-primary/30 transition-all cursor-pointer">kisimisi1990</span>
</td>
<td class="py-5 border-b border-slate-50 group-last:border-none">D5</td>
<td class="py-5 border-b border-slate-50 group-last:border-none">
<span
class="px-3 py-1 bg-green-50 text-green-600 text-[10px] font-bold uppercase rounded-full">普票</span>
</td>
<td class="py-5 border-b border-slate-50 group-last:border-none">
<div class="flex gap-2">
<button
class="px-3 py-1.5 bg-blue-50 text-blue-600 hover:bg-blue-600 hover:text-white rounded-lg text-xs font-medium transition-all">查看场次</button>
<button
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">查看收益</button>
</div>
</td>
<td class="py-5 border-b border-slate-50 group-last:border-none">哈萨克斯坦</td>
<td class="py-5 border-b border-slate-50 group-last:border-none">
<div class="flex flex-col">
<span>2026-01-13</span>
<span class="text-[10px] text-slate-400">20:23:52</span>
</div>
</td>
<td
class="py-5 border-b border-slate-50 group-last:border-none font-semibold text-slate-900">
1260</td>
<td class="py-5 border-b border-slate-50 group-last:border-none text-right pr-4">
<button class="p-1 hover:bg-slate-200 rounded transition-colors text-slate-400">
<span class="material-icons-round text-lg">more_horiz</span>
</button>
</td>
</tr>
<tr class="group hover:bg-slate-50/80 transition-colors">
<td class="py-5 pl-4 border-b border-slate-50 group-last:border-none">
<input class="rounded border-slate-300 text-primary focus:ring-primary/20"
type="checkbox" />
</td>
<td class="py-5 border-b border-slate-50 group-last:border-none">
<span
class="font-medium text-slate-900 border-b border-transparent group-hover:border-primary/30 transition-all cursor-pointer">maga44458</span>
</td>
<td class="py-5 border-b border-slate-50 group-last:border-none">D5</td>
<td class="py-5 border-b border-slate-50 group-last:border-none">
<span
class="px-3 py-1 bg-green-50 text-green-600 text-[10px] font-bold uppercase rounded-full">普票</span>
</td>
<td class="py-5 border-b border-slate-50 group-last:border-none">
<div class="flex gap-2">
<button
class="px-3 py-1.5 bg-blue-50 text-blue-600 hover:bg-blue-600 hover:text-white rounded-lg text-xs font-medium transition-all">查看场次</button>
<button
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">查看收益</button>
</div>
</td>
<td class="py-5 border-b border-slate-50 group-last:border-none">土库曼斯坦</td>
<td class="py-5 border-b border-slate-50 group-last:border-none">
<div class="flex flex-col">
<span>2026-01-13</span>
<span class="text-[10px] text-slate-400">20:23:52</span>
</div>
</td>
<td
class="py-5 border-b border-slate-50 group-last:border-none font-semibold text-slate-900">
3006</td>
<td class="py-5 border-b border-slate-50 group-last:border-none text-right pr-4">
<button class="p-1 hover:bg-slate-200 rounded transition-colors text-slate-400">
<span class="material-icons-round text-lg">more_horiz</span>
</button>
</td>
</tr>
<tr class="group hover:bg-slate-50/80 transition-colors">
<td class="py-5 pl-4 border-b border-slate-50 group-last:border-none">
<input class="rounded border-slate-300 text-primary focus:ring-primary/20"
type="checkbox" />
</td>
<td class="py-5 border-b border-slate-50 group-last:border-none">
<span
class="font-medium text-slate-900 border-b border-transparent group-hover:border-primary/30 transition-all cursor-pointer">erke_700</span>
</td>
<td class="py-5 border-b border-slate-50 group-last:border-none">B5</td>
<td class="py-5 border-b border-slate-50 group-last:border-none">
<span
class="px-3 py-1 bg-amber-50 text-amber-600 text-[10px] font-bold uppercase rounded-full">金票</span>
</td>
<td class="py-5 border-b border-slate-50 group-last:border-none">
<div class="flex gap-2">
<button
class="px-3 py-1.5 bg-blue-50 text-blue-600 hover:bg-blue-600 hover:text-white rounded-lg text-xs font-medium transition-all">查看场次</button>
<button
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">查看收益</button>
</div>
</td>
<td class="py-5 border-b border-slate-50 group-last:border-none">哈萨克斯坦</td>
<td class="py-5 border-b border-slate-50 group-last:border-none">
<div class="flex flex-col">
<span>2026-01-13</span>
<span class="text-[10px] text-slate-400">20:23:49</span>
</div>
</td>
<td
class="py-5 border-b border-slate-50 group-last:border-none font-semibold text-slate-900">
5572</td>
<td class="py-5 border-b border-slate-50 group-last:border-none text-right pr-4">
<button class="p-1 hover:bg-slate-200 rounded transition-colors text-slate-400">
<span class="material-icons-round text-lg">more_horiz</span>
</button>
</td>
</tr>
<tr class="group hover:bg-slate-50/80 transition-colors">
<td class="py-5 pl-4 border-b border-slate-50 group-last:border-none">
<input class="rounded border-slate-300 text-primary focus:ring-primary/20"
type="checkbox" />
</td>
<td class="py-5 border-b border-slate-50 group-last:border-none">
<span
class="font-medium text-slate-900 border-b border-transparent group-hover:border-primary/30 transition-all cursor-pointer">luckystar1458</span>
</td>
<td class="py-5 border-b border-slate-50 group-last:border-none">D1</td>
<td class="py-5 border-b border-slate-50 group-last:border-none">
<span
class="px-3 py-1 bg-green-50 text-green-600 text-[10px] font-bold uppercase rounded-full">普票</span>
</td>
<td class="py-5 border-b border-slate-50 group-last:border-none">
<div class="flex gap-2">
<button
class="px-3 py-1.5 bg-blue-50 text-blue-600 hover:bg-blue-600 hover:text-white rounded-lg text-xs font-medium transition-all">查看场次</button>
<button
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">查看收益</button>
</div>
</td>
<td class="py-5 border-b border-slate-50 group-last:border-none">摩尔多瓦</td>
<td class="py-5 border-b border-slate-50 group-last:border-none">
<div class="flex flex-col">
<span>2026-01-13</span>
<span class="text-[10px] text-slate-400">20:23:50</span>
</div>
</td>
<td
class="py-5 border-b border-slate-50 group-last:border-none font-semibold text-slate-900">
15107</td>
<td class="py-5 border-b border-slate-50 group-last:border-none text-right pr-4">
<button class="p-1 hover:bg-slate-200 rounded transition-colors text-slate-400">
<span class="material-icons-round text-lg">more_horiz</span>
</button>
</td>
</tr>
<tr class="group hover:bg-slate-50/80 transition-colors">
<td class="py-5 pl-4 border-b border-slate-50 group-last:border-none">
<input class="rounded border-slate-300 text-primary focus:ring-primary/20"
type="checkbox" />
</td>
<td class="py-5 border-b border-slate-50 group-last:border-none">
<span
class="font-medium text-slate-900 border-b border-transparent group-hover:border-primary/30 transition-all cursor-pointer">boss.667b1</span>
</td>
<td class="py-5 border-b border-slate-50 group-last:border-none">C5</td>
<td class="py-5 border-b border-slate-50 group-last:border-none">
<span
class="px-3 py-1 bg-amber-50 text-amber-600 text-[10px] font-bold uppercase rounded-full">金票</span>
</td>
<td class="py-5 border-b border-slate-50 group-last:border-none">
<div class="flex gap-2">
<button
class="px-3 py-1.5 bg-blue-50 text-blue-600 hover:bg-blue-600 hover:text-white rounded-lg text-xs font-medium transition-all">查看场次</button>
<button
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">查看收益</button>
</div>
</td>
<td class="py-5 border-b border-slate-50 group-last:border-none">哈萨克斯坦</td>
<td class="py-5 border-b border-slate-50 group-last:border-none">
<div class="flex flex-col">
<span>2026-01-13</span>
<span class="text-[10px] text-slate-400">20:23:49</span>
</div>
</td>
<td
class="py-5 border-b border-slate-50 group-last:border-none font-semibold text-slate-900">
4815</td>
<td class="py-5 border-b border-slate-50 group-last:border-none text-right pr-4">
<button class="p-1 hover:bg-slate-200 rounded transition-colors text-slate-400">
<span class="material-icons-round text-lg">more_horiz</span>
</button>
</td>
</tr>
</tbody>
</table>
</div>
<footer
class="px-8 py-6 border-t border-slate-100 dark:border-slate-200/60 bg-white/50 backdrop-blur-md flex flex-wrap items-center justify-between gap-4">
<div class="flex items-center gap-4">
<div class="relative">
<select
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">
<option>10/page</option>
<option>20/page</option>
<option>50/page</option>
</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>
</div>
<span class="text-xs text-slate-400 font-medium">到期时间: <span class="text-green-600">2036-08-01
00:00:00</span></span>
</div>
<div class="flex items-center gap-1">
<button class="p-2 text-slate-400 hover:bg-slate-100 rounded-lg transition-colors">
<span class="material-icons-round text-lg">chevron_left</span>
</button>
<button
class="w-8 h-8 rounded-lg bg-slate-900 text-white text-xs font-bold transition-all shadow-md">1</button>
<button
class="w-8 h-8 rounded-lg hover:bg-slate-100 text-slate-600 text-xs font-semibold transition-all">2</button>
<button
class="w-8 h-8 rounded-lg hover:bg-slate-100 text-slate-600 text-xs font-semibold transition-all">3</button>
<button
class="w-8 h-8 rounded-lg hover:bg-slate-100 text-slate-600 text-xs font-semibold transition-all">4</button>
<button
class="w-8 h-8 rounded-lg hover:bg-slate-100 text-slate-600 text-xs font-semibold transition-all">5</button>
<button
class="w-8 h-8 rounded-lg hover:bg-slate-100 text-slate-600 text-xs font-semibold transition-all">6</button>
<span class="px-2 text-slate-400 text-xs font-bold">...</span>
<button
class="w-8 h-8 rounded-lg hover:bg-slate-100 text-slate-600 text-xs font-semibold transition-all">46</button>
<button class="p-2 text-slate-400 hover:bg-slate-100 rounded-lg transition-colors">
<span class="material-icons-round text-lg">chevron_right</span>
</button>
</div>
</footer>
</main>
</div>
</body>
</html>

View File

@@ -1,89 +1,88 @@
<template> <template>
<div class="app-container"> <div class="nav-container bg-background-light dark:bg-background-dark text-slate-800 dark:text-slate-200 shadow-2xl overflow-hidden" :style="containerStyle">
<Sidebar class="noneText" @activeIndex="activeIndexFn" /> <div class="flex h-full overflow-hidden">
<div class="content "> <Sidebar class="flex-none h-full" @activeIndex="activeIndexFn" />
<div v-show="activeIndexA == 1"> <main class="flex-1 overflow-y-auto p-4 relative flex flex-col h-full bg-[#f3f5f9]">
<workbenches /> <div v-show="activeIndexA == 1" class="h-full">
</div> <workbenches />
<div v-show="activeIndexA == 2"> </div>
<hostsList /> <div v-show="activeIndexA == 2" class="h-full">
</div> <hostsList />
<div style="position: absolute; bottom: 0; right: 0;">{{ version }}</div> </div>
<!-- Decorative blobs -->
<!-- <div class="absolute -top-24 -right-24 w-64 h-64 bg-primary/5 rounded-full blur-3xl pointer-events-none"></div> -->
<!-- <div class="absolute -bottom-24 -left-24 w-64 h-64 bg-secondary/5 rounded-full blur-3xl pointer-events-none"></div> -->
<!-- <div class="absolute bottom-2 right-4 text-xs text-slate-400">{{ version }}</div> -->
</main>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import Sidebar from '../components/Sidebar.vue'; import Sidebar from '../components/Sidebar.vue';
import { RouterLink, RouterView } from 'vue-router'
import hostsList from '@/views/hosts/hostsList.vue' import hostsList from '@/views/hosts/hostsList.vue'
import workbenches from '@/views/hosts/workbenches.vue' import workbenches from '@/views/hosts/workbenches.vue'
import { ref } from 'vue' import { ref, computed, onMounted, onUnmounted } from 'vue'
import { getUser } from '@/utils/storage'
// import { usePythonBridge } from '@/utils/pythonBridge'
// 设计稿尺寸
const DESIGN_WIDTH = 1600
const DESIGN_HEIGHT = 900
let activeIndexA = ref(1) let activeIndexA = ref(1)
const version = ref('v0.1.0')
// 缩放比例
const zoomRatio = ref(1)
// 计算缩放比例
const calcZoom = () => {
const windowWidth = window.innerWidth
const windowHeight = window.innerHeight
// 根据宽高比例计算缩放值,取较小值保证完整显示
const widthRatio = windowWidth / DESIGN_WIDTH
const heightRatio = windowHeight / DESIGN_HEIGHT
zoomRatio.value = Math.min(widthRatio, heightRatio)
}
// 容器样式
const containerStyle = computed(() => ({
width: `${DESIGN_WIDTH}px`,
height: `${DESIGN_HEIGHT}px`,
zoom: zoomRatio.value
}))
function activeIndexFn(data) { function activeIndexFn(data) {
activeIndexA.value = data activeIndexA.value = data
console.log(data) console.log(data)
} }
onMounted(() => {
calcZoom()
window.addEventListener('resize', calcZoom)
})
onUnmounted(() => {
window.removeEventListener('resize', calcZoom)
})
</script> </script>
<style lang="less"> <style>
body, /* Global resets if needed, but Tailwind handles most */
html { body {
margin: 0; margin: 0;
padding: 0; padding: 0;
height: 100%; background-color: #f3f5f9; /* Match background */
}
.app-container {
display: flex; display: flex;
width: 1600px; justify-content: center;
height: 900px;
background-color: @bg-color;
position: relative;
}
.noneText {
/* 页面无法选中 */
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.sidebar {
width: 200px;
background-color: @bg-color;
padding: 20px;
/* box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1); */
}
.content {
margin-left: 280px;
width: 1304px;
height: 868px;
background: #FFFFFF;
border-radius: 36px;
margin-top: 16px;
}
.center-justify {
display: flex;
justify-content: space-around;
align-items: center; align-items: center;
min-height: 100vh;
overflow: hidden;
}
.nav-container {
transform-origin: center center;
} }
</style> </style>