Compare commits

1 Commits

Author SHA1 Message Date
ccd7759669 优化 2026-02-12 19:42:37 +08:00
9 changed files with 207 additions and 1478 deletions

View File

@@ -76,6 +76,9 @@ export default {
specifyStart: 'Start', specifyStart: 'Start',
enterRoomIds: 'Enter room IDs, separate multiple IDs with Enter key', enterRoomIds: 'Enter room IDs, separate multiple IDs with Enter key',
enterRoomId: 'Please enter room ID', enterRoomId: 'Please enter room ID',
currentCount: 'Current',
countSeparator: '/',
countUnit: 'items',
// 网络问题弹窗 // 网络问题弹窗
networkFailed: networkFailed:

View File

@@ -59,6 +59,9 @@ export default {
specifyStart: '开始', specifyStart: '开始',
networkFailed: '网络连接失败,无法访问网络,请查看网络设置。', networkFailed: '网络连接失败,无法访问网络,请查看网络设置。',
enterRoomIds: '请输入直播间id多个id用回车键隔开', enterRoomIds: '请输入直播间id多个id用回车键隔开',
currentCount: '当前',
countSeparator: '条 / 最大',
countUnit: '条',
// ==== 新增:表格列、排序使用 ==== // ==== 新增:表格列、排序使用 ====
userId: '用户id', userId: '用户id',

View File

@@ -69,15 +69,13 @@
// 玻璃态组件 Glass Components // 玻璃态组件 Glass Components
// ============================================ // ============================================
// 玻璃态头部卡片 // 玻璃态头部卡片 - 简化版(移除 backdrop-filter 提升性能)
.glass-header { .glass-header {
background: rgba(255, 255, 255, 0.85); background: #ffffff;
backdrop-filter: blur(12px); border: 1px solid #e8ecf0;
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.9);
border-radius: @radius-3xl; border-radius: @radius-3xl;
padding: 28px 32px; padding: 28px 32px;
box-shadow: @shadow-glass; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
} }
// 玻璃态内容卡片 // 玻璃态内容卡片
@@ -111,20 +109,11 @@
gap: 8px; gap: 8px;
&:hover { &:hover {
filter: brightness(1.1); filter: brightness(1.05);
box-shadow: @shadow-cyber-glow;
transform: translateY(-1px);
} }
&:active { &:active {
transform: translateY(0); opacity: 0.9;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
filter: none;
transform: none;
} }
} }
@@ -147,12 +136,6 @@
&:hover { &:hover {
background: @tech-gray-50; background: @tech-gray-50;
border-color: @tech-gray-300; border-color: @tech-gray-300;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
&:active {
transform: translateY(0);
} }
} }
@@ -473,9 +456,7 @@
padding: 0 24px !important; padding: 0 24px !important;
&:hover { &:hover {
filter: brightness(1.1); filter: brightness(1.05);
box-shadow: @shadow-cyber-glow !important;
transform: translateY(-1px);
} }
&:active { &:active {
@@ -505,7 +486,6 @@
&:hover { &:hover {
background: @tech-gray-50 !important; background: @tech-gray-50 !important;
border-color: @tech-gray-300 !important; border-color: @tech-gray-300 !important;
transform: translateY(-1px);
} }
} }
} }

26
src/utils/tenantConfig.js Normal file
View File

@@ -0,0 +1,26 @@
/**
* 租户配置管理
* 用于统一管理特定租户的差异化配置
*/
// 直播间ID限制配置
// key: tenantId (租户ID)
// value: limit (最大行数限制)
export const TENANT_LIMIT_CONFIG = {
'12741': 5000,
'12348': 5000,
// 在此处添加更多租户ID和对应的限制
// '10001': 1000,
}
export const DEFAULT_LIMIT = 50
/**
* 获取租户的直播间ID限制数量
* @param {string|number} tenantId 租户ID
* @returns {number} 限制数量
*/
export const getTenantLimit = (tenantId) => {
if (!tenantId) return DEFAULT_LIMIT
return TENANT_LIMIT_CONFIG[String(tenantId)] || DEFAULT_LIMIT
}

View File

@@ -1,77 +0,0 @@
<template>
<div class="home">
<!-- <div id="main" class="main"></div> -->
<div class="noData">
暂无数据
</div>
</div>
</template>
// <script setup>
// import { ref, watch, onMounted, onUpdated, onUnmounted } from "vue";
// const refname = ref('');
// const echarts = require('echarts/lib/echarts');
// require('echarts/lib/component/grid');
// require('echarts/lib/chart/bar');
// const initChart = () => {
// var chartDom = document.getElementById('main');
// var myChart = echarts.init(chartDom);
// var option = {
// xAxis: {
// type: 'category',
// data: ['用户量', '123', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
// },
// yAxis: {
// type: 'value'
// },
// series: [
// {
// data: [120, 200, 150, 80, 70, 110, 130],
// type: 'bar'
// }
// ]
// };
// myChart.setOption(option);
// };
// watch(refname, (newQuestion) => {
// // 变化后执行
// // 如果 refname 的变化影响图表数据,可以在这里更新 option 并调用 myChart.setOption(option)
// });
// onMounted(() => {
// initChart();
// });
// onUpdated(() => {
// // 组件更新后执行
// // 如果需要根据更新重新初始化图表或更新数据,可以在这里添加逻辑
// });
// onUnmounted(() => {
// // 组件销毁前执行
// // 如果需要销毁图表实例以释放资源,可以在这里添加逻辑
// });
</script>
<style scoped>
.home {
width: 100%;
height: 100%;
background-color: #ffffff;
border-radius: 10px;
display: flex;
justify-content: center;
align-items: center;
}
.noData{
font-size: 20px;
color: #999999;
}
.main{
width: 40%;
height: 40%;
}
</style>

View File

@@ -6,12 +6,8 @@
<div class="filter-row primary-filters"> <div class="filter-row primary-filters">
<!-- 左侧筛选控件 --> <!-- 左侧筛选控件 -->
<div class="filter-group flex-start gap-4"> <div class="filter-group flex-start gap-4">
<el-checkbox <el-checkbox class="cyber-checkbox" v-model="queryFormData.isFilter"
class="cyber-checkbox" :label="t('hostsList.filterPrivateUsers')" size="large" />
v-model="queryFormData.isFilter"
:label="t('hostsList.filterPrivateUsers')"
size="large"
/>
<div class="input-group"> <div class="input-group">
<label class="input-label"> <label class="input-label">
@@ -19,21 +15,11 @@
{{ t('hostsList.coins') }} {{ t('hostsList.coins') }}
</label> </label>
<div class="input-pair"> <div class="input-pair">
<el-input <el-input v-model="queryFormData.coinMin" :placeholder="t('hostsList.minCoins')" size="large"
v-model="queryFormData.coinMin" type="number" :disabled="streamdialogVisibletext || isRunnings" />
:placeholder="t('hostsList.minCoins')"
size="large"
type="number"
:disabled="streamdialogVisibletext || isRunnings"
/>
<span class="input-divider">/</span> <span class="input-divider">/</span>
<el-input <el-input v-model="queryFormData.coinMax" :placeholder="t('hostsList.maxCoins')" size="large"
v-model="queryFormData.coinMax" type="number" :disabled="streamdialogVisibletext || isRunnings" />
:placeholder="t('hostsList.maxCoins')"
size="large"
type="number"
:disabled="streamdialogVisibletext || isRunnings"
/>
</div> </div>
</div> </div>
@@ -43,33 +29,32 @@
{{ t('hostsList.level') }} {{ t('hostsList.level') }}
</label> </label>
<div class="input-pair"> <div class="input-pair">
<el-input <el-input v-model="queryFormData.levelMin" :placeholder="t('hostsList.minLevel')" size="large"
v-model="queryFormData.levelMin" type="number" :disabled="streamdialogVisibletext || isRunnings" />
:placeholder="t('hostsList.minLevel')"
size="large"
type="number"
:disabled="streamdialogVisibletext || isRunnings"
/>
<span class="input-divider">/</span> <span class="input-divider">/</span>
<el-input <el-input v-model="queryFormData.levelMax" :placeholder="t('hostsList.maxLevel')" size="large"
v-model="queryFormData.levelMax" type="number" :disabled="streamdialogVisibletext || isRunnings" />
:placeholder="t('hostsList.maxLevel')"
size="large"
type="number"
:disabled="streamdialogVisibletext || isRunnings"
/>
</div> </div>
</div> </div>
</div> </div>
<div class="info-pill" style="margin-top: 15px;">
<span class="info-label" style="font-size: 16px">{{ t('hostsList.runningTime') }}:</span>
<span class="info-value mono" style="font-size: 16px">
{{ String(hourstuo).padStart(2, '0') }}:{{
String(minutestuo).padStart(2, '0')
}}:{{ String(secondstuo).padStart(2, '0') }}
</span>
<span class="info-divider"></span>
<span class="info-label" style="font-size: 16px">{{ t('hostsList.total') }}:</span>
<span class="info-value" style="font-size: 16px">{{ getBrotherInfodata.total }}</span>
<span class="info-divider"></span>
<span class="info-label" style="font-size: 16px">{{ t('hostsList.valid') }}:</span>
<span class="info-value primary" style="font-size: 16px">{{ getBrotherInfodata.valid }}</span>
</div>
<!-- 右侧操作按钮 --> <!-- 右侧操作按钮 -->
<div class="action-group flex-start gap-3"> <div class="action-group flex-start gap-3">
<el-button <el-button @click="streamdialogVisible = true" :disabled="isRunnings" class="cyber-btn-primary"
@click="streamdialogVisible = true" type="primary">
:disabled="isRunnings"
class="cyber-btn-primary"
type="primary"
>
<span class="btn-icon">📍</span> <span class="btn-icon">📍</span>
{{ {{
streamdialogVisibletext streamdialogVisibletext
@@ -78,21 +63,11 @@
}} }}
</el-button> </el-button>
<el-button <el-button v-show="queryFormData.isRunning" class="cyber-btn-success" type="primary" @click="getBigBrother">
v-show="queryFormData.isRunning"
class="cyber-btn-success"
type="primary"
@click="getBigBrother"
>
{{ t('hostsList.start') }} {{ t('hostsList.start') }}
</el-button> </el-button>
<el-button <el-button v-show="!queryFormData.isRunning" class="cyber-btn-danger" type="primary" @click="BigBrotherstop">
v-show="!queryFormData.isRunning"
class="cyber-btn-danger"
type="primary"
@click="BigBrotherstop"
>
{{ t('hostsList.end') }} {{ t('hostsList.end') }}
</el-button> </el-button>
</div> </div>
@@ -104,28 +79,13 @@
<!-- 第二行搜索和操作 --> <!-- 第二行搜索和操作 -->
<div class="filter-row secondary-filters"> <div class="filter-row secondary-filters">
<div class="filter-group flex-start gap-4"> <div class="filter-group flex-start gap-4">
<el-select <el-select v-model="searchForm.region" filterable :placeholder="t('hostsList.selectCountry')" size="large"
v-model="searchForm.region" class="cyber-select">
filterable <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
:placeholder="t('hostsList.selectCountry')"
size="large"
class="cyber-select"
>
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select> </el-select>
<el-input <el-input v-model="searchForm.displayId" :placeholder="t('hostsList.bigBrotherId')" size="large"
v-model="searchForm.displayId" class="cyber-input-search" clearable>
:placeholder="t('hostsList.bigBrotherId')"
size="large"
class="cyber-input-search"
clearable
>
<template #prefix> <template #prefix>
<span class="input-prefix-icon">@</span> <span class="input-prefix-icon">@</span>
</template> </template>
@@ -141,27 +101,17 @@
{{ t('hostsList.reset') }} {{ t('hostsList.reset') }}
</el-button> </el-button>
<el-button <el-button class="ghost-btn" :disabled="tableData.length == 0" @click="exportList">
class="ghost-btn"
:disabled="tableData.length == 0"
@click="exportList"
>
<span class="btn-icon">📥</span> <span class="btn-icon">📥</span>
{{ t('hostsList.exportExcel') }} {{ t('hostsList.exportExcel') }}
</el-button> </el-button>
<el-button <el-button @click="filterdialogVisible = true" class="ghost-btn">
@click="filterdialogVisible = true"
class="ghost-btn"
>
<span class="btn-icon"></span> <span class="btn-icon"></span>
{{ t('hostsList.moreFilters') }} {{ t('hostsList.moreFilters') }}
</el-button> </el-button>
<el-button <el-button class="ghost-btn" @click="openTikTok">
class="ghost-btn"
@click="openTikTok"
>
<span class="btn-icon">🎵</span> <span class="btn-icon">🎵</span>
{{ t('hostsList.openTikTok') }} {{ t('hostsList.openTikTok') }}
</el-button> </el-button>
@@ -172,20 +122,10 @@
<div class="info-pill"> <div class="info-pill">
<span class="info-label">{{ t('hostsList.currentNetwork') }}:</span> <span class="info-label">{{ t('hostsList.currentNetwork') }}:</span>
<span class="info-value highlight">{{ countryData }}</span> <span class="info-value highlight">{{ countryData }}</span>
<span class="info-divider"></span>
<span class="info-label">{{ t('hostsList.runningTime') }}:</span>
<span class="info-value mono">
{{ String(hourstuo).padStart(2, '0') }}:{{
String(minutestuo).padStart(2, '0')
}}:{{ String(secondstuo).padStart(2, '0') }}
</span>
<span class="info-divider"></span>
<span class="info-label">{{ t('hostsList.total') }}:</span>
<span class="info-value">{{ getBrotherInfodata.total }}</span>
<span class="info-divider"></span>
<span class="info-label">{{ t('hostsList.valid') }}:</span>
<span class="info-value primary">{{ getBrotherInfodata.valid }}</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
@@ -193,57 +133,27 @@
<!-- 表格区域 - 卡片设计 --> <!-- 表格区域 - 卡片设计 -->
<div class="table-container glass-card cyber-table"> <div class="table-container glass-card cyber-table">
<div class="table-wrapper"> <div class="table-wrapper">
<el-table <el-table ref="multipleTableRef" :data="tableData" stripe v-loading="loading" height="100%"
ref="multipleTableRef" @cell-dblclick="handleCellDbClick" @selection-change="handleSelectionChange">
:data="tableData" <el-table-column fixed prop="displayId" :label="t('hostsList.id')" min-width="120">
stripe
v-loading="loading"
height="100%"
@cell-dblclick="handleCellDbClick"
@selection-change="handleSelectionChange"
>
<el-table-column
fixed
prop="displayId"
:label="t('hostsList.id')"
min-width="120"
>
<template #default="scope"> <template #default="scope">
<div <div class="link-text" @click="openHTML(scope.row.displayId)">
class="link-text"
@click="openHTML(scope.row.displayId)"
>
{{ scope.row.displayId }} {{ scope.row.displayId }}
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column prop="hostDisplayId" :label="t('hostsList.hostId')" min-width="120">
prop="hostDisplayId"
:label="t('hostsList.hostId')"
min-width="120"
>
<template #default="scope"> <template #default="scope">
<div <div class="link-text" @click.ctrl.exact="handleLongPress(scope.row.hostDisplayId)">
class="link-text"
@click.ctrl.exact="handleLongPress(scope.row.hostDisplayId)"
>
{{ scope.row.hostDisplayId }} {{ scope.row.hostDisplayId }}
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column v-for="label in labelList" :key="label.paramCode" :prop="label.paramCode"
v-for="label in labelList" :label="label.paramCodeMeaning" min-width="100">
:key="label.paramCode" <template v-if="label.paramCode != 'createDt'" #default="scope">
:prop="label.paramCode"
:label="label.paramCodeMeaning"
min-width="100"
>
<template
v-if="label.paramCode != 'createDt'"
#default="scope"
>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -257,32 +167,17 @@
{{ t('hostsList.reset') }} {{ t('hostsList.reset') }}
</el-button> --> </el-button> -->
</div> </div>
<el-pagination <el-pagination v-model:current-page="page" v-model:page-size="pageSize" background
v-model:current-page="page" layout="sizes, prev, pager, next" :total="total" :page-sizes="[10, 20, 50, 100, 500, 1000]"
v-model:page-size="pageSize" @size-change="handleSizeChange" @current-change="handleCurrentChange" />
background
layout="sizes, prev, pager, next"
:total="total"
:page-sizes="[10, 20, 50, 100, 500, 1000]"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div> </div>
</div> </div>
<!-- 更多筛选弹窗 --> <!-- 更多筛选弹窗 -->
<el-dialog <el-dialog v-model="filterdialogVisible" width="800px" :before-close="handleClose" class="cyber-dialog">
v-model="filterdialogVisible"
width="800px"
:before-close="handleClose"
class="cyber-dialog"
>
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="4"> <el-col :span="4">
<div <div style="height: 100%; padding-top: 10px" class="center-justify">
style="height: 100%; padding-top: 10px"
class="center-justify"
>
{{ t('hostsList.time') }} {{ t('hostsList.time') }}
</div> </div>
</el-col> </el-col>
@@ -293,23 +188,12 @@
</label> </label>
</div> </div>
<el-date-picker <el-date-picker v-model="createTimes" type="datetimerange" value-format="YYYY-MM-DD HH:mm:ss"
v-model="createTimes" :placeholder="t('hostsList.selectTime')" size="large" style="width: 600px; margin-top: 10px" />
type="datetimerange"
value-format="YYYY-MM-DD HH:mm:ss"
:placeholder="t('hostsList.selectTime')"
size="large"
style="width: 600px; margin-top: 10px"
/>
</el-col> </el-col>
</el-row> </el-row>
<el-row <el-row v-for="(field, index) in fields" :key="index" :gutter="20" style="margin-bottom: 10px">
v-for="(field, index) in fields"
:key="index"
:gutter="20"
style="margin-bottom: 10px"
>
<el-col :span="4"> <el-col :span="4">
<div style="height: 100%" class="center-justify"> <div style="height: 100%" class="center-justify">
{{ field.label }} {{ field.label }}
@@ -319,32 +203,21 @@
<div> <div>
<label>{{ t('hostsList.minValue') }}</label> <label>{{ t('hostsList.minValue') }}</label>
</div> </div>
<el-input <el-input type="number" :oninput="'if(value.length>9)value=value.slice(0,9)'"
type="number" v-model.number="searchForm[field.minModel]" :placeholder="t('hostsList.enterMinValue')" />
:oninput="'if(value.length>9)value=value.slice(0,9)'"
v-model.number="searchForm[field.minModel]"
:placeholder="t('hostsList.enterMinValue')"
/>
</el-col> </el-col>
<el-col :span="10"> <el-col :span="10">
<div> <div>
<label>{{ t('hostsList.maxValue') }}</label> <label>{{ t('hostsList.maxValue') }}</label>
</div> </div>
<el-input <el-input type="number" :oninput="'if(value.length>9)value=value.slice(0,9)'"
type="number" v-model.number="searchForm[field.maxModel]" :placeholder="t('hostsList.enterMaxValue')" />
:oninput="'if(value.length>9)value=value.slice(0,9)'"
v-model.number="searchForm[field.maxModel]"
:placeholder="t('hostsList.enterMaxValue')"
/>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="4"> <el-col :span="4">
<div <div style="height: 100%; padding-top: 10px" class="center-justify">
style="height: 100%; padding-top: 10px"
class="center-justify"
>
{{ t('hostsList.sort') }} {{ t('hostsList.sort') }}
</div> </div>
</el-col> </el-col>
@@ -353,18 +226,9 @@
<label>{{ t('hostsList.sortType') }}</label> <label>{{ t('hostsList.sortType') }}</label>
</div> </div>
<el-select <el-select v-model="sortData.sortName" filterable :placeholder="t('hostsList.pleaseSelect')"
v-model="sortData.sortName" style="width: 240px">
filterable <el-option v-for="item in sortNameOptions" :key="item.type" :label="item.label" :value="item.type" />
:placeholder="t('hostsList.pleaseSelect')"
style="width: 240px"
>
<el-option
v-for="item in sortNameOptions"
:key="item.type"
:label="item.label"
:value="item.type"
/>
</el-select> </el-select>
</el-col> </el-col>
<el-col :span="10"> <el-col :span="10">
@@ -372,21 +236,11 @@
<label>{{ t('hostsList.sortOrder') }}</label> <label>{{ t('hostsList.sortOrder') }}</label>
</div> </div>
<el-select <el-select v-model="sortData.sort" filterable :placeholder="t('hostsList.pleaseSelect')" style="width: 240px">
v-model="sortData.sort" <el-option v-for="item in [
filterable
:placeholder="t('hostsList.pleaseSelect')"
style="width: 240px"
>
<el-option
v-for="item in [
{ label: t('hostsList.ascending'), value: 'asc' }, { label: t('hostsList.ascending'), value: 'asc' },
{ label: t('hostsList.descending'), value: 'desc' }, { label: t('hostsList.descending'), value: 'desc' },
]" ]" :key="item.value" :label="item.label" :value="item.value" />
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select> </el-select>
</el-col> </el-col>
</el-row> </el-row>
@@ -404,41 +258,26 @@
</el-dialog> </el-dialog>
<!-- 指定直播间弹窗 --> <!-- 指定直播间弹窗 -->
<el-dialog <el-dialog v-model="streamdialogVisible" width="800px" :before-close="handleClose" class="cyber-dialog">
v-model="streamdialogVisible"
width="800px"
:before-close="handleClose"
class="cyber-dialog"
>
<div class="specify-dialog"> <div class="specify-dialog">
<el-input <el-input v-model="textarea" style="width: 100%" :rows="15" type="textarea"
v-model="textarea" :placeholder="t('hostsList.enterRoomIds')" @input="handleInput" />
style="width: 100%" <div v-if="maxSpecifyLines != 5000" class="specify-count-hint">
:rows="15" {{ t('hostsList.currentCount') }} {{ currentLineCount }} {{ t('hostsList.countSeparator') }} {{
type="textarea" maxSpecifyLines }}
:placeholder="t('hostsList.enterRoomIds')" {{ t('hostsList.countUnit') }}
@input="handleInput" </div>
/>
<div class="specify-footer"> <div class="specify-footer">
<el-button class="ghost-btn" @click="specifyCancel"> <el-button class="ghost-btn" @click="specifyCancel">
{{ t('hostsList.cancelSpecify') }} {{ t('hostsList.cancelSpecify') }}
</el-button> </el-button>
<el-button <el-button class="ghost-btn" @click="specifyreset">
class="ghost-btn"
@click="specifyreset"
>
{{ t('hostsList.specifyReset') }} {{ t('hostsList.specifyReset') }}
</el-button> </el-button>
<el-button <el-button type="primary" @click="specifyClick">
type="primary"
@click="specifyClick"
>
{{ t('hostsList.specifyConfirm') }} {{ t('hostsList.specifyConfirm') }}
</el-button> </el-button>
<el-button <el-button type="primary" @click="specifyClickStart">
type="primary"
@click="specifyClickStart"
>
{{ t('hostsList.specifyStart') }} {{ t('hostsList.specifyStart') }}
</el-button> </el-button>
</div> </div>
@@ -446,12 +285,7 @@
</el-dialog> </el-dialog>
<!-- 网络不可访问弹窗 --> <!-- 网络不可访问弹窗 -->
<el-dialog <el-dialog v-model="Inaccessible" width="800px" :before-close="handleClose" class="cyber-dialog">
v-model="Inaccessible"
width="800px"
:before-close="handleClose"
class="cyber-dialog"
>
<div class="inaccessible-dialog"> <div class="inaccessible-dialog">
<img class="inaccessible-img" src="@/assets/wifi.png" /> <img class="inaccessible-img" src="@/assets/wifi.png" />
</div> </div>
@@ -480,6 +314,7 @@ import {
} from "@/api/account"; } from "@/api/account";
import { usePythonBridge } from "@/utils/pythonBridge"; import { usePythonBridge } from "@/utils/pythonBridge";
import { getUser, setSerch, getSerch } from "@/utils/storage"; import { getUser, setSerch, getSerch } from "@/utils/storage";
import { getTenantLimit } from "@/utils/tenantConfig";
import { ref, reactive, onMounted, onBeforeUnmount } from "vue"; import { ref, reactive, onMounted, onBeforeUnmount } from "vue";
const hourstuo = ref(0); const hourstuo = ref(0);
const minutestuo = ref(0); const minutestuo = ref(0);
@@ -686,8 +521,30 @@ function specifyCancel() {
queryFormData.value.anchor_ids = []; queryFormData.value.anchor_ids = [];
} }
// 输入框 input // 输入框 input
function handleInput() { import { computed } from "vue";
// 动态计算最大行数限制
const maxSpecifyLines = computed(() => {
return getTenantLimit(userInfo.value.tenantId);
});
// 当前行数
const currentLineCount = computed(() => {
if (!textarea.value || textarea.value.trim() === "") {
return 0;
}
return textarea.value.split("\n").filter(line => line.trim() !== "").length;
});
function handleInput(value) {
streamdialogVisibletext.value = false; streamdialogVisibletext.value = false;
if (typeof value !== "string") {
return;
}
const lines = value.split("\n");
if (lines.length > maxSpecifyLines.value) {
textarea.value = lines.slice(0, maxSpecifyLines.value).join("\n");
}
} }
// 获取主播列表 - 开始爬取 // 获取主播列表 - 开始爬取
@@ -701,7 +558,7 @@ function getBigBrother() {
key: "UserSettings", key: "UserSettings",
data: queryFormData.value, data: queryFormData.value,
}); });
storageSetInfos(storageobject.value).then((res) => {}); storageSetInfos(storageobject.value).then((res) => { });
queryFormData.value.tenantId = userInfo.value.tenantId; queryFormData.value.tenantId = userInfo.value.tenantId;
queryFormData.value.region = countryData.value; queryFormData.value.region = countryData.value;
startTimerfun(); startTimerfun();
@@ -819,8 +676,8 @@ const lastVisibleTime = ref(null);
function tokenVerificationfun(params) { function tokenVerificationfun(params) {
tokenVerification(params) tokenVerification(params)
.then((res) => {}) .then((res) => { })
.catch((err) => {}); .catch((err) => { });
} }
onMounted(() => { onMounted(() => {
@@ -1078,16 +935,14 @@ function openhostDisplayId(hostDisplayId) {
} }
// ================================ // ================================
// 玻璃态头部区域 // 玻璃态头部区域 - 简化版移除 backdrop-filter 提升性能
// ================================ // ================================
.filter-header.glass-header { .filter-header.glass-header {
background: rgba(255, 255, 255, 0.329); background: #ffffff;
backdrop-filter: blur(12px); border: 1px solid #e8ecf0;
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.9);
border-radius: 24px; border-radius: 24px;
padding: 24px 28px; padding: 24px 28px;
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.06); box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
} }
.filter-row { .filter-row {
@@ -1156,7 +1011,7 @@ function openhostDisplayId(hostDisplayId) {
} }
// ================================ // ================================
// 按钮样式 // 按钮样式 - 简化版移除 transform 动画提升性能
// ================================ // ================================
.cyber-btn-primary { .cyber-btn-primary {
background: linear-gradient(135deg, #00D2FF 0%, #0052CC 100%) !important; background: linear-gradient(135deg, #00D2FF 0%, #0052CC 100%) !important;
@@ -1165,17 +1020,14 @@ function openhostDisplayId(hostDisplayId) {
font-weight: 600 !important; font-weight: 600 !important;
padding: 0 20px !important; padding: 0 20px !important;
height: 42px !important; height: 42px !important;
box-shadow: 0 4px 12px rgba(0, 82, 204, 0.25) !important; box-shadow: 0 2px 8px rgba(0, 82, 204, 0.2) !important;
transition: all 0.3s ease !important;
.btn-icon { .btn-icon {
margin-right: 6px; margin-right: 6px;
} }
&:hover { &:hover {
filter: brightness(1.1); filter: brightness(1.05);
box-shadow: 0 0 20px rgba(0, 210, 255, 0.4) !important;
transform: translateY(-1px);
} }
} }
@@ -1186,7 +1038,11 @@ function openhostDisplayId(hostDisplayId) {
font-weight: 600 !important; font-weight: 600 !important;
padding: 0 24px !important; padding: 0 24px !important;
height: 42px !important; height: 42px !important;
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3) !important; box-shadow: 0 2px 8px rgba(16, 185, 129, 0.2) !important;
&:hover {
filter: brightness(1.05);
}
} }
.cyber-btn-danger { .cyber-btn-danger {
@@ -1196,7 +1052,11 @@ function openhostDisplayId(hostDisplayId) {
font-weight: 600 !important; font-weight: 600 !important;
padding: 0 24px !important; padding: 0 24px !important;
height: 42px !important; height: 42px !important;
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3) !important; box-shadow: 0 2px 8px rgba(239, 68, 68, 0.2) !important;
&:hover {
filter: brightness(1.05);
}
} }
.ghost-btn { .ghost-btn {
@@ -1207,7 +1067,6 @@ function openhostDisplayId(hostDisplayId) {
font-weight: 600 !important; font-weight: 600 !important;
padding: 0 16px !important; padding: 0 16px !important;
height: 40px !important; height: 40px !important;
transition: all 0.2s ease !important;
display: inline-flex !important; display: inline-flex !important;
align-items: center !important; align-items: center !important;
gap: 6px !important; gap: 6px !important;
@@ -1219,8 +1078,6 @@ function openhostDisplayId(hostDisplayId) {
&:hover { &:hover {
background: #f8fafc !important; background: #f8fafc !important;
border-color: #cbd5e1 !important; border-color: #cbd5e1 !important;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
} }
&:disabled { &:disabled {
@@ -1266,6 +1123,7 @@ function openhostDisplayId(hostDisplayId) {
.info-label { .info-label {
color: #94a3b8; color: #94a3b8;
font-weight: 500; font-weight: 500;
} }
.info-value { .info-value {
@@ -1380,7 +1238,7 @@ function openhostDisplayId(hostDisplayId) {
.el-table__row { .el-table__row {
transition: background 0.15s ease; transition: background 0.15s ease;
&:hover > td.el-table__cell { &:hover>td.el-table__cell {
background: #f8fafc !important; background: #f8fafc !important;
} }
} }
@@ -1470,6 +1328,7 @@ function openhostDisplayId(hostDisplayId) {
transition: all 0.2s ease; transition: all 0.2s ease;
margin-right: 0 !important; margin-right: 0 !important;
margin-top: 22px !important; margin-top: 22px !important;
&:hover { &:hover {
border-color: #cbd5e1; border-color: #cbd5e1;
} }
@@ -1541,6 +1400,17 @@ function openhostDisplayId(hostDisplayId) {
min-height: 350px; min-height: 350px;
} }
.specify-count-hint {
margin-top: 10px;
padding: 8px 12px;
background: linear-gradient(135deg, rgba(0, 210, 255, 0.08) 0%, rgba(0, 82, 204, 0.08) 100%);
border-radius: 8px;
color: #475569;
font-size: 13px;
font-weight: 500;
text-align: right;
}
.specify-footer { .specify-footer {
width: 100%; width: 100%;
display: flex; display: flex;
@@ -1608,8 +1478,13 @@ function openhostDisplayId(hostDisplayId) {
justify-content: flex-start; justify-content: flex-start;
} }
.gap-3 { gap: 12px; } .gap-3 {
.gap-4 { gap: 16px; } gap: 12px;
}
.gap-4 {
gap: 16px;
}
.center-justify { .center-justify {
display: flex; display: flex;

View File

@@ -1,313 +0,0 @@
<!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 - Data Console</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/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap"
rel="stylesheet" />
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: "#0052CC",
"cyber-blue": "#00D2FF",
"cyber-blue-dark": "#0052CC",
"tech-gray-50": "#F8FAFC",
"tech-gray-100": "#F1F5F9",
"tech-gray-200": "#E2E8F0",
},
fontFamily: {
display: ["Inter", "system-ui", "sans-serif"],
},
borderRadius: {
DEFAULT: "12px",
'xl': '20px',
'2xl': '24px',
'3xl': '32px',
},
boxShadow: {
'soft-inner': 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.03)',
'cyber-glow': '0 0 20px rgba(0, 210, 255, 0.3)',
'premium': '0 10px 25px -5px rgba(0, 0, 0, 0.04), 0 8px 10px -6px rgba(0, 0, 0, 0.04)',
'glass': '0 8px 32px 0 rgba(31, 38, 135, 0.07)',
}
},
},
};
</script>
<style type="text/tailwindcss">
:root {
--primary-color: #0052CC;
--cyber-gradient: linear-gradient(135deg, #00D2FF 0%, #0052CC 100%);
}
body {
font-family: 'Inter', sans-serif;
background: radial-gradient(circle at top right, #f8fafc, #eff6ff);
}
.cyber-button {
background: var(--cyber-gradient);
transition: all 0.3s ease;
}
.cyber-button:hover {
filter: brightness(1.1);
box-shadow: 0 0 15px rgba(0, 210, 255, 0.4);
}
.ghost-button {
border: 1px solid #E2E8F0;
background: white;
transition: all 0.2s ease;
}
.ghost-button:hover {
background: #F8FAFC;
border-color: #CBD5E1;
transform: translateY(-1px);
}
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #CBD5E1; border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: #94A3B8; }
.input-tech {
@apply bg-white border border-tech-gray-200/60 rounded-xl px-4 py-2.5 text-sm transition-all focus:ring-2 focus:ring-primary/10 focus:border-primary/30 outline-none shadow-soft-inner;
}
.glass-header {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.8);
}
</style>
</head>
<body class="font-display text-slate-900 antialiased min-h-screen">
<div class="max-w-[1440px] mx-auto px-6 py-8">
<div class="flex items-center justify-between mb-8">
<div class="flex items-center gap-3">
<div
class="w-10 h-10 bg-primary rounded-xl flex items-center justify-center text-white font-bold text-xl shadow-lg shadow-primary/20">
y</div>
<div class="flex flex-col">
<span class="text-xl font-bold tracking-tight text-slate-900 leading-none">yolo</span>
<span class="text-[10px] font-semibold tracking-widest text-primary uppercase">Assistant</span>
</div>
</div>
<div
class="flex items-center gap-6 text-xs font-medium bg-white/50 px-4 py-2 rounded-full border border-white/50 shadow-sm">
<div class="flex items-center gap-2">
<span class="text-slate-400">当前网络:</span>
<span class="text-primary bg-blue-50 px-2 py-0.5 rounded-full">日本 (Japan)</span>
</div>
<div class="h-3 w-px bg-slate-200"></div>
<div class="flex items-center gap-2">
<span class="text-slate-400">运行时间:</span>
<span class="text-slate-700 font-mono">00:00:00</span>
</div>
</div>
</div>
<main class="space-y-6">
<div class="glass-header rounded-3xl p-8 shadow-glass">
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8 items-end">
<div class="lg:col-span-8 grid grid-cols-1 md:grid-cols-12 gap-6">
<div class="md:col-span-3 flex items-center h-full">
<label class="flex items-center gap-3 cursor-pointer group">
<div class="relative">
<input class="peer hidden" type="checkbox" />
<div
class="w-5 h-5 border-2 border-slate-200 rounded-lg peer-checked:bg-primary peer-checked:border-primary transition-all">
</div>
<span
class="material-symbols-outlined absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-white text-sm opacity-0 peer-checked:opacity-100">check</span>
</div>
<span
class="text-sm font-semibold text-slate-600 group-hover:text-primary transition-colors">过滤隐私用户</span>
</label>
</div>
<div class="md:col-span-4 space-y-2">
<label
class="text-[11px] font-bold text-slate-400 uppercase tracking-wider flex items-center gap-2 px-1">
<span class="material-symbols-outlined text-xs">payments</span> 金币范围
</label>
<div class="flex items-center gap-2">
<input class="input-tech w-full" placeholder="最小金币" type="text" />
<span class="text-slate-300">/</span>
<input class="input-tech w-full" placeholder="最大金币" type="text" />
</div>
</div>
<div class="md:col-span-5 space-y-2">
<label
class="text-[11px] font-bold text-slate-400 uppercase tracking-wider flex items-center gap-2 px-1">
<span class="material-symbols-outlined text-xs">trending_up</span> 等级范围
</label>
<div class="flex items-center gap-2">
<input class="input-tech w-full" placeholder="最小等级" type="text" />
<span class="text-slate-300">/</span>
<input class="input-tech w-full" placeholder="最大等级" type="text" />
</div>
</div>
</div>
<div class="lg:col-span-4 flex gap-3">
<button
class="cyber-button flex-[2] text-white px-4 py-3 rounded-2xl font-bold text-sm flex items-center justify-center gap-2 shadow-lg shadow-blue-500/20">
<span class="material-symbols-outlined text-lg">person_pin_circle</span>
选择直播间
</button>
<button
class="cyber-button flex-1 py-3 rounded-2xl text-white text-sm font-bold shadow-cyber-glow">
开始执行
</button>
</div>
</div>
<div class="flex flex-wrap items-center gap-4 mt-8 pt-8 border-t border-slate-200/50">
<div class="flex-1 min-w-[300px] flex items-center gap-4">
<div class="relative w-48">
<select class="input-tech w-full appearance-none pr-10 bg-slate-50/50">
<option>全部国家</option>
<option>美国</option>
<option>日本</option>
</select>
<span
class="material-symbols-outlined 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">
<span
class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 text-lg">alternate_email</span>
<input class="input-tech w-full pl-10" placeholder="请输入大哥 ID..." type="text" />
</div>
</div>
<div class="flex items-center gap-2">
<button
class="ghost-button px-6 py-2.5 rounded-xl text-sm font-semibold text-slate-600 flex items-center gap-2">
<span class="material-symbols-outlined text-xl">search</span>查询
</button>
<button
class="ghost-button px-6 py-2.5 rounded-xl text-sm font-semibold text-slate-600 flex items-center gap-2">
<span class="material-symbols-outlined text-xl">restart_alt</span>重置
</button>
<button
class="ghost-button px-6 py-2.5 rounded-xl text-sm font-semibold text-slate-600 flex items-center gap-2">
<span class="material-symbols-outlined text-xl">file_download</span>导出Excel
</button>
<button
class="ghost-button px-6 py-2.5 rounded-xl text-sm font-semibold text-slate-600 flex items-center gap-2">
<span class="material-symbols-outlined text-xl">tune</span>更多筛选
</button>
</div>
</div>
</div>
<div
class="bg-white rounded-3xl shadow-premium border border-white flex flex-col min-h-[500px] overflow-hidden">
<div class="flex-1 p-8">
<div class="w-full overflow-x-auto">
<table class="w-full text-left border-separate border-spacing-y-0">
<thead>
<tr class="text-slate-400 text-[11px] font-bold uppercase tracking-widest">
<th class="pb-6 px-4 border-b border-slate-100 font-bold">ID</th>
<th class="pb-6 px-4 border-b border-slate-100 font-bold">所在直播间</th>
<th class="pb-6 px-4 border-b border-slate-100 font-bold">用户ID</th>
<th class="pb-6 px-4 border-b border-slate-100 font-bold">等级</th>
<th class="pb-6 px-4 border-b border-slate-100 font-bold">打赏金币</th>
<th class="pb-6 px-4 border-b border-slate-100 font-bold">地区</th>
<th class="pb-6 px-4 border-b border-slate-100 font-bold">粉丝数</th>
<th class="pb-6 px-4 border-b border-slate-100 font-bold text-right">创建时间</th>
</tr>
</thead>
<tbody>
<tr class="group hover:bg-slate-50 transition-colors">
<td class="py-5 px-4 text-sm text-slate-500">1</td>
<td
class="py-5 px-4 text-sm font-semibold text-primary underline decoration-primary/20 underline-offset-4 cursor-pointer">
toxi_robert</td>
<td class="py-5 px-4 text-sm font-mono text-slate-600">706350628...</td>
<td class="py-5 px-4">
<span
class="px-2 py-1 bg-amber-50 text-amber-600 text-[10px] font-bold rounded-lg border border-amber-100">LV
29</span>
</td>
<td class="py-5 px-4 text-sm font-bold text-slate-700">37,420</td>
<td class="py-5 px-4 text-sm text-slate-600">意大利</td>
<td class="py-5 px-4 text-sm text-slate-500">562</td>
<td class="py-5 px-4 text-sm text-slate-400 font-mono text-right">2026-01-16
15:15:07</td>
</tr>
<tr class="hidden">
<td class="py-32" colspan="8">
<div class="flex flex-col items-center justify-center">
<div
class="w-16 h-16 bg-slate-50 rounded-full flex items-center justify-center mb-4">
<span
class="material-symbols-outlined text-3xl text-slate-300">database_off</span>
</div>
<p class="text-slate-400 text-sm">暂无匹配数据</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<footer
class="px-8 py-6 bg-slate-50/50 border-t border-slate-100 flex flex-wrap items-center justify-between gap-6">
<div class="flex items-center gap-6">
<div class="relative">
<select
class="bg-white border border-slate-200 rounded-xl py-2 pl-4 pr-10 text-xs font-bold text-slate-600 appearance-none focus:ring-2 focus:ring-primary/10 cursor-pointer">
<option>10条/页</option>
<option>20条/页</option>
<option>50条/页</option>
</select>
<span
class="material-symbols-outlined absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 text-lg pointer-events-none">expand_more</span>
</div>
<div class="h-6 w-[1px] bg-slate-200"></div>
<div class="flex items-center gap-8 text-xs">
<div class="flex flex-col gap-0.5">
<span class="text-slate-400 font-medium">总数据</span>
<span class="font-bold text-slate-900 text-sm">1,248</span>
</div>
<div class="flex flex-col gap-0.5">
<span class="text-slate-400 font-medium">有效数</span>
<span class="font-bold text-primary text-sm">842</span>
</div>
<div class="flex flex-col gap-0.5">
<span class="text-slate-400 uppercase tracking-tighter font-medium">服务到期</span>
<span class="text-slate-900 font-bold text-sm">2036-08-01</span>
</div>
</div>
</div>
<div class="flex items-center gap-2">
<button
class="p-2 text-slate-400 hover:bg-white hover:text-primary hover:shadow-sm rounded-xl transition-all">
<span class="material-symbols-outlined text-xl">chevron_left</span>
</button>
<div class="flex items-center gap-1">
<button
class="w-9 h-9 rounded-xl bg-slate-900 text-white text-xs font-bold transition-all shadow-lg shadow-slate-900/20">1</button>
<button
class="w-9 h-9 rounded-xl hover:bg-white text-slate-600 text-xs font-bold transition-all">2</button>
<button
class="w-9 h-9 rounded-xl hover:bg-white text-slate-600 text-xs font-bold transition-all">3</button>
<span class="px-2 text-slate-300">...</span>
<button
class="w-9 h-9 rounded-xl hover:bg-white text-slate-600 text-xs font-bold transition-all">12</button>
</div>
<button
class="p-2 text-slate-400 hover:bg-white hover:text-primary hover:shadow-sm rounded-xl transition-all">
<span class="material-symbols-outlined text-xl">chevron_right</span>
</button>
</div>
</footer>
</div>
</main>
</div>
</body>
</html>

View File

@@ -1,742 +0,0 @@
<template>
<div class="center-line workbenches">
<div class="center-align" style="width: 100%; margin: 0 20px;">
<div class="box-card-num1 center-line">
<div>总数量: <span>{{ hostData.totalCount }}</span></div>
<div>新建主播: <span>{{ hostData.validAnchorsCount }}</span></div>
<div> 已查询: <span>{{ hostData.checkedDataCount }}</span></div>
<div>可邀请: <span>{{ hostData.canInvitationCount }}</span></div>
<div>运行时间: <span>{{ formattedTime }}</span></div>
</div>
<div class="center-line" style="padding-top: 15vh;">
<el-button class="open-login" type="primary" @click="openTK">开启tk</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">
公会账号
</div>
<el-input :disabled="!(tkData[index].code == 0 && !isLogin[index])"
v-model="tkData[index].account" placeholder="请输入登录账号" clearable />
</div>
<div class="from-input-item">
<div class="from-input-item-title center-justify">
公会密码
</div>
<el-input :disabled="!(tkData[index].code == 0 && !isLogin[index])"
v-model="tkData[index].password" type="password" placeholder="请输入登录密码" show-password />
</div>
<el-button class="open-login" style="margin-left: 60px;"
:disabled="!(tkData[index].code == 0 && !isLogin[index])" type="primary"
@click="loginTK(index)">登录后台</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">今日已查询次数{{ 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">工作台 </span>
<div style="margin-right: 120px;">当前网络:{{ countryData }}
<!-- <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>设置金币数量</label>
<el-input type='number' v-model="pyData.gold.min" :min="0" :max="pyData.gold.max - 1"
placeholder="最小值" style="width: 100%" :disabled="!pyData.isStart">
<template #prepend>最小金币数</template>
</el-input>
<el-input type='number' v-model="pyData.gold.max" :min="pyData.gold.min + 1" :max="100"
placeholder="最大值" style="width: 100%; margin-top: 10px" :disabled="!pyData.isStart">
<template #prepend>最大金币数</template>
</el-input>
</div>
</el-col>
<el-col :span="8">
<div class="input-group">
<label>设置粉丝数量</label>
<el-input type='number' v-model="pyData.fans.min" :min="0" :max="pyData.fans.max - 1"
placeholder="最小值" style="width: 100%" :disabled="!pyData.isStart">
<template #prepend>最小粉丝数</template>
</el-input>
<el-input type='number' v-model="pyData.fans.max" :min="pyData.fans.min + 1" :max="100"
placeholder="最大值" style="width: 100%; margin-top: 10px" :disabled="!pyData.isStart">
<template #prepend>最大粉丝数</template>
</el-input>
</div>
</el-col>
<el-col :span="8">
<div class="input-group">
<label>后台查询频率</label>
<!-- <el-input type='number' v-model="pyData.frequency.hour" @input="handleInputHour" -->
<el-input type='number' v-model="pyData.frequency.hour" placeholder="次/小时"
style="width: 100%" :disabled="!pyData.isStart">
<template #append>/小时</template>
</el-input>
<!-- <el-input type='number' v-model="pyData.frequency.day" @input="handleInputDay" -->
<el-input type='number' v-model="pyData.frequency.day" placeholder="次/24小时"
style="width: 100%; margin-top: 10px" :disabled="!pyData.isStart">
<template #append>/24小时</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">开始获取数据</el-button>
<el-button v-show="!pyData.isStart" type="danger" @click="unsubmit">停止获取</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'
//导入python交互方法
const { fetchDataConfig, fetchDataCount, loginBackStage, loginTikTok, backStageloginStatus, backStageloginStatusCopy } = usePythonBridge();
//ip国家
let countryData = ref('');
//获取主播数量的定时器
let getHostTimer = 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);
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);
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;
// 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>

View File

@@ -72,37 +72,17 @@ html {
user-select: none; user-select: none;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.app-container.cyber-page { .app-container.cyber-page {
width: 1600px; width: 1600px;
height: 900px; height: 900px;
background: radial-gradient(ellipse at top right, #f0f9ff 0%, #e0f2fe 30%, #f1f5f9 100%); background: linear-gradient(135deg, #f0f9ff 0%, #e8f4fc 50%, #f1f5f9 100%);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
// 背景装饰
&::before {
content: '';
position: absolute;
top: -50%;
right: -20%;
width: 800px;
height: 800px;
background: radial-gradient(circle, rgba(0, 210, 255, 0.08) 0%, transparent 70%);
pointer-events: none;
}
&::after {
content: '';
position: absolute;
bottom: -30%;
left: -10%;
width: 600px;
height: 600px;
background: radial-gradient(circle, rgba(0, 82, 204, 0.06) 0%, transparent 70%);
pointer-events: none;
}
} }
.cyber-main-container { .cyber-main-container {
@@ -118,15 +98,10 @@ html {
.content-wrapper.glass-content { .content-wrapper.glass-content {
flex: 1; flex: 1;
background: rgba(255, 255, 255, 0.92); background: #ffffff;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-radius: 28px; border-radius: 28px;
border: 1px solid rgba(255, 255, 255, 0.95); border: 1px solid #e8ecf0;
box-shadow: box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
0 20px 60px -10px rgba(0, 0, 0, 0.08),
0 10px 30px -5px rgba(0, 0, 0, 0.04),
inset 0 1px 0 rgba(255, 255, 255, 0.9);
overflow: hidden; overflow: hidden;
position: relative; position: relative;
} }
@@ -144,11 +119,10 @@ html {
align-items: center; align-items: center;
gap: 10px; gap: 10px;
padding: 10px 24px; padding: 10px 24px;
background: rgba(255, 255, 255, 0.6); background: #ffffff;
backdrop-filter: blur(10px);
border-radius: 24px; border-radius: 24px;
border: 1px solid rgba(255, 255, 255, 0.8); border: 1px solid #e8ecf0;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
.footer-icon { .footer-icon {
font-size: 14px; font-size: 14px;