tk版私信出版
This commit is contained in:
@@ -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%);
|
||||
}
|
||||
|
||||
@@ -1,139 +1,187 @@
|
||||
<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="currentView === 'tk' ? 'bg-white text-blue-600 shadow shadow-blue-900/20' : 'text-slate-400 hover:bg-[rgba(21,96,250,0.06)]'">
|
||||
<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="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="currentView === 'hosts' ? 'bg-white text-blue-600 shadow shadow-blue-900/20' : 'text-slate-400 hover:bg-[rgba(21,96,250,0.06)]'">
|
||||
<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="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="currentView === 'auto_dm' ? 'bg-white text-blue-600 shadow shadow-blue-900/20' : 'text-slate-400 hover:bg-[rgba(21,96,250,0.06)]'">
|
||||
<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="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 = '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)]'">
|
||||
<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="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="currentView === 'pk_mini' ? 'bg-white text-blue-600 shadow shadow-blue-900/20' : 'text-slate-400 hover:bg-[rgba(21,96,250,0.06)]'">
|
||||
<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="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="currentView === 'shop' ? 'bg-white text-blue-600 shadow shadow-blue-900/20' : 'text-slate-400 hover:bg-[rgba(21,96,250,0.06)]'">
|
||||
<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="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>
|
||||
|
||||
<!-- 尽请期待 Tab -->
|
||||
<button class="w-full rounded-xl flex items-center gap-2 px-3 py-2.5 transition-all duration-200"
|
||||
style="height: 6vh; :disabled"
|
||||
: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>
|
||||
<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">
|
||||
<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" />
|
||||
<span class="text-base font-medium" style="color: #ED4949;">退出登录</span>
|
||||
</button>
|
||||
</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')"
|
||||
@config-updated="handleConfigUpdated" />
|
||||
<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 === '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">
|
||||
<PermissionMask permission-key="crawl" title="TK工作台未开通" description="您当前没有使用TK工作台功能的权限"
|
||||
:placeholder-image="placeholderTk" :contacts="serviceContacts">
|
||||
<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>
|
||||
@@ -148,11 +196,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'
|
||||
@@ -168,7 +216,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'
|
||||
@@ -176,30 +223,26 @@ 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 tkWorkbenchKey = ref(0) // 用于触发 TK 工作台重新加载
|
||||
const navSidebarWidth = ref(200)
|
||||
const tkWorkbenchKey = ref(0)
|
||||
|
||||
// 重新加载 TK 工作台
|
||||
const reloadTkWorkbench = () => {
|
||||
tkWorkbenchKey.value++
|
||||
console.log('TK 工作台已重新加载')
|
||||
}
|
||||
|
||||
// 将重新加载 TK 工作台的方法挂载到 window 对象
|
||||
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,
|
||||
@@ -214,14 +257,14 @@ const loadServiceContacts = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 监听菜单栏实际宽度,通知后端更新 BrowserView 定位
|
||||
let resizeObserver = null
|
||||
const notifySidebarWidth = (width) => {
|
||||
navSidebarWidth.value = Math.round(width)
|
||||
if (isElectron()) {
|
||||
window.electronAPI.setSidebarWidth(Math.round(width)).catch(() => { })
|
||||
window.electronAPI.setSidebarWidth(Math.round(width)).catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadServiceContacts()
|
||||
if (!isElectron()) return
|
||||
@@ -231,10 +274,10 @@ onMounted(() => {
|
||||
})
|
||||
if (sidebarRef.value) {
|
||||
resizeObserver.observe(sidebarRef.value)
|
||||
// 立即上报初始宽度
|
||||
notifySidebarWidth(sidebarRef.value.getBoundingClientRect().width)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
resizeObserver?.disconnect()
|
||||
})
|
||||
@@ -257,15 +300,11 @@ const handleStopAll = () => {
|
||||
emit('stop-all')
|
||||
}
|
||||
|
||||
// 处理配置更新事件
|
||||
const handleConfigUpdated = () => {
|
||||
// 触发自定义事件通知 YoloBrowser 重新加载配置
|
||||
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
|
||||
}
|
||||
@@ -273,19 +312,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 {
|
||||
@@ -295,24 +326,24 @@ 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
|
||||
|
||||
@@ -325,7 +356,6 @@ watch(autoDmMode, async (newVal) => {
|
||||
</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 {
|
||||
|
||||
32
src/types/electron.d.ts
vendored
32
src/types/electron.d.ts
vendored
@@ -77,6 +77,25 @@ export interface GreetingStats {
|
||||
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 {
|
||||
// 基础视图控制
|
||||
hideViews: () => Promise<{ success: boolean }>
|
||||
@@ -98,6 +117,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>
|
||||
|
||||
@@ -164,4 +194,4 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
export { }
|
||||
export { }
|
||||
642
src/views/auto-dm/AutoDmTkWorkbench.vue
Normal file
642
src/views/auto-dm/AutoDmTkWorkbench.vue
Normal 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>
|
||||
Reference in New Issue
Block a user