Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f45cde984 | |||
| 5bdf5722a1 | |||
| c0125a5a9f | |||
| 1f8b830d27 | |||
| e2e39cc674 | |||
| 5e2da72d88 | |||
| b2f9dbf2a2 | |||
| 466a853905 | |||
| b81a0377b8 | |||
| 1c67cbc5ea | |||
| 6c86c11e60 |
@@ -6,5 +6,7 @@ VITE_REGISTER_API_URL=http://192.168.2.22:48080
|
||||
# VITE_REGISTER_API_URL=https://backstageapi.yolozs.com
|
||||
# pk api地址
|
||||
VITE_PK_MINI_API_URL=http://192.168.2.22:8086
|
||||
# VITE_PK_MINI_API_URL=https://pk.yolozs.com
|
||||
|
||||
# 商店地址
|
||||
VITE_SHOP_URL=https://www.tkzyw.com
|
||||
|
||||
25
index.html
25
index.html
@@ -1,13 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Yolo(AI助手Web版)</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit" async defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Yolo终端</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit" async defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
133
src/App.vue
133
src/App.vue
@@ -20,18 +20,20 @@
|
||||
|
||||
<!-- 浏览器页面 -->
|
||||
<div class="h-full w-full animate-fadeIn" v-show="currentPage === 'browser'">
|
||||
<WorkbenchLayout
|
||||
:account-groups="accountGroups"
|
||||
:rotation-status="rotationStatus"
|
||||
:greeting-stats="greetingStats"
|
||||
:automation-logs="automationLogs"
|
||||
@go-back="handleGoToConfig"
|
||||
@stop-all="handleStopAll"
|
||||
@logout="handleLogout"
|
||||
/>
|
||||
<WorkbenchLayout :account-groups="accountGroups" :rotation-status="rotationStatus"
|
||||
:greeting-stats="greetingStats" :automation-logs="automationLogs" @go-back="handleGoToConfig"
|
||||
@stop-all="handleStopAll" @logout="handleLogout" />
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- 关闭中加载遮罩 -->
|
||||
<div v-if="isClosing" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div class="bg-white rounded-lg p-8 flex flex-col items-center">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mb-4"></div>
|
||||
<p class="text-lg font-medium text-gray-800">关闭中,请稍候...</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -44,6 +46,7 @@ import WorkbenchLayout from './layout/WorkbenchLayout.vue'
|
||||
import UpdateNotification from './components/UpdateNotification.vue'
|
||||
import NoticeBar from './components/NoticeBar.vue'
|
||||
import { useNoticeStore } from './stores/noticeStore'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
// Constants
|
||||
const USER_KEY = 'user_data'
|
||||
@@ -53,6 +56,7 @@ const CONFIG_KEY = 'autoDm_runConfig'
|
||||
const updateReady = ref(false)
|
||||
const currentPage = ref('login')
|
||||
const isLoading = ref(false)
|
||||
const isClosing = ref(false)
|
||||
const automationStatus = ref({})
|
||||
const accountGroups = ref([])
|
||||
const viewAccountMap = ref({})
|
||||
@@ -67,15 +71,32 @@ const isDev = window.location.port === '5173'
|
||||
const noticeStore = useNoticeStore()
|
||||
noticeStore.fetchNotices()
|
||||
|
||||
// 定期检查新通知
|
||||
let noticeCheckInterval = null
|
||||
const startNoticeCheck = () => {
|
||||
// 每 5 分钟检查一次新通知
|
||||
noticeCheckInterval = setInterval(() => {
|
||||
noticeStore.fetchNotices()
|
||||
}, 5 * 60 * 1000)
|
||||
}
|
||||
|
||||
const stopNoticeCheck = () => {
|
||||
if (noticeCheckInterval) {
|
||||
clearInterval(noticeCheckInterval)
|
||||
noticeCheckInterval = null
|
||||
}
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
// Set Title
|
||||
getAppVersion().then(version => {
|
||||
document.title = `Yolo(AI助手Web版)v${version}`
|
||||
document.title = `Yolo终端v${version}`
|
||||
}).catch(() => {
|
||||
document.title = 'Yolo(AI助手Web版)'
|
||||
document.title = 'Yolo终端'
|
||||
})
|
||||
console.log('[App]',!isDev , isElectronEnv , !updateReady.value)
|
||||
|
||||
console.log('[App]', !isDev, isElectronEnv, !updateReady.value)
|
||||
// Check Login
|
||||
try {
|
||||
const userData = localStorage.getItem(USER_KEY)
|
||||
@@ -94,6 +115,25 @@ onMounted(() => {
|
||||
localStorage.removeItem(USER_KEY)
|
||||
})
|
||||
|
||||
// 应用关闭事件
|
||||
window.electronAPI.onAppClosingStart(async () => {
|
||||
console.log('[App] 收到应用关闭开始事件')
|
||||
isClosing.value = true
|
||||
// 隐藏所有视图,避免遮罩被视图盖住
|
||||
if (isElectron()) {
|
||||
window.electronAPI.hideViews().catch((e) => {
|
||||
console.warn('[App] 隐藏视图失败:', e)
|
||||
})
|
||||
|
||||
await handleStopAll()
|
||||
}
|
||||
})
|
||||
|
||||
window.electronAPI.onAppClosingComplete(() => {
|
||||
console.log('[App] 收到应用关闭完成事件')
|
||||
isClosing.value = false
|
||||
})
|
||||
|
||||
// Rotation Status
|
||||
window.electronAPI.getRotationStatus().then(status => {
|
||||
rotationStatus.value = status
|
||||
@@ -101,7 +141,7 @@ onMounted(() => {
|
||||
|
||||
window.electronAPI.onRotationStatusChanged(status => {
|
||||
rotationStatus.value = status
|
||||
console.log('[App] 收到轮换状态变化123:', status)
|
||||
console.log('[App] 收到轮换状态变化123:', status)
|
||||
// Auto switch tab if group changes
|
||||
if (status && status.currentActiveGroup && status.enabled) {
|
||||
console.log('[App] 收到轮换状态变化456:', status)
|
||||
@@ -130,9 +170,17 @@ console.log('[App] 收到轮换状态变化123:', status)
|
||||
|
||||
window.addEventListener('beforeunload', handleBeforeUnload)
|
||||
window.addEventListener('storage', handleStorageChange)
|
||||
// 监听账号组配置更新事件
|
||||
window.addEventListener('config-updated', handleConfigUpdate)
|
||||
|
||||
window.addEventListener('auth-expired', handleAuthExpired)
|
||||
|
||||
loadConfig()
|
||||
|
||||
// 启动定期检查新通知
|
||||
startNoticeCheck()
|
||||
console.log('[App] 已启动通知检查')
|
||||
|
||||
// Health Check
|
||||
startHealthCheck()
|
||||
})
|
||||
@@ -140,6 +188,10 @@ console.log('[App] 收到轮换状态变化123:', status)
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload)
|
||||
window.removeEventListener('storage', handleStorageChange)
|
||||
window.removeEventListener('config-updated', handleConfigUpdate)
|
||||
window.removeEventListener('auth-expired', handleAuthExpired)
|
||||
stopNoticeCheck()
|
||||
console.log('[App] 已停止通知检查')
|
||||
stopHealthCheck()
|
||||
})
|
||||
|
||||
@@ -154,24 +206,59 @@ const handleStorageChange = (e) => {
|
||||
}
|
||||
}
|
||||
|
||||
const clearLoginState = () => {
|
||||
localStorage.removeItem(USER_KEY)
|
||||
localStorage.removeItem('user')
|
||||
localStorage.removeItem('token')
|
||||
}
|
||||
|
||||
// 处理配置更新事件
|
||||
const handleConfigUpdate = () => {
|
||||
console.log('[App] 收到配置更新事件,重新加载配置')
|
||||
loadConfig()
|
||||
}
|
||||
|
||||
let healthCheckInterval = null
|
||||
|
||||
const resetToLogin = async (message) => {
|
||||
if (message) {
|
||||
ElMessage.error(message)
|
||||
}
|
||||
|
||||
stopHealthCheck()
|
||||
clearLoginState()
|
||||
currentPage.value = 'login'
|
||||
|
||||
if (isElectron()) {
|
||||
try {
|
||||
await window.electronAPI.hideViews()
|
||||
await handleStopAll()
|
||||
} catch (e) {
|
||||
console.warn('[App] 娓呯悊瑙嗗浘澶辫触:', e)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const handleAuthExpired = async (event) => {
|
||||
await resetToLogin(event?.detail?.message)
|
||||
}
|
||||
|
||||
const startHealthCheck = () => {
|
||||
const check = async () => {
|
||||
if (currentPage.value === 'login' || !isElectron()) return
|
||||
try {
|
||||
const result = await window.electronAPI.checkHealth()
|
||||
if (result.success && result.code === 40400) {
|
||||
alert('当前账号已在其他地方登录,请重新登录')
|
||||
localStorage.removeItem(USER_KEY)
|
||||
await resetToLogin(result.message); return
|
||||
// 隐藏所有 BrowserView 并停止自动化,防止视图悬浮在登录页上方
|
||||
try {
|
||||
await window.electronAPI.hideViews()
|
||||
await handleStopAll()
|
||||
} catch (e) {
|
||||
console.warn('[App] 清理视图失败:', e)
|
||||
}
|
||||
currentPage.value = 'login'
|
||||
// try {
|
||||
// await window.electronAPI.hideViews()
|
||||
// await handleStopAll()
|
||||
// } catch (e) {
|
||||
// console.warn('[App] 清理视图失败:', e)
|
||||
// }
|
||||
// currentPage.value = 'login'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[App] 健康检查失败:', error)
|
||||
@@ -233,7 +320,7 @@ const handleGoToConfig = async () => {
|
||||
const handleLogout = async () => {
|
||||
stopHealthCheck()
|
||||
currentPage.value = 'login'
|
||||
localStorage.removeItem(USER_KEY)
|
||||
clearLoginState()
|
||||
|
||||
if (isElectron()) {
|
||||
try { await window.electronAPI.logout() } catch (e) { console.warn('[App] logout失败:', e) }
|
||||
|
||||
@@ -226,3 +226,12 @@ export function resendEmail(data) {
|
||||
export function logout(data) {
|
||||
return postAxios({ url: 'user/logout', data })
|
||||
}
|
||||
// 获取商品列表
|
||||
export function getPkItemList(data) {
|
||||
return getAxios({ url: 'pkItem/list', params: data })
|
||||
}
|
||||
|
||||
// 购买商品
|
||||
export function buyPkItem(data) {
|
||||
return postAxios({ url: 'pkItem/buy', data })
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 6.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 5.4 KiB |
337
src/components/BrotherInfoDialog.vue
Normal file
337
src/components/BrotherInfoDialog.vue
Normal file
@@ -0,0 +1,337 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div v-if="visible" class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50">
|
||||
<div class="mx-4 flex max-h-[84vh] w-full max-w-6xl flex-col overflow-hidden rounded-2xl bg-white shadow-2xl">
|
||||
<div class="flex items-center justify-between border-b border-gray-200 px-5 py-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-900">大哥池</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">读取本地 sqlite 的 `brother_info` 数据</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button @click="handleDeleteAll" :disabled="loading || total === 0"
|
||||
class="rounded-lg bg-red-50 px-3 py-2 text-sm text-red-600 transition-colors hover:bg-red-100 disabled:cursor-not-allowed disabled:opacity-50">全部删除</button>
|
||||
<button @click="handleClose"
|
||||
class="rounded-lg px-3 py-2 text-sm text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-b border-gray-100 bg-gray-50 px-5 py-4">
|
||||
<div class="grid grid-cols-1 gap-3 md:grid-cols-4">
|
||||
<input v-model.trim="filters.keyword" type="text" placeholder="关键词:displayId / 昵称 / userIdStr"
|
||||
class="h-10 rounded-lg border border-gray-300 bg-white px-3 text-sm outline-none focus:border-blue-500"
|
||||
@keyup.enter="handleSearch" />
|
||||
<input v-model.trim="filters.region" type="text" placeholder="地区"
|
||||
class="h-10 rounded-lg border border-gray-300 bg-white px-3 text-sm outline-none focus:border-blue-500"
|
||||
@keyup.enter="handleSearch" />
|
||||
<input v-model.trim="filters.hostDisplayId" type="text" placeholder="主播 displayId"
|
||||
class="h-10 rounded-lg border border-gray-300 bg-white px-3 text-sm outline-none focus:border-blue-500"
|
||||
@keyup.enter="handleSearch" />
|
||||
<input v-model.trim="filters.displayId" type="text" placeholder="大哥 displayId"
|
||||
class="h-10 rounded-lg border border-gray-300 bg-white px-3 text-sm outline-none focus:border-blue-500"
|
||||
@keyup.enter="handleSearch" />
|
||||
</div>
|
||||
<div class="mt-3 grid grid-cols-2 gap-3 md:grid-cols-6">
|
||||
<input v-model.number="filters.minLevel" type="number" min="0" placeholder="最低等级"
|
||||
class="h-10 rounded-lg border border-gray-300 bg-white px-3 text-sm outline-none focus:border-blue-500" />
|
||||
<input v-model.number="filters.maxLevel" type="number" min="0" placeholder="最高等级"
|
||||
class="h-10 rounded-lg border border-gray-300 bg-white px-3 text-sm outline-none focus:border-blue-500" />
|
||||
<input v-model.number="filters.minCoins" type="number" min="0" placeholder="最低金币"
|
||||
class="h-10 rounded-lg border border-gray-300 bg-white px-3 text-sm outline-none focus:border-blue-500" />
|
||||
<input v-model.number="filters.maxCoins" type="number" min="0" placeholder="最高金币"
|
||||
class="h-10 rounded-lg border border-gray-300 bg-white px-3 text-sm outline-none focus:border-blue-500" />
|
||||
<select v-model.number="pageSize"
|
||||
class="h-10 rounded-lg border border-gray-300 bg-white px-3 text-sm outline-none focus:border-blue-500">
|
||||
<option :value="10">10 / 页</option>
|
||||
<option :value="20">20 / 页</option>
|
||||
<option :value="50">50 / 页</option>
|
||||
<option :value="100">100 / 页</option>
|
||||
</select>
|
||||
<div class="flex gap-2">
|
||||
<button @click="handleSearch" :disabled="loading"
|
||||
class="flex-1 rounded-lg bg-blue-600 px-3 py-2 text-sm text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60">查询</button>
|
||||
<button @click="handleReset" :disabled="loading"
|
||||
class="flex-1 rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700 transition-colors hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-60">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between border-b border-gray-100 px-5 py-3 text-sm text-gray-500">
|
||||
<div>共 {{ total }} 条,本页 {{ rows.length }} 条</div>
|
||||
<div v-if="loading" class="text-blue-600">加载中...</div>
|
||||
</div>
|
||||
|
||||
<div class="min-h-0 flex-1 overflow-auto">
|
||||
<table class="min-w-full border-collapse text-left text-sm">
|
||||
<thead class="sticky top-0 bg-slate-900 text-slate-100">
|
||||
<tr>
|
||||
<th class="px-4 py-3 font-medium">ID</th>
|
||||
<th class="px-4 py-3 font-medium">displayId</th>
|
||||
<th class="px-4 py-3 font-medium">昵称</th>
|
||||
<th class="px-4 py-3 font-medium">地区</th>
|
||||
<th class="px-4 py-3 font-medium">等级</th>
|
||||
<th class="px-4 py-3 font-medium">打赏金币</th>
|
||||
<th class="px-4 py-3 font-medium">粉丝数</th>
|
||||
<th class="px-4 py-3 font-medium">主播ID</th>
|
||||
<th class="px-4 py-3 font-medium">创建时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in rows" :key="row.id" class="border-b border-gray-100 hover:bg-blue-50/50">
|
||||
<td class="px-4 py-3 text-gray-700">{{ row.id ?? '-' }}</td>
|
||||
<td class="px-4 py-3 font-medium text-gray-900">{{ row.displayId || '-' }}</td>
|
||||
<td class="px-4 py-3 text-gray-700">{{ row.nickname || '-' }}</td>
|
||||
<td class="px-4 py-3 text-gray-700">{{ row.region || '-' }}</td>
|
||||
<td class="px-4 py-3 text-gray-700">{{ row.level ?? '-' }}</td>
|
||||
<td class="px-4 py-3 text-gray-700">{{ formatNumber(row.hostcoins) }}</td>
|
||||
<td class="px-4 py-3 text-gray-700">{{ formatNumber(row.followerCount) }}</td>
|
||||
<td class="px-4 py-3 text-gray-700">{{ row.hostDisplayId || '-' }}</td>
|
||||
<td class="px-4 py-3 text-gray-700">{{ formatTime(row.createTime) }}</td>
|
||||
</tr>
|
||||
<tr v-if="!loading && rows.length === 0">
|
||||
<td colspan="9" class="px-4 py-12 text-center text-gray-400">暂无大哥池数据</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between border-t border-gray-200 px-5 py-4">
|
||||
<div class="text-sm text-gray-500">第 {{ page }} / {{ totalPages || 1 }} 页</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button @click="goPrev" :disabled="loading || page <= 1"
|
||||
class="rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700 transition-colors hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50">上一页</button>
|
||||
<button @click="goNext" :disabled="loading || page >= totalPages"
|
||||
class="rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700 transition-colors hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50">下一页</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showDeleteConfirm" class="absolute inset-0 z-[10001] flex items-center justify-center bg-black/45 px-4">
|
||||
<div class="w-full max-w-md rounded-2xl bg-white p-6 shadow-2xl">
|
||||
<div class="text-lg font-semibold text-gray-900">确认全部删除</div>
|
||||
<p class="mt-3 text-sm leading-6 text-gray-600">
|
||||
将删除本地 sqlite 中 <code class="rounded bg-gray-100 px-1.5 py-0.5 text-xs text-gray-700">brother_info</code>
|
||||
的全部数据,删除后不可恢复。
|
||||
</p>
|
||||
<div class="mt-6 flex justify-end gap-3">
|
||||
<button
|
||||
@click="showDeleteConfirm = false"
|
||||
:disabled="loading"
|
||||
class="rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm text-gray-700 transition-colors hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
@click="confirmDeleteAll"
|
||||
:disabled="loading"
|
||||
class="rounded-lg bg-red-600 px-4 py-2 text-sm text-white transition-colors hover:bg-red-700 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{{ loading ? '删除中...' : '确认删除' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const loading = ref(false)
|
||||
const rows = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const totalPages = ref(0)
|
||||
const showDeleteConfirm = ref(false)
|
||||
const filters = reactive({
|
||||
keyword: '',
|
||||
region: '',
|
||||
hostDisplayId: '',
|
||||
displayId: '',
|
||||
minLevel: undefined,
|
||||
maxLevel: undefined,
|
||||
minCoins: undefined,
|
||||
maxCoins: undefined
|
||||
})
|
||||
|
||||
function getTkBridge() {
|
||||
return window?.electronAPI?.tk || null
|
||||
}
|
||||
|
||||
function buildPayload() {
|
||||
const nextFilters = {}
|
||||
for (const [key, value] of Object.entries(filters)) {
|
||||
if (value === '' || value === null || value === undefined) continue
|
||||
nextFilters[key] = value
|
||||
}
|
||||
return {
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
filters: nextFilters
|
||||
}
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
const tkBridge = getTkBridge()
|
||||
if (!tkBridge?.queryBrotherInfo) {
|
||||
rows.value = []
|
||||
total.value = 0
|
||||
totalPages.value = 0
|
||||
ElMessage.error('当前客户端未加载大哥池查询桥接,请重启客户端后再试')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const raw = await tkBridge.queryBrotherInfo(JSON.stringify(buildPayload()))
|
||||
const result = typeof raw === 'string' ? JSON.parse(raw) : raw
|
||||
if (result?.status !== 'success') {
|
||||
throw new Error(result?.message || '查询大哥池失败')
|
||||
}
|
||||
rows.value = Array.isArray(result?.list) ? result.list : []
|
||||
total.value = Number(result?.total || 0)
|
||||
totalPages.value = Math.max(1, Number(result?.totalPages || 0))
|
||||
page.value = Number(result?.page || page.value || 1)
|
||||
} catch (error) {
|
||||
rows.value = []
|
||||
total.value = 0
|
||||
totalPages.value = 0
|
||||
ElMessage.error(error instanceof Error ? error.message : String(error))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteAll() {
|
||||
const tkBridge = getTkBridge()
|
||||
if (!tkBridge?.getAllBrotherInfo || !tkBridge?.deleteBrotherInfo) {
|
||||
ElMessage.error('当前客户端未加载大哥池删除桥接,请重启客户端后再试')
|
||||
return
|
||||
}
|
||||
|
||||
showDeleteConfirm.value = true
|
||||
}
|
||||
|
||||
async function confirmDeleteAll() {
|
||||
const tkBridge = getTkBridge()
|
||||
if (!tkBridge?.getAllBrotherInfo || !tkBridge?.deleteBrotherInfo) {
|
||||
showDeleteConfirm.value = false
|
||||
ElMessage.error('当前客户端未加载大哥池删除桥接,请重启客户端后再试')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const rawList = await tkBridge.getAllBrotherInfo()
|
||||
const listResult = typeof rawList === 'string' ? JSON.parse(rawList) : rawList
|
||||
if (listResult?.status !== 'success') {
|
||||
throw new Error(listResult?.message || '读取大哥池失败')
|
||||
}
|
||||
|
||||
const ids = (Array.isArray(listResult?.list) ? listResult.list : [])
|
||||
.map(item => Number(item?.id))
|
||||
.filter(id => Number.isFinite(id))
|
||||
|
||||
if (ids.length === 0) {
|
||||
showDeleteConfirm.value = false
|
||||
ElMessage.success('大哥池已经是空的')
|
||||
rows.value = []
|
||||
total.value = 0
|
||||
totalPages.value = 1
|
||||
page.value = 1
|
||||
return
|
||||
}
|
||||
|
||||
const rawDelete = await tkBridge.deleteBrotherInfo(JSON.stringify({ ids }))
|
||||
const deleteResult = typeof rawDelete === 'string' ? JSON.parse(rawDelete) : rawDelete
|
||||
if (deleteResult?.status !== 'success') {
|
||||
throw new Error(deleteResult?.message || '删除大哥池失败')
|
||||
}
|
||||
|
||||
ElMessage.success(`已删除 ${ids.length} 条大哥池数据`)
|
||||
showDeleteConfirm.value = false
|
||||
page.value = 1
|
||||
await loadData()
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : String(error))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
page.value = 1
|
||||
void loadData()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
filters.keyword = ''
|
||||
filters.region = ''
|
||||
filters.hostDisplayId = ''
|
||||
filters.displayId = ''
|
||||
filters.minLevel = undefined
|
||||
filters.maxLevel = undefined
|
||||
filters.minCoins = undefined
|
||||
filters.maxCoins = undefined
|
||||
page.value = 1
|
||||
pageSize.value = 20
|
||||
void loadData()
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
function goPrev() {
|
||||
if (page.value <= 1) return
|
||||
page.value -= 1
|
||||
void loadData()
|
||||
}
|
||||
|
||||
function goNext() {
|
||||
if (page.value >= totalPages.value) return
|
||||
page.value += 1
|
||||
void loadData()
|
||||
}
|
||||
|
||||
function formatNumber(value) {
|
||||
if (value === null || value === undefined || value === '') return '-'
|
||||
const numericValue = Number(value)
|
||||
return Number.isFinite(numericValue) ? numericValue.toLocaleString() : String(value)
|
||||
}
|
||||
|
||||
function formatTime(value) {
|
||||
const timestamp = Number(value)
|
||||
if (!Number.isFinite(timestamp) || timestamp <= 0) return '-'
|
||||
const date = new Date(timestamp)
|
||||
if (Number.isNaN(date.getTime())) return '-'
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hour = String(date.getHours()).padStart(2, '0')
|
||||
const minute = String(date.getMinutes()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hour}:${minute}`
|
||||
}
|
||||
|
||||
watch(() => props.visible, (visible) => {
|
||||
if (!visible) return
|
||||
void loadData()
|
||||
})
|
||||
|
||||
watch(pageSize, () => {
|
||||
if (!props.visible) return
|
||||
page.value = 1
|
||||
void loadData()
|
||||
})
|
||||
</script>
|
||||
@@ -70,9 +70,12 @@
|
||||
|
||||
<!-- 翻译开关 -->
|
||||
<div class="flex items-center gap-3">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" v-model="needTranslate" class="w-4 h-4" />
|
||||
<span class="text-sm text-gray-700">启用翻译</span>
|
||||
<label class="flex items-center gap-2 cursor-pointer"
|
||||
:class="{ 'opacity-50': inputMode === 'individual' }">
|
||||
<input type="checkbox" v-model="needTranslate" class="w-4 h-4"
|
||||
:disabled="inputMode === 'individual'" />
|
||||
<span class="text-sm text-gray-700">{{ inputMode === 'individual' ? '单个编辑模式下翻译不可用' : '启用翻译'
|
||||
}}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -212,9 +215,20 @@ const emit = defineEmits(['close', 'confirm'])
|
||||
const STORAGE_KEY = 'greeting_dialog_data'
|
||||
const REGION_LIST = getRegions()
|
||||
|
||||
// 为不同模式分别存储内容
|
||||
const sentences = ref([''])
|
||||
const bulkText = ref('')
|
||||
const inputMode = ref('bulk') // 'bulk' 或 'individual'
|
||||
// 为两个模式分别存储内容
|
||||
const modeData = ref({
|
||||
bulk: {
|
||||
sentences: [''],
|
||||
bulkText: ''
|
||||
},
|
||||
individual: {
|
||||
sentences: ['']
|
||||
}
|
||||
})
|
||||
const selectedRegions = ref([])
|
||||
const translations = ref({})
|
||||
const activeTab = ref('')
|
||||
@@ -286,18 +300,48 @@ watch(selectedLanguages, (newLangs) => {
|
||||
}
|
||||
})
|
||||
|
||||
// 当模式切换时,保存当前模式的内容并加载新模式的内容
|
||||
watch(inputMode, (newMode, oldMode) => {
|
||||
// 保存旧模式的内容
|
||||
if (oldMode) {
|
||||
if (oldMode === 'bulk') {
|
||||
modeData.value.bulk.sentences = [...sentences.value]
|
||||
modeData.value.bulk.bulkText = bulkText.value
|
||||
} else if (oldMode === 'individual') {
|
||||
modeData.value.individual.sentences = [...sentences.value]
|
||||
}
|
||||
}
|
||||
|
||||
// 加载新模式的内容
|
||||
if (newMode === 'bulk') {
|
||||
sentences.value = [...modeData.value.bulk.sentences]
|
||||
bulkText.value = modeData.value.bulk.bulkText
|
||||
} else if (newMode === 'individual') {
|
||||
sentences.value = [...modeData.value.individual.sentences]
|
||||
needTranslate.value = false
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
function loadFromStorage() {
|
||||
const saved = localStorage.getItem(STORAGE_KEY)
|
||||
if (saved) {
|
||||
try {
|
||||
const data = JSON.parse(saved)
|
||||
if (data.sentences?.length) sentences.value = data.sentences
|
||||
if (data.modeData) modeData.value = data.modeData
|
||||
if (data.selectedRegions?.length) selectedRegions.value = data.selectedRegions
|
||||
if (data.translations) translations.value = data.translations
|
||||
if (typeof data.needTranslate === 'boolean') needTranslate.value = data.needTranslate
|
||||
if (data.activeTab) activeTab.value = data.activeTab
|
||||
if (data.inputMode) inputMode.value = data.inputMode
|
||||
|
||||
// 加载当前模式的内容
|
||||
if (inputMode.value === 'bulk') {
|
||||
sentences.value = [...modeData.value.bulk.sentences]
|
||||
bulkText.value = modeData.value.bulk.bulkText
|
||||
} else if (inputMode.value === 'individual') {
|
||||
sentences.value = [...modeData.value.individual.sentences]
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载本地数据失败:', e)
|
||||
}
|
||||
@@ -305,8 +349,16 @@ function loadFromStorage() {
|
||||
}
|
||||
|
||||
function saveToStorage() {
|
||||
// 保存当前模式的内容到 modeData
|
||||
if (inputMode.value === 'bulk') {
|
||||
modeData.value.bulk.sentences = [...sentences.value]
|
||||
modeData.value.bulk.bulkText = bulkText.value
|
||||
} else if (inputMode.value === 'individual') {
|
||||
modeData.value.individual.sentences = [...sentences.value]
|
||||
}
|
||||
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify({
|
||||
sentences: sentences.value,
|
||||
modeData: modeData.value,
|
||||
selectedRegions: selectedRegions.value,
|
||||
translations: translations.value,
|
||||
needTranslate: needTranslate.value,
|
||||
|
||||
@@ -68,14 +68,12 @@
|
||||
<!-- 下拉菜单 -->
|
||||
<div v-if="showLevelDropdown"
|
||||
class="absolute top-full left-4 mt-1 bg-white border border-gray-200 rounded-lg shadow-lg z-50 p-3 w-72">
|
||||
<div class="text-xs text-gray-500 mb-2">选择接收的主播等级(不选则接收全部)</div>
|
||||
<div class="text-xs text-gray-500 mb-2">选择接收的主播等级(不勾选则过滤掉)</div>
|
||||
<div class="space-y-2 max-h-60 overflow-auto">
|
||||
<div v-for="parent in LEVEL_OPTIONS" :key="parent.value"
|
||||
class="border border-gray-100 rounded p-2">
|
||||
<label
|
||||
class="flex items-center gap-2 cursor-pointer font-medium text-gray-700">
|
||||
<input type="checkbox"
|
||||
:checked="isParentSelected(parent).allSelected"
|
||||
<label class="flex items-center gap-2 cursor-pointer font-medium text-gray-700">
|
||||
<input type="checkbox" :checked="isParentSelected(parent).allSelected"
|
||||
:indeterminate="isParentSelected(parent).partialSelected"
|
||||
@change="toggleParentLevel(parent.value)" class="w-4 h-4" />
|
||||
{{ parent.label }} 级
|
||||
@@ -95,10 +93,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 pt-2 border-t border-gray-100 flex justify-between">
|
||||
<button @click="updateLevelFilter(new Set())"
|
||||
class="text-xs text-gray-500 hover:text-gray-700">
|
||||
清空选择
|
||||
</button>
|
||||
<div class="flex gap-2">
|
||||
<button @click="selectAllLevels()"
|
||||
class="text-xs text-gray-500 hover:text-gray-700">
|
||||
全选
|
||||
</button>
|
||||
<button @click="selectNoneLevels()"
|
||||
class="text-xs text-gray-500 hover:text-gray-700">
|
||||
全不选
|
||||
</button>
|
||||
</div>
|
||||
<button @click="showLevelDropdown = false"
|
||||
class="text-xs text-blue-600 hover:text-blue-700">
|
||||
完成
|
||||
@@ -120,11 +124,10 @@
|
||||
<!-- 主播列表 -->
|
||||
<div class="flex-1 overflow-auto p-4">
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
|
||||
<div v-for="host in filteredHosts" :key="host.id" @click="toggleSelect(host.id)"
|
||||
:class="[
|
||||
'p-3 rounded-lg border cursor-pointer transition-all',
|
||||
selected.has(host.id) ? 'border-blue-500 bg-blue-50 shadow' : 'border-gray-200 hover:border-gray-300 hover:shadow-sm'
|
||||
]">
|
||||
<div v-for="host in filteredHosts" :key="host.id" @click="toggleSelect(host.id)" :class="[
|
||||
'p-3 rounded-lg border cursor-pointer transition-all',
|
||||
selected.has(host.id) ? 'border-blue-500 bg-blue-50 shadow' : 'border-gray-200 hover:border-gray-300 hover:shadow-sm'
|
||||
]">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="font-medium text-sm truncate flex-1" :title="host.anchorId">
|
||||
{{ host.anchorId }}
|
||||
@@ -182,7 +185,8 @@
|
||||
<!-- 主播ID输入 -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">主播ID(每行一个,支持批量粘贴)</label>
|
||||
<textarea v-model="addForm.idsText" rows="6" placeholder="粘贴主播ID,每行一个 例如: anchor_001 anchor_002 anchor_003"
|
||||
<textarea v-model="addForm.idsText" rows="6"
|
||||
placeholder="粘贴主播ID,每行一个 例如: anchor_001 anchor_002 anchor_003"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:border-blue-500 focus:outline-none resize-none font-mono"></textarea>
|
||||
<div class="text-xs text-gray-400 mt-1">
|
||||
已输入 {{ parsedIds.length }} 个ID
|
||||
@@ -229,7 +233,8 @@
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<div class="p-4 border-t border-gray-200 flex justify-between items-center">
|
||||
<span v-if="addStatus" :class="['text-sm', addStatus.type === 'success' ? 'text-green-600' : 'text-red-600']">
|
||||
<span v-if="addStatus"
|
||||
:class="['text-sm', addStatus.type === 'success' ? 'text-green-600' : 'text-red-600']">
|
||||
{{ addStatus.message }}
|
||||
</span>
|
||||
<span v-else></span>
|
||||
@@ -309,7 +314,7 @@ const filters = ref({
|
||||
minOnlineFans: '',
|
||||
maxOnlineFans: '',
|
||||
})
|
||||
const maxCount = ref(100)
|
||||
const maxCount = ref()
|
||||
const selectedLevels = ref(new Set())
|
||||
const showLevelDropdown = ref(false)
|
||||
|
||||
@@ -418,11 +423,16 @@ const loadConfig = async () => {
|
||||
if (!isElectron()) return
|
||||
try {
|
||||
const config = await window.electronAPI.getAutomationConfig()
|
||||
if (config?.maxAnchorCount !== undefined) {
|
||||
maxCount.value = config.maxAnchorCount
|
||||
if (config?.filters?.maxAnchorCount !== undefined) {
|
||||
// 如果后端返回 9999999,前端显示为空
|
||||
maxCount.value = config.filters.maxAnchorCount === 9999999 ? '' : config.filters.maxAnchorCount
|
||||
}
|
||||
if (config?.filters?.hostsLevelList) {
|
||||
selectedLevels.value = new Set(config.filters.hostsLevelList)
|
||||
// 计算反向等级列表:后端存储的是要过滤的等级,前端需要的是要显示的等级
|
||||
const allLevels = LEVEL_OPTIONS.flatMap(parent => parent.children.map(child => child.value))
|
||||
const excludedLevels = config.filters.hostsLevelList
|
||||
const includedLevels = allLevels.filter(level => !excludedLevels.includes(level))
|
||||
selectedLevels.value = new Set(includedLevels)
|
||||
}
|
||||
// 加载进阶票/普票过滤配置
|
||||
if (config?.filters?.gold !== undefined) {
|
||||
@@ -462,10 +472,21 @@ const updateLevelFilter = async (levels) => {
|
||||
selectedLevels.value = levels
|
||||
if (!isElectron()) return
|
||||
try {
|
||||
// 计算反向等级列表:前端勾选的是要显示的,后端需要的是要过滤的(不显示的)
|
||||
const allLevels = LEVEL_OPTIONS.flatMap(parent => parent.children.map(child => child.value))
|
||||
const includedLevels = Array.from(levels)
|
||||
|
||||
// 当全不选时,传给后端的是空数组(不过滤任何等级)
|
||||
// 当有选择时,传给后端的是未选择的等级(过滤这些等级)
|
||||
let excludedLevels = []
|
||||
if (includedLevels.length > 0) {
|
||||
excludedLevels = allLevels.filter(level => !includedLevels.includes(level))
|
||||
}
|
||||
|
||||
await window.electronAPI.updateAutomationConfig({
|
||||
filters: { hostsLevelList: Array.from(levels) }
|
||||
filters: { hostsLevelList: excludedLevels }
|
||||
})
|
||||
console.log('[HostListDialog] 等级过滤已更新:', Array.from(levels))
|
||||
console.log('[HostListDialog] 等级过滤已更新:', excludedLevels)
|
||||
} catch (e) {
|
||||
console.error('更新等级配置失败:', e)
|
||||
}
|
||||
@@ -494,12 +515,27 @@ const toggleParentLevel = (parentValue) => {
|
||||
updateLevelFilter(newSet)
|
||||
}
|
||||
|
||||
const selectAllLevels = () => {
|
||||
// 全选所有等级
|
||||
const allLevels = LEVEL_OPTIONS.flatMap(parent => parent.children.map(child => child.value))
|
||||
updateLevelFilter(new Set(allLevels))
|
||||
}
|
||||
|
||||
const selectNoneLevels = () => {
|
||||
// 全不选所有等级
|
||||
updateLevelFilter(new Set())
|
||||
}
|
||||
|
||||
const updateMaxCount = async (value) => {
|
||||
// value is already updated via v-model
|
||||
if (!isElectron()) return
|
||||
try {
|
||||
await window.electronAPI.updateAutomationConfig({ maxAnchorCount: value })
|
||||
console.log('[HostListDialog] 主播数据上限已更新:', value)
|
||||
// 如果不填写,传 9999999 表示无限制
|
||||
const maxAnchorCount = value || 9999999
|
||||
await window.electronAPI.updateAutomationConfig({
|
||||
filters: { maxAnchorCount }
|
||||
})
|
||||
console.log('[HostListDialog] 主播数据上限已更新:', maxAnchorCount)
|
||||
} catch (e) {
|
||||
console.error('更新配置失败:', e)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<!-- info / 无 category 的公告:滚动栏显示 title -->
|
||||
<div v-if="infoNotices.length > 0"
|
||||
:class="['notice-bar', 'notice-bar--info']">
|
||||
<div v-if="infoNotices.length > 0" :class="['notice-bar', 'notice-bar--info']">
|
||||
<!-- 图标 -->
|
||||
<span class="material-icons-round notice-bar__icon">campaign</span>
|
||||
|
||||
@@ -18,31 +17,20 @@
|
||||
</span>
|
||||
|
||||
<!-- 关闭按钮 -->
|
||||
<button v-if="closable" class="notice-bar__close" @click="handleClose"
|
||||
:title="t('notice.close')">
|
||||
<button v-if="closable" class="notice-bar__close" @click="handleClose" :title="t('notice.close')">
|
||||
<span class="material-icons-round text-base">close</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- danger / warning 的公告:弹窗逐条显示 title + content -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="currentAlert?.title"
|
||||
width="480px"
|
||||
:close-on-click-modal="false"
|
||||
align-center
|
||||
>
|
||||
<el-dialog v-model="dialogVisible" :title="currentAlert?.title" width="480px" align-center>
|
||||
<div class="alert-notice__content" v-html="currentAlert?.content"></div>
|
||||
<template #footer>
|
||||
<el-button
|
||||
v-if="alertIndex < alertNotices.length - 1"
|
||||
@click="nextAlert"
|
||||
>
|
||||
<el-button v-if="alertIndex < alertNotices.length - 1" @click="nextAlert">
|
||||
下一条 ({{ alertIndex + 1 }}/{{ alertNotices.length }})
|
||||
</el-button>
|
||||
<el-button type="primary" @click="closeAlert">
|
||||
{{ alertIndex < alertNotices.length - 1 ? '全部关闭' : '我知道了' }}
|
||||
</el-button>
|
||||
{{ alertIndex < alertNotices.length - 1 ? '全部关闭' : '我知道了' }} </el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@@ -250,6 +238,7 @@ onUnmounted(() => {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ const props = defineProps({
|
||||
permissionKey: {
|
||||
type: String,
|
||||
required: true,
|
||||
validator: (value) => ['bigBrother', 'crawl', 'webAi'].includes(value)
|
||||
validator: (value) => ['bigBrother', 'crawl', 'webAi', 'autotk'].includes(value)
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
@@ -120,11 +120,17 @@ const visibleIndices = ref([])
|
||||
const visibleContacts = ref([])
|
||||
|
||||
const guardKey = computed(() => `guard_${props.permissionKey}_${Date.now()}`)
|
||||
const effectivePermissionKey = computed(() => {
|
||||
if (props.permissionKey === 'webAi' && props.title.includes('TK')) {
|
||||
return 'autotk'
|
||||
}
|
||||
return props.permissionKey
|
||||
})
|
||||
|
||||
const permissionsData = ref(getPermissions())
|
||||
|
||||
const hasAccess = computed(() => {
|
||||
return permissionsData.value[props.permissionKey] === 1
|
||||
return permissionsData.value[effectivePermissionKey.value] === 1
|
||||
})
|
||||
|
||||
const pickRandomIndices = (sourceIndices, count) => {
|
||||
@@ -175,6 +181,7 @@ const refreshPage = async () => {
|
||||
bigBrother: res.bigBrother,
|
||||
crawl: res.crawl,
|
||||
webAi: res.webAi,
|
||||
autotk: res.autotk ?? res.autoTK,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,18 +307,20 @@ const showInviteList = async () => {
|
||||
|
||||
// 复制主播 ID
|
||||
const copyAnchorId = (id) => {
|
||||
// 去除前面的 @ 符号
|
||||
const cleanId = id.startsWith('@') ? id.substring(1) : id
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
// 现代浏览器使用 Clipboard API
|
||||
navigator.clipboard.writeText(id).then(() => {
|
||||
navigator.clipboard.writeText(cleanId).then(() => {
|
||||
showCopySuccess()
|
||||
}).catch(err => {
|
||||
console.error('复制失败:', err)
|
||||
// 回退到传统方法
|
||||
fallbackCopyTextToClipboard(id)
|
||||
fallbackCopyTextToClipboard(cleanId)
|
||||
})
|
||||
} else {
|
||||
// 传统方法
|
||||
fallbackCopyTextToClipboard(id)
|
||||
fallbackCopyTextToClipboard(cleanId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,11 +19,13 @@
|
||||
</div>
|
||||
<div class="country">{{ item.country }}</div>
|
||||
<div class="stat-item">
|
||||
<img class="stat-icon" src="https://vv-1317974657.cos.ap-shanghai.myqcloud.com/util/gold.png" alt="" />
|
||||
<img class="stat-icon" src="https://vv-1317974657.cos.ap-shanghai.myqcloud.com/util/gold.png"
|
||||
alt="" />
|
||||
<span>金币: <b>{{ item.coin }}K</b></span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<img class="stat-icon" src="https://vv-1317974657.cos.ap-shanghai.myqcloud.com/util/session.png" alt="" />
|
||||
<img class="stat-icon" src="https://vv-1317974657.cos.ap-shanghai.myqcloud.com/util/session.png"
|
||||
alt="" />
|
||||
<span>场次: <b>{{ item.pkNumber }}场</b></span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -66,35 +68,20 @@
|
||||
|
||||
<!-- 国家 -->
|
||||
<div class="form-row">
|
||||
<el-select-v2
|
||||
v-model="formData.country"
|
||||
:options="countryOptions"
|
||||
placeholder="请选择国家"
|
||||
filterable
|
||||
style="width: 100%"
|
||||
/>
|
||||
<el-select-v2 v-model="formData.country" :options="countryOptions" placeholder="请选择国家" filterable
|
||||
style="width: 100%" />
|
||||
</div>
|
||||
|
||||
<!-- 性别 -->
|
||||
<div class="form-row">
|
||||
<el-select-v2
|
||||
v-model="formData.gender"
|
||||
:options="genderOptions"
|
||||
placeholder="请选择性别"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<el-select-v2 v-model="formData.gender" :options="genderOptions" placeholder="请选择性别"
|
||||
style="width: 100%" />
|
||||
</div>
|
||||
|
||||
<!-- PK时间 -->
|
||||
<div class="form-row">
|
||||
<el-date-picker
|
||||
v-model="formData.pkTime"
|
||||
type="datetime"
|
||||
placeholder="请选择PK时间"
|
||||
style="width: 100%"
|
||||
format="YYYY/MM/DD HH:mm"
|
||||
value-format="x"
|
||||
/>
|
||||
<el-date-picker v-model="formData.pkTime" type="datetime" placeholder="请选择PK时间(北京时间)" style="width: 100%"
|
||||
:formatter="formatBeijingTime" :parser="parseBeijingTime" value-format="x" />
|
||||
</div>
|
||||
|
||||
<!-- 金币和场次 -->
|
||||
@@ -136,13 +123,8 @@
|
||||
<el-dialog v-model="showAnchorDialog" title="选择我的主播" width="800" align-center>
|
||||
<div class="anchor-dialog-content">
|
||||
<div class="anchor-list">
|
||||
<div
|
||||
v-for="(item, index) in anchorLibrary"
|
||||
:key="index"
|
||||
class="anchor-item"
|
||||
:class="{ selected: selectedAnchor === item }"
|
||||
@click="selectedAnchor = item"
|
||||
>
|
||||
<div v-for="(item, index) in anchorLibrary" :key="index" class="anchor-item"
|
||||
:class="{ selected: selectedAnchor === item }" @click="selectedAnchor = item">
|
||||
<img class="anchor-avatar" :src="item.headerIcon" alt="" />
|
||||
<div class="anchor-info">
|
||||
<div class="anchor-name">{{ item.anchorId }}</div>
|
||||
@@ -167,12 +149,7 @@
|
||||
<el-dialog v-model="showTopDialog" title="置顶" width="500" align-center>
|
||||
<div class="top-dialog-content">
|
||||
<p class="top-tip">置顶后,您的PK信息将在首页优先展示,可以获得更多曝光机会。</p>
|
||||
<el-select-v2
|
||||
v-model="topDuration"
|
||||
:options="topDurationOptions"
|
||||
placeholder="请选择置顶时长"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<el-select-v2 v-model="topDuration" :options="topDurationOptions" placeholder="请选择置顶时长" style="width: 100%" />
|
||||
<div class="dialog-btns">
|
||||
<div class="reset-btn" @click="showTopDialog = false">取消</div>
|
||||
<div class="confirm-btn" @click="confirmTop">确认置顶</div>
|
||||
@@ -194,7 +171,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import {
|
||||
getPkInfo,
|
||||
releasePkInfo,
|
||||
@@ -227,6 +204,36 @@ const list = ref([])
|
||||
const page = ref(0)
|
||||
const formatTime = TimestamptolocalTime
|
||||
|
||||
// 北京时间格式化函数
|
||||
function formatBeijingTime(timestamp) {
|
||||
// 创建一个UTC时间的Date对象
|
||||
const utcDate = new Date(timestamp)
|
||||
|
||||
// 将UTC时间转换为北京时间(UTC+8)
|
||||
const beijingDate = new Date(utcDate.getTime() + 8 * 60 * 60 * 1000)
|
||||
|
||||
const year = beijingDate.getUTCFullYear()
|
||||
const month = String(beijingDate.getUTCMonth() + 1).padStart(2, '0')
|
||||
const day = String(beijingDate.getUTCDate()).padStart(2, '0')
|
||||
const hours = String(beijingDate.getUTCHours()).padStart(2, '0')
|
||||
const minutes = String(beijingDate.getUTCMinutes()).padStart(2, '0')
|
||||
|
||||
return `${year}/${month}/${day} ${hours}:${minutes}`
|
||||
}
|
||||
|
||||
// 解析北京时间字符串为时间戳
|
||||
function parseBeijingTime(dateString) {
|
||||
const [datePart, timePart] = dateString.split(' ')
|
||||
const [year, month, day] = datePart.split('/').map(Number)
|
||||
const [hours, minutes] = timePart.split(':').map(Number)
|
||||
|
||||
// 创建一个UTC时间的Date对象,将北京时间的小时减去8小时
|
||||
const utcDate = new Date(Date.UTC(year, month - 1, day, hours - 8, minutes, 0, 0))
|
||||
|
||||
// 返回UTC时间的时间戳(毫秒)
|
||||
return utcDate.getTime()
|
||||
}
|
||||
|
||||
// 表单数据
|
||||
const formData = ref({
|
||||
anchorName: '',
|
||||
@@ -355,6 +362,7 @@ async function handleSubmit() {
|
||||
}
|
||||
if (formData.value.pkTime < Date.now()) {
|
||||
ElMessage.error('PK时间不能早于当前时间')
|
||||
console.log(formData.value.pkTime)
|
||||
return
|
||||
}
|
||||
if (!formData.value.country) {
|
||||
@@ -567,7 +575,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.card-content:hover {
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.2);
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
@@ -766,7 +774,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.confirm-btn:hover {
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.2);
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
@@ -785,7 +793,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.reset-btn:hover {
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.2);
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<div class="points-text">
|
||||
我的积分: <span class="points-num">{{ currentUser.points || 0 }}</span>
|
||||
</div>
|
||||
<button class="exchange-btn" @click="openExchangeDialog">积分兑换</button>
|
||||
</div>
|
||||
|
||||
<div class="points-list" v-if="pointsList.length > 0">
|
||||
@@ -18,14 +19,51 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="empty-tip" v-else>您还没有积分记录!</div>
|
||||
|
||||
<!-- 积分兑换弹窗 -->
|
||||
<div class="exchange-dialog" v-if="exchangeDialogVisible">
|
||||
<div class="dialog-overlay" @click="closeExchangeDialog"></div>
|
||||
<div class="dialog-content">
|
||||
<div class="dialog-header">
|
||||
<div class="dialog-title">积分兑换</div>
|
||||
<div class="close-btn" @click="closeExchangeDialog">×</div>
|
||||
</div>
|
||||
<div class="dialog-body">
|
||||
<div class="user-info">
|
||||
我的积分: <span class="points-num">{{ currentUser.points || 0 }}</span>
|
||||
</div>
|
||||
<div class="item-list" v-if="itemList.length > 0">
|
||||
<div class="item-card" v-for="item in itemList" :key="item.id">
|
||||
<div class="item-info">
|
||||
<div class="item-header">
|
||||
<div class="item-name">{{ item.itemName }}</div>
|
||||
<div class="item-price">
|
||||
<span class="price-tag">积分</span>
|
||||
<span class="price-num">{{ item.itemPrice }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-desc">{{ item.itemDesc }}</div>
|
||||
<button class="exchange-btn" @click="exchangeItem(item)"
|
||||
:disabled="(currentUser.points || 0) < item.itemPrice">
|
||||
{{ (currentUser.points || 0) < item.itemPrice ? '积分不足' : '立即兑换' }} </button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="empty-tip" v-else-if="!loading">暂无商品</div>
|
||||
<div class="loading-tip" v-else>加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getIntegralDetail } from '@/api/pk-mini'
|
||||
import { getIntegralDetail, getPkItemList, buyPkItem } from '@/api/pk-mini'
|
||||
import { getMainUserData } from '@/utils/pk-mini/storage'
|
||||
import { TimestamptolocalTime } from '@/utils/pk-mini/timeConversion'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getCurrent } from '@/api/account'
|
||||
|
||||
function getUserId(user) {
|
||||
return user?.id || user?.userId || user?.uid || null
|
||||
@@ -36,6 +74,66 @@ const pointsList = ref([])
|
||||
const page = ref(0)
|
||||
const formatTime = TimestamptolocalTime
|
||||
|
||||
// 弹窗状态
|
||||
const exchangeDialogVisible = ref(false)
|
||||
const itemList = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 打开积分兑换弹窗
|
||||
const openExchangeDialog = async () => {
|
||||
exchangeDialogVisible.value = true
|
||||
await loadItemList()
|
||||
}
|
||||
|
||||
// 关闭积分兑换弹窗
|
||||
const closeExchangeDialog = () => {
|
||||
exchangeDialogVisible.value = false
|
||||
}
|
||||
|
||||
// 加载商品列表
|
||||
const loadItemList = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const res = await getPkItemList({})
|
||||
if (res && res.length > 0) {
|
||||
itemList.value = res
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载商品列表失败', e)
|
||||
ElMessage.error('加载商品列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 兑换商品
|
||||
const exchangeItem = async (item) => {
|
||||
const userPoints = currentUser.value.points || 0
|
||||
if (userPoints < item.itemPrice) {
|
||||
ElMessage.warning('积分不足')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 调用兑换接口
|
||||
const res = await buyPkItem({ itemId: item.id })
|
||||
if (res) {
|
||||
ElMessage.success(`兑换 ${item.itemName} 成功`)
|
||||
// 重新获取用户信息,更新积分
|
||||
const userRes = await getCurrent()
|
||||
if (userRes) {
|
||||
currentUser.value = userRes
|
||||
}
|
||||
closeExchangeDialog()
|
||||
} else {
|
||||
ElMessage.error('兑换失败,请稍后重试')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('兑换商品失败', e)
|
||||
ElMessage.error('兑换失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPointsList() {
|
||||
const userId = getUserId(currentUser.value)
|
||||
if (!userId) return
|
||||
@@ -54,8 +152,22 @@ async function loadPointsList() {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
currentUser.value = getMainUserData() || {}
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// 获取最新的用户信息,包括积分
|
||||
const res = await getCurrent()
|
||||
if (res) {
|
||||
currentUser.value = res
|
||||
} else {
|
||||
// 如果获取失败,使用缓存中的数据
|
||||
currentUser.value = getMainUserData() || {}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取用户信息失败', e)
|
||||
// 出错时使用缓存中的数据
|
||||
currentUser.value = getMainUserData() || {}
|
||||
}
|
||||
|
||||
const userId = getUserId(currentUser.value)
|
||||
if (userId) {
|
||||
loadPointsList()
|
||||
@@ -73,8 +185,27 @@ onMounted(() => {
|
||||
width: 100%;
|
||||
height: 70px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.exchange-btn {
|
||||
padding: 8px 16px;
|
||||
background: #ff6b35;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: #ff5216;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.points-icon {
|
||||
@@ -148,4 +279,181 @@ onMounted(() => {
|
||||
font-size: 18px;
|
||||
color: #03aba8;
|
||||
}
|
||||
|
||||
/* 积分兑换弹窗样式 */
|
||||
.exchange-dialog {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.dialog-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
position: relative;
|
||||
width: 90%;
|
||||
max-width: 800px;
|
||||
max-height: 80vh;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||
|
||||
.dialog-header {
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.dialog-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
font-size: 24px;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
padding: 20px;
|
||||
max-height: calc(80vh - 60px);
|
||||
overflow: auto;
|
||||
|
||||
.user-info {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.points-num {
|
||||
font-weight: bold;
|
||||
color: #ff6b35;
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.item-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.item-card {
|
||||
background: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
.item-info {
|
||||
padding: 15px;
|
||||
|
||||
.item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.item-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
margin-right: 10px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.item-price {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.price-tag {
|
||||
font-size: 12px;
|
||||
color: #ff6b35;
|
||||
background: rgba(255, 107, 53, 0.1);
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.price-num {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #ff6b35;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-desc {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 15px;
|
||||
line-height: 1.4;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.exchange-btn {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
background: #ff6b35;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #ff5216;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-tip,
|
||||
.loading-tip {
|
||||
height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,73 +1,71 @@
|
||||
<template>
|
||||
<div class="flex h-screen w-screen overflow-hidden bg-white">
|
||||
<!-- Left Navigation Sidebar -->
|
||||
<div ref="sidebarRef" class="flex flex-col items-center py-4 border-r z-50" style="flex: 0 0 calc(100vw * 2 / 19); min-width: 96px; max-width: 400px; background-color: #F8F9FA;">
|
||||
<div ref="sidebarRef" class="flex flex-col items-center py-4 border-r z-50"
|
||||
style="flex: 0 0 calc(100vw * 2 / 19); min-width: 96px; max-width: 400px; background-color: #F8F9FA;">
|
||||
<div class="mb-6" style="border-bottom: 1px solid #A0AEC023; padding: 10%;">
|
||||
<!-- Logo or Brand -->
|
||||
<div class="" >
|
||||
<div>
|
||||
<img :src="yoloIcon" class="yolo-logo" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 flex flex-col w-full px-2" style="gap: 2vh;">
|
||||
|
||||
<!-- TK Workbench Tab -->
|
||||
<button @click="currentView = 'tk'"
|
||||
class="w-full rounded-xl flex items-center gap-2 px-3 py-2.5 transition-all duration-200"
|
||||
style="height: 6vh;"
|
||||
class="w-full rounded-xl flex items-center gap-2 px-3 py-2.5 transition-all duration-200" style="height: 6vh;"
|
||||
:class="currentView === 'tk' ? 'bg-white text-blue-600 shadow shadow-blue-900/20' : 'text-slate-400 hover:bg-[rgba(21,96,250,0.06)]'">
|
||||
<img :src="currentView === 'tk' ? nav11 : nav1" class="w-9 h-9 object-contain flex-shrink-0" />
|
||||
<span class="text-base font-medium truncate">TK 工作台</span>
|
||||
</button>
|
||||
|
||||
<!-- Hosts List Tab -->
|
||||
<button @click="currentView = 'hosts'"
|
||||
class="w-full rounded-xl flex items-center gap-2 px-3 py-2.5 transition-all duration-200"
|
||||
style="height: 6vh;"
|
||||
class="w-full rounded-xl flex items-center gap-2 px-3 py-2.5 transition-all duration-200" style="height: 6vh;"
|
||||
:class="currentView === 'hosts' ? 'bg-white text-blue-600 shadow shadow-blue-900/20' : 'text-slate-400 hover:bg-[rgba(21,96,250,0.06)]'">
|
||||
<img :src="currentView === 'hosts' ? nav22 : nav2" class="w-9 h-9 object-contain flex-shrink-0" />
|
||||
<span class="text-base font-medium truncate">主播列表</span>
|
||||
</button>
|
||||
|
||||
<!-- Auto DM Workbench Tab -->
|
||||
<button @click="currentView = 'auto_dm'"
|
||||
class="w-full rounded-xl flex items-center gap-2 px-3 py-2.5 transition-all duration-200"
|
||||
style="height: 6vh;"
|
||||
class="w-full rounded-xl flex items-center gap-2 px-3 py-2.5 transition-all duration-200" style="height: 6vh;"
|
||||
:class="currentView === 'auto_dm' ? 'bg-white text-blue-600 shadow shadow-blue-900/20' : 'text-slate-400 hover:bg-[rgba(21,96,250,0.06)]'">
|
||||
<img :src="currentView === 'auto_dm' ? nav33 : nav3" class="w-9 h-9 object-contain flex-shrink-0" />
|
||||
<span class="text-base font-medium truncate">自动私信</span>
|
||||
</button>
|
||||
|
||||
<!-- Fan Workbench Tab -->
|
||||
<button @click="currentView = 'auto_dm_tk'"
|
||||
class="w-full rounded-xl flex items-center gap-2 px-3 py-2.5 transition-all duration-200" style="height: 6vh;"
|
||||
:class="currentView === 'auto_dm_tk' ? 'bg-white text-blue-600 shadow shadow-blue-900/20' : 'text-slate-400 hover:bg-[rgba(21,96,250,0.06)]'">
|
||||
<img :src="currentView === 'auto_dm_tk' ? nav33 : nav3" class="w-9 h-9 object-contain flex-shrink-0" />
|
||||
<span class="text-base font-medium truncate">自动私信TK版</span>
|
||||
</button>
|
||||
|
||||
<button @click="currentView = 'FanWorkbench'"
|
||||
class="w-full rounded-xl flex items-center gap-2 px-3 py-2.5 transition-all duration-200"
|
||||
style="height: 6vh;"
|
||||
class="w-full rounded-xl flex items-center gap-2 px-3 py-2.5 transition-all duration-200" style="height: 6vh;"
|
||||
:class="currentView === 'FanWorkbench' ? 'bg-white text-blue-600 shadow shadow-blue-900/20' : 'text-slate-400 hover:bg-[rgba(21,96,250,0.06)]'">
|
||||
<img :src="currentView === 'FanWorkbench' ? nav44 : nav4" class="w-9 h-9 object-contain flex-shrink-0" />
|
||||
<span class="text-base font-medium truncate">大哥工作台</span>
|
||||
</button>
|
||||
|
||||
<!-- PK 工作台 Tab -->
|
||||
<button @click="currentView = 'pk_mini'"
|
||||
class="w-full rounded-xl flex items-center gap-2 px-3 py-2.5 transition-all duration-200"
|
||||
style="height: 6vh;"
|
||||
class="w-full rounded-xl flex items-center gap-2 px-3 py-2.5 transition-all duration-200" style="height: 6vh;"
|
||||
:class="currentView === 'pk_mini' ? 'bg-white text-blue-600 shadow shadow-blue-900/20' : 'text-slate-400 hover:bg-[rgba(21,96,250,0.06)]'">
|
||||
<img :src="currentView === 'pk_mini' ? nav55 : nav5" class="w-9 h-9 object-contain flex-shrink-0" />
|
||||
<span class="text-base font-medium truncate">PK 工作台</span>
|
||||
</button>
|
||||
|
||||
<!-- yolo商店 Tab -->
|
||||
<button @click="currentView = 'shop'"
|
||||
class="w-full rounded-xl flex items-center gap-2 px-3 py-2.5 transition-all duration-200"
|
||||
style="height: 6vh;"
|
||||
class="w-full rounded-xl flex items-center gap-2 px-3 py-2.5 transition-all duration-200" style="height: 6vh;"
|
||||
:class="currentView === 'shop' ? 'bg-white text-blue-600 shadow shadow-blue-900/20' : 'text-slate-400 hover:bg-[rgba(21,96,250,0.06)]'">
|
||||
<img :src="currentView === 'shop' ? nav66 : nav6" class="w-9 h-9 object-contain flex-shrink-0" />
|
||||
<span class="text-base font-medium truncate">TK商店</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="w-full rounded-xl flex items-center gap-2 px-3 py-2.5 transition-all duration-200 text-slate-400 hover:bg-[rgba(21,96,250,0.06)]"
|
||||
style="height: 6vh;">
|
||||
<span class="text-base font-medium truncate">敬请期待...</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto w-full px-2">
|
||||
<!-- Logout -->
|
||||
<button @click="$emit('logout')"
|
||||
class="w-full rounded-xl flex items-center gap-2 px-3 py-2.5 text-slate-400 bg-white shadow shadow-blue-900/20 transition-all">
|
||||
<img :src="backIcon" class="w-9 h-9 object-contain flex-shrink-0" />
|
||||
@@ -76,91 +74,61 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<div class="flex-1 h-full relative">
|
||||
|
||||
<!-- Tab 1: Auto DM Workbench (Config + Browser) - webAi 权限 -->
|
||||
<div v-show="currentView === 'auto_dm'" class="absolute inset-0 z-10 h-full w-full">
|
||||
<PermissionMask
|
||||
permission-key="webAi"
|
||||
title="自动私信工作台未开通"
|
||||
description="您当前没有使用自动私信功能的权限"
|
||||
:placeholder-image="placeholderWebAi"
|
||||
:contacts="serviceContacts"
|
||||
>
|
||||
<PermissionMask permission-key="webAi" title="自动私信工作台未开通" description="您当前没有使用自动私信功能的权限"
|
||||
:placeholder-image="placeholderWebAi" :contacts="serviceContacts">
|
||||
<div v-if="autoDmMode === 'config'" class="h-full w-full bg-slate-50 overflow-auto">
|
||||
<ConfigPage
|
||||
@go-to-browser="handleGoToBrowser"
|
||||
@logout="$emit('logout')"
|
||||
/>
|
||||
<ConfigPage @go-to-browser="handleGoToBrowser" @logout="$emit('logout')"
|
||||
@config-updated="handleConfigUpdated" />
|
||||
</div>
|
||||
<div v-show="autoDmMode === 'browser'" class="h-full w-full">
|
||||
<YoloBrowser
|
||||
v-bind="$attrs"
|
||||
:nav-sidebar-width="navSidebarWidth"
|
||||
@go-back="handleBackToConfig"
|
||||
@stop-all="handleStopAll"
|
||||
/>
|
||||
<YoloBrowser v-bind="$attrs" :nav-sidebar-width="navSidebarWidth" @go-back="handleBackToConfig"
|
||||
@stop-all="handleStopAll" />
|
||||
</div>
|
||||
</PermissionMask>
|
||||
</div>
|
||||
|
||||
<!-- Tab 2: TK Workbench - crawl 权限 -->
|
||||
<div v-show="currentView === 'tk'" class="absolute inset-0 z-20 bg-gray-50 h-full overflow-hidden">
|
||||
<PermissionMask
|
||||
permission-key="crawl"
|
||||
title="TK工作台未开通"
|
||||
description="您当前没有使用TK工作台功能的权限"
|
||||
:placeholder-image="placeholderTk"
|
||||
:contacts="serviceContacts"
|
||||
>
|
||||
<TkWorkbenches />
|
||||
<div v-show="currentView === 'auto_dm_tk'" class="absolute inset-0 z-20 h-full w-full">
|
||||
<PermissionMask permission-key="autotk" title="自动私信(TK版)工作台未开通" description="您当前没有使用自动私信(TK版)功能的权限"
|
||||
:placeholder-image="placeholderWebAi" :contacts="serviceContacts">
|
||||
<AutoDmTkWorkbench :nav-sidebar-width="navSidebarWidth" />
|
||||
</PermissionMask>
|
||||
</div>
|
||||
|
||||
<div v-show="currentView === 'tk'" class="absolute inset-0 z-20 bg-gray-50 h-full overflow-hidden">
|
||||
<PermissionMask permission-key="crawl" title="TK工作台未开通" description="您当前没有使用TK工作台功能的权限"
|
||||
:placeholder-image="placeholderTk" :contacts="serviceContacts">
|
||||
<TkWorkbenches :key="tkWorkbenchKey" />
|
||||
</PermissionMask>
|
||||
</div>
|
||||
|
||||
<!-- Tab 3: Hosts List - crawl 权限 -->
|
||||
<div v-show="currentView === 'hosts'" class="absolute inset-0 z-20 bg-gray-50 h-full overflow-hidden">
|
||||
<PermissionMask
|
||||
permission-key="crawl"
|
||||
title="主播列表未开通"
|
||||
description="您当前没有使用主播列表功能的权限"
|
||||
:placeholder-image="placeholderHosts"
|
||||
:contacts="serviceContacts"
|
||||
>
|
||||
<PermissionMask permission-key="crawl" title="主播列表未开通" description="您当前没有使用主播列表功能的权限"
|
||||
:placeholder-image="placeholderHosts" :contacts="serviceContacts">
|
||||
<HostsList />
|
||||
</PermissionMask>
|
||||
</div>
|
||||
|
||||
<!-- Tab 4: Fan Workbench - bigBrother 权限 -->
|
||||
<div v-show="currentView === 'FanWorkbench'" class="absolute inset-0 z-20 bg-gray-50 h-full overflow-hidden">
|
||||
<PermissionMask
|
||||
permission-key="bigBrother"
|
||||
title="大哥工作台未开通"
|
||||
description="您当前没有使用大哥工作台功能的权限"
|
||||
:placeholder-image="placeholderBigBrother"
|
||||
:contacts="serviceContacts"
|
||||
>
|
||||
<PermissionMask permission-key="bigBrother" title="大哥工作台未开通" description="您当前没有使用大哥工作台功能的权限"
|
||||
:placeholder-image="placeholderBigBrother" :contacts="serviceContacts">
|
||||
<FanWorkbench />
|
||||
</PermissionMask>
|
||||
</div>
|
||||
|
||||
<!-- Tab 5: PK Mini 工作台 - 无需权限控制 -->
|
||||
<div v-show="currentView === 'pk_mini'" class="absolute inset-0 z-20 h-full overflow-hidden">
|
||||
<PkMiniWorkbench />
|
||||
</div>
|
||||
|
||||
<!-- Tab 6: yolo商店 - Electron 用 BrowserView,Web 用 iframe 兜底 -->
|
||||
<div v-show="currentView === 'shop'" class="absolute inset-0 z-20 h-full overflow-hidden">
|
||||
<div v-if="isElectron()" class="w-full h-full flex items-center justify-center text-base text-slate-500 bg-white">
|
||||
<div v-if="isElectron()"
|
||||
class="w-full h-full flex items-center justify-center text-base text-slate-500 bg-white">
|
||||
正在进入商店...
|
||||
</div>
|
||||
<iframe
|
||||
v-else-if="adminLoaded"
|
||||
:src="shopUrl"
|
||||
class="w-full h-full border-0"
|
||||
<iframe v-else-if="adminLoaded" :src="shopUrl" class="w-full h-full border-0"
|
||||
allow="clipboard-read; clipboard-write"
|
||||
sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-downloads"
|
||||
></iframe>
|
||||
sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-downloads" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -175,11 +143,11 @@ import HostsList from '@/views/tk/HostsList.vue'
|
||||
import ConfigPage from '@/pages/ConfigPage.vue'
|
||||
import FanWorkbench from '@/views/tk/FanWorkbench.vue'
|
||||
import PkMiniWorkbench from '@/views/pk-mini/PkMiniWorkbench.vue'
|
||||
import AutoDmTkWorkbench from '@/views/auto-dm/AutoDmTkWorkbench.vue'
|
||||
import PermissionMask from '@/components/PermissionMask.vue'
|
||||
import { ENV } from '@/config'
|
||||
import { getCustomServiceInfo } from '@/api/account'
|
||||
|
||||
// 导航图标
|
||||
import yoloIcon from '@/assets/nav/yolo.png'
|
||||
import nav1 from '@/assets/nav/nav1.png'
|
||||
import nav11 from '@/assets/nav/nav11.png'
|
||||
@@ -195,7 +163,6 @@ import nav6 from '@/assets/nav/nav6.png'
|
||||
import nav66 from '@/assets/nav/nav66.png'
|
||||
import backIcon from '@/assets/nav/back.png'
|
||||
|
||||
// 占位图片 - 无权限时显示的工作台截图
|
||||
import placeholderTk from '@/assets/placeholder-tk.png'
|
||||
import placeholderHosts from '@/assets/placeholder-hosts.png'
|
||||
import placeholderWebAi from '@/assets/placeholder-webai.png'
|
||||
@@ -203,80 +170,88 @@ import placeholderBigBrother from '@/assets/placeholder-bigbrother.png'
|
||||
|
||||
const emit = defineEmits(['logout', 'go-back', 'stop-all'])
|
||||
|
||||
const currentView = ref('tk') // Default Tab
|
||||
const autoDmMode = ref('config') // Default Sub-state: 'config' or 'browser'
|
||||
const adminLoaded = ref(false) // Web iframe 懒加载(仅非 Electron)
|
||||
const shopOpened = ref(false) // Electron 只首开加载一次
|
||||
const currentView = ref('tk')
|
||||
const autoDmMode = ref('config')
|
||||
const adminLoaded = ref(false)
|
||||
const shopOpened = ref(false)
|
||||
const shopUrl = ENV.SHOP_URL
|
||||
const sidebarRef = useTemplateRef('sidebarRef')
|
||||
const navSidebarWidth = ref(200) // 左侧导航菜单的实际宽度(px),传给 YoloBrowser/Sidebar 使用
|
||||
const navSidebarWidth = ref(200)
|
||||
const tkWorkbenchKey = ref(0)
|
||||
|
||||
const reloadTkWorkbench = () => {
|
||||
tkWorkbenchKey.value++
|
||||
console.log('TK 工作台已重新加载')
|
||||
}
|
||||
|
||||
window.reloadTkWorkbench = reloadTkWorkbench
|
||||
|
||||
// 客服名片
|
||||
const serviceContacts = ref([])
|
||||
const loadServiceContacts = async () => {
|
||||
try {
|
||||
const res = await getCustomServiceInfo()
|
||||
console.log("获取名片",res)
|
||||
if (res) {
|
||||
serviceContacts.value = res.map(item => ({
|
||||
avatar: item.avater,
|
||||
name: item.name,
|
||||
desc: item.description,
|
||||
qrcode: item.concat,
|
||||
phone: item.phone
|
||||
}))
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取客服名片失败:', e)
|
||||
try {
|
||||
const res = await getCustomServiceInfo()
|
||||
if (res) {
|
||||
serviceContacts.value = res.map(item => ({
|
||||
avatar: item.avater,
|
||||
name: item.name,
|
||||
desc: item.description,
|
||||
qrcode: item.concat,
|
||||
phone: item.phone
|
||||
}))
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取客服名片失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 监听菜单栏实际宽度,通知后端更新 BrowserView 定位
|
||||
let resizeObserver = null
|
||||
const notifySidebarWidth = (width) => {
|
||||
navSidebarWidth.value = Math.round(width)
|
||||
if (isElectron()) {
|
||||
window.electronAPI.setSidebarWidth(Math.round(width)).catch(() => {})
|
||||
}
|
||||
navSidebarWidth.value = Math.round(width)
|
||||
if (isElectron()) {
|
||||
window.electronAPI.setSidebarWidth(Math.round(width)).catch(() => { })
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadServiceContacts()
|
||||
if (!isElectron()) return
|
||||
resizeObserver = new ResizeObserver((entries) => {
|
||||
const width = entries[0]?.contentRect.width
|
||||
if (width) notifySidebarWidth(width)
|
||||
})
|
||||
if (sidebarRef.value) {
|
||||
resizeObserver.observe(sidebarRef.value)
|
||||
// 立即上报初始宽度
|
||||
notifySidebarWidth(sidebarRef.value.getBoundingClientRect().width)
|
||||
}
|
||||
loadServiceContacts()
|
||||
if (!isElectron()) return
|
||||
resizeObserver = new ResizeObserver((entries) => {
|
||||
const width = entries[0]?.contentRect.width
|
||||
if (width) notifySidebarWidth(width)
|
||||
})
|
||||
if (sidebarRef.value) {
|
||||
resizeObserver.observe(sidebarRef.value)
|
||||
notifySidebarWidth(sidebarRef.value.getBoundingClientRect().width)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
resizeObserver?.disconnect()
|
||||
resizeObserver?.disconnect()
|
||||
})
|
||||
|
||||
const handleGoToBrowser = async () => {
|
||||
autoDmMode.value = 'browser'
|
||||
if (isElectron()) {
|
||||
await window.electronAPI.showViews()
|
||||
}
|
||||
autoDmMode.value = 'browser'
|
||||
if (isElectron()) {
|
||||
await window.electronAPI.showViews()
|
||||
}
|
||||
}
|
||||
|
||||
const handleBackToConfig = async () => {
|
||||
autoDmMode.value = 'config'
|
||||
if (isElectron()) {
|
||||
await window.electronAPI.hideViews()
|
||||
}
|
||||
autoDmMode.value = 'config'
|
||||
if (isElectron()) {
|
||||
await window.electronAPI.hideViews()
|
||||
}
|
||||
}
|
||||
|
||||
const handleStopAll = () => {
|
||||
emit('stop-all')
|
||||
emit('stop-all')
|
||||
}
|
||||
|
||||
const handleConfigUpdated = () => {
|
||||
window.dispatchEvent(new CustomEvent('config-updated'))
|
||||
}
|
||||
|
||||
// Watch for view changes to manage native Electron BrowserViews
|
||||
watch(currentView, async (newVal, oldVal) => {
|
||||
// 懒加载 Web 端 iframe(仅非 Electron)
|
||||
if (newVal === 'shop' && !adminLoaded.value && !isElectron()) {
|
||||
adminLoaded.value = true
|
||||
}
|
||||
@@ -284,19 +259,11 @@ watch(currentView, async (newVal, oldVal) => {
|
||||
if (!isElectron()) return
|
||||
|
||||
if (newVal === 'shop') {
|
||||
if (!shopOpened.value) {
|
||||
try {
|
||||
shopOpened.value = true
|
||||
try {
|
||||
await window.electronAPI.openShop(shopUrl)
|
||||
} catch (e) {
|
||||
console.error('打开商店失败:', e)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await window.electronAPI.openShop(shopUrl)
|
||||
} catch (e) {
|
||||
console.error('打开商店失败:', e)
|
||||
}
|
||||
await window.electronAPI.openShop(shopUrl)
|
||||
} catch (e) {
|
||||
console.error('打开商店失败:', e)
|
||||
}
|
||||
} else if (oldVal === 'shop') {
|
||||
try {
|
||||
@@ -306,43 +273,39 @@ watch(currentView, async (newVal, oldVal) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (newVal === 'auto_dm' && autoDmMode.value === 'browser') {
|
||||
// Switching TO Auto DM tab AND we are in browser mode: Show views
|
||||
const shouldShowAutoDmViews =
|
||||
newVal === 'auto_dm' && autoDmMode.value === 'browser'
|
||||
|
||||
if (shouldShowAutoDmViews) {
|
||||
try {
|
||||
await window.electronAPI.showViews()
|
||||
} catch (e) {
|
||||
console.error('Failed to show views:', e)
|
||||
}
|
||||
} else {
|
||||
// Switching AWAY from Auto DM tab OR we are in config mode: Hide views
|
||||
try {
|
||||
await window.electronAPI.hideViews()
|
||||
} catch (e) {
|
||||
// console.error('Failed to hide views:', e)
|
||||
console.error('Failed to hide views:', e)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Watch sub-mode changes
|
||||
watch(autoDmMode, async (newVal) => {
|
||||
if (currentView.value !== 'auto_dm') return
|
||||
|
||||
if (newVal === 'browser') {
|
||||
if (isElectron()) await window.electronAPI.showViews()
|
||||
} else {
|
||||
if (isElectron()) await window.electronAPI.hideViews()
|
||||
}
|
||||
if (currentView.value !== 'auto_dm') return
|
||||
|
||||
if (newVal === 'browser') {
|
||||
if (isElectron()) await window.electronAPI.showViews()
|
||||
} else {
|
||||
if (isElectron()) await window.electronAPI.hideViews()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Material Icons support - simplistic import, ideal to put in index.html or main.js */
|
||||
@import url('https://fonts.googleapis.com/icon?family=Material+Icons+Round');
|
||||
|
||||
.yolo-logo{
|
||||
.yolo-logo {
|
||||
width: 70%;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -59,8 +59,7 @@
|
||||
<!-- 卡片头部 -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="w-1 h-4 rounded-full bg-gradient-to-b from-blue-500 to-green-500" />
|
||||
<span class="w-1 h-4 rounded-full bg-gradient-to-b from-blue-500 to-green-500" />
|
||||
<span class="font-medium text-gray-900">运行配置</span>
|
||||
</div>
|
||||
|
||||
@@ -86,8 +85,7 @@
|
||||
@click="handleStart(config.accountGroups.indexOf(group))"
|
||||
class="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-600 transition-colors flex items-center justify-between group/item">
|
||||
<span>运行 {{ group.name }}</span>
|
||||
<span
|
||||
class="text-xs text-gray-400 group-hover/item:text-blue-400">
|
||||
<span class="text-xs text-gray-400 group-hover/item:text-blue-400">
|
||||
{{ config.accountGroups.indexOf(group) + 1 }}
|
||||
</span>
|
||||
</button>
|
||||
@@ -152,15 +150,16 @@
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 focus:outline-none p-1">
|
||||
<svg v-if="showPasswordMap[`${gIndex}-${aIndex}`]" class="w-4 h-4"
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
<svg v-else class="w-4 h-4" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
|
||||
</svg>
|
||||
</button>
|
||||
@@ -183,16 +182,16 @@
|
||||
<!-- 轮换设置 -->
|
||||
<div class="mt-6 space-y-4">
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<label class="text-sm font-medium text-gray-700 w-28">国家语言</label>
|
||||
<select :value="config.lang || 'en'"
|
||||
@change="updateConfig('lang', $event.target.value)"
|
||||
class="flex-1 px-3 py-2 text-sm text-gray-900 rounded-lg border border-gray-300 focus:border-blue-500 focus:outline-none bg-white">
|
||||
<option v-for="lang in languageList" :key="lang.code" :value="lang.code">
|
||||
{{ lang.name }} ({{ lang.code }})
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<label class="text-sm font-medium text-gray-700 w-28">国家语言</label>
|
||||
<select :value="config.lang || 'en'"
|
||||
@change="updateConfig('lang', $event.target.value)"
|
||||
class="flex-1 px-3 py-2 text-sm text-gray-900 rounded-lg border border-gray-300 focus:border-blue-500 focus:outline-none bg-white">
|
||||
<option v-for="lang in languageList" :key="lang.code" :value="lang.code">
|
||||
{{ lang.name }} ({{ lang.code }})
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<label class="text-sm font-medium text-gray-700 w-28">轮换账号组</label>
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
@@ -219,7 +218,7 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<label class="text-sm font-medium text-gray-700 w-28">轮换间隔(分钟)</label>
|
||||
<input type="number" min="1"
|
||||
@@ -324,9 +323,10 @@
|
||||
<HostListDialog :visible="showHostDialog" @close="showHostDialog = false" @save="() => { }" />
|
||||
|
||||
<!-- 打招呼内容弹窗 -->
|
||||
<GreetingDialog :visible="showGreetingDialog" @close="showGreetingDialog = false" @confirm="handleGreetingConfirm" />
|
||||
|
||||
<!-- 预热 Loading 遮罩 -->
|
||||
<GreetingDialog :visible="showGreetingDialog" @close="showGreetingDialog = false"
|
||||
@confirm="handleGreetingConfirm" />
|
||||
|
||||
<!-- 预热 Loading 遮罩 -->
|
||||
<transition name="fade">
|
||||
<div v-if="warmingUp"
|
||||
class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/30 backdrop-blur-sm">
|
||||
@@ -363,7 +363,7 @@ import GreetingDialog from '../components/GreetingDialog.vue'
|
||||
import AIConfigDialog from '../components/AIConfigDialog.vue'
|
||||
import { isElectron } from '../utils/electronBridge'
|
||||
|
||||
const emit = defineEmits(['goToBrowser', 'logout'])
|
||||
const emit = defineEmits(['goToBrowser', 'logout', 'configUpdated'])
|
||||
|
||||
const CONFIG_KEY = 'autoDm_runConfig'
|
||||
|
||||
@@ -383,7 +383,9 @@ const defaultConfig = {
|
||||
switchMinutes: 60,
|
||||
prologueList: {},
|
||||
needTranslate: false,
|
||||
maxAnchorCount: 100,
|
||||
filters: {
|
||||
maxAnchorCount: 99999
|
||||
},
|
||||
lang: 'en'
|
||||
}
|
||||
|
||||
@@ -702,7 +704,9 @@ const handleStart = async (specificGroupIndex) => {
|
||||
inviteThreshold: config.value.inviteThreshold,
|
||||
prologueList,
|
||||
needTranslate: config.value.needTranslate, // 添加翻译开关配置
|
||||
maxAnchorCount: config.value.maxAnchorCount,
|
||||
filters: {
|
||||
maxAnchorCount: config.value.filters?.maxAnchorCount !== undefined ? config.value.filters.maxAnchorCount : 100
|
||||
},
|
||||
rotationEnabled: config.value.rotateEnabled,
|
||||
rotationIntervalMinutes: config.value.switchMinutes,
|
||||
currentActiveGroup: activeGroupName,
|
||||
@@ -726,8 +730,8 @@ const handleStart = async (specificGroupIndex) => {
|
||||
const cleanAccount = JSON.parse(JSON.stringify(acc))
|
||||
startTasks.push({
|
||||
viewId: currentViewId,
|
||||
account: {
|
||||
...cleanAccount,
|
||||
account: {
|
||||
...cleanAccount,
|
||||
group: group.name,
|
||||
lang: config.value.lang || 'en' // 传递语言配置
|
||||
},
|
||||
@@ -738,7 +742,7 @@ const handleStart = async (specificGroupIndex) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 预热所有视图,确保后台视图完成渲染,解决自动化执行失败问题
|
||||
// 预热所有视图,确保后台视图完成渲染,解决自动化执行失败问题
|
||||
warmingUp.value = true
|
||||
try {
|
||||
console.log('[ConfigPage] 预热所有视图...')
|
||||
@@ -789,6 +793,8 @@ const handleStart = async (specificGroupIndex) => {
|
||||
rotationStatus.value = status
|
||||
handleStatusChange(status)
|
||||
warmingUp.value = false //关闭遮罩
|
||||
// 触发自定义事件通知配置已更新
|
||||
emit('configUpdated')
|
||||
emit('goToBrowser')
|
||||
}
|
||||
|
||||
|
||||
@@ -403,6 +403,7 @@ const handleSubmit = async () => {
|
||||
bigBrother: result.user.bigBrother,
|
||||
crawl: result.user.crawl,
|
||||
webAi: result.user.webAi,
|
||||
autotk: result.user.autoTk
|
||||
});
|
||||
|
||||
emit('loginSuccess')
|
||||
|
||||
68
src/types/electron.d.ts
vendored
68
src/types/electron.d.ts
vendored
@@ -77,6 +77,29 @@ export interface GreetingStats {
|
||||
details: ViewStats[]
|
||||
}
|
||||
|
||||
export interface StandaloneTikTokAutomationOptions {
|
||||
greetingMessages?: string[]
|
||||
prologueList?: Record<string, string[]>
|
||||
needTranslate?: boolean
|
||||
replyMessages?: string[]
|
||||
replyUnreadMessages?: boolean
|
||||
dataPoolSource?: 'anchor_hosts' | 'brother_info' | '/api/anchor/hosts/getAll' | '/api/gifters/brotherInfo/getAll'
|
||||
groupSwitchMinutes?: number
|
||||
groupViewCounts?: number[]
|
||||
continuousMode?: boolean
|
||||
runMode?: string
|
||||
homeUrl?: string
|
||||
waitForManualLogin?: boolean
|
||||
searchKeyword?: string
|
||||
}
|
||||
|
||||
export interface ViewProxyConfig {
|
||||
mode: 'fixed_servers' | string
|
||||
proxyRules: string
|
||||
username?: string
|
||||
password?: string
|
||||
}
|
||||
|
||||
export interface ElectronAPI {
|
||||
// 基础视图控制
|
||||
hideViews: () => Promise<{ success: boolean }>
|
||||
@@ -98,6 +121,17 @@ export interface ElectronAPI {
|
||||
// TikTok 自动化
|
||||
startTikTokAutomation: (viewId: number, account: Account) => Promise<{ success: boolean; message?: string; error?: string }>
|
||||
stopTikTokAutomation: (viewId: number) => Promise<{ success: boolean; message?: string; error?: string }>
|
||||
startStandaloneTikTokAutomationAll: (
|
||||
options: StandaloneTikTokAutomationOptions
|
||||
) => Promise<{ success: boolean; error?: string }>
|
||||
stopStandaloneTikTokAutomationAll: () => Promise<{ success: boolean; error?: string }>
|
||||
getViewProxy: (viewId: number) => Promise<{ success: boolean; proxyConfig?: ViewProxyConfig | null; error?: string }>
|
||||
setViewProxy: (
|
||||
viewId: number,
|
||||
proxyConfig: ViewProxyConfig,
|
||||
reloadCurrentView?: boolean
|
||||
) => Promise<{ success: boolean; warning?: string; error?: string }>
|
||||
clearViewProxy: (viewId: number, reloadCurrentView?: boolean) => Promise<{ success: boolean; error?: string }>
|
||||
updateAutomationConfig: (config: Partial<AutomationConfig>) => Promise<{ success: boolean }>
|
||||
getAutomationConfig: () => Promise<AutomationConfig>
|
||||
|
||||
@@ -145,6 +179,8 @@ export interface ElectronAPI {
|
||||
onRotationStatusChanged: (callback: (status: RotationStatus) => void) => () => void
|
||||
onRequestSaveConfig: (callback: () => void) => () => void
|
||||
onRequestClearLogin: (callback: () => void) => () => void
|
||||
onAppClosingStart: (callback: () => void) => () => void
|
||||
onAppClosingComplete: (callback: () => void) => () => void
|
||||
onUpdateChecking: (callback: () => void) => () => void
|
||||
onUpdateAvailable: (callback: (info: UpdateInfo) => void) => () => void
|
||||
onUpdateNotAvailable: (callback: () => void) => () => void
|
||||
@@ -153,6 +189,38 @@ export interface ElectronAPI {
|
||||
onUpdateError: (callback: (error: { message: string }) => void) => () => void
|
||||
onUpdateManualInstall: (callback: (info: { path: string }) => void) => () => void
|
||||
onGreetingStatsChanged: (callback: (stats: GreetingStats) => void) => () => void
|
||||
|
||||
tk?: {
|
||||
updateStartConfig: (config: string) => Promise<{ success: boolean; error?: string }>
|
||||
getDataCount: () => Promise<string>
|
||||
loginTikTok: () => Promise<void>
|
||||
loginBigTikTok: () => Promise<void>
|
||||
loginBackStage: (data: string) => Promise<void>
|
||||
loginBackStageCopy: (data: string) => Promise<void>
|
||||
checkBackStageLoginStatus: (account?: string) => Promise<string>
|
||||
checkBackStageLoginStatusCopy: () => Promise<string>
|
||||
stopCrawl: () => Promise<void>
|
||||
getVersion: () => Promise<string>
|
||||
checkTkLoginStatus: () => Promise<string>
|
||||
visitAnchor: (id: string) => Promise<void>
|
||||
exportData: (data: string) => Promise<void>
|
||||
controlTask: (data: string) => Promise<{ success: boolean; message?: string; error?: string }>
|
||||
getBrotherInfo: () => Promise<string>
|
||||
queryBrotherInfo: (data: string) => Promise<string>
|
||||
getAllBrotherInfo: () => Promise<string>
|
||||
deleteBrotherInfo: (data: string) => Promise<string>
|
||||
findBigBrother: (data: string) => Promise<{ success: boolean }>
|
||||
storageSet: (data: string) => Promise<{ success: boolean; error?: string }>
|
||||
storageRead: (data: string) => Promise<string>
|
||||
openRoom: (id: string) => Promise<void>
|
||||
storageAccount: (data: string) => Promise<{ success: boolean; error?: string }>
|
||||
readAccount: (data: string) => Promise<string>
|
||||
setClipboard: (text: string) => Promise<{ success: boolean; error?: string }>
|
||||
startBrotherMonitor: () => Promise<{ success: boolean; error?: string }>
|
||||
getBrotherLoginStatus: () => Promise<string>
|
||||
visitGifter: (data: string) => Promise<{ success: boolean; error?: string }>
|
||||
closeAllBrowsers: () => Promise<{ success: boolean; error?: string }>
|
||||
}
|
||||
}
|
||||
|
||||
// 声明全局类型
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
*/
|
||||
import axios from 'axios'
|
||||
import { getToken } from '@/utils/storage'
|
||||
import router from '@/router'
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { usePythonBridge, } from '@/utils/pythonBridge'
|
||||
@@ -16,6 +15,13 @@ const { stopScript } = usePythonBridge();
|
||||
// 请求地址前缀
|
||||
const baseURL = ENV.API_BASE_URL
|
||||
|
||||
function emitAuthExpired(code, message) {
|
||||
|
||||
window.dispatchEvent(new CustomEvent('auth-expired', {
|
||||
detail: { code, message }
|
||||
}))
|
||||
}
|
||||
|
||||
// 请求拦截器
|
||||
axios.interceptors.request.use((config) => {
|
||||
// console.log("config", config)
|
||||
@@ -43,10 +49,12 @@ axios.interceptors.response.use((response) => {
|
||||
return response.data.data
|
||||
} else if (response.data.code == 40400) {
|
||||
stopScript();
|
||||
router.push('/')
|
||||
emitAuthExpired(response.data.code, response.data.message)
|
||||
ElMessage.error(response.data.code + '' + response.data.message);
|
||||
return Promise.reject(response.data)
|
||||
} else {
|
||||
ElMessage.error(response.data.code + '' + response.data.message);
|
||||
return Promise.reject(response.data)
|
||||
}
|
||||
|
||||
}, (error) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// 国家数据 - 简化版本,直接内嵌数据
|
||||
// 国家数据 - 完整补全版本
|
||||
const zhCountries = {
|
||||
"CN": "中国",
|
||||
"US": "美国",
|
||||
@@ -23,6 +23,7 @@ const zhCountries = {
|
||||
"PH": "菲律宾",
|
||||
"TW": "中国台湾",
|
||||
"HK": "中国香港",
|
||||
"MO": "中国澳门",
|
||||
"AE": "阿联酋",
|
||||
"SA": "沙特阿拉伯",
|
||||
"TR": "土耳其",
|
||||
@@ -51,7 +52,200 @@ const zhCountries = {
|
||||
"IL": "以色列",
|
||||
"PK": "巴基斯坦",
|
||||
"BD": "孟加拉国",
|
||||
"NZ": "新西兰"
|
||||
"NZ": "新西兰",
|
||||
"HR": "克罗地亚",
|
||||
"BG": "保加利亚",
|
||||
// --- 新增补全 ---
|
||||
"KP": "朝鲜",
|
||||
"IE": "爱尔兰",
|
||||
"AG": "安提瓜和巴布达",
|
||||
"BS": "巴哈马",
|
||||
"BB": "巴巴多斯",
|
||||
"BZ": "伯利兹",
|
||||
"BW": "博茨瓦纳",
|
||||
"KY": "开曼群岛",
|
||||
"CX": "圣诞岛",
|
||||
"CC": "科科斯群岛",
|
||||
"CK": "库克群岛",
|
||||
"DM": "多米尼克",
|
||||
"SZ": "埃斯瓦蒂尼",
|
||||
"FK": "福克兰群岛",
|
||||
"FJ": "斐济",
|
||||
"GM": "冈比亚",
|
||||
"GH": "加纳",
|
||||
"GI": "直布罗陀",
|
||||
"GD": "格林纳达",
|
||||
"GU": "关岛",
|
||||
"GG": "根西岛",
|
||||
"GY": "圭亚那",
|
||||
"HM": "赫德岛和麦克唐纳群岛",
|
||||
"IM": "曼岛",
|
||||
"JM": "牙买加",
|
||||
"JE": "泽西岛",
|
||||
"KE": "肯尼亚",
|
||||
"KI": "基里巴斯",
|
||||
"LS": "莱索托",
|
||||
"LR": "利比里亚",
|
||||
"MW": "马拉维",
|
||||
"MH": "马绍尔群岛",
|
||||
"MU": "毛里求斯",
|
||||
"FM": "密克罗尼西亚",
|
||||
"MS": "蒙特塞拉特",
|
||||
"NA": "纳米比亚",
|
||||
"NR": "瑙鲁",
|
||||
"NU": "纽埃",
|
||||
"NF": "诺福克岛",
|
||||
"MP": "北马里亚纳群岛",
|
||||
"PW": "帕劳",
|
||||
"PG": "巴布亚新几内亚",
|
||||
"PN": "皮特凯恩群岛",
|
||||
"SH": "圣赫勒拿",
|
||||
"KN": "圣基茨和尼维斯",
|
||||
"LC": "圣卢西亚",
|
||||
"VC": "圣文森特和格林纳丁斯",
|
||||
"SL": "塞拉利昂",
|
||||
"SB": "所罗门群岛",
|
||||
"GS": "南乔治亚岛和南桑威奇群岛",
|
||||
"SS": "南苏丹",
|
||||
"TK": "托克劳",
|
||||
"TT": "特立尼达和多巴哥",
|
||||
"TC": "特克斯和凯科斯群岛",
|
||||
"TV": "图瓦卢",
|
||||
"UG": "乌干达",
|
||||
"UM": "美国本土外小岛屿",
|
||||
"VG": "英属维尔京群岛",
|
||||
"VI": "美属维尔京群岛",
|
||||
"ZM": "赞比亚",
|
||||
"ZW": "津巴布韦",
|
||||
"AS": "美属萨摩亚",
|
||||
"AI": "安圭拉",
|
||||
"AQ": "南极洲",
|
||||
"BM": "百幕大",
|
||||
"IO": "英属印度洋领地",
|
||||
"BJ": "贝宁",
|
||||
"BF": "布基纳法索",
|
||||
"BI": "布隆迪",
|
||||
"CM": "喀麦隆",
|
||||
"CF": "中非共和国",
|
||||
"TD": "乍得",
|
||||
"CD": "刚果民主共和国",
|
||||
"CI": "科特迪瓦",
|
||||
"DJ": "吉布提",
|
||||
"GF": "法属圭亚那",
|
||||
"PF": "法属波利尼西亚",
|
||||
"TF": "法属南部领地",
|
||||
"GA": "加蓬",
|
||||
"GP": "瓜德罗普",
|
||||
"GN": "几内亚",
|
||||
"HT": "海地",
|
||||
"LU": "卢森堡",
|
||||
"MG": "马达加斯加",
|
||||
"ML": "马里",
|
||||
"MQ": "马提尼克",
|
||||
"YT": "马约特",
|
||||
"MC": "摩纳哥",
|
||||
"NC": "新喀里多尼亚",
|
||||
"NE": "尼日尔",
|
||||
"RE": "留尼汪",
|
||||
"BL": "圣巴泰勒米",
|
||||
"MF": "法属圣马丁",
|
||||
"PM": "圣皮埃尔和密克隆",
|
||||
"SN": "塞内加尔",
|
||||
"SC": "塞舌尔",
|
||||
"TG": "多哥",
|
||||
"WF": "瓦利斯和富图纳",
|
||||
"LI": "列支敦士登",
|
||||
"VA": "梵蒂冈",
|
||||
"SM": "圣马力诺",
|
||||
"BO": "玻利维亚",
|
||||
"CR": "哥斯达黎加",
|
||||
"CU": "古巴",
|
||||
"DO": "多米尼加共和国",
|
||||
"EC": "厄瓜多尔",
|
||||
"SV": "萨尔瓦多",
|
||||
"GQ": "赤道几内亚",
|
||||
"GT": "危地马拉",
|
||||
"HN": "洪都拉斯",
|
||||
"NI": "尼加拉瓜",
|
||||
"PA": "巴拿马",
|
||||
"PY": "巴拉圭",
|
||||
"PR": "波多黎各",
|
||||
"UY": "乌拉圭",
|
||||
"VE": "委内瑞拉",
|
||||
"CV": "佛得角",
|
||||
"GW": "几内亚比绍",
|
||||
"MZ": "莫桑比克",
|
||||
"ST": "圣多美和普林西比",
|
||||
"AO": "安哥拉",
|
||||
"BN": "文莱",
|
||||
"BH": "巴林",
|
||||
"KM": "科摩罗",
|
||||
"IQ": "伊拉克",
|
||||
"JO": "约旦",
|
||||
"KW": "科威特",
|
||||
"LB": "黎巴嫩",
|
||||
"LY": "利比亚",
|
||||
"MR": "毛里塔尼亚",
|
||||
"MA": "摩洛哥",
|
||||
"OM": "阿曼",
|
||||
"PS": "巴勒斯坦",
|
||||
"QA": "卡塔尔",
|
||||
"SD": "苏丹",
|
||||
"SY": "叙利亚",
|
||||
"TN": "突尼斯",
|
||||
"EH": "西撒哈拉",
|
||||
"YE": "也门",
|
||||
"DZ": "阿尔及利亚",
|
||||
"MM": "缅甸",
|
||||
"LK": "斯里兰卡",
|
||||
"CW": "库拉索",
|
||||
"SX": "荷属圣马丁",
|
||||
"SR": "苏里南",
|
||||
"BQ": "荷属加勒比区",
|
||||
"MD": "摩尔多瓦",
|
||||
"LA": "老挝",
|
||||
"AL": "阿尔巴尼亚",
|
||||
"AD": "安道尔",
|
||||
"AM": "亚美尼亚",
|
||||
"AZ": "阿塞拜疆",
|
||||
"BY": "白俄罗斯",
|
||||
"BT": "不丹",
|
||||
"BA": "波斯尼亚和黑塞哥维那",
|
||||
"KH": "柬埔寨",
|
||||
"CY": "塞浦路斯",
|
||||
"ER": "厄立特里亚",
|
||||
"EE": "爱沙尼亚",
|
||||
"ET": "埃塞俄比亚",
|
||||
"GE": "格鲁吉亚",
|
||||
"GL": "格陵兰",
|
||||
"IS": "冰岛",
|
||||
"IR": "伊朗",
|
||||
"AF": "阿富汗",
|
||||
"KZ": "哈萨克斯坦",
|
||||
"KG": "吉尔吉斯斯坦",
|
||||
"LV": "拉脱维亚",
|
||||
"LT": "立陶宛",
|
||||
"MV": "马尔代夫",
|
||||
"MT": "马耳他",
|
||||
"MN": "蒙古",
|
||||
"ME": "黑山",
|
||||
"RS": "塞尔维亚",
|
||||
"NP": "尼泊尔",
|
||||
"MK": "北马其顿",
|
||||
"SJ": "斯瓦尔巴群岛和扬马延岛",
|
||||
"BV": "布韦岛",
|
||||
"RW": "卢旺达",
|
||||
"WS": "萨摩亚",
|
||||
"SK": "斯洛伐克",
|
||||
"SI": "斯洛文尼亚",
|
||||
"SO": "索马里",
|
||||
"TJ": "塔吉克斯坦",
|
||||
"TZ": "坦桑尼亚",
|
||||
"TL": "东帝汶",
|
||||
"TO": "汤加",
|
||||
"TM": "土库曼斯坦",
|
||||
"UZ": "乌兹别克斯坦",
|
||||
"VU": "瓦努阿图"
|
||||
}
|
||||
|
||||
const enCountries = {
|
||||
@@ -78,6 +272,7 @@ const enCountries = {
|
||||
"PH": "Philippines",
|
||||
"TW": "Taiwan",
|
||||
"HK": "Hong Kong",
|
||||
"MO": "Macao",
|
||||
"AE": "UAE",
|
||||
"SA": "Saudi Arabia",
|
||||
"TR": "Turkey",
|
||||
@@ -106,7 +301,200 @@ const enCountries = {
|
||||
"IL": "Israel",
|
||||
"PK": "Pakistan",
|
||||
"BD": "Bangladesh",
|
||||
"NZ": "New Zealand"
|
||||
"NZ": "New Zealand",
|
||||
"HR": "Croatia",
|
||||
"BG": "Bulgaria",
|
||||
// --- 新增补全 ---
|
||||
"KP": "North Korea",
|
||||
"IE": "Ireland",
|
||||
"AG": "Antigua and Barbuda",
|
||||
"BS": "Bahamas",
|
||||
"BB": "Barbados",
|
||||
"BZ": "Belize",
|
||||
"BW": "Botswana",
|
||||
"KY": "Cayman Islands",
|
||||
"CX": "Christmas Island",
|
||||
"CC": "Cocos Islands",
|
||||
"CK": "Cook Islands",
|
||||
"DM": "Dominica",
|
||||
"SZ": "Eswatini",
|
||||
"FK": "Falkland Islands",
|
||||
"FJ": "Fiji",
|
||||
"GM": "Gambia",
|
||||
"GH": "Ghana",
|
||||
"GI": "Gibraltar",
|
||||
"GD": "Grenada",
|
||||
"GU": "Guam",
|
||||
"GG": "Guernsey",
|
||||
"GY": "Guyana",
|
||||
"HM": "Heard Island and McDonald Islands",
|
||||
"IM": "Isle of Man",
|
||||
"JM": "Jamaica",
|
||||
"JE": "Jersey",
|
||||
"KE": "Kenya",
|
||||
"KI": "Kiribati",
|
||||
"LS": "Lesotho",
|
||||
"LR": "Liberia",
|
||||
"MW": "Malawi",
|
||||
"MH": "Marshall Islands",
|
||||
"MU": "Mauritius",
|
||||
"FM": "Micronesia",
|
||||
"MS": "Montserrat",
|
||||
"NA": "Namibia",
|
||||
"NR": "Nauru",
|
||||
"NU": "Niue",
|
||||
"NF": "Norfolk Island",
|
||||
"MP": "Northern Mariana Islands",
|
||||
"PW": "Palau",
|
||||
"PG": "Papua New Guinea",
|
||||
"PN": "Pitcairn Islands",
|
||||
"SH": "Saint Helena",
|
||||
"KN": "Saint Kitts and Nevis",
|
||||
"LC": "Saint Lucia",
|
||||
"VC": "Saint Vincent and the Grenadines",
|
||||
"SL": "Sierra Leone",
|
||||
"SB": "Solomon Islands",
|
||||
"GS": "South Georgia and the South Sandwich Islands",
|
||||
"SS": "South Sudan",
|
||||
"TK": "Tokelau",
|
||||
"TT": "Trinidad and Tobago",
|
||||
"TC": "Turks and Caicos Islands",
|
||||
"TV": "Tuvalu",
|
||||
"UG": "Uganda",
|
||||
"UM": "U.S. Minor Outlying Islands",
|
||||
"VG": "British Virgin Islands",
|
||||
"VI": "U.S. Virgin Islands",
|
||||
"ZM": "Zambia",
|
||||
"ZW": "Zimbabwe",
|
||||
"AS": "American Samoa",
|
||||
"AI": "Anguilla",
|
||||
"AQ": "Antarctica",
|
||||
"BM": "Bermuda",
|
||||
"IO": "British Indian Ocean Territory",
|
||||
"BJ": "Benin",
|
||||
"BF": "Burkina Faso",
|
||||
"BI": "Burundi",
|
||||
"CM": "Cameroon",
|
||||
"CF": "Central African Republic",
|
||||
"TD": "Chad",
|
||||
"CD": "DR Congo",
|
||||
"CI": "Ivory Coast",
|
||||
"DJ": "Djibouti",
|
||||
"GF": "French Guiana",
|
||||
"PF": "French Polynesia",
|
||||
"TF": "French Southern Territories",
|
||||
"GA": "Gabon",
|
||||
"GP": "Guadeloupe",
|
||||
"GN": "Guinea",
|
||||
"HT": "Haiti",
|
||||
"LU": "Luxembourg",
|
||||
"MG": "Madagascar",
|
||||
"ML": "Mali",
|
||||
"MQ": "Martinique",
|
||||
"YT": "Mayotte",
|
||||
"MC": "Monaco",
|
||||
"NC": "New Caledonia",
|
||||
"NE": "Niger",
|
||||
"RE": "Réunion",
|
||||
"BL": "Saint Barthélemy",
|
||||
"MF": "Saint Martin",
|
||||
"PM": "Saint Pierre and Miquelon",
|
||||
"SN": "Senegal",
|
||||
"SC": "Seychelles",
|
||||
"TG": "Togo",
|
||||
"WF": "Wallis and Futuna",
|
||||
"LI": "Liechtenstein",
|
||||
"VA": "Vatican City",
|
||||
"SM": "San Marino",
|
||||
"BO": "Bolivia",
|
||||
"CR": "Costa Rica",
|
||||
"CU": "Cuba",
|
||||
"DO": "Dominican Republic",
|
||||
"EC": "Ecuador",
|
||||
"SV": "El Salvador",
|
||||
"GQ": "Equatorial Guinea",
|
||||
"GT": "Guatemala",
|
||||
"HN": "Honduras",
|
||||
"NI": "Nicaragua",
|
||||
"PA": "Panama",
|
||||
"PY": "Paraguay",
|
||||
"PR": "Puerto Rico",
|
||||
"UY": "Uruguay",
|
||||
"VE": "Venezuela",
|
||||
"CV": "Cape Verde",
|
||||
"GW": "Guinea-Bissau",
|
||||
"MZ": "Mozambique",
|
||||
"ST": "São Tomé and Príncipe",
|
||||
"AO": "Angola",
|
||||
"BN": "Brunei",
|
||||
"BH": "Bahrain",
|
||||
"KM": "Comoros",
|
||||
"IQ": "Iraq",
|
||||
"JO": "Jordan",
|
||||
"KW": "Kuwait",
|
||||
"LB": "Lebanon",
|
||||
"LY": "Libya",
|
||||
"MR": "Mauritania",
|
||||
"MA": "Morocco",
|
||||
"OM": "Oman",
|
||||
"PS": "Palestine",
|
||||
"QA": "Qatar",
|
||||
"SD": "Sudan",
|
||||
"SY": "Syria",
|
||||
"TN": "Tunisia",
|
||||
"EH": "Western Sahara",
|
||||
"YE": "Yemen",
|
||||
"DZ": "Algeria",
|
||||
"MM": "Myanmar",
|
||||
"LK": "Sri Lanka",
|
||||
"CW": "Curaçao",
|
||||
"SX": "Sint Maarten",
|
||||
"SR": "Suriname",
|
||||
"BQ": "Caribbean Netherlands",
|
||||
"MD": "Moldova",
|
||||
"LA": "Laos",
|
||||
"AL": "Albania",
|
||||
"AD": "Andorra",
|
||||
"AM": "Armenia",
|
||||
"AZ": "Azerbaijan",
|
||||
"BY": "Belarus",
|
||||
"BT": "Bhutan",
|
||||
"BA": "Bosnia and Herzegovina",
|
||||
"KH": "Cambodia",
|
||||
"CY": "Cyprus",
|
||||
"ER": "Eritrea",
|
||||
"EE": "Estonia",
|
||||
"ET": "Ethiopia",
|
||||
"GE": "Georgia",
|
||||
"GL": "Greenland",
|
||||
"IS": "Iceland",
|
||||
"IR": "Iran",
|
||||
"AF": "Afghanistan",
|
||||
"KZ": "Kazakhstan",
|
||||
"KG": "Kyrgyzstan",
|
||||
"LV": "Latvia",
|
||||
"LT": "Lithuania",
|
||||
"MV": "Maldives",
|
||||
"MT": "Malta",
|
||||
"MN": "Mongolia",
|
||||
"ME": "Montenegro",
|
||||
"RS": "Serbia",
|
||||
"NP": "Nepal",
|
||||
"MK": "North Macedonia",
|
||||
"SJ": "Svalbard and Jan Mayen",
|
||||
"BV": "Bouvet Island",
|
||||
"RW": "Rwanda",
|
||||
"WS": "Samoa",
|
||||
"SK": "Slovakia",
|
||||
"SI": "Slovenia",
|
||||
"SO": "Somalia",
|
||||
"TJ": "Tajikistan",
|
||||
"TZ": "Tanzania",
|
||||
"TL": "Timor-Leste",
|
||||
"TO": "Tonga",
|
||||
"TM": "Turkmenistan",
|
||||
"UZ": "Uzbekistan",
|
||||
"VU": "Vanuatu"
|
||||
}
|
||||
|
||||
// 创建中文名称到国家代码的映射
|
||||
|
||||
@@ -11,3 +11,19 @@ export function TimestamptolocalTime(date) {
|
||||
|
||||
return `${year}/${month}/${day} ${hours}:${minutes}`
|
||||
}
|
||||
|
||||
// 时间戳转换为北京时间,格式为 YYYY/MM/DD hh:mm
|
||||
export function TimestampttoBeijingTime(date) {
|
||||
if (!date || isNaN(date)) return ''
|
||||
|
||||
const d = new Date(date)
|
||||
// 北京时间是 UTC+8
|
||||
const beijingTime = new Date(d.getTime() + 8 * 60 * 60 * 1000)
|
||||
const year = beijingTime.getUTCFullYear()
|
||||
const month = String(beijingTime.getUTCMonth() + 1).padStart(2, '0')
|
||||
const day = String(beijingTime.getUTCDate()).padStart(2, '0')
|
||||
const hours = String(beijingTime.getUTCHours()).padStart(2, '0')
|
||||
const minutes = String(beijingTime.getUTCMinutes()).padStart(2, '0')
|
||||
|
||||
return `${year}/${month}/${day} ${hours}:${minutes}`
|
||||
}
|
||||
|
||||
@@ -58,26 +58,28 @@ const PERMISSIONS_KEY = 'user_permissions';
|
||||
|
||||
/**
|
||||
* 存储权限信息
|
||||
* @param {Object} permissions - 权限对象 { bigBrother, crawl, webAi }
|
||||
* @param {Object} permissions - 权限对象 { bigBrother, crawl, webAi, autotk }
|
||||
*/
|
||||
export function setPermissions(permissions) {
|
||||
const autotkValue = permissions.autotk ?? permissions.autoTK ?? 0;
|
||||
localStorage.setItem(PERMISSIONS_KEY, JSON.stringify({
|
||||
bigBrother: permissions.bigBrother ?? 0,
|
||||
crawl: permissions.crawl ?? 0,
|
||||
webAi: permissions.webAi ?? 0,
|
||||
autotk: autotkValue,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取权限信息
|
||||
* @returns {Object} 权限对象 { bigBrother, crawl, webAi }
|
||||
* @returns {Object} 权限对象 { bigBrother, crawl, webAi, autotk }
|
||||
*/
|
||||
export function getPermissions() {
|
||||
try {
|
||||
const permissions = JSON.parse(localStorage.getItem(PERMISSIONS_KEY));
|
||||
return permissions || { bigBrother: 0, crawl: 0, webAi: 0 };
|
||||
return permissions || { bigBrother: 0, crawl: 0, webAi: 0, autotk: 0 };
|
||||
} catch {
|
||||
return { bigBrother: 0, crawl: 0, webAi: 0 };
|
||||
return { bigBrother: 0, crawl: 0, webAi: 0, autotk: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -109,10 +109,42 @@ onMounted(() => {
|
||||
buildViewMap(props.accountGroups)
|
||||
}
|
||||
|
||||
// 监听本地存储变化,当配置更新时重新加载
|
||||
window.addEventListener('storage', handleStorageChange)
|
||||
// 监听自定义配置更新事件
|
||||
window.addEventListener('config-updated', handleConfigUpdate)
|
||||
|
||||
// Listeners specific to browser view
|
||||
// ...
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清理监听器
|
||||
window.removeEventListener('storage', handleStorageChange)
|
||||
window.removeEventListener('config-updated', handleConfigUpdate)
|
||||
})
|
||||
|
||||
// 处理本地存储变化
|
||||
const handleStorageChange = (event) => {
|
||||
if (event.key === CONFIG_KEY) {
|
||||
loadConfig()
|
||||
// 强制重新构建 viewAccountMap,确保使用最新的账号信息
|
||||
if (props.accountGroups) {
|
||||
buildViewMap(props.accountGroups)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理配置更新事件
|
||||
const handleConfigUpdate = () => {
|
||||
console.log('[YoloBrowser] 收到配置更新事件,重新加载配置')
|
||||
loadConfig()
|
||||
// 强制重新构建 viewAccountMap,确保使用最新的账号信息
|
||||
if (props.accountGroups) {
|
||||
buildViewMap(props.accountGroups)
|
||||
}
|
||||
}
|
||||
|
||||
// Original loadConfig logic
|
||||
const loadConfig = () => {
|
||||
try {
|
||||
@@ -142,7 +174,7 @@ const buildViewMap = (groups) => {
|
||||
|
||||
watch(() => props.accountGroups, (newVal) => {
|
||||
if (newVal) buildViewMap(newVal)
|
||||
})
|
||||
}, { deep: true })
|
||||
|
||||
// Actions
|
||||
const handleTabSwitch = async (tab) => {
|
||||
@@ -166,6 +198,12 @@ const handleTabSwitch = async (tab) => {
|
||||
|
||||
const handleViewSwitch = async (viewId) => {
|
||||
selectedViewId.value = viewId
|
||||
// 重新加载配置,确保获取最新的账号信息
|
||||
loadConfig()
|
||||
// 强制重新构建 viewAccountMap,确保使用最新的账号信息
|
||||
if (props.accountGroups) {
|
||||
buildViewMap(props.accountGroups)
|
||||
}
|
||||
if (isElectron()) {
|
||||
await window.electronAPI.switchToView(viewId)
|
||||
}
|
||||
|
||||
496
src/views/auto-dm/AutoDmTkWorkbench.vue
Normal file
496
src/views/auto-dm/AutoDmTkWorkbench.vue
Normal file
@@ -0,0 +1,496 @@
|
||||
<template>
|
||||
<div class="h-full w-full overflow-hidden bg-gradient-to-br from-slate-100 to-slate-200">
|
||||
<div v-if="pageMode === 'config'" class="h-full overflow-auto p-6">
|
||||
<div class="mx-auto max-w-5xl pb-8">
|
||||
<div class="rounded-2xl bg-white p-8 shadow-xl">
|
||||
<div class="mb-4 flex items-end justify-between gap-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-gray-900">自动私信工作台(TK版)</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span :class="statusChipClass">{{ statusText }}</span>
|
||||
<button @click="handlePrepareViews" :disabled="isPreparing || !isElectronEnv" class="flex items-center gap-2 rounded-lg bg-gradient-to-r from-emerald-500 to-teal-500 px-4 py-2 text-sm text-white shadow-sm transition-all hover:from-emerald-600 hover:to-teal-600 disabled:cursor-not-allowed disabled:opacity-60">
|
||||
<span>{{ isPreparing ? '预热中...' : '预热视图' }}</span>
|
||||
</button>
|
||||
<button @click="openBrowserView" class="flex items-center gap-2 rounded-lg bg-gradient-to-r from-blue-500 to-cyan-500 px-4 py-2 text-sm text-white shadow-sm transition-all hover:from-blue-600 hover:to-cyan-600">
|
||||
<span>打开浏览器视图</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6 flex w-fit items-center gap-2 rounded-full border border-blue-200 bg-gradient-to-r from-blue-50 to-green-50 px-4 py-2 text-sm text-gray-700">
|
||||
<span class="h-2 w-2 animate-pulse rounded-full bg-green-500" />
|
||||
先填写配置,再预热视图,登录完成后再启动脚本。
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-6">
|
||||
<div class="col-span-2 space-y-6">
|
||||
<section class="rounded-xl border border-gray-200 bg-gradient-to-b from-white to-gray-50 p-6 shadow-sm">
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="h-4 w-1 rounded-full bg-gradient-to-b from-blue-500 to-green-500" />
|
||||
<span class="font-medium text-gray-900">运行参数</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-3 text-center">
|
||||
<div class="min-w-[88px] rounded-lg border border-gray-200 bg-white px-3 py-2">
|
||||
<div class="text-[10px] text-gray-500">话术数</div>
|
||||
<div class="mt-1 text-base font-semibold text-gray-800">{{ greetingCount }}</div>
|
||||
</div>
|
||||
<div class="min-w-[88px] rounded-lg border border-gray-200 bg-white px-3 py-2">
|
||||
<div class="text-[10px] text-gray-500">数据池</div>
|
||||
<div class="mt-1 text-base font-semibold text-gray-800">{{ dataPoolLabel }}</div>
|
||||
</div>
|
||||
<div class="min-w-[88px] rounded-lg border border-gray-200 bg-white px-3 py-2">
|
||||
<div class="text-[10px] text-gray-500">开启视图</div>
|
||||
<div class="mt-1 text-base font-semibold text-gray-800">{{ totalEnabledViews }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-5">
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-4">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-700">打招呼内容</label>
|
||||
</div>
|
||||
<button @click="showGreetingDialog = true" :disabled="isStarting || !isElectronEnv" class="rounded-lg border border-blue-200 bg-blue-50 px-4 py-2 text-sm font-medium text-blue-600 transition-colors hover:bg-blue-100 disabled:cursor-not-allowed disabled:opacity-60">
|
||||
打招呼内容配置
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-3 flex items-center gap-3 text-xs text-gray-500">
|
||||
<span class="rounded-full border border-gray-200 bg-gray-100 px-3 py-1">基础话术 {{ greetingCount }} 条</span>
|
||||
<span class="rounded-full border border-gray-200 bg-gray-100 px-3 py-1">翻译 {{ configForm.needTranslate ? '已开启' : '未开启' }}</span>
|
||||
<span class="rounded-full border border-gray-200 bg-gray-100 px-3 py-1">语种 {{ languageCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-[minmax(0,1fr),220px] gap-4 items-end">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700">分组轮换间隔(分钟)</label>
|
||||
<input v-model.number="configForm.groupSwitchMinutes" :disabled="isStarting || !isElectronEnv" type="number" min="1" step="1" class="h-11 w-full rounded-lg border border-gray-300 bg-white px-4 text-sm text-gray-800 outline-none focus:border-blue-500 disabled:cursor-not-allowed disabled:opacity-60" @blur="handleSwitchMinutesBlur" />
|
||||
</div>
|
||||
<label class="flex h-11 items-center gap-3 rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700">
|
||||
<input v-model="configForm.replyUnreadMessages" :disabled="isStarting || !isElectronEnv" type="checkbox" class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" />
|
||||
<span>开启 AI 回复未读消息</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-4">
|
||||
<div class="mb-4 flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-gray-700">数据池来源</label>
|
||||
<p class="text-xs text-gray-500">启动时只需告诉后端用主播池还是大哥池。</p>
|
||||
</div>
|
||||
<span class="rounded-full border border-gray-200 bg-gray-100 px-3 py-1 text-xs text-gray-600">{{ dataPoolLabel }}</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<label class="flex items-center gap-3 rounded-lg border px-4 py-3 text-sm transition-colors" :class="configForm.dataPoolSource === 'anchor_hosts' ? 'border-blue-500 bg-blue-50 text-blue-700' : 'border-gray-200 bg-gray-50 text-gray-700'">
|
||||
<input v-model="configForm.dataPoolSource" :disabled="isStarting || !isElectronEnv" type="radio" value="anchor_hosts" class="h-4 w-4 border-gray-300 text-blue-600 focus:ring-blue-500" />
|
||||
<span>主播池</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-3 rounded-lg border px-4 py-3 text-sm transition-colors" :class="configForm.dataPoolSource === 'brother_info' ? 'border-purple-500 bg-purple-50 text-purple-700' : 'border-gray-200 bg-gray-50 text-gray-700'">
|
||||
<input v-model="configForm.dataPoolSource" :disabled="isStarting || !isElectronEnv" type="radio" value="brother_info" class="h-4 w-4 border-gray-300 text-purple-600 focus:ring-purple-500" />
|
||||
<span>大哥池</span>
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="isBrotherInfoMode" class="mt-4">
|
||||
<div class="mb-2 flex items-center justify-between gap-3">
|
||||
<label class="block text-sm font-medium text-gray-700">大哥随机回复话术</label>
|
||||
<span class="rounded-full border border-gray-200 bg-gray-100 px-3 py-1 text-xs text-gray-600">{{ replyMessageCount }} 条</span>
|
||||
</div>
|
||||
<textarea v-model="configForm.replyMessagesText" :disabled="isStarting || !isElectronEnv" rows="5" placeholder="一行一条回复话术,启动时传给后端 replyMessages" class="w-full rounded-lg border border-gray-300 bg-white px-4 py-3 text-sm text-gray-800 outline-none focus:border-purple-500 disabled:cursor-not-allowed disabled:opacity-60" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-xl border border-gray-200 bg-gradient-to-b from-white to-gray-50 p-6 shadow-sm">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="h-4 w-1 rounded-full bg-gradient-to-b from-purple-500 to-blue-500" />
|
||||
<span class="font-medium text-gray-900">视图分组</span>
|
||||
</div>
|
||||
<span class="text-xs text-gray-500">每组支持 0-3 个视图</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div v-for="(count, index) in configForm.groupViewCounts" :key="index" class="rounded-xl border border-gray-200 bg-white p-4">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-800">第 {{ index + 1 }} 组</div>
|
||||
<div class="mt-1 text-xs text-gray-500">{{ groupRangeLabel(index) }}</div>
|
||||
</div>
|
||||
<span class="rounded-full bg-gray-100 px-2 py-1 text-[10px] text-gray-600">{{ groupViewText(count) }}</span>
|
||||
</div>
|
||||
<input v-model.number="configForm.groupViewCounts[index]" :disabled="isStarting || !isElectronEnv" type="number" min="0" max="3" step="1" class="mt-4 h-11 w-full rounded-lg border border-gray-300 bg-gray-50 px-4 text-sm text-gray-800 outline-none focus:border-blue-500 disabled:cursor-not-allowed disabled:opacity-60" @blur="handleGroupCountBlur(index)" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<section class="rounded-xl border border-gray-200 bg-white p-5 shadow-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<div><div class="text-sm font-medium text-gray-900">AI 人设</div></div>
|
||||
<span :class="['rounded-full px-3 py-1 text-xs font-medium', aiConfigured ? 'bg-green-100 text-green-700' : 'bg-yellow-100 text-yellow-700']">{{ aiConfigured ? '已配置' : '未配置' }}</span>
|
||||
</div>
|
||||
<button @click="showAIDialog = true" class="mt-4 w-full rounded-lg border border-blue-200 bg-blue-50 px-4 py-3 text-sm font-medium text-blue-600 transition-colors hover:bg-blue-100">配置 / 修改 AI 人设</button>
|
||||
</section>
|
||||
|
||||
<section class="rounded-xl border border-gray-200 bg-white p-5 shadow-sm">
|
||||
<div class="text-sm font-medium text-gray-900">执行主播库</div>
|
||||
<button @click="showHostDialog = true" class="mt-4 w-full rounded-lg border border-purple-200 bg-purple-50 px-4 py-3 text-sm font-medium text-purple-600 transition-colors hover:bg-purple-100">打开执行主播库</button>
|
||||
</section>
|
||||
|
||||
<section class="rounded-xl border border-gray-200 bg-white p-5 shadow-sm">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-900">大哥池</div>
|
||||
<p class="mt-1 text-xs leading-5 text-gray-500">参考 brother_info 数据池,维护大哥列表并支持切换为大哥池模式启动任务。</p>
|
||||
</div>
|
||||
<span :class="['rounded-full px-3 py-1 text-xs font-medium', isBrotherInfoMode ? 'bg-fuchsia-100 text-fuchsia-700' : 'bg-gray-100 text-gray-600']">{{ isBrotherInfoMode ? '当前使用中' : '可切换' }}</span>
|
||||
</div>
|
||||
<button @click="showBrotherInfoDialog = true" class="mt-4 w-full rounded-lg border border-fuchsia-200 bg-fuchsia-50 px-4 py-3 text-sm font-medium text-fuchsia-600 transition-colors hover:bg-fuchsia-100">打开大哥池</button>
|
||||
</section>
|
||||
|
||||
<section class="rounded-xl bg-slate-950 p-5 shadow-sm">
|
||||
<div class="text-sm font-medium text-slate-100">本次提交参数</div>
|
||||
<pre class="mt-3 overflow-x-auto text-xs leading-6 text-slate-300">{{ payloadPreview }}</pre>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!isElectronEnv" class="mt-6 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-xs leading-5 text-amber-700">当前是 Web 环境,无法调用 Electron 的 TikTok 自动私信能力。</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex h-full w-full bg-gradient-to-br from-gray-50 to-gray-100 animate-fadeIn">
|
||||
<aside :style="sidebarStyle" class="h-full flex-shrink-0 border-r border-gray-200 bg-white shadow-sm">
|
||||
<div class="m-3 mb-0 flex gap-2">
|
||||
<button @click="backToConfig" class="flex-1 rounded-lg border border-gray-200 bg-gray-100 px-3 py-2 text-left text-xs text-gray-700 transition-colors hover:bg-gray-200">返回配置</button>
|
||||
<button @click="handleStop" :disabled="isStopping || !isElectronEnv" class="rounded-lg bg-red-500 px-3 py-2 text-xs text-white transition-colors hover:bg-red-600 disabled:cursor-not-allowed disabled:opacity-50">{{ isStopping ? '停止中' : '停止任务' }}</button>
|
||||
</div>
|
||||
<div class="border-b border-gray-200 p-4">
|
||||
<h1 class="bg-gradient-to-r from-blue-600 to-cyan-500 bg-clip-text text-lg font-bold text-transparent">自动私信(TK版)</h1>
|
||||
<p class="mt-1 text-xs text-gray-500">浏览器视图页,位置与 BrowserView 完全对齐</p>
|
||||
</div>
|
||||
<div class="space-y-4 overflow-auto p-4">
|
||||
<div class="rounded-xl border border-gray-200 bg-gray-50 p-4">
|
||||
<div class="mb-3 text-xs font-semibold uppercase tracking-wider text-gray-500">视图分组</div>
|
||||
<div class="space-y-3">
|
||||
<div v-for="group in browserViewGroups" :key="group.groupIndex" class="rounded-lg border border-gray-200 bg-white p-3">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-800">第 {{ group.groupIndex + 1 }} 组</span>
|
||||
<span class="text-xs text-gray-500">{{ group.label }}</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<button v-for="view in group.views" :key="view.viewId" :disabled="!view.enabled || isSwitchingView || !isElectronEnv" @click="handleSwitchView(view.viewId)" :class="['h-10 rounded-lg border text-sm font-medium transition-all disabled:cursor-not-allowed', view.active ? 'border-blue-500 bg-blue-500 text-white shadow-sm' : view.enabled ? 'border-gray-200 bg-gray-50 text-gray-700 hover:border-blue-300 hover:bg-blue-50' : 'border-dashed border-gray-200 bg-gray-100 text-gray-400 opacity-70']">{{ view.viewId }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-xl border border-gray-200 bg-gray-50 p-4">
|
||||
<div class="mb-3 text-xs font-semibold uppercase tracking-wider text-gray-500">快速操作</div>
|
||||
<div class="space-y-3">
|
||||
<label class="flex items-center gap-3 rounded-lg border border-gray-200 bg-white px-3 py-3 text-sm text-gray-700">
|
||||
<input v-model="loginConfirmed" :disabled="isStarting || !isElectronEnv" type="checkbox" class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" />
|
||||
<span>当前视图已完成手动登录</span>
|
||||
</label>
|
||||
<button class="h-9 w-full rounded-lg border border-blue-600 bg-blue-600 text-sm text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60" :disabled="isStarting || !isElectronEnv" @click="handleStart">{{ isStarting ? '启动中...' : '启动任务' }}</button>
|
||||
<div class="text-xs leading-5 text-gray-500">先打开浏览器视图,在当前视图里手动登录账号;确认登录完成后,再勾选并启动脚本。</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="relative flex min-w-0 flex-1 flex-col">
|
||||
<div class="flex h-12 items-center gap-2 border-b border-gray-200 bg-white px-4 shadow-sm">
|
||||
<span class="mr-2 text-sm text-gray-500">视图:</span>
|
||||
<button v-for="viewId in viewIds" :key="viewId" @click="handleSwitchView(viewId)" :disabled="isSwitchingView || !isElectronEnv" :class="['rounded-lg px-3 py-1.5 text-sm font-medium transition-all disabled:cursor-not-allowed disabled:opacity-60', activeViewId === viewId ? 'bg-blue-500 text-white shadow-md' : 'border border-gray-200 bg-gray-100 text-gray-600 hover:bg-gray-200']">视图 {{ viewId }}</button>
|
||||
<div class="flex-1" />
|
||||
<span class="rounded border border-gray-200 bg-gray-100 px-2 py-1 text-xs text-gray-500">{{ statusText }}</span>
|
||||
<span v-if="isSwitchingView" class="rounded border border-blue-200 bg-blue-50 px-2 py-1 text-xs text-blue-600">切换中...</span>
|
||||
</div>
|
||||
<div class="relative flex-1">
|
||||
<ViewPlaceholder class="absolute inset-0" />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<AIConfigDialog :visible="showAIDialog" :config="aiConfig" @close="showAIDialog = false" @save="handleSaveAIConfig" @change="(key, value) => aiConfig[key] = value" />
|
||||
<HostListDialog :visible="showHostDialog" @close="showHostDialog = false" @save="() => {}" />
|
||||
<BrotherInfoDialog :visible="showBrotherInfoDialog" @close="showBrotherInfoDialog = false" />
|
||||
<GreetingDialog :visible="showGreetingDialog" @close="showGreetingDialog = false" @confirm="handleGreetingConfirm" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { isElectron } from '@/utils/electronBridge'
|
||||
import ViewPlaceholder from '@/components/ViewPlaceholder.vue'
|
||||
import HostListDialog from '@/components/HostListDialog.vue'
|
||||
import AIConfigDialog from '@/components/AIConfigDialog.vue'
|
||||
import GreetingDialog from '@/components/GreetingDialog.vue'
|
||||
import BrotherInfoDialog from '@/components/BrotherInfoDialog.vue'
|
||||
|
||||
const props = defineProps({ navSidebarWidth: { type: Number, default: 144 } })
|
||||
const TIKTOK_VIEW_IDS = Array.from({ length: 9 }, (_, index) => index + 10)
|
||||
const DEFAULT_GROUP_COUNTS = [3, 3, 3]
|
||||
const isElectronEnv = isElectron()
|
||||
const pageMode = ref('config')
|
||||
const activeViewId = ref(TIKTOK_VIEW_IDS[0])
|
||||
const statusText = ref('待启动')
|
||||
const showAIDialog = ref(false)
|
||||
const showHostDialog = ref(false)
|
||||
const showGreetingDialog = ref(false)
|
||||
const showBrotherInfoDialog = ref(false)
|
||||
const aiConfigured = ref(false)
|
||||
const loginConfirmed = ref(false)
|
||||
const preparedConfigKey = ref('')
|
||||
const isPreparing = ref(false)
|
||||
const isStarting = ref(false)
|
||||
const isStopping = ref(false)
|
||||
const isSwitchingView = ref(false)
|
||||
const configForm = reactive({ prologueList: {}, needTranslate: false, dataPoolSource: 'anchor_hosts', replyMessagesText: '', replyUnreadMessages: true, groupSwitchMinutes: 10, groupViewCounts: [...DEFAULT_GROUP_COUNTS] })
|
||||
const aiConfig = ref({ agentName: '', guildName: '', contactTool: '', contact: '' })
|
||||
const sidebarStyle = computed(() => ({ width: `${props.navSidebarWidth}px`, minWidth: '96px', maxWidth: '400px' }))
|
||||
const greetingMessages = computed(() => {
|
||||
const baseMessages = Array.isArray(configForm.prologueList?.yolo) ? configForm.prologueList.yolo : []
|
||||
return baseMessages.map(item => String(item || '').trim()).filter(Boolean)
|
||||
})
|
||||
const replyMessages = computed(() => String(configForm.replyMessagesText || '').split(/\r?\n/).map(item => item.trim()).filter(Boolean))
|
||||
const greetingCount = computed(() => greetingMessages.value.length)
|
||||
const replyMessageCount = computed(() => replyMessages.value.length)
|
||||
const languageCount = computed(() => Object.keys(configForm.prologueList || {}).length)
|
||||
const isBrotherInfoMode = computed(() => configForm.dataPoolSource === 'brother_info')
|
||||
const dataPoolLabel = computed(() => (isBrotherInfoMode.value ? '大哥池' : '主播池'))
|
||||
const normalizedGroupSwitchMinutes = computed(() => clampMinutes(configForm.groupSwitchMinutes))
|
||||
const normalizedGroupViewCounts = computed(() => normalizeGroupViewCounts(configForm.groupViewCounts))
|
||||
const browserViewGroups = computed(() => normalizedGroupViewCounts.value.map((enabledCount, groupIndex) => {
|
||||
const baseViewId = TIKTOK_VIEW_IDS[groupIndex * 3]
|
||||
return { groupIndex, label: `视图 ${baseViewId}-${baseViewId + 2}`, views: Array.from({ length: 3 }, (_, offset) => ({ viewId: baseViewId + offset, enabled: offset < enabledCount, active: activeViewId.value === baseViewId + offset })) }
|
||||
}))
|
||||
const viewIds = computed(() => browserViewGroups.value.flatMap(group => group.views.filter(view => view.enabled).map(view => view.viewId)))
|
||||
const totalEnabledViews = computed(() => normalizedGroupViewCounts.value.reduce((sum, count) => sum + count, 0))
|
||||
const payloadPreview = computed(() => JSON.stringify(buildStartPayload(), null, 2))
|
||||
const currentPrepareKey = computed(() => JSON.stringify({ groupViewCounts: normalizedGroupViewCounts.value, groupSwitchMinutes: normalizedGroupSwitchMinutes.value, replyUnreadMessages: Boolean(configForm.replyUnreadMessages), dataPoolSource: configForm.dataPoolSource, replyMessages: replyMessages.value, prologueList: configForm.prologueList || {}, needTranslate: Boolean(configForm.needTranslate) }))
|
||||
const hasPreparedViews = computed(() => preparedConfigKey.value === currentPrepareKey.value)
|
||||
const statusChipClass = computed(() => statusText.value.includes('运行') ? 'rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1 text-xs font-medium text-emerald-700' : statusText.value.includes('失败') ? 'rounded-full border border-rose-200 bg-rose-50 px-3 py-1 text-xs font-medium text-rose-700' : 'rounded-full border border-amber-200 bg-amber-50 px-3 py-1 text-xs font-medium text-amber-700')
|
||||
function clampMinutes(value) { const numericValue = Number(value); return !Number.isFinite(numericValue) || numericValue <= 0 ? 10 : Math.max(1, Math.round(numericValue)) }
|
||||
function clampGroupCount(value) { const numericValue = Number(value); return !Number.isFinite(numericValue) ? 0 : Math.min(3, Math.max(0, Math.round(numericValue))) }
|
||||
function normalizeGroupViewCounts(values) { const safeValues = Array.isArray(values) ? values.slice(0, 3) : []; while (safeValues.length < 3) safeValues.push(0); return safeValues.map(clampGroupCount) }
|
||||
function buildStartPayload() {
|
||||
const payload = { dataPoolSource: configForm.dataPoolSource, replyUnreadMessages: Boolean(configForm.replyUnreadMessages), groupSwitchMinutes: normalizedGroupSwitchMinutes.value, groupViewCounts: normalizedGroupViewCounts.value, prologueList: JSON.parse(JSON.stringify(configForm.prologueList || {})), needTranslate: Boolean(configForm.needTranslate) }
|
||||
if (greetingMessages.value.length > 0) payload.greetingMessages = greetingMessages.value
|
||||
if (replyMessages.value.length > 0) payload.replyMessages = replyMessages.value
|
||||
return payload
|
||||
}
|
||||
function ensureElectronCapability(methodName) { if (!isElectronEnv || !window.electronAPI?.[methodName]) { ElMessage.error('当前环境不支持该功能'); return false } return true }
|
||||
function invalidatePreparedState(nextStatus = '配置已变更,请重新预热视图') { preparedConfigKey.value = ''; loginConfirmed.value = false; if (!statusText.value.includes('运行')) statusText.value = nextStatus }
|
||||
function handleGreetingConfirm(data) { configForm.prologueList = { yolo: data.sentences || [], ...(data.translations || {}) }; configForm.needTranslate = Boolean(data.needTranslate); showGreetingDialog.value = false; invalidatePreparedState(); void saveSharedConfig() }
|
||||
function handleSwitchMinutesBlur() { configForm.groupSwitchMinutes = clampMinutes(configForm.groupSwitchMinutes); invalidatePreparedState() }
|
||||
function handleGroupCountBlur(index) { configForm.groupViewCounts[index] = clampGroupCount(configForm.groupViewCounts[index]); invalidatePreparedState('视图配置已变更,请重新预热视图') }
|
||||
function groupViewText(count) { return count <= 0 ? '不启用' : `开启 ${count} 个` }
|
||||
function groupRangeLabel(index) { const startViewId = TIKTOK_VIEW_IDS[index * 3]; const endViewId = TIKTOK_VIEW_IDS[index * 3 + 2]; return `视图 ${startViewId}-${endViewId}` }
|
||||
async function checkAIConfig() {
|
||||
if (!isElectronEnv || !window.electronAPI?.loadAIConfig) return
|
||||
try {
|
||||
const saved = await window.electronAPI.loadAIConfig()
|
||||
aiConfigured.value = Boolean(saved && (saved.agentName || saved.guildName || saved.contactTool || saved.contact))
|
||||
if (aiConfigured.value) aiConfig.value = saved
|
||||
} catch {
|
||||
aiConfigured.value = false
|
||||
}
|
||||
}
|
||||
async function handleSaveAIConfig() {
|
||||
if (isElectronEnv && window.electronAPI?.saveAIConfig) {
|
||||
await window.electronAPI.saveAIConfig(JSON.parse(JSON.stringify(aiConfig.value)))
|
||||
}
|
||||
aiConfigured.value = true
|
||||
showAIDialog.value = false
|
||||
}
|
||||
async function loadSharedConfig() {
|
||||
if (!isElectronEnv || !window.electronAPI?.loadRunConfig) return
|
||||
try {
|
||||
const saved = await window.electronAPI.loadRunConfig()
|
||||
if (saved?.prologueList && typeof saved.prologueList === 'object') configForm.prologueList = JSON.parse(JSON.stringify(saved.prologueList))
|
||||
if (typeof saved?.needTranslate === 'boolean') configForm.needTranslate = saved.needTranslate
|
||||
if (saved?.standaloneTikTokDataPoolSource === 'anchor_hosts' || saved?.standaloneTikTokDataPoolSource === 'brother_info') configForm.dataPoolSource = saved.standaloneTikTokDataPoolSource
|
||||
if (Array.isArray(saved?.standaloneTikTokReplyMessages)) configForm.replyMessagesText = saved.standaloneTikTokReplyMessages.join('\n')
|
||||
} catch (error) {
|
||||
console.error('load shared config failed:', error)
|
||||
}
|
||||
}
|
||||
async function saveSharedConfig() {
|
||||
if (!isElectronEnv || !window.electronAPI?.saveRunConfig || !window.electronAPI?.loadRunConfig) return
|
||||
try {
|
||||
const saved = await window.electronAPI.loadRunConfig()
|
||||
const nextConfig = {
|
||||
...(saved || {}),
|
||||
prologueList: JSON.parse(JSON.stringify(configForm.prologueList || {})),
|
||||
needTranslate: Boolean(configForm.needTranslate),
|
||||
standaloneTikTokDataPoolSource: configForm.dataPoolSource,
|
||||
standaloneTikTokReplyMessages: replyMessages.value
|
||||
}
|
||||
await window.electronAPI.saveRunConfig(nextConfig)
|
||||
} catch (error) {
|
||||
console.error('save shared config failed:', error)
|
||||
}
|
||||
}
|
||||
async function handlePrepareViews() {
|
||||
if (!ensureElectronCapability('prepareStandaloneTikTokViews')) return
|
||||
if (viewIds.value.length === 0) {
|
||||
ElMessage.warning('请至少启用一个视图后再预热')
|
||||
return
|
||||
}
|
||||
loginConfirmed.value = false
|
||||
isPreparing.value = true
|
||||
try {
|
||||
const result = await window.electronAPI.prepareStandaloneTikTokViews({ groupViewCounts: normalizedGroupViewCounts.value, targetViewId: activeViewId.value })
|
||||
if (!result?.success) throw new Error(result?.error || 'prepare standalone tiktok views failed')
|
||||
if (result.currentViewId) activeViewId.value = result.currentViewId
|
||||
preparedConfigKey.value = currentPrepareKey.value
|
||||
await saveSharedConfig()
|
||||
pageMode.value = 'browser'
|
||||
await window.electronAPI.showViews()
|
||||
statusText.value = `视图已预热,请先在视图 ${activeViewId.value} 手动登录,再启动脚本`
|
||||
ElMessage.success('视图预热完成,已切换到浏览器视图')
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
ElMessage.error(`预热视图失败:${message}`)
|
||||
} finally {
|
||||
isPreparing.value = false
|
||||
}
|
||||
}
|
||||
async function openBrowserView() {
|
||||
if (!ensureElectronCapability('switchToView')) return
|
||||
pageMode.value = 'browser'
|
||||
try {
|
||||
await window.electronAPI.switchToView(activeViewId.value)
|
||||
await window.electronAPI.showViews()
|
||||
if (!statusText.value.includes('运行')) {
|
||||
statusText.value = hasPreparedViews.value ? `已打开预热视图 ${activeViewId.value},请手动登录后启动脚本` : '当前配置尚未预热,请先返回工作台执行预热视图'
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
ElMessage.error(`打开浏览器视图失败:${message}`)
|
||||
}
|
||||
}
|
||||
async function backToConfig() {
|
||||
loginConfirmed.value = false
|
||||
pageMode.value = 'config'
|
||||
if (!isElectronEnv || !window.electronAPI?.hideViews) return
|
||||
try {
|
||||
await window.electronAPI.hideViews()
|
||||
} catch (error) {
|
||||
console.error('hide views failed:', error)
|
||||
}
|
||||
}
|
||||
async function handleSwitchView(viewId) {
|
||||
if (!ensureElectronCapability('switchToView')) return
|
||||
isSwitchingView.value = true
|
||||
try {
|
||||
await window.electronAPI.switchToView(viewId)
|
||||
loginConfirmed.value = false
|
||||
activeViewId.value = viewId
|
||||
statusText.value = `已切换到视图 ${viewId},请先手动登录再启动脚本`
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
statusText.value = '切换失败'
|
||||
ElMessage.error(`切换视图失败:${message}`)
|
||||
} finally {
|
||||
isSwitchingView.value = false
|
||||
}
|
||||
}
|
||||
async function handleStart() {
|
||||
if (!ensureElectronCapability('startStandaloneTikTokAutomationAll')) return
|
||||
if (!hasPreparedViews.value) {
|
||||
ElMessage.warning('请先完成当前配置对应的视图预热,再启动脚本')
|
||||
return
|
||||
}
|
||||
if (isBrotherInfoMode.value && replyMessages.value.length === 0) {
|
||||
ElMessage.warning('选择大哥池后,请先填写大哥随机回复话术')
|
||||
return
|
||||
}
|
||||
if (!loginConfirmed.value) {
|
||||
ElMessage.warning('请先在当前视图手动登录,并勾选“当前视图已完成手动登录”后再启动脚本')
|
||||
return
|
||||
}
|
||||
isStarting.value = true
|
||||
try {
|
||||
await saveSharedConfig()
|
||||
await window.electronAPI.switchToView(activeViewId.value)
|
||||
await window.electronAPI.showViews()
|
||||
const result = await window.electronAPI.startStandaloneTikTokAutomationAll(buildStartPayload())
|
||||
if (!result?.success) {
|
||||
statusText.value = '启动失败'
|
||||
ElMessage.error(result?.error || '启动自动私信(TK版)失败')
|
||||
return
|
||||
}
|
||||
statusText.value = `运行中(视图 ${activeViewId.value})`
|
||||
ElMessage.success('自动私信(TK版)已启动')
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
statusText.value = '启动失败'
|
||||
ElMessage.error(`启动失败:${message}`)
|
||||
} finally {
|
||||
isStarting.value = false
|
||||
}
|
||||
}
|
||||
async function handleStop() {
|
||||
if (!ensureElectronCapability('stopStandaloneTikTokAutomationAll')) return
|
||||
isStopping.value = true
|
||||
try {
|
||||
const result = await window.electronAPI.stopStandaloneTikTokAutomationAll()
|
||||
if (!result?.success) {
|
||||
statusText.value = '停止失败'
|
||||
ElMessage.error(result?.error || '停止自动私信(TK版)失败')
|
||||
return
|
||||
}
|
||||
preparedConfigKey.value = ''
|
||||
loginConfirmed.value = false
|
||||
statusText.value = '已停止'
|
||||
ElMessage.success('自动私信(TK版)已停止')
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
statusText.value = '停止失败'
|
||||
ElMessage.error(`停止失败:${message}`)
|
||||
} finally {
|
||||
isStopping.value = false
|
||||
}
|
||||
}
|
||||
watch(currentPrepareKey, (nextKey, prevKey) => {
|
||||
if (!prevKey) return
|
||||
if (preparedConfigKey.value && preparedConfigKey.value !== nextKey) invalidatePreparedState()
|
||||
})
|
||||
watch(() => configForm.dataPoolSource, () => {
|
||||
invalidatePreparedState()
|
||||
void saveSharedConfig()
|
||||
})
|
||||
watch(() => configForm.replyMessagesText, () => {
|
||||
if (!isBrotherInfoMode.value) return
|
||||
invalidatePreparedState()
|
||||
})
|
||||
watch(viewIds, (nextViewIds) => {
|
||||
if (nextViewIds.length === 0) {
|
||||
activeViewId.value = TIKTOK_VIEW_IDS[0]
|
||||
return
|
||||
}
|
||||
if (!nextViewIds.includes(activeViewId.value)) activeViewId.value = nextViewIds[0]
|
||||
}, { immediate: true })
|
||||
watch(pageMode, async (newVal) => {
|
||||
if (!isElectronEnv) return
|
||||
if (newVal === 'config' && window.electronAPI?.hideViews) await window.electronAPI.hideViews().catch(() => {})
|
||||
})
|
||||
onMounted(async () => {
|
||||
await checkAIConfig()
|
||||
await loadSharedConfig()
|
||||
if (!isElectronEnv || !window.electronAPI?.hideViews) return
|
||||
await window.electronAPI.hideViews().catch(() => {})
|
||||
})
|
||||
onUnmounted(async () => {
|
||||
if (!isElectronEnv || !window.electronAPI?.hideViews) return
|
||||
await window.electronAPI.hideViews().catch(() => {})
|
||||
})
|
||||
</script>
|
||||
@@ -14,21 +14,9 @@
|
||||
|
||||
<!-- 国家和性别选择 -->
|
||||
<div class="select-box">
|
||||
<el-select-v2
|
||||
v-model="countryValue"
|
||||
:options="countryOptions"
|
||||
placeholder="国家"
|
||||
filterable
|
||||
clearable
|
||||
class="filter-select"
|
||||
/>
|
||||
<el-select-v2
|
||||
v-model="genderValue"
|
||||
:options="genderOptions"
|
||||
placeholder="性别"
|
||||
clearable
|
||||
class="filter-select"
|
||||
/>
|
||||
<el-select-v2 v-model="countryValue" :options="countryOptions" placeholder="国家" filterable clearable
|
||||
class="filter-select" />
|
||||
<el-select-v2 v-model="genderValue" :options="genderOptions" placeholder="性别" clearable class="filter-select" />
|
||||
</div>
|
||||
|
||||
<!-- 金币数量 -->
|
||||
@@ -46,15 +34,8 @@
|
||||
<!-- 时间选择 (仅PK大厅模式) -->
|
||||
<div class="time-box" :class="{ 'is-hidden': !isHallMode }">
|
||||
|
||||
<el-date-picker
|
||||
v-model="timeRange"
|
||||
type="datetimerange"
|
||||
range-separator="至"
|
||||
start-placeholder="最小PK时间"
|
||||
end-placeholder="最大PK时间"
|
||||
format="YYYY/MM/DD HH:mm"
|
||||
value-format="x"
|
||||
/>
|
||||
<el-date-picker v-model="timeRange" type="datetimerange" range-separator="至" start-placeholder="最小PK时间"
|
||||
end-placeholder="最大PK时间" format="YYYY/MM/DD HH:mm" value-format="x" />
|
||||
</div>
|
||||
|
||||
<!-- 搜索和重置按钮 -->
|
||||
@@ -74,19 +55,10 @@
|
||||
<div class="content-area">
|
||||
<!-- 列表面板 -->
|
||||
<div class="list-panel">
|
||||
<div
|
||||
v-infinite-scroll="loadMore"
|
||||
:infinite-scroll-distance="100"
|
||||
:infinite-scroll-disabled="loading || noMore"
|
||||
class="pk-list"
|
||||
>
|
||||
<div
|
||||
v-for="(item, index) in pkList"
|
||||
:key="index"
|
||||
class="pk-card"
|
||||
:class="{ selected: selectedItem === item }"
|
||||
@click="handleItemClick(item)"
|
||||
>
|
||||
<div v-infinite-scroll="loadMore" :infinite-scroll-distance="50" :infinite-scroll-disabled="loading || noMore"
|
||||
:infinite-scroll-immediate="true" class="pk-list">
|
||||
<div v-for="item in pkList" :key="item.id || item.anchorId" class="pk-card"
|
||||
:class="{ selected: selectedItem === item }" @click="handleItemClick(item)">
|
||||
<!-- 头像 -->
|
||||
<div class="pk-avatar">
|
||||
<img :src="item.anchorIcon" alt="" />
|
||||
@@ -95,18 +67,20 @@
|
||||
<!-- 个人信息 -->
|
||||
<div class="pk-personal">
|
||||
<span class="pk-name">{{ item.disPlayId }}</span>
|
||||
<span class="pk-gender" :class="item.sex === 1 ? 'male' : 'female'">
|
||||
{{ item.sex === 1 ? '男' : '女' }}
|
||||
<span class="pk-gender" :class="item.sex === '1' ? 'male' : 'female'">
|
||||
{{ item.sex === '1' ? '男' : '女' }}
|
||||
</span>
|
||||
<span class="pk-country">{{ item.country }}</span>
|
||||
</div>
|
||||
<!-- 时间 -->
|
||||
<div class="pk-time">PK时间(本地时间): {{ formatTime(item.pkTime * 1000) }}</div>
|
||||
<div class="pk-time">PK时间(本地时间): {{ formatTime(item.pkTime * 1000) }} · 北京时间: {{
|
||||
TimestampttoBeijingTime(item.pkTime * 1000) }}</div>
|
||||
<!-- PK信息 -->
|
||||
<div class="pk-stats">
|
||||
<img class="stat-icon" src="https://vv-1317974657.cos.ap-shanghai.myqcloud.com/util/gold.png" alt="" />
|
||||
<span>金币: {{ item.coin }}K</span>
|
||||
<img class="stat-icon session-icon" src="https://vv-1317974657.cos.ap-shanghai.myqcloud.com/util/session.png" alt="" />
|
||||
<img class="stat-icon session-icon"
|
||||
src="https://vv-1317974657.cos.ap-shanghai.myqcloud.com/util/session.png" alt="" />
|
||||
<span>场次: {{ item.pkNumber }}场</span>
|
||||
</div>
|
||||
<!-- 备注 -->
|
||||
@@ -115,6 +89,14 @@
|
||||
</div>
|
||||
|
||||
<div v-if="pkList.length === 0" class="empty-tip">暂无数据</div>
|
||||
<!-- 加载状态提示 -->
|
||||
<div v-if="loading" class="loading-tip">
|
||||
<span class="loading-spinner"></span>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
<div v-else-if="noMore && pkList.length > 0" class="no-more-tip">
|
||||
已加载全部数据
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -125,13 +107,10 @@
|
||||
<span class="chat-header-label">聊天</span>
|
||||
<span v-if="chatUserInfo.nickName" class="chat-header-name">· {{ chatUserInfo.nickName }}</span>
|
||||
</div>
|
||||
<div class="chat-messages" ref="chatMessagesRef" :style="{ visibility: isScrollReady ? 'visible' : 'hidden' }">
|
||||
<div
|
||||
v-for="(msg, index) in messagesList"
|
||||
:key="index"
|
||||
class="message-item"
|
||||
:class="{ mine: msg.senderId == currentUser.id, 'pk-message': msg.type === 'pk' }"
|
||||
>
|
||||
<div class="chat-messages" ref="chatMessagesRef"
|
||||
:style="{ visibility: isScrollReady ? 'visible' : 'hidden' }">
|
||||
<div v-for="(msg, index) in messagesList" :key="index" class="message-item"
|
||||
:class="{ mine: msg.senderId == currentUser.id, 'pk-message': msg.type === 'pk' }">
|
||||
<div class="message-triangle" v-if="msg.type === 'text'"></div>
|
||||
<div class="message-content">
|
||||
<div v-if="msg.type === 'text'" class="text-message">{{ msg.payload.text }}</div>
|
||||
@@ -155,11 +134,7 @@
|
||||
<div class="send-btn" @click="sendMessage">发送</div>
|
||||
</div>
|
||||
<div class="input-box">
|
||||
<textarea
|
||||
v-model="inputText"
|
||||
placeholder="输入消息..."
|
||||
@keydown.enter.prevent="sendMessage"
|
||||
></textarea>
|
||||
<textarea v-model="inputText" placeholder="输入消息..." @keydown.enter.prevent="sendMessage"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -170,13 +145,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 隐藏的文件输入 -->
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
style="display: none"
|
||||
@change="handleFileSelect"
|
||||
/>
|
||||
<input ref="fileInputRef" type="file" accept="image/*" style="display: none" @change="handleFileSelect" />
|
||||
|
||||
<!-- PK邀请弹窗 -->
|
||||
<el-dialog v-model="inviteDialogVisible" title="选择主播发起PK邀请" width="500" align-center>
|
||||
@@ -185,23 +154,19 @@
|
||||
暂无可用主播,请先在"我的"页面添加主播
|
||||
</div>
|
||||
<div v-else class="anchor-list">
|
||||
<div
|
||||
v-for="anchor in myAnchorList"
|
||||
:key="anchor.id"
|
||||
class="anchor-item"
|
||||
:class="{ selected: selectedAnchor?.id === anchor.id }"
|
||||
@click="selectedAnchor = anchor"
|
||||
>
|
||||
<div v-for="anchor in myAnchorList" :key="anchor.id" class="anchor-item"
|
||||
:class="{ selected: selectedAnchor?.id === anchor.id }" @click="selectedAnchor = anchor">
|
||||
<img class="anchor-avatar" :src="anchor.anchorIcon" alt="" />
|
||||
<div class="anchor-info">
|
||||
<div class="anchor-name">{{ anchor.anchorId }}</div>
|
||||
<div class="anchor-detail">
|
||||
<span class="anchor-gender" :class="anchor.sex === 1 ? 'male' : 'female'">
|
||||
{{ anchor.sex === 1 ? '男' : '女' }}
|
||||
<span class="anchor-gender" :class="anchor.sex === '1' ? 'male' : 'female'">
|
||||
{{ anchor.sex === '1' ? '男' : '女' }}
|
||||
</span>
|
||||
<span class="anchor-coin">{{ anchor.coin }}K</span>
|
||||
</div>
|
||||
<div class="anchor-time">PK时间: {{ formatTime(anchor.pkTime * 1000) }}</div>
|
||||
<div class="anchor-time">PK时间(本地时间): {{ formatTime(anchor.pkTime * 1000) }} · 北京时间: {{
|
||||
TimestampttoBeijingTime(anchor.pkTime * 1000) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -222,7 +187,7 @@
|
||||
import { ref, onMounted, onUnmounted, onActivated, nextTick } from 'vue'
|
||||
import { getPkList, getUserInfo, getAnchorListById, createPkRecord } from '@/api/pk-mini'
|
||||
import { getCountryNamesArray } from '@/utils/pk-mini/countryUtil'
|
||||
import { TimestamptolocalTime } from '@/utils/pk-mini/timeConversion'
|
||||
import { TimestamptolocalTime, TimestampttoBeijingTime } from '@/utils/pk-mini/timeConversion'
|
||||
import { getMainUserData } from '@/utils/pk-mini/storage'
|
||||
import { isGoEasyEnabled } from '@/config/pk-mini'
|
||||
import {
|
||||
@@ -292,11 +257,20 @@ const formatTime = TimestamptolocalTime
|
||||
function switchMode() {
|
||||
isHallMode.value = !isHallMode.value
|
||||
selectedItem.value = null
|
||||
// 重置分页和加载状态
|
||||
page.value = 0
|
||||
loading.value = false
|
||||
noMore.value = false
|
||||
// 清空当前模式的数据
|
||||
if (isHallMode.value) {
|
||||
hallList.value = []
|
||||
pkList.value = hallList.value
|
||||
} else {
|
||||
todayList.value = []
|
||||
pkList.value = todayList.value
|
||||
}
|
||||
// 重新加载数据
|
||||
loadPkList()
|
||||
}
|
||||
|
||||
// 搜索
|
||||
@@ -324,6 +298,7 @@ function handleReset() {
|
||||
|
||||
// 加载更多
|
||||
function loadMore() {
|
||||
console.log('[PkHall] loadMore 被调用')
|
||||
loadPkList()
|
||||
}
|
||||
|
||||
@@ -384,9 +359,27 @@ async function loadPkList() {
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载 PK 列表失败', e)
|
||||
noMore.value = true
|
||||
// ElMessage.error('加载失败,请稍后重试')
|
||||
} finally {
|
||||
loading.value = false
|
||||
// 数据加载完成后,使用 nextTick 强制重新计算滚动位置
|
||||
nextTick(() => {
|
||||
const pkListEl = document.querySelector('.pk-list')
|
||||
if (pkListEl) {
|
||||
// 触发一次滚动事件,确保无限滚动指令能够正确检测滚动位置
|
||||
pkListEl.dispatchEvent(new Event('scroll'))
|
||||
|
||||
// 手动检查是否已经滚动到底部,如果是,则直接调用 loadMore
|
||||
const { scrollTop, scrollHeight, clientHeight } = pkListEl
|
||||
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 10
|
||||
if (isAtBottom && !loading.value && !noMore.value) {
|
||||
console.log('[PkHall] 检测到已滚动到底部,手动调用 loadMore')
|
||||
setTimeout(() => {
|
||||
loadMore()
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,7 +398,7 @@ async function handleItemClick(item) {
|
||||
// 隐藏后滚到底部再显示,避免视觉跳动
|
||||
scrollToBottomHidden()
|
||||
// 标记消息已读
|
||||
goEasyMessageRead({ id: String(item.senderId) }).catch(() => {})
|
||||
goEasyMessageRead({ id: String(item.senderId) }).catch(() => { })
|
||||
} else {
|
||||
messagesList.value = []
|
||||
ElMessage.warning('聊天功能暂时不可用(GoEasy 订阅未续费)')
|
||||
@@ -463,14 +456,14 @@ async function sendMessage() {
|
||||
scrollToBottom()
|
||||
// 发送消息后标记已读,清除导航栏红点
|
||||
const senderId = String(selectedItem.value.senderId)
|
||||
goEasyMessageRead({ id: senderId }).catch(() => {})
|
||||
goEasyMessageRead({ id: senderId }).catch(() => { })
|
||||
unreadStore.decrease(1)
|
||||
} catch (e) {
|
||||
console.error('发送消息失败', e)
|
||||
if(e =='Error: id can not be the same as your id'){
|
||||
if (e == 'Error: id can not be the same as your id') {
|
||||
ElMessage.error('不能给自己发消息')
|
||||
}else{
|
||||
ElMessage.error('发送失败')
|
||||
} else {
|
||||
ElMessage.error('发送失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -589,7 +582,7 @@ async function confirmInvite() {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
countryOptions.value = getCountryNamesArray()
|
||||
currentUser.value = getMainUserData() || {}
|
||||
const userId = getUserId(currentUser.value)
|
||||
@@ -599,7 +592,27 @@ onMounted(() => {
|
||||
|
||||
// 初始加载 PK 大厅数据(通过 loadPkList 统一管理 page/loading/noMore 状态)
|
||||
if (userId) {
|
||||
loadPkList()
|
||||
await loadPkList()
|
||||
// 延迟一段时间,确保无限滚动指令已经完全初始化
|
||||
setTimeout(() => {
|
||||
const pkListEl = document.querySelector('.pk-list')
|
||||
if (pkListEl) {
|
||||
const { scrollTop, scrollHeight, clientHeight } = pkListEl
|
||||
console.log('[PkHall] pk-list 元素信息:', {
|
||||
scrollTop,
|
||||
scrollHeight,
|
||||
clientHeight,
|
||||
isAtBottom: scrollTop + clientHeight >= scrollHeight - 10
|
||||
})
|
||||
|
||||
// 手动滚动到底部,然后触发滚动事件
|
||||
pkListEl.scrollTop = scrollHeight - clientHeight
|
||||
setTimeout(() => {
|
||||
pkListEl.dispatchEvent(new Event('scroll'))
|
||||
console.log('[PkHall] 手动滚动到底部并触发滚动事件')
|
||||
}, 100)
|
||||
}
|
||||
}, 500)
|
||||
} else {
|
||||
console.warn('[PkHall] 未找到用户 ID,无法加载数据')
|
||||
}
|
||||
@@ -741,7 +754,8 @@ onUnmounted(() => {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-btn, .reset-btn {
|
||||
.search-btn,
|
||||
.reset-btn {
|
||||
width: 80px;
|
||||
height: 30px;
|
||||
border-radius: 5px;
|
||||
@@ -765,7 +779,8 @@ onUnmounted(() => {
|
||||
color: #2563eb; // blue-600
|
||||
}
|
||||
|
||||
.search-btn:hover, .reset-btn:hover {
|
||||
.search-btn:hover,
|
||||
.reset-btn:hover {
|
||||
transform: scale(1.05);
|
||||
opacity: 0.9;
|
||||
}
|
||||
@@ -901,6 +916,43 @@ onUnmounted(() => {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
// 加载状态提示
|
||||
.loading-tip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 20px;
|
||||
color: #2563eb; // blue-600
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-top: 2px solid #2563eb;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.no-more-tip {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #94a3b8; // slate-400
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
// 聊天面板
|
||||
.chat-panel {
|
||||
width: 350px;
|
||||
@@ -1013,7 +1065,7 @@ onUnmounted(() => {
|
||||
|
||||
.control-btn:hover {
|
||||
background: white;
|
||||
box-shadow: 2px 2px 5px rgba(0,0,0,0.1);
|
||||
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.control-btn img {
|
||||
@@ -1064,8 +1116,8 @@ onUnmounted(() => {
|
||||
|
||||
.time-box.is-hidden {
|
||||
opacity: 0;
|
||||
visibility: hidden; // 仍然占位,但看不见
|
||||
pointer-events: none; // 不能点击
|
||||
visibility: hidden; // 仍然占位,但看不见
|
||||
pointer-events: none; // 不能点击
|
||||
}
|
||||
|
||||
// 邀请弹窗样式
|
||||
|
||||
@@ -276,16 +276,23 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-6 text-center">
|
||||
<button v-if="pyData.isStart" @click="submit"
|
||||
class="bg-slate-900 hover:scale-[1.02] active:scale-[0.98] text-white px-10 py-3 rounded-xl font-bold text-lg shadow-xl shadow-slate-900/10 transition-all flex items-center gap-2 mx-auto">
|
||||
<span class="material-icons-round">bolt</span>
|
||||
{{ $t('workbenchesSetup.start') }}
|
||||
</button>
|
||||
<button v-else @click="unsubmit"
|
||||
class="bg-red-500 hover:bg-red-600 hover:scale-[1.02] active:scale-[0.98] text-white px-12 py-4 rounded-xl font-bold text-lg shadow-xl shadow-red-500/20 transition-all flex items-center gap-3 mx-auto">
|
||||
<span class="material-icons-round">stop</span>
|
||||
{{ $t('workbenchesSetup.stop') }}
|
||||
</button>
|
||||
<div class="flex flex-col md:flex-row gap-4 justify-center items-center">
|
||||
<button v-if="pyData.isStart" @click="submit"
|
||||
class="bg-slate-900 hover:scale-[1.02] active:scale-[0.98] text-white px-10 py-3 rounded-xl font-bold text-lg shadow-xl shadow-slate-900/10 transition-all flex items-center gap-2">
|
||||
<span class="material-icons-round">bolt</span>
|
||||
{{ $t('workbenchesSetup.start') }}
|
||||
</button>
|
||||
<button v-else @click="unsubmit"
|
||||
class="bg-red-500 hover:bg-red-600 hover:scale-[1.02] active:scale-[0.98] text-white px-12 py-3 rounded-xl font-bold text-lg shadow-xl shadow-red-500/20 transition-all flex items-center gap-3">
|
||||
<span class="material-icons-round">stop</span>
|
||||
{{ $t('workbenchesSetup.stop') }}
|
||||
</button>
|
||||
<button @click="resetTask"
|
||||
class="bg-amber-500 hover:bg-amber-600 hover:scale-[1.02] active:scale-[0.98] text-white px-10 py-3 rounded-xl font-bold text-lg shadow-xl shadow-amber-500/20 transition-all flex items-center gap-2">
|
||||
<span class="material-icons-round">refresh</span>
|
||||
重置任务
|
||||
</button>
|
||||
</div>
|
||||
<p class="mt-4 text-xs font-medium text-emerald-600">
|
||||
到期时间: {{ timestampToTime(expiredTime) }}
|
||||
</p>
|
||||
@@ -297,7 +304,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { ref, onMounted, computed, onUnmounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { usePythonBridge, } from '@/utils/pythonBridge'
|
||||
import { setNumData, getNumData, getUser, setTkUser, getTkUser } from '@/utils/storage'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
@@ -306,6 +314,24 @@ import { tkaccountuseinfo, getExpiredTime } from '@/api/account'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useCountryInfo } from '@/composables/useCountryInfo'
|
||||
|
||||
// 获取路由实例
|
||||
const router = useRouter();
|
||||
|
||||
// 重启 Python 服务的方法
|
||||
const restartPythonService = async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (window.electronAPI && window.electronAPI.restartPythonService) {
|
||||
window.electronAPI.restartPythonService().then(result => {
|
||||
resolve(result)
|
||||
}).catch(err => {
|
||||
reject(err)
|
||||
})
|
||||
} else {
|
||||
reject(new Error('重启 Python 服务的方法不可用'))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
// 使用独立的国家信息管理(不与其他页面共享)
|
||||
const {
|
||||
@@ -602,6 +628,44 @@ const toggleFilter = (filterName) => {
|
||||
pyData.value[filterName] = !pyData.value[filterName];
|
||||
};
|
||||
|
||||
// 重置任务 - 重启 Python 服务
|
||||
const resetTask = async () => {
|
||||
try {
|
||||
ElMessageBox.confirm(
|
||||
'确定要重置任务吗?这将重启服务并中断当前正在进行的获取主播和大哥的任务。',
|
||||
'重置任务',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
).then(async () => {
|
||||
ElMessage.info('正在重启 Python 服务...');
|
||||
|
||||
const result = await restartPythonService();
|
||||
await restartPythonService();
|
||||
if (result.success) {
|
||||
ElMessage.success('Python 服务已重启,正在重新加载工作台页面...');
|
||||
|
||||
// 重新加载 TK 工作台页面(相当于重新进入 tk 工作台)
|
||||
setTimeout(() => {
|
||||
// 调用 window.reloadTkWorkbench() 方法触发 TK 工作台重新加载
|
||||
if (window.reloadTkWorkbench) {
|
||||
window.reloadTkWorkbench();
|
||||
}
|
||||
}, 1000);
|
||||
} else {
|
||||
ElMessage.error(`重启失败: ${result.error}`);
|
||||
}
|
||||
}).catch(() => {
|
||||
// 用户取消
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('重置任务失败:', error);
|
||||
ElMessage.error('重置任务失败,请稍后重试');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const loginTK = (index) => {
|
||||
setTkUser(tkData.value)
|
||||
@@ -639,6 +703,11 @@ const openTK = () => {
|
||||
const checkTkLoginStatus = () => {
|
||||
getTkLoginStatus().then((res) => {
|
||||
isTkLoggedIn.value = res === true || res === 'true';
|
||||
|
||||
if (isTkLoggedIn.value) {
|
||||
clearInterval(tkStatusTimer.value);
|
||||
tkStatusTimer.value = null;
|
||||
}
|
||||
}).catch(() => {
|
||||
isTkLoggedIn.value = false;
|
||||
});
|
||||
@@ -746,6 +815,52 @@ const formattedTime = computed(() => {
|
||||
].join(':');
|
||||
});
|
||||
|
||||
// 清理所有定时器
|
||||
const clearAllTimers = () => {
|
||||
// 清理获取主播数量的定时器
|
||||
if (getHostTimer.value) {
|
||||
clearInterval(getHostTimer.value);
|
||||
getHostTimer.value = null;
|
||||
}
|
||||
|
||||
// 清理获取查询次数的定时器
|
||||
if (getNumTimer.value) {
|
||||
clearInterval(getNumTimer.value);
|
||||
getNumTimer.value = null;
|
||||
}
|
||||
|
||||
// 清理 TK 状态轮询定时器
|
||||
if (tkStatusTimer.value) {
|
||||
clearInterval(tkStatusTimer.value);
|
||||
tkStatusTimer.value = null;
|
||||
}
|
||||
|
||||
// 清理设置状态轮询定时器
|
||||
if (statusTimer) {
|
||||
clearInterval(statusTimer);
|
||||
statusTimer = null;
|
||||
}
|
||||
|
||||
// 清理设置状态轮询定时器(副本)
|
||||
if (statusTimerCopy) {
|
||||
clearInterval(statusTimerCopy);
|
||||
statusTimerCopy = null;
|
||||
}
|
||||
|
||||
// 清理运行时间的定时器
|
||||
if (timerCrawl) {
|
||||
clearInterval(timerCrawl);
|
||||
timerCrawl = null;
|
||||
}
|
||||
|
||||
console.log('所有定时器已清理');
|
||||
};
|
||||
|
||||
// 在组件卸载时清理定时器
|
||||
onUnmounted(() => {
|
||||
clearAllTimers();
|
||||
});
|
||||
|
||||
function timestampToTime(timestamp_ms) {
|
||||
const date = new Date(timestamp_ms);
|
||||
const year = date.getFullYear();
|
||||
|
||||
Reference in New Issue
Block a user