初始化仓库

This commit is contained in:
2025-07-01 21:02:39 +08:00
parent ed0ca8d330
commit c33ae29649
46 changed files with 17385 additions and 0 deletions

5
.env.development Normal file
View File

@@ -0,0 +1,5 @@
# .env.development
VUE_APP_API_URL="123123123"
# VUE_APP_FEATURE_FLAG=true

5
.env.production Normal file
View File

@@ -0,0 +1,5 @@
# .env.production
VUE_APP_API_URL="434534534"
# VUE_APP_FEATURE_FLAG=true

23
.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

5
babel.config.js Normal file
View File

@@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

19
jsconfig.json Normal file
View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}

14092
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

39
package.json Normal file
View File

@@ -0,0 +1,39 @@
{
"name": "tk-page",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build"
},
"dependencies": {
"axios": "^1.8.4",
"core-js": "^3.8.3",
"echarts": "^5.6.0",
"element-plus": "^2.9.7",
"pinia": "^3.0.1",
"qwebchannel": "^6.2.0",
"vue": "^3.2.13",
"vue-i18n": "^11.1.7",
"vue-router": "^4.0.3",
"vuex": "^4.0.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-plugin-vuex": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"less": "^4.2.2",
"less-loader": "^12.2.0",
"postcss-preset-env": "^10.1.5",
"postcss-px-to-viewport": "^1.1.1",
"postcss-px-viewport": "^0.0.4",
"postcss-viewport-units": "^0.1.6"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"not ie 11"
]
}

13
postcss.config.js Normal file
View File

@@ -0,0 +1,13 @@
module.exports = {
plugins: {
'postcss-px-to-viewport': {
viewportWidth: 1600, // 视窗的宽度,对应设计稿宽度
viewportHeight: 900, // 视窗的高度,对应设计稿高度
unitPrecision: 3, // 指定 px 转换为视窗单位值的小数位数
viewportUnit: 'vw', // 指定需要转换成的视窗单位vw 或者 vh
selectorBlackList: ['.ignore', '.hairlines'], // 指定不需要转换的类
minPixelValue: 1, // 小于或等于 1 px 不转换为视窗单位
mediaQuery: false // 允许在媒体查询中转换 px
}
}
};

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

32
public/index.html Normal file
View File

@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>
<%= webpackConfig.name %>
</title>
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
</head>
<body>
<noscript>
<strong>We're sorry but <%= webpackConfig.name %> doesn't work properly without JavaScript enabled. Please enable it
to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
<style>
body {
margin: 0;
padding: 0;
/* width: 1600px;
height: 900px; */
}
</style>
</html>

35
src/App.vue Normal file
View File

@@ -0,0 +1,35 @@
<template>
<div>
<router-view />
</div>
</template>
<script setup>
import { useI18n } from 'vue-i18n'
const { locale } = useI18n()
locale.value = localStorage.getItem('lang') || 'zh'
const debounce = (fn, delay) => {
let timer
return (...args) => {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fn(...args)
}, delay)
}
}
const _ResizeObserver = window.ResizeObserver
window.ResizeObserver = class ResizeObserver extends _ResizeObserver {
constructor(callback) {
callback = debounce(callback, 200)
super(callback)
}
}
</script>
<!-- <style lang="less">
/*每个页面公共css */
@import "@/static/css/app.less";
</style> -->

72
src/api/account.js Normal file
View File

@@ -0,0 +1,72 @@
import { getAxios, postAxios, downFile } from '@/utils/axios.js'
export function getIdByName(name) {
return getAxios({ url: `/api/tenant/get-id-by-name?name=${name}` })
}
export function login(data) {
return postAxios({ url: '/api/user/doLogin', data })
}
//获取国家
export function getCountryinfo(data) {
return postAxios({ url: '/api/common/country_info', data })
}
//查询tk账号查询次数
export function tkaccountuseinfo(accountName) {
return getAxios({ url: `/api/common/accountCount?accountName=${accountName}` })
}
export function tkhostdata(data) {
return postAxios({ url: '/api/save_data/hosts_info', data })
}
export function apiGetCart() {
return getAxios({ url: '/cgi-bin/cart/latest' })
}
// export function login(data) {
// return postAxios({ url: 'api/account/login', data })
// }
export function cheekalive(data) {
return postAxios({ url: 'api/account/cheekalive', data })
}
export function dicts(data) {
return postAxios({ url: 'api/param/dicts', data })
}
export function tkhostdetail(data) {
return postAxios({ url: 'api/tkinfo/tkhostdetail', data })
}
//导出表格
export function exporthosts(data) {
return postAxios({ url: 'api/export/hostsinfo', data })
}
export function downList(url, data) {
return downFile(url, data)
}
//查询员工
export function getStaffList(data) {
return postAxios({ url: 'api/account/list', data })
}
//分配主播
export function managerhosts(data) {
return postAxios({ url: 'api/account/managerhosts', data })
}
//编辑主播
export function upholdinfo(data) {
return postAxios({ url: 'api/tkinfo/upholdinfo', data })
}
//查看名字
export function accountName(str) {
return postAxios({ url: 'api/account/accountName?accounts=' + str })
}

BIN
src/assets/filter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
src/assets/list.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
src/assets/listAction.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

BIN
src/assets/logo1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
src/assets/logoBg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

BIN
src/assets/logoBg1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
src/assets/logotext.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

BIN
src/assets/logotext1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
src/assets/navAction.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
src/assets/password.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 806 B

BIN
src/assets/username.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 945 B

BIN
src/assets/work.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 993 B

BIN
src/assets/workAction.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 993 B

BIN
src/assets/worklogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,160 @@
<template>
<div ref="chart" style="width: 500px; height: 300px;"></div>
</template>
<script>
import * as echarts from 'echarts';
import { tkhostdata, dicts, tkhostdetail } from '@/api/account';
export default {
name: 'EChartsComponent',
props: {
title: {
type: String,
required: true
},
id: {
type: String,
required: true
},
dataType: {
type: String,
required: true
},
time: {
type: String,
required: true
}
},
data() {
return {
seriesData: [],
num: 0,
inputTime: '',
}
},
mounted() {
this.inputTime = this.time
this.getTkhostdetail();
console.log(this.time)
console.log(this.getPrevious7Days(this.inputTime))
},
methods: {
initChart() {
if (!this.$refs.chart) {
console.error('DOM element not found');
return;
}
const myChart = echarts.init(this.$refs.chart);
const option = {
title: {
text: this.title
},
tooltip: {},
legend: {
data: [this.title]
},
xAxis: {
data: [this.getPrevious7Days(this.inputTime)[0].slice(4), this.getPrevious7Days(this.inputTime)[1].slice(4), this.getPrevious7Days(this.inputTime)[2].slice(4), this.getPrevious7Days(this.inputTime)[3].slice(4), this.getPrevious7Days(this.inputTime)[4].slice(4), this.getPrevious7Days(this.inputTime)[5].slice(4), this.getPrevious7Days(this.inputTime)[6].slice(4)]
},
yAxis: {},
series: [
{
name: this.title,
type: 'line',
data: [this.seriesData[0], this.seriesData[1], this.seriesData[2], this.seriesData[3], this.seriesData[4], this.seriesData[5], this.seriesData[6]],
smooth: true
}
]
};
myChart.setOption(option);
console.log(this.dataTime)
},
getTkhostdetail() {
tkhostdetail({
hostId: this.id,
dataType: this.dataType,
searchTimeStart: this.getPrevious7Days(this.inputTime)[0],
searchTimeEnd: this.getPrevious7Days(this.inputTime)[6]
}).then(res => {
// console.log("返回数据", res[0][this.getPrevious7Days(this.inputTime)[2]])
//echarts 数据初始化
this.seriesData = [
res[0][this.getPrevious7Days(this.inputTime)[0]] == null ? 0 : Number(res[0][this.getPrevious7Days(this.inputTime)[0]][this.dataType]),
res[0][this.getPrevious7Days(this.inputTime)[1]] == null ? 0 : Number(res[0][this.getPrevious7Days(this.inputTime)[1]][this.dataType]),
res[0][this.getPrevious7Days(this.inputTime)[2]] == null ? 0 : Number(res[0][this.getPrevious7Days(this.inputTime)[2]][this.dataType]),
res[0][this.getPrevious7Days(this.inputTime)[3]] == null ? 0 : Number(res[0][this.getPrevious7Days(this.inputTime)[3]][this.dataType]),
res[0][this.getPrevious7Days(this.inputTime)[4]] == null ? 0 : Number(res[0][this.getPrevious7Days(this.inputTime)[4]][this.dataType]),
res[0][this.getPrevious7Days(this.inputTime)[5]] == null ? 0 : Number(res[0][this.getPrevious7Days(this.inputTime)[5]][this.dataType]),
res[0][this.getPrevious7Days(this.inputTime)[6]] == null ? 0 : Number(res[0][this.getPrevious7Days(this.inputTime)[6]][this.dataType]),
]
this.initChart();
this.num++
console.log("返回数据", this.seriesData)
console.log("返回数据", this.num)
})
},
// getCurrentDate() {
// const dates = [];
// const today = new Date();
// for (let i = 6; i >= 0; i--) {
// const date = new Date(today);
// date.setDate(today.getDate() - i);
// const year = date.getFullYear();
// const month = String(date.getMonth() + 1).padStart(2, '0');
// const day = String(date.getDate()).padStart(2, '0');
// dates.push(`${year}${month}${day}`);
// }
// return dates;
// },
getPrevious7Days(dateStr) {
// 验证输入格式是否正确
if (!/^\d{8}$/.test(dateStr)) {
console.error('输入的格式不是YYYYMMDD.');
return [];
}
// 解析输入的日期字符串
const year = parseInt(dateStr.substring(0, 4));
const month = parseInt(dateStr.substring(4, 6)) - 1; // 月份从 0 开始
const day = parseInt(dateStr.substring(6, 8));
// 创建日期对象
const date = new Date(year, month, day);
// 存储结果的数组
const result = [];
// 计算前 7 天的日期
for (let i = 0; i <= 6; i++) {
const currentDate = new Date(date);
currentDate.setDate(date.getDate() - i); // 减去 i 天
// 格式化为 YYYYMMDD
const formattedDate = `${currentDate.getFullYear()}${(currentDate.getMonth() + 1).toString().padStart(2, '0')}${currentDate.getDate().toString().padStart(2, '0')}`;
result.push(formattedDate);
}
// 按时间顺序排序(从最早到最晚)
result.sort((a, b) => a.localeCompare(b));
return result;
}
}
};
</script>

148
src/components/Sidebar.vue Normal file
View File

@@ -0,0 +1,148 @@
<template>
<div class="sidebar">
<div class="logo">
<!-- <img style="margin-right: 10px;" src="@/assets/logo.png"> -->
<img src="@/assets/logotext.png">
</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>
</div>
</li>
<li @click="updateActiveIndex(2)">
<div>
<img v-show="activeIndex == 2" src="@/assets/navAction.png" autoplay loop muted class="background-img">
<div style="display: flex;">
<img v-show="activeIndex == 2" src="@/assets/listAction.png" style="margin-right: 10px;">
<img v-show="activeIndex == 1" src="@/assets/listAction.png" style="margin-right: 10px;">
<div :style="activeIndex == 2 ? 'color: #000' : 'color: #fff'" class="center-justify">{{
$t('menu.hostList')
}}</div>
</div>
</div>
</li>
</ul>
<a @click="$router.push('/')" href="javascript:void(0);"
style="position: absolute; bottom: 30px; color: aliceblue; font-size: 20px; font-weight: 500;">
{{
$t('menu.logout')
}}
</a>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { getUser } from '@/utils/storage'
import { defineEmits } from 'vue';
const userInfo = ref(getUser())
let activeIndex = ref(1);
const emit = defineEmits(['activeIndex']);
const updateActiveIndex = (index) => {
activeIndex.value = index;
emit('activeIndex', index);
};
</script>
<style scoped lang="less">
.sidebar {
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>

16
src/i18n/index.js Normal file
View File

@@ -0,0 +1,16 @@
import { createI18n } from 'vue-i18n'
import en from '@/locales/en'
import zh from '@/locales/zh'
const messages = {
en,
zh
}
const i18n = createI18n({
locale: 'zh', // 默认语言
fallbackLocale: 'en',
messages
})
export default i18n

78
src/locales/en.js Normal file
View File

@@ -0,0 +1,78 @@
export default {
login: {
title: 'Account login',
version: 'VERSION',
login: 'Login',
tenantName: 'Tenant Name',
account: 'account',
password: 'password',
Language: 'Language',
network: 'NetWork',
},
menu: {
workbenches: 'Work Benches',
hostList: 'Streamer List',
logout: 'Logout',
},
workbenches: {
openTK: 'OpenTK',
totalnumber: 'Total Number',
createHost: 'Create Streamer',
query: 'Can query',
invite: 'Can invited',
runTime: 'Run Time',
guildAccount: 'Guild account',
guildPass: 'Guild password',
guildAccountPlace: 'Please enter your login account',
guildPassPlace: 'Please enter your login password',
queriedNum: 'Today queried times',
loginBackend: 'Login backend',
workbenches: 'Work Benches',
},
workbenchesSetup: {
workbenches: 'Work Benches',
network: 'Current Network',
setCoinsNum: 'Set Coins Number',
setFansNum: 'Set Fans Number',
setQuery: 'Set Query Frequency',
minCoinsNum: 'Min Coins Number',
maxCoinsNum: 'Max Coins Number',
minFansNum: 'Min Fans Number',
maxFansNum: 'Max Fans Number',
hour: 'times/hour',
hour24: 'times/24hour',
start: 'Start Obtaining Data',
stop: 'Stop',
},
hostList: {
placeCountry: 'Select country',
placeSeletTime: 'Select query time',
placeHostId: 'Please enter the anchor ID',
selectAll: 'All',
query: 'Query',
export: 'Export Excel data',
hostId: 'Streamer ID',
grade: 'Grade',
country: 'Country',
creationTime: 'Creation Time',
anchorcoins: 'Anchor Coins',
yesterdayGoldCoins: 'Yesterday Gold Coins',
fansNum: 'Number Fans',
followersNum: 'Number Followers',
onlineFans: 'Online Fans',
anchorType: 'Anchor Type',
min: 'min',
max: 'max',
placeMin: 'Please enter the minimum value',
placeMax: 'Please enter the maximum value',
sort: 'sort',
sortType: 'sort Type',
ascending: 'ascending',
descending: 'descending',
reset: 'reset',
sure: 'sure',
invitationType: 'invitationType',
invitationType1: 'Regular',
invitationType2: 'Golden',
}
}

77
src/locales/zh.js Normal file
View File

@@ -0,0 +1,77 @@
export default {
login: {
title: '账号登陆',
version: '版本号',
login: '登录',
tenantName: '租户名称',
account: '账户',
password: '密码',
Language: '语言设置',
network: '网络设置',
},
menu: {
workbenches: '工作台',
hostList: '主播列表',
logout: '退出登录',
},
workbenches: {
openTK: '开启TK',
totalnumber: '总数量',
createHost: '新建主播',
query: '查询',
invite: '邀请',
runTime: '运行时间',
guildAccount: '公会账号',
guildPass: '公会密码',
guildAccountPlace: '请输入登录账号',
guildPassPlace: '请输入登录密码',
queriedNum: '今日已查询次数',
loginBackend: '登录后台',
},
workbenchesSetup: {
workbenches: '工作台',
network: '当前网络',
setCoinsNum: '设置金币数量',
setFansNum: '设置粉丝数量',
setQuery: '后台查询频率',
minCoinsNum: '最小金币数量',
maxCoinsNum: '最大金币数量',
minFansNum: '最小粉丝数量',
maxFansNum: '最大粉丝数量',
hour: '次/小时',
hour24: '次/24小时',
start: '开始获取数据',
stop: '停止',
},
hostList: {
placeCountry: '选择国家',
placeSeletTime: '选择查询时间',
placeHostId: '请输入主播id',
selectAll: '全部',
query: '查询',
export: '导出Excel数据',
hostId: '主播id',
grade: '等级',
country: '国家',
creationTime: '创建时间',
anchorcoins: '主播金币',
yesterdayGoldCoins: '昨日金币',
fansNum: '粉丝数',
followersNum: '关注数',
onlineFans: '在线粉丝',
anchorType: '主播类型',
min: '最小值',
max: '最大值',
placeMin: '请输入最小值',
placeMax: '请输入最大值',
sort: '排序',
sortType: '排序方式',
ascending: '升序',
descending: '降序',
reset: '重置',
sure: '确定',
invitationType: '邀请类型',
invitationType1: '普票',
invitationType2: '金票',
}
}

24
src/main.js Normal file
View File

@@ -0,0 +1,24 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import { createPinia } from 'pinia';
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'; // 引入中文语言包
import i18n from './i18n'
// createApp(App).use(store).use(router).mount('#app')
const app = createApp(App);
// app.use(ElementPlus, {
// locale: zhCn, // 配置中文
// });
app.use(i18n) // 注册 i18n
app.use(ElementPlus) // 注册 ElementPlus
app.use(createPinia()); // 注册 Pinia
app.use(store); // 注册 store
app.use(router); // 注册 router
app.mount('#app');

34
src/router/index.js Normal file
View File

@@ -0,0 +1,34 @@
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/nav',
name: 'nav',
// redirect: '/nav/hostsList', // 默认跳转
component: () => import(/* webpackChunkName: "hostsList" */ '../views/nav.vue'),
children: [
{
path: 'hostsList',
name: 'hostsList',
component: () => import(/* webpackChunkName: "hostsList" */ '../views/hosts/hostsList.vue')
},
{
path: 'workBenches',
name: 'workBenches',
component: () => import(/* webpackChunkName: "hostsList" */ '../views/hosts/workbenches.vue')
},]
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router

4
src/static/css/app.less Normal file
View File

@@ -0,0 +1,4 @@
@bg-color: #022b4e; // 主色
@bg-color-light: #022b4eaf; // 浅主色
@bg-color-light-light: #022b4e1c; // 浅浅主色
@btn-bg-color: #045dac; // 黄色按钮主色

14
src/store/index.js Normal file
View File

@@ -0,0 +1,14 @@
import { createStore } from 'vuex'
export default createStore({
state: {
},
getters: {
},
mutations: {
},
actions: {
},
modules: {
}
})

12
src/stores/notice.js Normal file
View File

@@ -0,0 +1,12 @@
import { defineStore } from 'pinia';
export const noticeStore = defineStore('noticeNum', {
state: () => {
return { data: { num: 0 } };
},
actions: {
increment() {
this.data.num++;
},
},
});

187
src/utils/axios.js Normal file
View File

@@ -0,0 +1,187 @@
/**
* axios请求封装
* https://rudon.blog.csdn.net/
*/
import axios from 'axios'
import { getToken, getUser } from '@/utils/storage'
import router from '@/router'
import { ElMessage } from 'element-plus';
import { usePythonBridge, } from '@/utils/pythonBridge'
const { stopScript } = usePythonBridge();
// 请求地址前缀
let baseURL = ''
if (process.env.NODE_ENV === 'development') {
// 生产环境
// baseURL = "https://api.tkpage.yolozs.com"
baseURL = "http://192.168.1.174:8101"
// baseURL = "http://47.79.98.113:8101"
} else {
// 测试环境
// baseURL = "http://120.26.251.180:8085/"
// 开发环境
baseURL = "http://47.79.98.113:8101"
// baseURL = "http://api.tkpage.vvtiktok.cn"
}
// 请求拦截器
axios.interceptors.request.use((config) => {
console.log("config", config)
const url = sliceUrl(config.url)
console.log("url", url)
if (!(config.url == 'doLogin' || config.url == 'get-id-by-name')) {
config.headers['vvtoken'] = getToken();
}
// 请求超时时间 - 毫秒
config.timeout = 60000
config.baseURL = baseURL
// 自定义Content-type
config.headers['Content-type'] = 'application/json'
return config;
}, (error) => {
return Promise.reject(error)
})
// 响应拦截器
axios.interceptors.response.use((response) => {
console.log("response", response.data)
if (response.data.code == 0) {
console.log("response", response.data.data)
return response.data.data
} else if (response.data.code == 40400) {
stopScript();
router.push('/')
ElMessage.error(response.data.code + '' + response.data.message);
} else {
ElMessage.error(response.data.code + '' + response.data.message);
}
}, (error) => {
// 可添加请求失败后的处理逻辑
return Promise.reject(error)
})
// axios的get请求
export function getAxios({ url, params }) {
// 使用axios发送GET请求
return new Promise((resolve, reject) => {
axios.get(url, {
params
// 请求成功将返回的数据传递给resolve函数
}).then(res => {
resolve(res)
// 请求失败将错误信息传递给reject函数
}).catch(err => {
reject(err)
})
})
}
// axios的post请求
export function postAxios({ url, data }) {
return new Promise((resolve, reject) => {
axios.post(
url,
data,
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
).then(res => {
resolve(res)
}).catch(err => {
reject(err)
})
})
}
export const downFile = async (urlstr, data) => {
// 发送请求,获取文件流
const response = await axios.post(urlstr, data, { responseType: 'blob' });
// 获取文件名(如果后端设置了 Content-Disposition
const contentDisposition = response.headers['content-disposition'];
let fileName = 'default-file-name'; // 默认文件名
console.log(contentDisposition)
console.log(response)
if (contentDisposition) {
// 从响应头中提取文件名
const fileNameMatch = contentDisposition.match(/filename="(.+)"/);
if (fileNameMatch && fileNameMatch.length > 1) {
fileName = fileNameMatch[1];
}
}
// 创建一个临时的下载链接
const blob = new Blob([response.data], { type: response.headers['content-type'] });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName; // 设置下载的文件名
a.click();
// 释放 URL 对象
window.URL.revokeObjectURL(url);
}
//请求前验证
// function cheekalive() {
// axios.post('api/account/cheekalive', {
// userId: getUser().userId,
// currcode: getToken(),
// },
// {
// headers: {
// 'Content-Type': 'application/x-www-form-urlencoded'
// }
// }
// ).then(res => {
// console.log(res.data)
// if (res.data) {
// } else {
// stopScript();
// alert("账号在其他地方登录!")
// window.location.href = '/';
// }
// })
// }
//节流函数
function throttle(func, limit) {
let inThrottle;
return function () {
const args = arguments;
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
}
}
function sliceUrl(url) {
const lastSlash = url.lastIndexOf('/');
const questionMark = url.indexOf('?');
if (questionMark == -1) {
const result = url.slice(lastSlash + 1, url.length);
return result;
} else {
const result = url.slice(lastSlash + 1, questionMark);
return result;
}
}
export default axios

261
src/utils/countryUtil.js Normal file
View File

@@ -0,0 +1,261 @@
// country-utils.js
export const CountryCode = {
AD: "安道尔",
AE: "阿拉伯联合酋长国",
AF: "阿富汗",
AG: "安提瓜和巴布达",
AI: "安圭拉",
AL: "阿尔巴尼亚",
AM: "亚美尼亚",
AO: "安哥拉",
AQ: "南极洲",
AR: "阿根廷",
AS: "美属萨摩亚",
AT: "奥地利",
AU: "澳大利亚",
AU1: "澳大利亚",
AW: "阿鲁巴",
AX: "奥兰群岛",
AZ: "阿塞拜疆",
BA: "波斯尼亚和黑塞哥维那",
BB: "巴巴多斯",
BD: "孟加拉国",
BE: "比利时",
BF: "布基纳法索",
BG: "保加利亚",
BH: "巴林",
BI: "布隆迪",
BJ: "贝宁",
BL: "圣巴泰勒米",
BM: "百慕大群岛",
BN: "文莱达鲁萨兰国",
BO: "玻利维亚",
BQ: "博奈尔、圣尤斯特歇斯和萨巴",
BR: "巴西",
BS: "巴哈马",
BT: "不丹",
BV: "布韦岛",
BW: "博茨瓦纳",
BY: "白俄罗斯",
BZ: "伯利兹",
CA: "加拿大",
CA1: "加拿大",
CC: "科科斯(基林)群岛",
CD: "刚果民主共和国",
CF: "中非共和国",
CG: "刚果共和国",
CH: "瑞士",
CI: "科特迪瓦",
CK: "库克群岛",
CL: "智利",
CM: "喀麦隆",
CN: "中国",
CO: "哥伦比亚",
CR: "哥斯达黎加",
CU: "古巴",
CV: "佛得角",
CW: "库拉索",
CX: "圣诞岛",
CY: "塞浦路斯",
CZ: "捷克共和国",
DE: "德国",
DG: "迪戈加西亚岛",
DJ: "吉布提",
DK: "丹麦",
DM: "多米尼克",
DO: "多米尼加共和国",
DZ: "阿尔及利亚",
EC: "厄瓜多尔",
EE: "爱沙尼亚",
EG: "埃及",
EH: "西撒哈拉",
ER: "厄立特里亚",
ES: "西班牙",
ET: "埃塞俄比亚",
FI: "芬兰",
FJ: "斐济",
FK: "福克兰群岛",
FM: "密克罗尼西亚",
FO: "法罗群岛",
FR: "法国",
GA: "加蓬",
GB: "英国",
GD: "格林纳达",
GE: "格鲁吉亚",
GF: "法属圭亚那",
GG: "根西岛",
GH: "加纳",
GI: "直布罗陀",
GL: "格陵兰",
GM: "冈比亚",
GN: "几内亚",
GP: "瓜德罗普",
GQ: "赤道几内亚",
GR: "希腊",
GS: "南乔治亚和南桑德威奇群岛",
GT: "危地马拉",
GU: "关岛",
GW: "几内亚比绍",
GY: "圭亚那",
HK: "中国香港特别行政区",
HM: "赫德岛和麦克唐纳群岛",
HN: "洪都拉斯",
HR: "克罗地亚",
HT: "海地",
HU: "匈牙利",
ID: "印度尼西亚",
IE: "爱尔兰",
IL: "以色列",
IM: "马恩岛",
IN: "印度",
IO: "英属印度洋领地",
IQ: "伊拉克",
IR: "伊朗",
IS: "冰岛",
IT: "意大利",
JE: "泽西岛",
JM: "牙买加",
JO: "约旦",
JP: "日本",
JP1: "日本",
KE: "肯尼亚",
KG: "吉尔吉斯斯坦",
KH: "柬埔寨",
KI: "基里巴斯",
KM: "科摩罗",
KN: "圣基茨和尼维斯",
KP: "朝鲜",
KR: "韩国",
KR1: "韩国",
KR1_UXWAUDIT: "韩国",
KW: "科威特",
KY: "开曼群岛",
KZ: "哈萨克斯坦",
LA: "老挝",
LB: "黎巴嫩",
LC: "圣卢西亚",
LI: "列支敦士登",
LK: "斯里兰卡",
LR: "利比里亚",
LS: "莱索托",
LT: "立陶宛",
LU: "卢森堡",
LV: "拉脱维亚",
LY: "利比亚",
MA: "摩洛哥",
MC: "摩纳哥",
MD: "摩尔多瓦",
ME: "黑山",
MF: "圣马丁",
MG: "马达加斯加",
MH: "马绍尔群岛",
MK: "北马其顿",
ML: "马里",
MM: "缅甸",
MN: "蒙古",
MO: "中国澳门特别行政区",
MP: "北马里亚纳群岛",
MQ: "马提尼克",
MR: "毛里塔尼亚",
MS: "蒙特塞拉特",
MT: "马耳他",
MU: "毛里求斯",
MV: "马尔代夫",
MW: "马拉维",
MX: "墨西哥",
MY: "马来西亚",
MZ: "莫桑比克",
NA: "纳米比亚",
NC: "新喀里多尼亚",
NE: "尼日尔",
NF: "诺福克岛",
NG: "尼日利亚",
NI: "尼加拉瓜",
NL: "荷兰",
NO: "挪威",
NP: "尼泊尔",
NR: "瑙鲁",
NU: "纽埃",
NZ: "新西兰",
OM: "阿曼",
PA: "巴拿马",
PE: "秘鲁",
PF: "法属玻利尼西亚",
PG: "巴布亚新几内亚",
PH: "菲律宾",
PK: "巴基斯坦",
PL: "波兰",
PM: "圣皮埃尔和密克隆群岛",
PN: "皮特凯恩群岛",
PR: "波多黎各",
PS: "巴勒斯坦",
PT: "葡萄牙",
PW: "帕劳",
PY: "巴拉圭",
QA: "卡塔尔",
RE: "留尼汪",
RO: "罗马尼亚",
RS: "塞尔维亚",
RU: "俄罗斯",
RW: "卢旺达",
SA: "沙特阿拉伯",
SB: "索罗门群岛",
SC: "塞舌尔",
SD: "苏丹",
SE: "瑞典",
SG: "新加坡",
SI: "斯洛文尼亚",
SJ: "斯瓦尔巴和扬马延",
SK: "斯洛伐克",
SL: "塞拉利昂",
SM: "圣马利诺",
SN: "塞内加尔",
SO: "索马里",
SR: "苏里南",
SS: "南苏丹",
ST: "圣多美和普林西比",
SV: "萨尔瓦多",
SX: "荷属圣马丁",
SY: "叙利亚",
SZ: "斯威士兰",
TC: "特克斯和凯科斯群岛",
TD: "乍得",
TF: "法属南部领地",
TG: "多哥",
TH: "泰国",
TJ: "塔吉克斯坦",
TK: "托克劳群岛",
TL: "东帝汶",
TM: "土库曼斯坦",
TN: "突尼斯",
TO: "汤加",
TR: "土耳其",
TT: "特立尼达和多巴哥",
TV: "图瓦卢",
TW: "台湾",
TZ: "坦桑尼亚",
UA: "乌克兰",
UG: "乌干达",
UM: "美国本土外小岛屿",
US: "美国",
UY: "乌拉圭",
UZ: "乌兹别克斯坦",
VA: "梵蒂冈",
VC: "圣文森特",
VE: "委内瑞拉",
VG: "英属维尔京群岛",
VI: "美属维尔京群岛",
VN: "越南",
VN1: "越南",
VU: "瓦努阿图",
WS: "萨摩亚",
YE: "也门",
YT: "马约特岛",
ZA: "南非",
ZM: "赞比亚",
ZW: "津巴布韦"
};
export function getCountryName(code) {
return CountryCode[code] || null;
}

134
src/utils/pythonBridge.js Normal file
View File

@@ -0,0 +1,134 @@
// pythonBridge.js
import { ref, onMounted } from 'vue';
const bridge = ref(null);
// 初始化 QWebChannel
const initBridge = () => {
if (/localhost/.test(window.location.href)) return
new QWebChannel(qt.webChannelTransport, (channel) => {
bridge.value = channel.objects.bridge;
});
};
export function usePythonBridge() {
// 调用 Python 方法
const fetchDataConfig = (data) => {
return new Promise((resolve, reject) => {
if (bridge.value) {
bridge.value.fetchDataConfig(data, function (result) {
resolve(result);
});
}
});
};
// 查询获取主播的数据
const fetchDataCount = () => {
return new Promise((resolve, reject) => {
if (bridge.value) {
bridge.value.fetchDataCount(function (result) {
resolve(result);
});
}
});
};
// 打开tk后台
const loginTikTok = () => {
if (bridge.value) {
bridge.value.loginTikTok(function (result) {
});
}
};
// 登录tk后台
const loginBackStage = (data) => {
if (bridge.value) {
if (data.index == 0) {
bridge.value.loginBackStage(JSON.stringify(data));
} else if (data.index == 1) {
bridge.value.loginBackStageCopy(JSON.stringify(data));
}
}
};
//跳转到主播页面
const givePyAnchorId = (id) => {
if (bridge.value) {
bridge.value.givePyAnchorId(id, function (result) {
});
}
};
//查询登录状态
const backStageloginStatus = () => {
return new Promise((resolve, reject) => {
if (bridge.value) {
bridge.value.backStageloginStatus(function (result) {
resolve(result);
});
}
});
};
//查询登录状态
const backStageloginStatusCopy = () => {
return new Promise((resolve, reject) => {
if (bridge.value) {
bridge.value.backStageloginStatusCopy(function (result) {
resolve(result);
});
}
});
};
//导出表格
const exportToExcel = (data) => {
if (bridge.value) {
bridge.value.exportToExcel(JSON.stringify(data));
}
};
const stopScript = () => {
if (bridge.value) {
bridge.value.stopScript();
}
};
//获取版本号
const getVersion = () => {
return new Promise((resolve, reject) => {
if (bridge.value) {
bridge.value.currentVersion(function (result) {
resolve(result);
});
}
});
};
// 在组件挂载时初始化桥接
onMounted(initBridge);
return {
fetchDataConfig,
fetchDataCount,
loginBackStage,
loginTikTok,
givePyAnchorId,
backStageloginStatus,
backStageloginStatusCopy,
exportToExcel,
stopScript,
getVersion
};
}

54
src/utils/storage.js Normal file
View File

@@ -0,0 +1,54 @@
export function setToken(token) {
localStorage.setItem('token', token);
}
export function getToken() {
return localStorage.getItem('token');
}
export function removeToken() {
localStorage.removeItem('token');
}
export function setUser(user) {
localStorage.setItem('user', JSON.stringify(user));
}
export function getUser() {
return JSON.parse(localStorage.getItem('user'));
}
export function setNumData(numData) {
localStorage.setItem('num', JSON.stringify(numData));
}
export function getNumData() {
return JSON.parse(localStorage.getItem('num'));
}
// 导出一个函数,用于设置用户密码
export function setUserPass(userdata) {
localStorage.setItem('userPass', JSON.stringify(userdata));
}
// 导出一个函数,用于获取用户密码
export function getUserPass() {
return JSON.parse(localStorage.getItem('userPass'));
}
// 用于设置tk账户密码
export function setTkUser(userdata) {
localStorage.setItem('tkuser', JSON.stringify(userdata));
}
// 用于获取tk账户密码
export function getTkUser() {
return JSON.parse(localStorage.getItem('tkuser'));
}
// 用于列表筛选条件
export function setSerch(data) {
localStorage.setItem('Serch', JSON.stringify(data));
}
// 用于获取列表筛选条件
export function getSerch() {
return JSON.parse(localStorage.getItem('Serch'));
}

366
src/views/HomeView.vue Normal file
View File

@@ -0,0 +1,366 @@
<template>
<div class="main">
<div class="container">
<div class="right">
<img src="../assets/logoBg.png" class="background-video" alt="">
<!-- 设置 -->
<div class="center-align">
<div></div>
<div class="setup">
<div class="setup-item center-justify">
<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 class="center-line" style="margin-top: 40px;">
<!-- logo -->
<div class="logo">
<div class="center-justify" style="height: 80px; width: 300px;">
<!-- <img style="margin-right: 20px;height: 100%;" src="@/assets/logo.png"> -->
<img style="height: 100%;" src="@/assets/logotext.png">
</div>
</div>
<!-- From -->
<div class="from">
<div class="from-title center-justify">
<div>{{ $t('login.title') }}</div>
</div>
<div class="from-input">
<el-form label-position="left" label-width="100px" :model="formData">
<div class="from-input-item1">
<img src="@/assets/username.png" alt="">
<el-input style="height: 25px;" v-model="formData.tenantName"
:placeholder="$t('login.tenantName')" clearable @keyup.enter="onSubmit" />
</div>
<div class="from-input-item1">
<img src="@/assets/username.png" alt="">
<el-input style="height: 25px;" v-model="formData.userId"
:placeholder="$t('login.account')" clearable @keyup.enter="onSubmit" />
</div>
<div class="from-input-item1">
<img src="@/assets/password.png" alt="">
<el-input style="height: 25px; " v-model="formData.password" type="password"
:placeholder="$t('login.password')" show-password @keyup.enter="onSubmit" />
</div>
<div class="from-input-item">
<el-button class="loginButton" color="#8f7ee7" type="primary" @click="onSubmit">{{
$t('login.login') }}</el-button>
</div>
</el-form>
</div>
</div>
</div>
<div class="version center-justify ">{{ $t('login.version') }}{{ version }}</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { login, getIdByName } from '@/api/account';
import { getToken, setToken, setUser, setUserPass, getUserPass } from '@/utils/storage';
import { ElLoading } from 'element-plus';
import { usePythonBridge } from '@/utils/pythonBridge'
import { useI18n } from 'vue-i18n'
const { locale } = useI18n()
let localeData = ref(locale.value == 'zh' ? '中文' : 'English')
function switchLanguage(lang) {
console.log(localeData.value)
locale.value = lang
localStorage.setItem('lang', lang)
}
const { getVersion, stopScript } = usePythonBridge();
let version = ref('0.0.0');
onMounted(() => {
stopScript();
setTimeout(() => {
getVersion().then((res) => {
version.value = res;
})
}, 500);
})
const router = useRouter();
const formData = ref({
tenantName: getUserPass() == null ? '' : getUserPass().tenantName,
userId: getUserPass() == null ? '' : getUserPass().userId,
password: getUserPass() == null ? '' : getUserPass().password,
});
const onSubmit = () => {
const loading = ElLoading.service({
lock: true,
text: 'Loading',
background: 'rgba(0, 0, 0, 0.7)',
});
setUserPass(formData.value);
getIdByName(formData.value.tenantName).then((tenantId) => {
console.log(tenantId)
login({
tenantId: Number(tenantId),
username: formData.value.userId,
password: formData.value.password,
}).then((res) => {
loading.close();
console.log(res)
setToken(res.tokenValue);
setUser(res);
router.push('/nav');
}).catch((err) => {
loading.close();
});
})
};
</script>
<style lang="less">
.main {
width: 1600px;
height: 900px;
overflow: hidden;
box-sizing: border-box;
/* 页面无法选中 */
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
.container {
display: flex;
box-sizing: border-box;
width: 1600px;
height: 900px;
.right {
box-sizing: border-box;
position: relative;
width: 1600px;
height: 900px;
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 {
display: flex;
flex-direction: column;
align-items: center;
}
.center-justify {
display: flex;
justify-content: space-around;
align-items: center;
}
.center-align {
display: flex;
justify-content: space-between;
}
.center-flex {
display: flex;
justify-content: center;
align-items: center;
}
.el-input__wrapper {
--el-input-focus-border-color: rgba(255, 255, 0, 0);
--el-menu-hover-bg-color: rgba(255, 255, 0, 0);
}
</style>
<style scoped lang="less">
::v-deep(.el-input__wrapper) {
background-color: rgba(255, 0, 0, 0);
box-shadow: none;
}
::v-deep(.el-input__inner) {
color: #fff;
}
::v-deep(.el-input__inner::placeholder) {
color: @bg-color;
}
</style>

View File

@@ -0,0 +1,595 @@
<template>
<div class="hostList">
<div>
<div style="display: flex;">
<el-select v-model="searchForm.country" filterable :placeholder="$t('hostList.placeCountry')" size="large"
style="width: 160px">
<el-option :label="$t('hostList.selectAll')" :value="''" />
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<div></div>
<el-date-picker v-model="searchForm.createTime" type="date" value-format="YYYY-MM-DD"
:placeholder="$t('hostList.placeSeletTime')" size="large" style="margin-left: 50px;width: 160px;" />
<el-input v-model="searchForm.hostsId" :placeholder="$t('hostList.placeHostId')" size="large"
style="width: 160px; margin-left: 50px;" clearable />
<el-button class="serch-button" style="margin-left: 50px;" type="primary" @click="serch">{{
$t('hostList.query') }}</el-button>
<el-button class="put-button" :disabled="tableData.length == 0" type="primary" @click="exportList">{{
$t('hostList.export') }}</el-button>
<!-- <el-button class="put-button" type="primary" @click="dialogFormVisible = true">分配给指定员工</el-button> -->
<el-button @click="filterdialogVisible = true" style="width: 50px;" class="put-button" type="primary"><img
style="height: 30px;" src="@/assets/filter.png"></el-button>
</div>
<div class="hostTable center-justify">
<el-table ref="multipleTableRef" :data="tableData" stripe v-loading="loading" height="500"
@selection-change="handleSelectionChange">
<el-table-column type="selection" width="35" />
<el-table-column fixed prop="hostId" :label="$t('hostList.hostId')" width="160">
<template #default="scope">
<div class="hostIdText" @click="openHTML(scope.row.hostId)"> {{
scope.row.hostId }}</div>
</template>
</el-table-column>
<el-table-column prop="hostlevel" :label="$t('hostList.grade')" width="80">
<template #default="scope">
{{ scope.row.hostlevel }}
</template>
</el-table-column>
<el-table-column prop="invitationType" :label="$t('hostList.invitationType')" width="80">
<template #default="scope">
<el-tag :type="scope.row.invitationType == 1 ? 'success' : 'warning'">
{{ scope.row.invitationType == 1 ? $t('hostList.invitationType1') : $t('hostList.invitationType2') }}
</el-tag>
</template>
</el-table-column>
<el-table-column v-for="label in labelList" :key="label.paramCode" :prop="label.paramCode"
:label="label.paramCodeMeaning" width="120">
<template v-if="label.paramCode != 'createDt'" #default="scope">
</template>
</el-table-column>
<!-- <el-table-column label="操作">
<template #default="scope">
<div style="display: flex; align-items: center">
<el-button type="primary" @click="getTkhostdetail(scope.row.hostId)">查看</el-button>
</div>
</template>
</el-table-column> -->
</el-table>
</div>
<!-- 分页 -->
<div class="center-justify" style="margin-top: 30px;">
<el-pagination v-model:current-page="page" v-model:page-size="pageSize" background
layout="sizes, prev, pager, next" :total="total" :page-sizes="[10, 20, 50, 100]"
@size-change="handleSizeChange" @current-change="handleCurrentChange" />
</div>
</div>
<el-dialog v-model="filterdialogVisible" width="800px" :before-close="handleClose">
<el-row v-for="(field, index) in fields" :key="index" :gutter="20" style="margin-bottom: 10px">
<el-col :span="4">
<div style="height: 100%; padding-top: 10px" class="center-justify">
{{ field.label }}
</div>
</el-col>
<el-col :span="10">
<div><label>{{ $t('hostList.min') }}</label></div>
<el-input type="number" :oninput="'if(value.length>9)value=value.slice(0,9)'"
v-model.number="searchForm[field.minModel]" :placeholder="$t('hostList.placeMin')" />
</el-col>
<el-col :span="10">
<div><label>{{ $t('hostList.max') }}</label></div>
<el-input type="number" :oninput="'if(value.length>9)value=value.slice(0,9)'"
v-model.number="searchForm[field.maxModel]" :placeholder="$t('hostList.placeMax')" />
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="4">
<!-- <label>选择筛选条件</label> -->
<div style="height: 100%;padding-top: 10px;" class="center-justify">
{{ $t('hostList.sort') }}
</div>
</el-col>
<el-col :span="10">
<div><label>{{ $t('hostList.sortType') }}</label></div>
<el-select v-model="sortData.sortType" filterable placeholder="请选择" style="width: 240px">
<el-option v-for="item in sortNameOptions" :key="item.type" :label="item.label" :value="item.type" />
</el-select>
</el-col>
<el-col :span="10">
<div><label>{{ $t('hostList.ascending') }}/{{ $t('hostList.descending') }}</label></div>
<el-select v-model="sortData.sortForm" filterable placeholder="请选择" style="width: 240px">
<el-option
v-for="item in [{ label: $t('hostList.ascending'), value: 'asc' }, { label: $t('hostList.descending'), value: 'desc' }]"
:key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-col>
</el-row>
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="reset">
{{ $t('hostList.reset') }}
</el-button>
<!-- <el-button @click="filterdialogVisible = false">取消</el-button> -->
<el-button type="primary" @click="handelClick">
{{ $t('hostList.sure') }}
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
// import { getToken, setToken, removeToken } from '@/utils/storage'
import { tkhostdata, dicts, tkhostdetail, downList, getStaffList, managerhosts, upholdinfo, getCountryinfo, accountName } from '@/api/account';
import { usePythonBridge, } from '@/utils/pythonBridge'
import { getUser, setSerch, getSerch } from '@/utils/storage'
import { ref, reactive, onMounted } from 'vue';
// import EChartsComponent from '@/components/EChartsComponent.vue';
// import { ElMessage, ElMessageBox } from 'element-plus'
// import { color } from 'echarts';
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const loading = ref(false)
//py方法
const { givePyAnchorId, exportToExcel } = usePythonBridge();
let num = ref(0)
//账号信息
const userInfo = ref(getUser())
//主播列表DOM
const multipleTableRef = ref(null)
let labelList = ref([
{ paramCode: 'country', paramCodeMeaning: t('hostList.country') },
{ paramCode: 'createTime', paramCodeMeaning: t('hostList.creationTime') },
{ paramCode: 'hostsCoins', paramCodeMeaning: t('hostList.anchorcoins') },
{ paramCode: 'yesterdayCoins', paramCodeMeaning: t('hostList.yesterdayGoldCoins') },
{ paramCode: 'fans', paramCodeMeaning: t('hostList.fansNum') },
{ paramCode: 'fllowernum', paramCodeMeaning: t('hostList.followersNum') },
{ paramCode: 'onlineFans', paramCodeMeaning: t('hostList.onlineFans') },
{ paramCode: 'hostsKind', paramCodeMeaning: t('hostList.anchorType') },
// { paramCode: 'invitationType', paramCodeMeaning: t('hostList.invitationType') },
]);
const tableData = ref([])
//主播列表传参
const searchForm = ref({})
const fields = [
{ label: t('hostList.fansNum'), minModel: 'fansMin', maxModel: 'fansMax' },
{ label: t('hostList.onlineFans'), minModel: 'onlineFansMin', maxModel: 'onlineFansMax' },
{ label: t('hostList.anchorcoins'), minModel: 'hostsCoinsMin', maxModel: 'hostsCoinsMax' },
{ label: t('hostList.followersNum'), minModel: 'fllowernumMin', maxModel: 'fllowernumMax' },
]
//排序
let sortData = ref({ sortForm: 'desc', sortType: "createTime" })
//排序类型
let sortNameOptions = ref([
{ label: t('hostList.creationTime'), type: 'createTime' },
{ label: t('hostList.anchorcoins'), type: 'hostsCoins' },
{ label: t('hostList.fansNum'), type: 'fans' },
{ label: t('hostList.yesterdayGoldCoins'), type: 'yesterdayCoins' },
{ label: t('hostList.onlineFans'), type: 'onlineFans' },
{ label: t('hostList.followersNum'), type: 'fllowernum' },
])
//员工选择列表
let staffOptions = ref([])
//筛选条件选择列表
//选择的员工
let staffValue = ref('')
//选择的主播列表
let selectHostList = ref([])
//分配弹窗是否弹出
let dialogFormVisible = ref(false)
//分配情况弹窗是否弹出
let hostNameVisible = ref(false)
//备注弹窗是否弹出
let commentVisible = ref(false)
//筛选弹窗是否弹出
let filterdialogVisible = ref(false)
//分配的员工
let staffId = ref({})
//备注信息
let commentInfo = ref('')
//备注信息主播
let commentHost = ref('')
//分页
let pageSize = ref(10)
let page = ref(1)
let total = ref(0)
//是否渲染
const isPopoverVisible = reactive({})
let options = ref([])
let version = ref('0.0.0');
onMounted(() => {
getCountry(); //获取国家
// getSerchStorage();//获取搜索条件
getlist();//获取主播列表
})
function serch() {
page.value = 1
console.log(t('hostList.yesterdayGoldCoins'))
getlist();
}
function exportList() {
if (searchForm.value.dataType == 'InvitationType') {
searchForm.value.dataEnd = searchForm.value.dataStart
}
exportToExcel({})
// //浏览器导出方法
// downList('export/hostsinfo',
// {
// searchTime: searchForm.value.time,
// region: searchForm.value.country,
// pageSize: pageSize.value,
// page: page.value,
// userId: userInfo.value.userId,
// }
// );
}
//分页每页条数
function handleSizeChange(val) {
console.log(`${val} items per page`)
getlist();
}
//分页页数
function handleCurrentChange(val) {
console.log(`current page: ${val}`)
getlist();
}
//选择行
function handleSelectionChange(data) {
console.log(data)
selectHostList.value = []
data.forEach(item => {
selectHostList.value.push(item.hostId)
})
// multipleTableRef.value = data
// console.log(multipleTableRef.value)
}
//获取主播列表
const getlist = () => {
loading.value = true
console.log(searchForm.value)
tkhostdata({
tenantId: Number(userInfo.value.tenantId),
sort: sortData.value.sortForm,//正序倒序
sortName: sortData.value.sortType,//排序类型
"current": page.value,
"pageSize": pageSize.value,
...searchForm.value,//筛选条件
}).then(res => {
loading.value = false
if (res) {
console.log('主播列表', res)
total.value = Number(res.total)
tableData.value = res.records.map(item => ({
hostId: item.hostsId, // 注意:原字段是 hostId你的数据是 hostsId需手动映射
hostlevel: item.hostsLevel, // 原字段 hostlevel 对应你的数据 hostsLevel
country: item.country,
createTime: item.createTime,
fans: item.fans,
fllowernum: item.fllowernum,
hostsCoins: item.hostsCoins,
hostsKind: item.hostsKind,
onlineFans: item.onlineFans,
yesterdayCoins: item.yesterdayCoins,
// 保留原有字段(如 belongBy、useable 等)
belongBy: item.belongBy,
useable: item.useable,
invitationType: item.invitationType,
}));
}
})
}
function handelClick() {
filterdialogVisible.value = false
}
function reset() {
searchForm.value.fansMin = null
searchForm.value.fansMax = null
searchForm.value.onlineFansMin = null
searchForm.value.onlineFansMax = null
searchForm.value.hostsCoinsMin = null
searchForm.value.hostsCoinsMax = null
searchForm.value.fllowernumMin = null
searchForm.value.fllowernumMax = null
}
function handleClose(done) {
console.log('关闭')
// searchForm.value = {
// dataType: '',
// dataStart: '',
// dataEnd: '',
// }
done()
}
//修改主播维护状态
// function handleSelectChange(event, data) {
// upholdinfo({
// "hostId": data.hostId,
// "userId": userInfo.value.userId,
// "tenantId": userInfo.value.tenantId,
// // "comment": "我已经尽力维护,但是失败了",
// "useable": event.target.value
// }).then(res => {
// console.log(res)
// })
// }
//更改主播维护备注
// function uphostcomment() {
// upholdinfo({
// "hostId": commentHost.value,
// "userId": userInfo.value.userId,
// "tenantId": userInfo.value.tenantId,
// "comment": commentInfo.value,
// }).then(res => {
// console.log(res)
// serch()
// commentVisible.value = false
// })
// }
function filterTag(value, row) {
console.log(row.useable, value)
return row.useable === value;
}
//获取国家
function getCountry() {
getCountryinfo({}).then(res => {
console.log(res)
res.forEach(item => {
if (item.countryGroupName) {
options.value.push({ value: item.countryGroupName, label: item.countryGroupName })
}
})
console.log(options.value)
}).catch(err => {
console.log('getCountry', err)
})
}
//获取下级员工
// const getStaff = () => {
// getStaffList({
// userId: userInfo.value.userId,
// activeYn: userInfo.value.activeYn,
// tenantId: userInfo.value.tenantId,
// }).then(res => {
// console.log(res)
// res.forEach(item => {
// staffOptions.value.push({
// value: item.userId,
// label: item.userName
// })
// })
// })
// }
// //分配主播给员工
// function allocation() {
// managerhosts({
// "manaId": userInfo.value.userId,
// "userId": staffValue.value,
// "tenantId": userInfo.value.tenantId,
// "hostIds": selectHostList.value
// }).then(res => {
// if (res) {
// dialogFormVisible.value = false
// }
// })
// }
//获取主播信息
// const getTkhostdetail = (id) => {
// tkhostdetail({
// hostId: id,
// // page: 1,
// searchTimeStart: '20250401',
// searchTimeEnd: '20250403'
// }).then(res => {
// console.log(labelList.value)
// })
// }
function openPopover(hostId, paramCode) {
isPopoverVisible[`${hostId}-${paramCode}`] = true;
}
function closePopover(hostId, paramCode) {
// isPopoverVisible[`${hostId}-${paramCode}`] = false;
}
function openHTML(id) {
givePyAnchorId(id)
// upholdinfo({
// "hostId": id,
// "userId": userInfo.value.userId,
// "tenantId": userInfo.value.tenantId,
// // "comment": "我已经尽力维护,但是失败了",
// "useable": "Y"
// }).then(res => {
// getlist();
// })
}
// function openAccountName(idStr) {
// if (idStr) {
// hostNameVisible.value = true
// accountName(idStr).then(res => {
// staffId.value = JSON.stringify(res).replace(/[{}"]/g, '') // 移除所有 {} 和 "
// .split(',') // 按逗号分割成数组
// console.log(res)
// })
// }
// }
</script>
<style lang="less">
.hostList {
box-sizing: border-box;
// height: 100vh;
padding: 40px;
/* 页面无法选中 */
// -webkit-user-select: none;
// -moz-user-select: none;
// -ms-user-select: none;
// user-select: none;
.hostTable {
width: 100%;
padding: 40px 0;
.hostIdText {
text-decoration: underline;
cursor: pointer;
}
}
.serch-button {
width: 80px;
height: 47px;
background: @btn-bg-color;
border-radius: 10px;
border: none;
}
.put-button {
width: 132px;
height: 47px;
background: @btn-bg-color;
border-radius: 10px;
border: none;
}
}
.el-dialog {
--el-dialog-font-line-height: 50px;
--el-dialog-width: 600px;
--el-dialog-border-radius: 8px;
// border: 10px solid @bg-color-light;
}
.center-line {
display: flex;
flex-direction: column;
align-items: center;
// justify-content: center;
}
.center-justify {
display: flex;
justify-content: space-around;
align-items: center;
}
.center-align {
display: flex;
justify-content: space-between;
}
.center-flex {
display: flex;
justify-content: center;
align-items: center;
}
</style>
<style scoped lang="less">
::v-deep(.el-input__wrapper) {
background-color: #F2FAF9;
border: 1px solid @bg-color;
height: 44px;
}
::v-deep(.el-select__wrapper) {
background-color: #F2FAF9;
border: 1px solid @bg-color;
height: 48px;
}
::v-deep(.el-pagination.is-background .el-pager li.is-active) {
background-color: @bg-color;
}
</style>

View File

@@ -0,0 +1,765 @@
<template>
<div class="center-line workbenches">
<div class="center-align" style="width: 100%; margin: 0 20px;">
<div class="box-card-num1 center-line">
<div>{{ $t('workbenches.totalnumber') }}: <span>{{ hostData.totalCount }}</span></div>
<div>{{ $t('workbenches.createHost') }}: <span>{{ hostData.validAnchorsCount }}</span></div>
<div> {{ $t('workbenches.query') }}: <span>{{ hostData.checkedDataCount }}</span></div>
<div>{{ $t('workbenches.invite') }}: <span>{{ hostData.canInvitationCount }}</span></div>
<div>{{ $t('workbenches.runTime') }}: <span>{{ formattedTime }}</span></div>
</div>
<div class="center-line" style="padding-top: 15vh;">
<el-button class="open-login" type="primary" @click="openTK">{{ $t('workbenches.openTK') }}</el-button>
<!-- <el-button class="open-login" type="primary" @click="startTimer">计时开始</el-button> -->
</div>
<div>
<el-card class="box-card-num" v-for="(item, index) in 2" :key="index">
<div class="center-justify">
<div class="from-input-item">
<div class="from-input-item-title center-justify">
{{ $t('workbenches.guildAccount') }}
</div>
<el-input :disabled="!(tkData[index].code == 0 && !isLogin[index])"
v-model="tkData[index].account" :placeholder="$t('workbenches.guildAccountPlace')"
clearable />
</div>
<div class="from-input-item">
<div class="from-input-item-title center-justify">
{{ $t('workbenches.guildPass') }}
</div>
<el-input :disabled="!(tkData[index].code == 0 && !isLogin[index])"
v-model="tkData[index].password" type="password"
:placeholder="$t('workbenches.guildPassPlace')" show-password />
</div>
<el-button class="open-login" style="margin-left: 60px;"
:disabled="!(tkData[index].code == 0 && !isLogin[index])" type="primary"
@click="loginTK(index)">{{ $t('workbenches.loginBackend') }}</el-button>
<div v-if="tkData[index].code == 0" class="loginState"></div>
<div v-if="tkData[index].code == 1" class="loginState" style="background-color: green;"></div>
</div>
<div class="todayCount"> {{ $t('workbenches.queriedNum') }}{{ tkData[index].num }}</div>
</el-card>
</div>
</div>
<div class="container ">
<el-card class="box-card">
<template #header>
<div class="card-header">
<span class="center-justify"><img src="@/assets/worklogo.png">
{{ $t('workbenchesSetup.workbenches') }} </span>
<div style="margin-right: 120px;">{{ $t('workbenchesSetup.workbenches') }}:{{
locale == 'zh' ? countryData : countryDataEN }}
<!-- <el-button class="reset-button" @click="reset">重置数据</el-button> -->
</div>
</div>
</template>
<el-row :gutter="20">
<el-col :span="8">
<div class="input-group">
<label>{{ $t('workbenchesSetup.setCoinsNum') }}</label>
<el-input type='number' v-model="pyData.gold.min" :min="0" :max="pyData.gold.max - 1"
:placeholder="$t('workbenchesSetup.minCoinsNum')" style="width: 100%"
:disabled="!pyData.isStart">
<template #prepend>{{ $t('workbenchesSetup.minCoinsNum') }}</template>
</el-input>
<el-input type='number' v-model="pyData.gold.max" :min="pyData.gold.min + 1" :max="100"
:placeholder="$t('workbenchesSetup.maxCoinsNum')" style="width: 100%; margin-top: 10px"
:disabled="!pyData.isStart">
<template #prepend>{{ $t('workbenchesSetup.maxCoinsNum') }}</template>
</el-input>
</div>
</el-col>
<el-col :span="8">
<div class="input-group">
<label>{{ $t('workbenchesSetup.setFansNum') }}</label>
<el-input type='number' v-model="pyData.fans.min" :min="0" :max="pyData.fans.max - 1"
:placeholder="$t('workbenchesSetup.minFansNum')" style="width: 100%"
:disabled="!pyData.isStart">
<template #prepend>{{ $t('workbenchesSetup.minFansNum') }}</template>
</el-input>
<el-input type='number' v-model="pyData.fans.max" :min="pyData.fans.min + 1" :max="100"
:placeholder="$t('workbenchesSetup.maxFansNum')" style="width: 100%; margin-top: 10px"
:disabled="!pyData.isStart">
<template #prepend>{{ $t('workbenchesSetup.maxFansNum') }}</template>
</el-input>
</div>
</el-col>
<el-col :span="8">
<div class="input-group">
<label>{{ $t('workbenchesSetup.setQuery') }}</label>
<!-- <el-input type='number' v-model="pyData.frequency.hour" @input="handleInputHour" -->
<el-input type='number' v-model="pyData.frequency.hour"
:placeholder="$t('workbenchesSetup.hour')" style="width: 100%"
:disabled="!pyData.isStart">
<template #append>{{ $t('workbenchesSetup.hour') }}</template>
</el-input>
<!-- <el-input type='number' v-model="pyData.frequency.day" @input="handleInputDay" -->
<el-input type='number' v-model="pyData.frequency.day"
:placeholder="$t('workbenchesSetup.hour24')" style="width: 100%; margin-top: 10px"
:disabled="!pyData.isStart">
<template #append>{{ $t('workbenchesSetup.hour24') }}</template>
</el-input>
</div>
</el-col>
</el-row>
<div style="margin-top: 20px; text-align: center">
<el-button class="submit-button" :disabled="submitting" v-show="pyData.isStart" type="primary"
@click="submit">{{
$t('workbenchesSetup.start') }}</el-button>
<el-button v-show="!pyData.isStart" type="danger" @click="unsubmit">{{
$t('workbenchesSetup.stop') }}</el-button>
</div>
</el-card>
</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 } 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 getHostTimer = ref(null);
//获取查询次数定时器
let getNumTimer = ref(null);
//获取的主播信息
let hostData = ref({
totalCount: 0,
validAnchorsCount: 0,
canInvitationCount: 0,
checkedDataCount: 0,
});
//是否开启tk
// let isTk = ref(true);
//账号是否登陆中
let isLogin = ref([false, false]);
//设置状态轮询定时器
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,
test: '123',
test1: { test: 123, test12: 123 },
tenantId: getUser().tenantId,
userId: getUser().userId,
});
//按钮提交状态
let submitting = ref(true);
onMounted(() => {
//从缓存获取数据
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()
})
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);
} catch (error) {
console.error('请求出错:', error);
ElMessageBox.prompt('请输入将要获取国家的中文名', '获取国家失败', {
confirmButtonText: '确认',
cancelButtonText: '取消',
showClose: false,
closeOnClickModal: false,
showCancelButton: false,
})
.then(({ value }) => {
countryData.value = value
})
// .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;
}
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,
country: countryData.value,
tenantId: getUser().tenantId,
userId: getUser().userId,
})).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,
country: countryData.value,
tenantId: getUser().tenantId,
userId: getUser().userId,
})).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 };
};
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;
tkaccountuseinfo(id).then((res) => {
if (res) {
num = res
tkData.value[index].num = num
console.log('账号使用次数', tkData.value[index].num)
}
}).catch((err) => {
console.log('账号使用次数', err)
})
}
const isRunning = ref(false);
const totalSeconds = ref(0);
//定时器
let timerCrawl = null;
const startTimedata = ref(null);
//清空时间 并开始运行
const startTimer = () => {
resetTimer();
if (isRunning.value) return;
isRunning.value = true;
startTimedata.value = Date.now();
timerCrawl = setInterval(() => {
totalSeconds.value = Math.floor((Date.now() - startTimedata.value) / 1000);
}, 1000);
};
//结束运行 暂停
const pauseTimer = () => {
isRunning.value = false;
clearInterval(timerCrawl);
};
//清空时间
const resetTimer = () => {
isRunning.value = false;
clearInterval(timerCrawl);
totalSeconds.value = 0;
};
// 格式化时间为 HH:MM:SS
const formattedTime = computed(() => {
const hours = Math.floor(totalSeconds.value / 3600);
const minutes = Math.floor((totalSeconds.value % 3600) / 60);
const seconds = totalSeconds.value % 60;
return [
hours.toString().padStart(2, '0'),
minutes.toString().padStart(2, '0'),
seconds.toString().padStart(2, '0')
].join(':');
});
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;
}
</script>
<style scoped lang="less">
.container {
margin: 0 auto;
}
.workbenches {
padding: 45px 29px 22px 27px;
/* 页面无法选中 */
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.box-card {
// width: 1240px;
height: 436px;
background: #FFFFFF;
box-shadow: 0px 0px 21px 0px rgba(183, 183, 183, 0.33);
border-radius: 24px;
}
.box-card-num1 {
width: 197px;
height: 321px;
background: #FFFFFF;
box-shadow: 0px 0px 21px 0px rgba(183, 183, 183, 0.33);
border-radius: 24px;
// padding-top: 60px;
box-sizing: border-box;
div {
height: 20%;
display: flex;
justify-content: space-around;
align-items: center;
color: #8D8E8E;
span {
color: #000;
padding-left: 10px;
}
}
}
.box-card-num {
width: 897px;
height: 145px;
background: #FFFFFF;
box-shadow: 0px 0px 21px 0px rgba(183, 183, 183, 0.33);
border-radius: 24px;
margin-bottom: 30px;
padding-top: 18px;
box-sizing: border-box;
.todayCount {
padding: 15px 21px;
font-size: 14px;
}
}
.from-input-item {
display: flex;
.from-input-item-title {
color: #000000;
font-size: 14px;
font-weight: 500;
width: 100px;
height: 50px;
}
.loginButton {
width: 100%;
height: 40px;
color: #ffffff;
font-size: 16px;
}
}
.loginState {
width: 15px;
height: 15px;
border-radius: 50%;
background-color: #b90000;
margin-left: 15px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
span {
font-family: Source Han Sans SC;
font-weight: 400;
font-size: 20px;
color: #2D2727;
line-height: 37px;
img {
margin-right: 16px;
}
}
}
.input-group {
margin-bottom: 20px;
.el-input {
margin: 22px 0;
}
}
label {
display: block;
margin-bottom: 8px;
font-weight: bold;
color: #606266;
}
.open-login {
width: 100px;
height: 47px;
background: @btn-bg-color;
border-radius: 10px;
border: none;
}
.reset-button {
width: 132px;
height: 47px;
background: @btn-bg-color;
border-radius: 10px;
font-family: Source Han Sans SC;
font-weight: 400;
font-size: 18px;
color: #FFFFFF;
margin-left: 50px;
}
.submit-button {
width: 160px;
height: 47px;
background: @bg-color;
border-radius: 10px;
}
.center-line {
display: flex;
flex-direction: column;
align-items: center;
// justify-content: center;
}
.center-justify {
display: flex;
justify-content: space-around;
align-items: center;
}
.center-align {
display: flex;
justify-content: space-between;
}
.center-flex {
display: flex;
justify-content: center;
align-items: center;
}
</style>
<style scoped lang="less">
::v-deep(.el-input-group__prepend) {
background: @bg-color-light;
border-radius: 10px 0px 0px 10px;
border: 1px solid #B7CEC5;
font-family: Source Han Sans SC;
font-weight: 400;
font-size: 18px;
color: #FFFFFF;
line-height: 37px;
}
::v-deep(.el-input-group__append) {
background: @bg-color-light;
border-radius: 0px 10px 10px 0px;
border: 1px solid #B7CEC5;
font-family: Source Han Sans SC;
font-weight: 400;
font-size: 18px;
color: #FFFFFF;
line-height: 37px;
}
::v-deep(.el-input__wrapper) {
width: 218px;
height: 44px;
}
.el-input {
width: 200px;
height: 48px;
background: #FFFFFF;
border-radius: 10px;
border: 1px solid #B7CEC5;
}
</style>

89
src/views/nav.vue Normal file
View File

@@ -0,0 +1,89 @@
<template>
<div class="app-container">
<Sidebar class="noneText" @activeIndex="activeIndexFn" />
<div class="content ">
<div v-show="activeIndexA == 1">
<workbenches />
</div>
<div v-show="activeIndexA == 2">
<hostsList />
</div>
<div style="position: absolute; bottom: 0; right: 0;">{{ version }}</div>
</div>
</div>
</template>
<script setup>
import Sidebar from '../components/Sidebar.vue';
import { RouterLink, RouterView } from 'vue-router'
import hostsList from '@/views/hosts/hostsList.vue'
import workbenches from '@/views/hosts/workbenches.vue'
import { ref } from 'vue'
import { getUser } from '@/utils/storage'
// import { usePythonBridge } from '@/utils/pythonBridge'
let activeIndexA = ref(1)
function activeIndexFn(data) {
activeIndexA.value = data
console.log(data)
}
</script>
<style lang="less">
body,
html {
margin: 0;
padding: 0;
height: 100%;
}
.app-container {
display: flex;
width: 1600px;
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;
}
</style>

27
vue.config.js Normal file
View File

@@ -0,0 +1,27 @@
const { defineConfig } = require('@vue/cli-service');
module.exports = defineConfig({
transpileDependencies: true,
css: {
loaderOptions: {
postcss: {
postcssOptions: {
plugins: [
require('postcss-px-to-viewport')({
viewportWidth: 1600, // 视窗的宽度,对应设计稿宽度
viewportHeight: 900, // 视窗的高度,对应设计稿高度
unitPrecision: 3, // 指定 px 转换为视窗单位值的小数位数
viewportUnit: 'vw', // 指定需要转换成的视窗单位vw 或者 vh
selectorBlackList: ['.ignore', '.hairlines'], // 指定不需要转换的类
minPixelValue: 1, // 小于或等于 1 px 不转换为视窗单位
mediaQuery: false // 允许在媒体查询中转换 px
})
]
}
},
less: {
additionalData: `@import "@/static/css/app.less";` // 注入全局变量文件
}
}
}
});