tk版私信出版

This commit is contained in:
2026-04-16 17:31:45 +08:00
parent 1f8b830d27
commit c0125a5a9f
4 changed files with 808 additions and 117 deletions

View File

@@ -1,7 +1,6 @@
<template> <template>
<!-- info / category 的公告滚动栏显示 title --> <!-- info / category 的公告滚动栏显示 title -->
<div v-if="infoNotices.length > 0" <div v-if="infoNotices.length > 0" :class="['notice-bar', 'notice-bar--info']">
:class="['notice-bar', 'notice-bar--info']">
<!-- 图标 --> <!-- 图标 -->
<span class="material-icons-round notice-bar__icon">campaign</span> <span class="material-icons-round notice-bar__icon">campaign</span>
@@ -18,31 +17,20 @@
</span> </span>
<!-- 关闭按钮 --> <!-- 关闭按钮 -->
<button v-if="closable" class="notice-bar__close" @click="handleClose" <button v-if="closable" class="notice-bar__close" @click="handleClose" :title="t('notice.close')">
:title="t('notice.close')">
<span class="material-icons-round text-base">close</span> <span class="material-icons-round text-base">close</span>
</button> </button>
</div> </div>
<!-- danger / warning 的公告弹窗逐条显示 title + content --> <!-- danger / warning 的公告弹窗逐条显示 title + content -->
<el-dialog <el-dialog v-model="dialogVisible" :title="currentAlert?.title" width="480px" align-center>
v-model="dialogVisible"
:title="currentAlert?.title"
width="480px"
:close-on-click-modal="false"
align-center
>
<div class="alert-notice__content" v-html="currentAlert?.content"></div> <div class="alert-notice__content" v-html="currentAlert?.content"></div>
<template #footer> <template #footer>
<el-button <el-button v-if="alertIndex < alertNotices.length - 1" @click="nextAlert">
v-if="alertIndex < alertNotices.length - 1"
@click="nextAlert"
>
下一条 ({{ alertIndex + 1 }}/{{ alertNotices.length }}) 下一条 ({{ alertIndex + 1 }}/{{ alertNotices.length }})
</el-button> </el-button>
<el-button type="primary" @click="closeAlert"> <el-button type="primary" @click="closeAlert">
{{ alertIndex < alertNotices.length - 1 ? '全部关闭' : '我知道了' }} {{ alertIndex < alertNotices.length - 1 ? '全部关闭' : '我知道了' }} </el-button>
</el-button>
</template> </template>
</el-dialog> </el-dialog>
</template> </template>
@@ -250,6 +238,7 @@ onUnmounted(() => {
0% { 0% {
transform: translateX(0); transform: translateX(0);
} }
100% { 100% {
transform: translateX(-100%); transform: translateX(-100%);
} }

View File

@@ -1,139 +1,187 @@
<template> <template>
<div class="flex h-screen w-screen overflow-hidden bg-white"> <div class="flex h-screen w-screen overflow-hidden bg-white">
<!-- Left Navigation Sidebar --> <div
<div ref="sidebarRef" class="flex flex-col items-center py-4 border-r z-50" ref="sidebarRef"
style="flex: 0 0 calc(100vw * 2 / 19); min-width: 96px; max-width: 400px; background-color: #F8F9FA;"> 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%;"> <div class="mb-6" style="border-bottom: 1px solid #A0AEC023; padding: 10%;">
<!-- Logo or Brand --> <div>
<div class="">
<img :src="yoloIcon" class="yolo-logo" /> <img :src="yoloIcon" class="yolo-logo" />
</div> </div>
</div> </div>
<div class="flex-1 flex flex-col w-full px-2" style="gap: 2vh;"> <div class="flex-1 flex flex-col w-full px-2" style="gap: 2vh;">
<button
<!-- TK Workbench Tab --> @click="currentView = 'tk'"
<button @click="currentView = 'tk'" class="w-full rounded-xl flex items-center gap-2 px-3 py-2.5 transition-all duration-200"
class="w-full rounded-xl flex items-center gap-2 px-3 py-2.5 transition-all duration-200" style="height: 6vh;" 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)]'"> :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" /> <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> <span class="text-base font-medium truncate">TK 工作台</span>
</button> </button>
<!-- Hosts List Tab --> <button
<button @click="currentView = 'hosts'" @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"
:class="currentView === 'hosts' ? 'bg-white text-blue-600 shadow shadow-blue-900/20' : 'text-slate-400 hover:bg-[rgba(21,96,250,0.06)]'"> 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" /> <img :src="currentView === 'hosts' ? nav22 : nav2" class="w-9 h-9 object-contain flex-shrink-0" />
<span class="text-base font-medium truncate">主播列表</span> <span class="text-base font-medium truncate">主播列表</span>
</button> </button>
<!-- Auto DM Workbench Tab --> <button
<button @click="currentView = 'auto_dm'" @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"
: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)]'"> 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" /> <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> <span class="text-base font-medium truncate">自动私信</span>
</button> </button>
<!-- Fan Workbench Tab --> <button
<button @click="currentView = 'FanWorkbench'" @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="w-full rounded-xl flex items-center gap-2 px-3 py-2.5 transition-all duration-200"
:class="currentView === 'FanWorkbench' ? 'bg-white text-blue-600 shadow shadow-blue-900/20' : 'text-slate-400 hover:bg-[rgba(21,96,250,0.06)]'"> 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="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" /> <img :src="currentView === 'FanWorkbench' ? nav44 : nav4" class="w-9 h-9 object-contain flex-shrink-0" />
<span class="text-base font-medium truncate">大哥工作台</span> <span class="text-base font-medium truncate">大哥工作台</span>
</button> </button>
<!-- PK 工作台 Tab --> <button
<button @click="currentView = 'pk_mini'" @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"
: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)]'"> 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" /> <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> <span class="text-base font-medium truncate">PK 工作台</span>
</button> </button>
<!-- yolo商店 Tab --> <button
<button @click="currentView = 'shop'" @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"
:class="currentView === 'shop' ? 'bg-white text-blue-600 shadow shadow-blue-900/20' : 'text-slate-400 hover:bg-[rgba(21,96,250,0.06)]'"> 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" /> <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> <span class="text-base font-medium truncate">TK商店</span>
</button> </button>
<!-- 尽请期待 Tab --> <button
<button class="w-full rounded-xl flex items-center gap-2 px-3 py-2.5 transition-all duration-200" 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; :disabled" style="height: 6vh;"
:class="currentView === 'test' ? 'bg-white text-blue-600 shadow shadow-blue-900/20' : 'text-slate-400 hover:bg-[rgba(21,96,250,0.06)]'"> >
<span class="text-base font-medium truncate">请期待...</span> <span class="text-base font-medium truncate">请期待...</span>
</button> </button>
</div> </div>
<div class="mt-auto w-full px-2"> <div class="mt-auto w-full px-2">
<!-- Logout --> <button
<button @click="$emit('logout')" @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"> 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" /> <img :src="backIcon" class="w-9 h-9 object-contain flex-shrink-0" />
<span class="text-base font-medium" style="color: #ED4949;">退出登录</span> <span class="text-base font-medium" style="color: #ED4949;">退出登录</span>
</button> </button>
</div> </div>
</div> </div>
<!-- Main Content Area -->
<div class="flex-1 h-full relative"> <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"> <div v-show="currentView === 'auto_dm'" class="absolute inset-0 z-10 h-full w-full">
<PermissionMask permission-key="webAi" title="自动私信工作台未开通" description="您当前没有使用自动私信功能的权限" <PermissionMask
:placeholder-image="placeholderWebAi" :contacts="serviceContacts"> 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"> <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" />
@config-updated="handleConfigUpdated" />
</div> </div>
<div v-show="autoDmMode === 'browser'" class="h-full w-full"> <div v-show="autoDmMode === 'browser'" class="h-full w-full">
<YoloBrowser v-bind="$attrs" :nav-sidebar-width="navSidebarWidth" @go-back="handleBackToConfig" <YoloBrowser v-bind="$attrs" :nav-sidebar-width="navSidebarWidth" @go-back="handleBackToConfig" @stop-all="handleStopAll" />
@stop-all="handleStopAll" />
</div> </div>
</PermissionMask> </PermissionMask>
</div> </div>
<!-- Tab 2: TK Workbench - crawl 权限 --> <div v-show="currentView === 'auto_dm_tk'" class="absolute inset-0 z-20 h-full w-full">
<PermissionMask
permission-key="webAi"
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"> <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工作台功能的权限" <PermissionMask
:placeholder-image="placeholderTk" :contacts="serviceContacts"> permission-key="crawl"
title="TK工作台未开通"
description="您当前没有使用TK工作台功能的权限"
:placeholder-image="placeholderTk"
:contacts="serviceContacts"
>
<TkWorkbenches :key="tkWorkbenchKey" /> <TkWorkbenches :key="tkWorkbenchKey" />
</PermissionMask> </PermissionMask>
</div> </div>
<!-- Tab 3: Hosts List - crawl 权限 -->
<div v-show="currentView === 'hosts'" class="absolute inset-0 z-20 bg-gray-50 h-full overflow-hidden"> <div v-show="currentView === 'hosts'" class="absolute inset-0 z-20 bg-gray-50 h-full overflow-hidden">
<PermissionMask permission-key="crawl" title="主播列表未开通" description="您当前没有使用主播列表功能的权限" <PermissionMask
:placeholder-image="placeholderHosts" :contacts="serviceContacts"> permission-key="crawl"
title="主播列表未开通"
description="您当前没有使用主播列表功能的权限"
:placeholder-image="placeholderHosts"
:contacts="serviceContacts"
>
<HostsList /> <HostsList />
</PermissionMask> </PermissionMask>
</div> </div>
<!-- Tab 4: Fan Workbench - bigBrother 权限 -->
<div v-show="currentView === 'FanWorkbench'" class="absolute inset-0 z-20 bg-gray-50 h-full overflow-hidden"> <div v-show="currentView === 'FanWorkbench'" class="absolute inset-0 z-20 bg-gray-50 h-full overflow-hidden">
<PermissionMask permission-key="bigBrother" title="大哥工作台未开通" description="您当前没有使用大哥工作台功能的权限" <PermissionMask
:placeholder-image="placeholderBigBrother" :contacts="serviceContacts"> permission-key="bigBrother"
title="大哥工作台未开通"
description="您当前没有使用大哥工作台功能的权限"
:placeholder-image="placeholderBigBrother"
:contacts="serviceContacts"
>
<FanWorkbench /> <FanWorkbench />
</PermissionMask> </PermissionMask>
</div> </div>
<!-- Tab 5: PK Mini 工作台 - 无需权限控制 -->
<div v-show="currentView === 'pk_mini'" class="absolute inset-0 z-20 h-full overflow-hidden"> <div v-show="currentView === 'pk_mini'" class="absolute inset-0 z-20 h-full overflow-hidden">
<PkMiniWorkbench /> <PkMiniWorkbench />
</div> </div>
<!-- Tab 6: yolo商店 - Electron BrowserViewWeb iframe 兜底 -->
<div v-show="currentView === 'shop'" class="absolute inset-0 z-20 h-full overflow-hidden"> <div v-show="currentView === 'shop'" class="absolute inset-0 z-20 h-full overflow-hidden">
<div v-if="isElectron()" <div v-if="isElectron()" class="w-full h-full flex items-center justify-center text-base text-slate-500 bg-white">
class="w-full h-full flex items-center justify-center text-base text-slate-500 bg-white">
正在进入商店... 正在进入商店...
</div> </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" 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> </div>
</div> </div>
@@ -148,11 +196,11 @@ import HostsList from '@/views/tk/HostsList.vue'
import ConfigPage from '@/pages/ConfigPage.vue' import ConfigPage from '@/pages/ConfigPage.vue'
import FanWorkbench from '@/views/tk/FanWorkbench.vue' import FanWorkbench from '@/views/tk/FanWorkbench.vue'
import PkMiniWorkbench from '@/views/pk-mini/PkMiniWorkbench.vue' import PkMiniWorkbench from '@/views/pk-mini/PkMiniWorkbench.vue'
import AutoDmTkWorkbench from '@/views/auto-dm/AutoDmTkWorkbench.vue'
import PermissionMask from '@/components/PermissionMask.vue' import PermissionMask from '@/components/PermissionMask.vue'
import { ENV } from '@/config' import { ENV } from '@/config'
import { getCustomServiceInfo } from '@/api/account' import { getCustomServiceInfo } from '@/api/account'
// 导航图标
import yoloIcon from '@/assets/nav/yolo.png' import yoloIcon from '@/assets/nav/yolo.png'
import nav1 from '@/assets/nav/nav1.png' import nav1 from '@/assets/nav/nav1.png'
import nav11 from '@/assets/nav/nav11.png' import nav11 from '@/assets/nav/nav11.png'
@@ -168,7 +216,6 @@ import nav6 from '@/assets/nav/nav6.png'
import nav66 from '@/assets/nav/nav66.png' import nav66 from '@/assets/nav/nav66.png'
import backIcon from '@/assets/nav/back.png' import backIcon from '@/assets/nav/back.png'
// 占位图片 - 无权限时显示的工作台截图
import placeholderTk from '@/assets/placeholder-tk.png' import placeholderTk from '@/assets/placeholder-tk.png'
import placeholderHosts from '@/assets/placeholder-hosts.png' import placeholderHosts from '@/assets/placeholder-hosts.png'
import placeholderWebAi from '@/assets/placeholder-webai.png' import placeholderWebAi from '@/assets/placeholder-webai.png'
@@ -176,30 +223,26 @@ import placeholderBigBrother from '@/assets/placeholder-bigbrother.png'
const emit = defineEmits(['logout', 'go-back', 'stop-all']) const emit = defineEmits(['logout', 'go-back', 'stop-all'])
const currentView = ref('tk') // Default Tab const currentView = ref('tk')
const autoDmMode = ref('config') // Default Sub-state: 'config' or 'browser' const autoDmMode = ref('config')
const adminLoaded = ref(false) // Web iframe 懒加载(仅非 Electron const adminLoaded = ref(false)
const shopOpened = ref(false) // Electron 只首开加载一次 const shopOpened = ref(false)
const shopUrl = ENV.SHOP_URL const shopUrl = ENV.SHOP_URL
const sidebarRef = useTemplateRef('sidebarRef') const sidebarRef = useTemplateRef('sidebarRef')
const navSidebarWidth = ref(200) // 左侧导航菜单的实际宽度px传给 YoloBrowser/Sidebar 使用 const navSidebarWidth = ref(200)
const tkWorkbenchKey = ref(0) // 用于触发 TK 工作台重新加载 const tkWorkbenchKey = ref(0)
// 重新加载 TK 工作台
const reloadTkWorkbench = () => { const reloadTkWorkbench = () => {
tkWorkbenchKey.value++ tkWorkbenchKey.value++
console.log('TK 工作台已重新加载') console.log('TK 工作台已重新加载')
} }
// 将重新加载 TK 工作台的方法挂载到 window 对象
window.reloadTkWorkbench = reloadTkWorkbench window.reloadTkWorkbench = reloadTkWorkbench
// 客服名片
const serviceContacts = ref([]) const serviceContacts = ref([])
const loadServiceContacts = async () => { const loadServiceContacts = async () => {
try { try {
const res = await getCustomServiceInfo() const res = await getCustomServiceInfo()
console.log("获取名片", res)
if (res) { if (res) {
serviceContacts.value = res.map(item => ({ serviceContacts.value = res.map(item => ({
avatar: item.avater, avatar: item.avater,
@@ -214,14 +257,14 @@ const loadServiceContacts = async () => {
} }
} }
// 监听菜单栏实际宽度,通知后端更新 BrowserView 定位
let resizeObserver = null let resizeObserver = null
const notifySidebarWidth = (width) => { const notifySidebarWidth = (width) => {
navSidebarWidth.value = Math.round(width) navSidebarWidth.value = Math.round(width)
if (isElectron()) { if (isElectron()) {
window.electronAPI.setSidebarWidth(Math.round(width)).catch(() => { }) window.electronAPI.setSidebarWidth(Math.round(width)).catch(() => {})
} }
} }
onMounted(() => { onMounted(() => {
loadServiceContacts() loadServiceContacts()
if (!isElectron()) return if (!isElectron()) return
@@ -231,10 +274,10 @@ onMounted(() => {
}) })
if (sidebarRef.value) { if (sidebarRef.value) {
resizeObserver.observe(sidebarRef.value) resizeObserver.observe(sidebarRef.value)
// 立即上报初始宽度
notifySidebarWidth(sidebarRef.value.getBoundingClientRect().width) notifySidebarWidth(sidebarRef.value.getBoundingClientRect().width)
} }
}) })
onUnmounted(() => { onUnmounted(() => {
resizeObserver?.disconnect() resizeObserver?.disconnect()
}) })
@@ -257,15 +300,11 @@ const handleStopAll = () => {
emit('stop-all') emit('stop-all')
} }
// 处理配置更新事件
const handleConfigUpdated = () => { const handleConfigUpdated = () => {
// 触发自定义事件通知 YoloBrowser 重新加载配置
window.dispatchEvent(new CustomEvent('config-updated')) window.dispatchEvent(new CustomEvent('config-updated'))
} }
// Watch for view changes to manage native Electron BrowserViews
watch(currentView, async (newVal, oldVal) => { watch(currentView, async (newVal, oldVal) => {
// 懒加载 Web 端 iframe仅非 Electron
if (newVal === 'shop' && !adminLoaded.value && !isElectron()) { if (newVal === 'shop' && !adminLoaded.value && !isElectron()) {
adminLoaded.value = true adminLoaded.value = true
} }
@@ -273,20 +312,12 @@ watch(currentView, async (newVal, oldVal) => {
if (!isElectron()) return if (!isElectron()) return
if (newVal === 'shop') { if (newVal === 'shop') {
if (!shopOpened.value) { try {
shopOpened.value = true shopOpened.value = true
try {
await window.electronAPI.openShop(shopUrl) await window.electronAPI.openShop(shopUrl)
} catch (e) { } catch (e) {
console.error('打开商店失败:', e) console.error('打开商店失败:', e)
} }
} else {
try {
await window.electronAPI.openShop(shopUrl)
} catch (e) {
console.error('打开商店失败:', e)
}
}
} else if (oldVal === 'shop') { } else if (oldVal === 'shop') {
try { try {
await window.electronAPI.hideShop() await window.electronAPI.hideShop()
@@ -295,24 +326,24 @@ watch(currentView, async (newVal, oldVal) => {
} }
} }
if (newVal === 'auto_dm' && autoDmMode.value === 'browser') { const shouldShowAutoDmViews =
// Switching TO Auto DM tab AND we are in browser mode: Show views newVal === 'auto_dm' && autoDmMode.value === 'browser'
if (shouldShowAutoDmViews) {
try { try {
await window.electronAPI.showViews() await window.electronAPI.showViews()
} catch (e) { } catch (e) {
console.error('Failed to show views:', e) console.error('Failed to show views:', e)
} }
} else { } else {
// Switching AWAY from Auto DM tab OR we are in config mode: Hide views
try { try {
await window.electronAPI.hideViews() await window.electronAPI.hideViews()
} catch (e) { } catch (e) {
// console.error('Failed to hide views:', e) console.error('Failed to hide views:', e)
} }
} }
}) })
// Watch sub-mode changes
watch(autoDmMode, async (newVal) => { watch(autoDmMode, async (newVal) => {
if (currentView.value !== 'auto_dm') return if (currentView.value !== 'auto_dm') return
@@ -325,7 +356,6 @@ watch(autoDmMode, async (newVal) => {
</script> </script>
<style scoped> <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'); @import url('https://fonts.googleapis.com/icon?family=Material+Icons+Round');
.yolo-logo { .yolo-logo {

View File

@@ -77,6 +77,25 @@ export interface GreetingStats {
details: ViewStats[] details: ViewStats[]
} }
export interface StandaloneTikTokAutomationOptions {
greetingMessages?: string[]
replyUnreadMessages?: boolean
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 { export interface ElectronAPI {
// 基础视图控制 // 基础视图控制
hideViews: () => Promise<{ success: boolean }> hideViews: () => Promise<{ success: boolean }>
@@ -98,6 +117,17 @@ export interface ElectronAPI {
// TikTok 自动化 // TikTok 自动化
startTikTokAutomation: (viewId: number, account: Account) => Promise<{ success: boolean; message?: string; error?: string }> startTikTokAutomation: (viewId: number, account: Account) => Promise<{ success: boolean; message?: string; error?: string }>
stopTikTokAutomation: (viewId: number) => 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 }> updateAutomationConfig: (config: Partial<AutomationConfig>) => Promise<{ success: boolean }>
getAutomationConfig: () => Promise<AutomationConfig> getAutomationConfig: () => Promise<AutomationConfig>

View File

@@ -0,0 +1,642 @@
<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="max-w-5xl mx-auto pb-8">
<div class="bg-white rounded-2xl shadow-xl p-8">
<div class="flex items-end justify-between mb-4 gap-6">
<div>
<h1 class="text-2xl font-semibold text-gray-900">自动私信工作台TK版</h1>
<p class="text-sm text-gray-500 mt-1">
先配置发送话术AI 回复和分组视图再进入浏览器视图页执行任务
</p>
</div>
<div class="flex items-center gap-3">
<span :class="statusChipClass">
{{ statusText }}
</span>
<button @click="handlePrepareViews" :disabled="isPreparing || !isElectronEnv"
class="px-4 py-2 text-sm bg-gradient-to-r from-emerald-500 to-teal-500 text-white rounded-lg hover:from-emerald-600 hover:to-teal-600 transition-all shadow-sm flex items-center gap-2 disabled:cursor-not-allowed disabled:opacity-60">
<span>{{ isPreparing ? '预热中...' : '预热视图' }}</span>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M12 5l7 7-7 7" />
</svg>
</button>
<button @click="openBrowserView"
class="px-4 py-2 text-sm bg-gradient-to-r from-blue-500 to-cyan-500 text-white rounded-lg hover:from-blue-600 hover:to-cyan-600 transition-all shadow-sm flex items-center gap-2">
<span>打开浏览器视图</span>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
</button>
</div>
</div>
<div
class="flex items-center gap-2 mb-6 px-4 py-2 rounded-full text-sm text-gray-700 bg-gradient-to-r from-blue-50 to-green-50 border border-blue-200 w-fit">
<span class="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
`groupViewCounts` 固定传 3 个元素分别对应第一组第二组第三组开启的视图数
</div>
<div class="grid grid-cols-3 gap-6">
<div class="col-span-2 space-y-6">
<section class="bg-gradient-to-b from-white to-gray-50 rounded-xl border border-gray-200 shadow-sm p-6">
<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="font-medium text-gray-900">运行参数</span>
</div>
<div class="grid grid-cols-3 gap-3 text-center">
<div class="rounded-lg border border-gray-200 bg-white px-3 py-2 min-w-[88px]">
<div class="text-[10px] text-gray-500">话术数</div>
<div class="mt-1 text-base font-semibold text-gray-800">{{ greetingCount }}</div>
</div>
<div class="rounded-lg border border-gray-200 bg-white px-3 py-2 min-w-[88px]">
<div class="text-[10px] text-gray-500">AI回复</div>
<div class="mt-1 text-base font-semibold text-gray-800">{{ configForm.replyUnreadMessages ? '开启' :
'关闭' }}</div>
</div>
<div class="rounded-lg border border-gray-200 bg-white px-3 py-2 min-w-[88px]">
<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>
<label class="block text-sm font-medium text-gray-700 mb-2">发送话术</label>
<textarea v-model="configForm.greetingText" :disabled="isStarting || !isElectronEnv" rows="6"
placeholder="一行一条话术"
class="w-full rounded-lg border border-gray-300 bg-white px-4 py-3 text-sm text-gray-800 outline-none focus:border-blue-500 disabled:cursor-not-allowed disabled:opacity-60"
@blur="handleGreetingBlur" />
<p class="mt-2 text-xs text-gray-500">`greetingMessages` 会按一行一条组装提交</p>
</div>
<div class="grid grid-cols-[minmax(0,1fr),220px] gap-4 items-end">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">分组轮换间隔分钟</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 items-center gap-3 rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700 h-11">
<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>
</section>
<section class="bg-gradient-to-b from-white to-gray-50 rounded-xl border border-gray-200 shadow-sm p-6">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-2">
<span class="w-1 h-4 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="px-2 py-1 rounded-full text-[10px] bg-gray-100 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="bg-white rounded-xl border border-gray-200 shadow-sm p-5">
<div class="flex items-center justify-between">
<div>
<div class="text-sm font-medium text-gray-900">AI 人设</div>
<div class="mt-1 text-xs text-gray-500">沿用自动私信工作台的 AI 配置入口</div>
</div>
<span :class="[
'px-3 py-1 rounded-full 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 px-4 py-3 text-sm font-medium text-blue-600 border border-blue-200 bg-blue-50 hover:bg-blue-100 rounded-lg transition-colors">
配置 / 修改 AI 人设
</button>
</section>
<section class="bg-white rounded-xl border border-gray-200 shadow-sm p-5">
<div class="text-sm font-medium text-gray-900">执行主播库</div>
<div class="mt-1 text-xs text-gray-500">复用主播库弹窗单独维护执行名单</div>
<button @click="showHostDialog = true"
class="mt-4 w-full px-4 py-3 text-sm font-medium text-purple-600 border border-purple-200 bg-purple-50 hover:bg-purple-100 rounded-lg transition-colors">
打开执行主播库
</button>
</section>
<section class="bg-slate-950 rounded-xl shadow-sm p-5">
<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 bg-white border-r border-gray-200 flex flex-col shadow-sm flex-shrink-0">
<div class="m-3 mb-0 flex gap-2">
<button @click="backToConfig"
class="flex-1 px-3 py-2 text-xs bg-gray-100 text-gray-700 border border-gray-200 rounded-lg hover:bg-gray-200 transition-colors text-left">
返回配置
</button>
<button @click="handleStop" :disabled="isStopping || !isElectronEnv"
class="px-3 py-2 text-xs bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title="停止独立版自动私信任务">
{{ isStopping ? '停止中' : '停止任务' }}
</button>
</div>
<div class="p-4 border-b border-gray-200">
<h1 class="text-lg font-bold bg-gradient-to-r from-blue-600 to-cyan-500 bg-clip-text text-transparent">
自动私信TK版
</h1>
<p class="text-xs text-gray-500 mt-1">浏览器视图页位置与 BrowserView 完全对齐</p>
</div>
<div class="p-4 space-y-4 overflow-auto">
<div v-if="false" class="rounded-xl border border-gray-200 bg-gray-50 p-4">
<div class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3">任务摘要</div>
<div class="grid grid-cols-2 gap-3 text-sm">
<div class="rounded-lg bg-white border border-gray-200 px-3 py-2">
<div class="text-[10px] text-gray-500">当前视图</div>
<div class="mt-1 font-semibold text-gray-800">{{ activeViewId }}</div>
</div>
<div class="rounded-lg bg-white border border-gray-200 px-3 py-2">
<div class="text-[10px] text-gray-500">任务状态</div>
<div class="mt-1 font-semibold text-gray-800">{{ statusText }}</div>
</div>
<div class="rounded-lg bg-white border border-gray-200 px-3 py-2">
<div class="text-[10px] text-gray-500">发送话术</div>
<div class="mt-1 font-semibold text-gray-800">{{ greetingCount }} </div>
</div>
<div class="rounded-lg bg-white border border-gray-200 px-3 py-2">
<div class="text-[10px] text-gray-500">AI 回复</div>
<div class="mt-1 font-semibold text-gray-800">{{ configForm.replyUnreadMessages ? '开启' : '关闭' }}</div>
</div>
</div>
</div>
<div class="rounded-xl border border-gray-200 bg-gray-50 p-4">
<div class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3">视图分组</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="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3">快速操作</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="w-full h-9 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 || !loginConfirmed" @click="handleStart">
{{ isStarting ? '启动中...' : '启动任务' }}
</button>
<div class="text-xs leading-5 text-gray-500">
先打开浏览器视图在当前视图里手动登录账号确认登录完成后再勾选并启动脚本
</div>
</div>
</div>
</div>
</aside>
<main class="flex-1 flex flex-col relative min-w-0">
<div class="h-12 bg-white border-b border-gray-200 flex items-center px-4 gap-2 shadow-sm">
<span class="text-gray-500 text-sm mr-2">视图:</span>
<button v-for="viewId in viewIds" :key="viewId" @click="handleSwitchView(viewId)"
:disabled="isSwitchingView || !isElectronEnv" :class="[
'px-3 py-1.5 rounded-lg text-sm font-medium transition-all disabled:opacity-60 disabled:cursor-not-allowed',
activeViewId === viewId
? 'bg-blue-500 text-white shadow-md'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 border border-gray-200'
]">
视图 {{ viewId }}
</button>
<div class="flex-1" />
<span class="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded border border-gray-200">
{{ statusText }}
</span>
<span v-if="isSwitchingView"
class="text-xs text-blue-600 bg-blue-50 px-2 py-1 rounded border border-blue-200">
切换中...
</span>
</div>
<div class="flex-1 relative">
<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="() => { }" />
</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'
const props = defineProps({
navSidebarWidth: {
type: Number,
default: 144
}
})
const TIKTOK_VIEW_IDS = Array.from({ length: 9 }, (_, index) => index + 10)
const DEFAULT_GREETING_TEXT = 'hello\nhi'
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 aiConfigured = ref(false)
const loginConfirmed = ref(false)
const isPreparing = ref(false)
const isStarting = ref(false)
const isStopping = ref(false)
const isSwitchingView = ref(false)
const configForm = reactive({
greetingText: DEFAULT_GREETING_TEXT,
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(() => {
return configForm.greetingText
.split(/\r?\n/)
.map(item => item.trim())
.filter(Boolean)
})
const greetingCount = computed(() => greetingMessages.value.length)
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) => {
const viewId = baseViewId + offset
return {
viewId,
enabled: offset < enabledCount,
active: activeViewId.value === viewId
}
})
}
})
)
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 statusChipClass = computed(() => {
if (statusText.value.includes('运行')) {
return 'px-3 py-1 rounded-full text-xs font-medium border bg-emerald-50 text-emerald-700 border-emerald-200'
}
if (statusText.value.includes('失败')) {
return 'px-3 py-1 rounded-full text-xs font-medium border bg-rose-50 text-rose-700 border-rose-200'
}
return 'px-3 py-1 rounded-full text-xs font-medium border bg-amber-50 text-amber-700 border-amber-200'
})
function clampMinutes(value) {
const numericValue = Number(value)
if (!Number.isFinite(numericValue) || numericValue <= 0) {
return 10
}
return Math.max(1, Math.round(numericValue))
}
function clampGroupCount(value) {
const numericValue = Number(value)
if (!Number.isFinite(numericValue)) {
return 0
}
return 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 = {
replyUnreadMessages: Boolean(configForm.replyUnreadMessages),
groupSwitchMinutes: normalizedGroupSwitchMinutes.value,
groupViewCounts: normalizedGroupViewCounts.value
}
if (greetingMessages.value.length > 0) {
payload.greetingMessages = greetingMessages.value
}
return payload
}
function ensureElectronCapability(methodName) {
if (!isElectronEnv || !window.electronAPI?.[methodName]) {
ElMessage.error('当前环境不支持该功能')
return false
}
return true
}
function handleGreetingBlur() {
configForm.greetingText = greetingMessages.value.join('\n')
}
function handleSwitchMinutesBlur() {
configForm.groupSwitchMinutes = clampMinutes(configForm.groupSwitchMinutes)
}
function handleGroupCountBlur(index) {
configForm.groupViewCounts[index] = clampGroupCount(configForm.groupViewCounts[index])
}
function groupViewText(count) {
if (count <= 0) {
return '不启用'
}
return `开启 ${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()
if (saved && (saved.agentName || saved.guildName || saved.contactTool || saved.contact)) {
aiConfig.value = saved
aiConfigured.value = true
return
}
aiConfigured.value = false
} 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 handlePrepareViews() {
if (!ensureElectronCapability('prepareStandaloneTikTokViews')) 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
}
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 = `已打开视图 ${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 (!loginConfirmed.value) {
ElMessage.warning('请先在当前视图手动登录,并勾选“已完成登录”后再启动脚本')
return
}
isStarting.value = true
try {
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
}
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(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()
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>