3.5.0版本
This commit is contained in:
@@ -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
|
||||||
@@ -10,27 +10,48 @@
|
|||||||
|
|
||||||
<!-- 工具栏 -->
|
<!-- 工具栏 -->
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<el-button size="small" @click="selectAll">全选</el-button>
|
<!-- 操作按钮行 -->
|
||||||
<el-button size="small" @click="selectNone">全不选</el-button>
|
<div class="toolbar-row">
|
||||||
<el-button size="small" @click="invertSelect">反选</el-button>
|
<el-button size="small" @click="selectAll">全选</el-button>
|
||||||
<el-button size="small" type="danger" :disabled="!selectedCount" @click="deleteSelected">删除选中</el-button>
|
<el-button size="small" @click="selectNone">全不选</el-button>
|
||||||
<el-switch v-model="gold" :loading="goldLoading" :before-change="goldBeforeChange" inline-prompt
|
<el-button size="small" @click="invertSelect">反选</el-button>
|
||||||
active-text="金票" inactive-text="金票" size="large" style="--el-switch-on-color: #db9600; " />
|
<el-button size="small" type="danger" :disabled="!selectedCount"
|
||||||
<el-switch v-model="ordinary" :loading="ordinaryLoading" :before-change="ordinaryBeforeChange" inline-prompt
|
@click="deleteSelected">删除选中</el-button>
|
||||||
inactive-text="普票" active-text="普票" size="large" />
|
<el-button size="small" @click="resetFilter">重置</el-button>
|
||||||
<!-- ✅ 新增:一键删除已处理 -->
|
|
||||||
<!-- <el-button size="small" type="warning" :disabled="!processedCount" @click="deleteProcessed">
|
<el-tooltip placement="bottom" effect="dark">
|
||||||
删除已处理
|
<template #content>
|
||||||
</el-button> -->
|
在空白区域按下左键拖拽进行框选<br />
|
||||||
|
</template>
|
||||||
|
<el-icon class="hint">i</el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
</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" />
|
||||||
|
|
||||||
|
|
||||||
<el-tooltip placement="bottom" effect="dark">
|
<!-- 在线人数 -->
|
||||||
<template #content>
|
<span class="filter-label">在线人数</span>
|
||||||
在空白区域按下左键拖拽进行框选<br />
|
<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" />
|
||||||
|
|
||||||
</template>
|
<!-- 主播等级(多选) -->
|
||||||
<el-icon class="hint">i</el-icon>
|
<span class="filter-label">主播等级</span>
|
||||||
</el-tooltip>
|
<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>
|
||||||
|
|
||||||
<!-- 列表区域 -->
|
<!-- 列表区域 -->
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 关掉的话,直接不处理
|
||||||
@@ -780,7 +789,7 @@ onMounted(async () => {
|
|||||||
// 1️⃣ 判断来源:_mqMeta = 1(q.tenant.*) / 2(b.tenant.*)
|
// 1️⃣ 判断来源:_mqMeta = 1(q.tenant.*) / 2(b.tenant.*)
|
||||||
const metaCode = data && data._mqMeta
|
const metaCode = data && data._mqMeta
|
||||||
const fromCrawler = metaCode === 1 || metaCode === '1' // 爬虫队列 q.tenant.*
|
const fromCrawler = metaCode === 1 || metaCode === '1' // 爬虫队列 q.tenant.*
|
||||||
const fromBoss = metaCode === 2 || metaCode === '2' // 大哥队列 b.tenant.*
|
const fromBoss = metaCode === 2 || metaCode === '2' // 大哥队列 b.tenant.*
|
||||||
|
|
||||||
// 没有标记的老数据,一律按爬虫处理(可选)
|
// 没有标记的老数据,一律按爬虫处理(可选)
|
||||||
const isUnknown = !fromCrawler && !fromBoss
|
const isUnknown = !fromCrawler && !fromBoss
|
||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user