3.5.0版本

This commit is contained in:
2026-01-05 21:36:20 +08:00
parent c35ae1b2bc
commit c889c80747
7 changed files with 419 additions and 55 deletions

View File

@@ -5,7 +5,7 @@ VUE_APP_BASE_LOCAL=https://127.0.0.1:34567/
# 业务后端(开发用内网地址) # 业务后端(开发用内网地址)
VUE_APP_BASE_REMOTE=https://crawlclient.api.yolozs.com VUE_APP_BASE_REMOTE=https://crawlclient.api.yolozs.com
# VUE_APP_BASE_REMOTE=http://192.168.1.144:8101/ # VUE_APP_BASE_REMOTE=http://192.168.2.21:8101/
# AI 服务 # AI 服务
VUE_APP_BASE_SPECIAL=https://ai.yolozs.com VUE_APP_BASE_SPECIAL=https://ai.yolozs.com

View File

@@ -10,29 +10,50 @@
<!-- 工具栏 --> <!-- 工具栏 -->
<div class="toolbar"> <div class="toolbar">
<!-- 操作按钮行 -->
<div class="toolbar-row">
<el-button size="small" @click="selectAll">全选</el-button> <el-button size="small" @click="selectAll">全选</el-button>
<el-button size="small" @click="selectNone">全不选</el-button> <el-button size="small" @click="selectNone">全不选</el-button>
<el-button size="small" @click="invertSelect">反选</el-button> <el-button size="small" @click="invertSelect">反选</el-button>
<el-button size="small" type="danger" :disabled="!selectedCount" @click="deleteSelected">删除选中</el-button> <el-button size="small" type="danger" :disabled="!selectedCount"
<el-switch v-model="gold" :loading="goldLoading" :before-change="goldBeforeChange" inline-prompt @click="deleteSelected">删除选中</el-button>
active-text="金票" inactive-text="金票" size="large" style="--el-switch-on-color: #db9600; " /> <el-button size="small" @click="resetFilter">重置</el-button>
<el-switch v-model="ordinary" :loading="ordinaryLoading" :before-change="ordinaryBeforeChange" inline-prompt
inactive-text="普票" active-text="普票" size="large" />
<!-- 新增一键删除已处理 -->
<!-- <el-button size="small" type="warning" :disabled="!processedCount" @click="deleteProcessed">
删除已处理
</el-button> -->
<el-tooltip placement="bottom" effect="dark"> <el-tooltip placement="bottom" effect="dark">
<template #content> <template #content>
在空白区域按下左键拖拽进行框选<br /> 在空白区域按下左键拖拽进行框选<br />
</template> </template>
<el-icon class="hint">i</el-icon> <el-icon class="hint">i</el-icon>
</el-tooltip> </el-tooltip>
</div> </div>
<!-- 金票 / 普票 + 筛选条件行支持自动换行 -->
<div class="toolbar-row toolbar-filter">
<el-switch v-model="filters.gold" :loading="goldLoading" :before-change="goldBeforeChange" inline-prompt
active-text="金票" inactive-text="金票" size="large" style="--el-switch-on-color: #db9600;" />
<el-switch v-model="filters.ordinary" :loading="ordinaryLoading" :before-change="ordinaryBeforeChange"
inline-prompt inactive-text="普票" active-text="普票" size="large" />
<!-- 在线人数 -->
<span class="filter-label">在线人数</span>
<el-input-number v-model="filters.min_onlineFans" :min="0" size="small" controls-position="right"
placeholder="最小" class="filter-input-number" />
<span>~</span>
<el-input-number v-model="filters.max_onlineFans" :min="0" size="small" controls-position="right"
placeholder="最大" class="filter-input-number" />
<!-- 主播等级多选 -->
<span class="filter-label">主播等级</span>
<el-tree-select v-model="filters.hostslevel" :data="levelTreeData" multiple show-checkbox collapse-tags
collapse-tags-tooltip size="small" placeholder="选择等级" class="filter-select-multi" node-key="value"
:render-after-expand="false" />
</div>
</div>
<!-- 列表区域 --> <!-- 列表区域 -->
<div ref="gridRef" class="grid" @mousedown="onMouseDown" @mousemove="onMouseMove" @mouseup="onMouseUp" <div ref="gridRef" class="grid" @mousedown="onMouseDown" @mousemove="onMouseMove" @mouseup="onMouseUp"
@mouseleave="onMouseUp" @scroll="recalcRectsSoon"> @mouseleave="onMouseUp" @scroll="recalcRectsSoon">
@@ -42,13 +63,12 @@
<span class="id" :title="it.anchorId">{{ it.anchorId }}</span> <span class="id" :title="it.anchorId">{{ it.anchorId }}</span>
<button v-if="it.state == 0" class="x" title="不执行">X</button> <button v-if="it.state == 0" class="x" title="不执行">X</button>
<button v-else class="y" title="执行"></button> <button v-else class="y" title="执行"></button>
</div> </div>
<div class="row meta"> <div class="row meta">
<span class="country" :title="it.country">{{ it.country || '—' }}</span> <span class="country" :title="it.country">{{ it.country || '—' }}</span>
<span class="state" :class="{ done: it.invitationType == 2 }">{{ it.invitationType == 2 ? '金票' : <span class="state" :class="{ done: it.invitationType == 2 }">
'普票' {{ it.invitationType == 2 ? '金票' : '普票' }}
}}</span> </span>
</div> </div>
</div> </div>
@@ -58,18 +78,18 @@
<template #footer> <template #footer>
<div class="foot"> <div class="foot">
<el-button @click="show = false">关闭</el-button> <el-button type="primary" @click="applyFilter">保存</el-button>
<el-button @click="show = false;">关闭</el-button>
</div> </div>
</template> </template>
</el-dialog> </el-dialog>
</template> </template>
<script setup> <script setup>
import { ref, reactive, computed, watch, nextTick } from 'vue' import { ref, reactive, computed, watch, nextTick, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { getHostList, setHostList } from '@/stores/storage' import { getHostfilters, setHostfilters } from '@/stores/storage'
import { anchorList, deleteAnchorWithIds, updateAnchorList } from '@/api/ios' import { anchorList, deleteAnchorWithIds, updateAnchorList, } from '@/api/ios'
// v-model:visible 接口 // v-model:visible 接口
const props = defineProps({ const props = defineProps({
visible: { type: Boolean, default: false } visible: { type: Boolean, default: false }
@@ -81,25 +101,82 @@ const show = computed({
set: (v) => emit('update:visible', v) set: (v) => emit('update:visible', v)
}) })
const defaultFilters = {
gold: null,
ordinary: null,
min_onlineFans: null,
max_onlineFans: null,
hostslevel: []
}
// 数据 // 数据
const hosts = ref([]) // {country, text, state} const hosts = ref([]) // {country, text, state}
const selected = reactive(new Set()) // 选中的 text 集合 const selected = reactive(new Set()) // 选中的 text 集合
//金票 // 主播等级树形数据A / B / C / D
let gold = ref(true) const levelTreeData = [
{
label: 'A',
value: 'A',
children: [
{ label: 'A1', value: 'A1' },
{ label: 'A2', value: 'A2' },
{ label: 'A3', value: 'A3' },
]
},
{
label: 'B',
value: 'B',
children: [
{ label: 'B1', value: 'B1' },
{ label: 'B2', value: 'B2' },
{ label: 'B3', value: 'B3' },
{ label: 'B4', value: 'B4' },
{ label: 'B5', value: 'B5' },
]
},
{
label: 'C',
value: 'C',
children: [
{ label: 'C1', value: 'C1' },
{ label: 'C2', value: 'C2' },
{ label: 'C3', value: 'C3' },
{ label: 'C4', value: 'C4' },
{ label: 'C5', value: 'C5' },
]
},
{
label: 'D',
value: 'D',
children: [
{ label: 'D1', value: 'D1' },
{ label: 'D2', value: 'D2' },
{ label: 'D3', value: 'D3' },
{ label: 'D4', value: 'D4' },
{ label: 'D5', value: 'D5' },
]
}
]
let goldLoading = ref(false) let goldLoading = ref(false)
//普票
let ordinary = ref(true)
let ordinaryLoading = ref(false) let ordinaryLoading = ref(false)
// 筛选参数
const filters = reactive({ ...defaultFilters })
// 卡片 DOM 引用与位置缓存 // 卡片 DOM 引用与位置缓存
const gridRef = ref(null) const gridRef = ref(null)
const cardRefs = reactive({}) // text -> el const cardRefs = reactive({}) // text -> el
const rectCache = reactive({}) // text -> DOMRect const rectCache = reactive({}) // text -> DOMRect
let rectRecalcTimer = null let rectRecalcTimer = null
const processedCount = computed(() => hosts.value.filter(it => !!it?.state).length) const processedCount = computed(() => hosts.value.filter(it => !!it?.state).length)
function setCardRef(key, el) { function setCardRef(key, el) {
if (el) { if (el) {
@@ -112,7 +189,6 @@ function setCardRef(key, el) {
async function getStoredHostList() { async function getStoredHostList() {
const v = await anchorList() const v = await anchorList()
console.log("v", v)
return v ? v : [] return v ? v : []
} }
@@ -124,6 +200,51 @@ async function onOpen() {
recalcRects() recalcRects()
} }
// 应用筛选(通过 getMQMessagesByFilter
async function applyFilter() {
try {
const params = {
gold: filters.gold,
ordinary: filters.ordinary,
min_onlineFans: filters.min_onlineFans,
max_onlineFans: filters.max_onlineFans,
// 多选等级转成 "D5,D4,B3" 这种格式给后端
hostslevel: filters.hostslevel
}
// 清除 null / 空字符串,避免传一堆 undefined
const cleanParams = {}
Object.keys(params).forEach(key => {
const v = params[key]
if (v !== null && v !== '' && v !== undefined) {
cleanParams[key] = v
}
})
console.log(filters)
setHostfilters(filters)
show.value = false
selected.clear()
await nextTick()
recalcRects()
} catch (error) {
console.error(error)
ElMessage.error('筛选失败:' + (error.message || '未知错误'))
}
}
// 重置筛选条件
async function resetFilter() {
filters.gold = true
filters.ordinary = true
filters.min_onlineFans = null
filters.max_onlineFans = null
filters.hostslevel = [] // 清空多选
// 恢复默认列表
hosts.value = await getStoredHostList()
selected.clear()
await nextTick()
recalcRects()
}
// 选择相关 // 选择相关
const selectedCount = computed(() => selected.size) const selectedCount = computed(() => selected.size)
@@ -178,6 +299,7 @@ function deleteOne(id) {
} }
// —— 框选逻辑 —— // —— 框选逻辑 ——
// ... 这部分不变 ...
const selecting = ref(false) const selecting = ref(false)
const anchor = ref({ x: 0, y: 0 }) const anchor = ref({ x: 0, y: 0 })
const cursor = ref({ x: 0, y: 0 }) const cursor = ref({ x: 0, y: 0 })
@@ -253,7 +375,6 @@ function recalcRectsSoon() {
rectRecalcTimer = setTimeout(() => nextTick().then(recalcRects), 16) rectRecalcTimer = setTimeout(() => nextTick().then(recalcRects), 16)
} }
function deleteProcessed() { function deleteProcessed() {
if (!processedCount.value) return if (!processedCount.value) return
ElMessageBox.confirm( ElMessageBox.confirm(
@@ -278,12 +399,10 @@ function deleteProcessed() {
.catch(() => { }) .catch(() => { })
} }
const ordinaryBeforeChange = () => { const ordinaryBeforeChange = () => {
const next = !ordinary.value // 目标值(切换后) const next = !filters.ordinary // 目标值(切换后)
ordinaryLoading.value = true ordinaryLoading.value = true
return new Promise((resolve) => { return new Promise((resolve) => {
updateAnchorList({ invitationType: 1, state: next ? 1 : 0 }) updateAnchorList({ invitationType: 1, state: next ? 1 : 0 })
.then(async () => { .then(async () => {
@@ -301,11 +420,9 @@ const ordinaryBeforeChange = () => {
} }
const goldBeforeChange = () => { const goldBeforeChange = () => {
console.log(ordinary.value, gold.value) const next = !filters.gold
const next = !gold.value
goldLoading.value = true goldLoading.value = true
return new Promise((resolve) => { return new Promise((resolve) => {
updateAnchorList({ invitationType: 2, state: next ? 1 : 0 }) updateAnchorList({ invitationType: 2, state: next ? 1 : 0 })
.then(async () => { .then(async () => {
@@ -322,10 +439,37 @@ const goldBeforeChange = () => {
}) })
} }
// 从 localStorage 读取并回显
function loadFiltersFromCache() {
try {
const raw = getHostfilters()
if (!raw) return
// 只覆盖存在的字段,防止结构变更报错
Object.keys(defaultFilters).forEach((key) => {
if (raw[key] !== undefined) {
// @ts-ignore
filters[key] = raw[key]
}
})
} catch (e) {
console.error('读取筛选缓存失败:', e)
}
}
// 监听 filters 变化,自动持久化
watch(hosts, () => nextTick().then(recalcRects)) watch(hosts, () => nextTick().then(recalcRects))
// 组件挂载时加载一次缓存
onMounted(() => {
loadFiltersFromCache()
applyFilter()
})
</script> </script>
<style scoped> <style scoped>
.dlg-title { .dlg-title {
display: flex; display: flex;
@@ -339,10 +483,23 @@ watch(hosts, () => nextTick().then(recalcRects))
} }
.toolbar { .toolbar {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 10px;
}
/* 每一行都是 flex允许内部自动换行 */
.toolbar-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
margin-bottom: 10px; flex-wrap: wrap;
}
/* 第二行筛选条件,稍微紧凑一点 */
.toolbar-filter {
align-items: center;
} }
.toolbar .hint { .toolbar .hint {
@@ -357,6 +514,20 @@ watch(hosts, () => nextTick().then(recalcRects))
color: #666; color: #666;
} }
.filter-label {
font-size: 12px;
color: #666;
margin-left: 8px;
}
.filter-input-number {
width: 110px;
}
.filter-select-multi {
width: 180px;
}
.grid { .grid {
position: relative; position: relative;
height: 60vh; height: 60vh;
@@ -370,6 +541,7 @@ watch(hosts, () => nextTick().then(recalcRects))
border-radius: 8px; border-radius: 8px;
} }
/* 后面样式保持不变 */
.item-card { .item-card {
border: 1px solid var(--el-border-color); border: 1px solid var(--el-border-color);
border-radius: 10px; border-radius: 10px;

View File

@@ -42,6 +42,7 @@
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { ref, computed, watch } from 'vue'; import { ref, computed, watch } from 'vue';
import { prologue, comment } from '@/api/account'; import { prologue, comment } from '@/api/account';
import { getMultiLineInputCache, setMultiLineInputCache } from '@/stores/storage';
const MAX_LINES_FOR_ANCHOR = 100; const MAX_LINES_FOR_ANCHOR = 100;
@@ -63,12 +64,36 @@ const props = defineProps({
const emit = defineEmits(['update:visible', 'confirm', 'cancel']); const emit = defineEmits(['update:visible', 'confirm', 'cancel']);
const rawText = ref(props.initialText); const rawText = ref('');
// 区分关闭来源true=通过“确定”关闭false=取消/遮罩/ESC/右上角关闭 // 区分关闭来源true=通过“确定”关闭false=取消/遮罩/ESC/右上角关闭
const closingByConfirm = ref(false); const closingByConfirm = ref(false);
watch(() => props.initialText, (v) => { rawText.value = v; }); function loadCachedText() {
const cached = getMultiLineInputCache(props.title);
if (cached !== null && cached !== undefined) {
rawText.value = cached;
return;
}
rawText.value = props.initialText || '';
}
watch(() => props.visible, (v) => {
if (v) loadCachedText();
});
watch(() => props.title, () => {
if (props.visible) loadCachedText();
});
watch(() => props.initialText, (v) => {
const cached = getMultiLineInputCache(props.title);
if (props.visible && cached === null) rawText.value = v;
});
watch(rawText, (v) => {
setMultiLineInputCache(props.title, v);
});
const visibleLocal = computed({ const visibleLocal = computed({
get: () => props.visible, get: () => props.visible,
@@ -105,7 +130,6 @@ function onClosed() {
const byConfirm = closingByConfirm.value; const byConfirm = closingByConfirm.value;
// 重置表单状态 // 重置表单状态
rawText.value = '';
// data.value = { // data.value = {
// needTranslate: false, // needTranslate: false,
// auto: false, // auto: false,

View File

@@ -549,3 +549,17 @@ video {
transform: scale(1); transform: scale(1);
} }
} }
/* 新增:中间区域底部的到期时间一行 */
.expire-line {
grid-column: 1 / -1;
/* ⭐ 跨越所有列不管你是3列还是其他 */
justify-self: center;
/* 水平居中 */
align-self: end;
/* 在当前这行的底部对齐 */
margin-top: 16px;
font-size: 14px;
color: #ffffff;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}

View File

@@ -106,3 +106,27 @@ export function setsessionId(data) {
export function getsessionId() { export function getsessionId() {
return JSON.parse(sessionStorage.getItem('sessionList')); return JSON.parse(sessionStorage.getItem('sessionList'));
} }
// 导出一个函数,用于获取用户密码
export function setHostfilters(data) {
localStorage.setItem('host_filters_cache', JSON.stringify(data));
}
// 导出一个函数,用于获取用户密码
export function getHostfilters() {
return JSON.parse(localStorage.getItem('host_filters_cache'));
}
const MULTI_LINE_INPUT_CACHE_KEY = 'multi_line_input_cache';
export function getMultiLineInputCache(title) {
if (!title) return null;
const cache = JSON.parse(localStorage.getItem(MULTI_LINE_INPUT_CACHE_KEY) || '{}');
return Object.prototype.hasOwnProperty.call(cache, title) ? cache[title] : null;
}
export function setMultiLineInputCache(title, text) {
if (!title) return;
const cache = JSON.parse(localStorage.getItem(MULTI_LINE_INPUT_CACHE_KEY) || '{}');
cache[title] = text ?? '';
localStorage.setItem(MULTI_LINE_INPUT_CACHE_KEY, JSON.stringify(cache));
}

View File

@@ -79,7 +79,7 @@ import { getToken, setToken, setUser, setUserPass, getUserPass } from '@/stores/
import { ElLoading, ElMessage } from 'element-plus'; import { ElLoading, ElMessage } from 'element-plus';
import { passToken } from '@/api/ios'; import { passToken } from '@/api/ios';
let version = ref('3.2.0'); let version = ref('3.3.0');
onMounted(() => { onMounted(() => {

View File

@@ -57,12 +57,17 @@
</div> </div>
</div> </div>
</div> </div>
<div class="right center-line" @click.self="selectedDevice = 999"> <div class="right center-line" @click.self="selectedDevice = 999">
<!-- <div style="margin: 30px;"></div> --> <!-- <div style="margin: 30px;"></div> -->
<ChatDialog :visible="openShowChat" :messages="chatList" /> <ChatDialog :visible="openShowChat" :messages="chatList" />
<MessageDialogd :visible="openShowChat" :messages="MesNewList" :sound-src="ding" /> <MessageDialogd :visible="openShowChat" :messages="MesNewList" :sound-src="ding" />
<div class="expire-time">
到期时间{{ timestampToTime(userdata.aiExpireTime) }}
</div>
</div> </div>
<img v-if="isWifi" style="position: absolute; right: 20px; top: 10px; height: 30px;" src="@/assets/wifi.png"></img> <img v-if="isWifi" style="position: absolute; right: 20px; top: 10px; height: 30px;" src="@/assets/wifi.png"></img>
<MultiLineInputDialog v-model:visible="showDialog" :initialText='initialTextStr' :title="dialogTitle" <MultiLineInputDialog v-model:visible="showDialog" :initialText='initialTextStr' :title="dialogTitle"
@@ -113,7 +118,7 @@
<div style="display:flex; gap:8px; align-items:center;"> <div style="display:flex; gap:8px; align-items:center;">
<el-switch v-model="interruptEnabled" active-text="开启换号" /> <el-switch v-model="interruptEnabled" active-text="开启换号" />
<el-input-number v-model="interruptEveryMin" :min="1" :max="24" /> <el-input-number v-model="interruptEveryMin" :min="1" :max="24" />
<span>小时换一次</span> <span>分钟换一次</span>
</div> </div>
<div>联盟号</div> <div>联盟号</div>
<div style="display:flex; gap:8px; align-items:center;"> <div style="display:flex; gap:8px; align-items:center;">
@@ -138,7 +143,7 @@ import {
setphoneXYinfo, getphoneXYinfo, getUser, setphoneXYinfo, getphoneXYinfo, getUser,
getHostList, setHostList, getContentpriList, getHostList, setHostList, getContentpriList,
setContentpriList, getContentList, setContentList, setContentpriList, getContentList, setContentList,
getContentListMultiline, getContentpriListMultiline getContentListMultiline, getContentpriListMultiline, getHostfilters
} from '@/stores/storage' } from '@/stores/storage'
import { connectSSE } from '@/utils/sseUtils' import { connectSSE } from '@/utils/sseUtils'
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus' import { ElMessage, ElMessageBox, ElLoading } from 'element-plus'
@@ -383,10 +388,12 @@ const buttons = [
// runType.value = 'like' // runType.value = 'like'
// deviceInformation.value.forEach((item) => growAccount({ udid: item.deviceId })) // deviceInformation.value.forEach((item) => growAccount({ udid: item.deviceId }))
dialogTitle.value = '视频评论'; dialogTitle.value = '视频评论';
initialTextStr.value = getContentListMultiline();
setTimeout(() => { setTimeout(() => {
console.log("内容", initialTextStr.value, getContentListMultiline())
showDialog.value = true; showDialog.value = true;
initialTextStr.value = getContentListMultiline();
}, 500) }, 500)
}, },
@@ -466,7 +473,7 @@ const buttons = [
} }
} }
] ]
//联盟号
const isAlliance = ref(false) const isAlliance = ref(false)
@@ -696,6 +703,8 @@ onMounted(async () => {
ElMessage.error(`未检测到设备`) ElMessage.error(`未检测到设备`)
} }
console.log("sse过滤的数据", getHostfilters())
//MQ链接 //MQ链接
window.electronAPI.startMq(userdata.tenantId, userdata.id) window.electronAPI.startMq(userdata.tenantId, userdata.id)
@@ -765,7 +774,7 @@ onMounted(async () => {
} }
}, delay); }, delay);
} }
let FWnum = 0
// —— SSE 接收 —— // —— SSE 接收 ——
const es = connectSSE('http://localhost:3312/events', (data) => { const es = connectSSE('http://localhost:3312/events', (data) => {
// 所有 SSE 关掉的话,直接不处理 // 所有 SSE 关掉的话,直接不处理
@@ -796,6 +805,12 @@ onMounted(async () => {
let invitationType = '' let invitationType = ''
let id = '' let id = ''
// ⭐ 用于筛选的字段
let fans = null
let coins = null
let onlineFans = null
let level = '' // 主播等级
if (fromBoss) { if (fromBoss) {
// 大哥队列 b.tenant.* 进来的那条 JSON 结构: // 大哥队列 b.tenant.* 进来的那条 JSON 结构:
// {"id":5681,"displayId":"80",...,"region":"西南",...,"_mqMeta":2} // {"id":5681,"displayId":"80",...,"region":"西南",...,"_mqMeta":2}
@@ -807,14 +822,43 @@ onMounted(async () => {
|| (data.userId != null ? String(data.userId) : '') || (data.userId != null ? String(data.userId) : '')
invitationType = data.invitationType != null ? data.invitationType : 2 // 金票默认 2 invitationType = data.invitationType != null ? data.invitationType : 2 // 金票默认 2
id = data.id != null ? data.id : '' id = data.id != null ? data.id : ''
// ⭐ 这里根据真实字段来改,如果你后端是别的字段名,改这一段就行
fans = data.fllowernum ?? null
coins = data.hostsCoins ?? null
onlineFans = data.onlineFans ?? null
level = data.hostsLevel || ''
} else { } else {
// 爬虫队列 q.tenant.*(以前的老逻辑) // 爬虫队列 q.tenant.*(以前的老逻辑)
country = data && data.country != null ? data.country : '' country = data && data.country != null ? data.country : ''
text = data && (data.hostsId != null ? data.hostsId : data.text) text = data && (data.hostsId != null ? data.hostsId : data.text)
invitationType = data && (data.invitationType != null ? data.invitationType : '') invitationType = data && (data.invitationType != null ? data.invitationType : '')
id = data && data.id != null ? data.id : '' id = data && data.id != null ? data.id : ''
// ⭐ 同样根据真实字段改
fans = data.fllowernum ?? null
coins = data.hostsCoins ?? null
onlineFans = data.onlineFans ?? null
level = data.hostsLevel || ''
} }
// 4⃣ 先根据筛选条件过滤,不符合的直接丢掉
const pass = matchByHostFilters({
onlineFans,
level,
invitationType, // 把票种也传进去
})
if (!pass) {
FWnum++
console.log(
'丢弃' + FWnum + '条不符合筛选条件的主播:',
JSON.stringify({ text, invitationType, onlineFans, level })
)
return
}
if (!text) return if (!text) return
batch.push({ country, text, invitationType, id }) batch.push({ country, text, invitationType, id })
@@ -838,6 +882,7 @@ onMounted(async () => {
}) })
onUnmounted(() => { onUnmounted(() => {
@@ -1164,11 +1209,11 @@ function withTimeout(p, ms) {
} }
function startScheduleLoop() { function startScheduleLoop() {
lastInterruptTs = Date.now() lastInterruptTs = Date.now()
localStorage.setItem('INT_LAST_TS', String(lastInterruptTs)) localStorage.setItem('INT_LAST_TS', String(lastInterruptTs))
// 先按当前 index 跑一次,保持“即刻对齐” // 先按当前 index 跑一次,保持“即刻对齐”
runTask(schedulePlan[scheduleState.index].key, null, 1) runTask(schedulePlan[scheduleState.index].key, null, 1)
scheduleEnabled.value = true; // ✅ 关键:启动调度就必须开启
if (scheduleTimer) clearInterval(scheduleTimer) if (scheduleTimer) clearInterval(scheduleTimer)
@@ -1181,7 +1226,8 @@ function startScheduleLoop() {
const now = Date.now() const now = Date.now()
if (!lastInterruptTs) lastInterruptTs = now // 首次初始化 if (!lastInterruptTs) lastInterruptTs = now // 首次初始化
const due = now - lastInterruptTs >= interruptEveryMin.value * 60_000 * 60 // const due = now - lastInterruptTs >= interruptEveryMin.value * 60_000 * 60
const due = now - lastInterruptTs >= interruptEveryMin.value * 60_000
console.log( console.log(
'due=', due, 'due=', due,
@@ -1617,6 +1663,90 @@ const onToggleCrawler = (val) => {
const onToggleBoss = (val) => { const onToggleBoss = (val) => {
if (val) sseCrawlerEnabled.value = false if (val) sseCrawlerEnabled.value = false
} }
function timestampToTime(timestamp_ms) {
const date = new Date(timestamp_ms);
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
const seconds = date.getSeconds().toString().padStart(2, '0');
const milliseconds = date.getMilliseconds().toString().padStart(3, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
// ========= 主播过滤工具:使用 getHostfilters() 的参数 =========
// 区间匹配:如果范围没配就不拦
function matchRange(val, min, max) {
if (val == null || val === '') return true // 没值直接放过(按需改)
const n = Number(val)
if (!Number.isFinite(n)) return true // 非数字也直接放过
if (min != null && n < min) return false
if (max != null && n > max) return false
return true
}
// 等级匹配:没选等级就不拦,有选择时必须包含
function matchLevel(level, selected) {
if (!selected || !selected.length) return true
if (!level) return false
return selected.includes(level)
}
// 金票 / 普票过滤true = 不过滤, false = 过滤掉
function matchTicket(invitationType, goldFlag, ordinaryFlag) {
// invitationType 可能是字符串,统一转成数字
const t = Number(invitationType)
// 没配置时不限制
const gold = goldFlag
const ordinary = ordinaryFlag
// t == 2 金票
if (t === 2) {
if (gold === false) return false // 关闭金票 → 过滤掉
}
// t == 1 普票(或者其它都按普票看也行)
if (t === 1) {
if (ordinary === false) return false // 关闭普票 → 过滤掉
}
return true
}
// 根据 filters 判断当前主播数据是否命中
function matchByHostFilters(payload) {
const filters = getHostfilters() || {}
const {
min_onlineFans,
max_onlineFans,
hostslevel,
gold, // 新增
ordinary, // 新增
} = filters
const {
onlineFans,
level,
invitationType,
} = payload
// 在线人数
if (!matchRange(onlineFans, min_onlineFans, max_onlineFans)) return false
// 等级
if (!matchLevel(level, hostslevel)) return false
// 金票 / 普票
if (!matchTicket(invitationType, gold, ordinary)) return false
return true
}
</script> </script>
<style scoped lang="less"> <style scoped lang="less">