新增tk版 自动私信
This commit is contained in:
@@ -93,7 +93,7 @@ const props = defineProps({
|
|||||||
permissionKey: {
|
permissionKey: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
validator: (value) => ['bigBrother', 'crawl', 'webAi'].includes(value)
|
validator: (value) => ['bigBrother', 'crawl', 'webAi', 'autotk'].includes(value)
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -120,11 +120,17 @@ const visibleIndices = ref([])
|
|||||||
const visibleContacts = ref([])
|
const visibleContacts = ref([])
|
||||||
|
|
||||||
const guardKey = computed(() => `guard_${props.permissionKey}_${Date.now()}`)
|
const guardKey = computed(() => `guard_${props.permissionKey}_${Date.now()}`)
|
||||||
|
const effectivePermissionKey = computed(() => {
|
||||||
|
if (props.permissionKey === 'webAi' && props.title.includes('TK')) {
|
||||||
|
return 'autotk'
|
||||||
|
}
|
||||||
|
return props.permissionKey
|
||||||
|
})
|
||||||
|
|
||||||
const permissionsData = ref(getPermissions())
|
const permissionsData = ref(getPermissions())
|
||||||
|
|
||||||
const hasAccess = computed(() => {
|
const hasAccess = computed(() => {
|
||||||
return permissionsData.value[props.permissionKey] === 1
|
return permissionsData.value[effectivePermissionKey.value] === 1
|
||||||
})
|
})
|
||||||
|
|
||||||
const pickRandomIndices = (sourceIndices, count) => {
|
const pickRandomIndices = (sourceIndices, count) => {
|
||||||
@@ -175,6 +181,7 @@ const refreshPage = async () => {
|
|||||||
bigBrother: res.bigBrother,
|
bigBrother: res.bigBrother,
|
||||||
crawl: res.crawl,
|
crawl: res.crawl,
|
||||||
webAi: res.webAi,
|
webAi: res.webAi,
|
||||||
|
autotk: res.autotk ?? res.autoTK,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-screen w-screen overflow-hidden bg-white">
|
<div class="flex h-screen w-screen overflow-hidden bg-white">
|
||||||
<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%;">
|
||||||
<div>
|
<div>
|
||||||
<img :src="yoloIcon" class="yolo-logo" />
|
<img :src="yoloIcon" class="yolo-logo" />
|
||||||
@@ -12,89 +9,65 @@
|
|||||||
</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
|
<button @click="currentView = 'tk'"
|
||||||
@click="currentView = 'tk'"
|
class="w-full rounded-xl flex items-center gap-2 px-3 py-2.5 transition-all duration-200" style="height: 6vh;"
|
||||||
class="w-full rounded-xl flex items-center gap-2 px-3 py-2.5 transition-all duration-200"
|
:class="currentView === 'tk' ? '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 === '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>
|
||||||
|
|
||||||
<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>
|
||||||
|
|
||||||
<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>
|
||||||
|
|
||||||
<button
|
<button @click="currentView = 'auto_dm_tk'"
|
||||||
@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 === 'auto_dm_tk' ? '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" />
|
<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>
|
<span class="text-base font-medium truncate">自动私信TK版</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button @click="currentView = 'FanWorkbench'"
|
||||||
@click="currentView = 'FanWorkbench'"
|
class="w-full rounded-xl flex items-center gap-2 px-3 py-2.5 transition-all duration-200" style="height: 6vh;"
|
||||||
class="w-full rounded-xl flex items-center gap-2 px-3 py-2.5 transition-all duration-200"
|
: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 === '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>
|
||||||
|
|
||||||
<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>
|
||||||
|
|
||||||
<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>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="w-full rounded-xl flex items-center gap-2 px-3 py-2.5 transition-all duration-200 text-slate-400 hover:bg-[rgba(21,96,250,0.06)]"
|
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;"
|
style="height: 6vh;">
|
||||||
>
|
|
||||||
<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">
|
||||||
<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>
|
||||||
@@ -103,66 +76,43 @@
|
|||||||
|
|
||||||
<div class="flex-1 h-full relative">
|
<div class="flex-1 h-full relative">
|
||||||
<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
|
<PermissionMask permission-key="webAi" title="自动私信工作台未开通" description="您当前没有使用自动私信功能的权限"
|
||||||
permission-key="webAi"
|
:placeholder-image="placeholderWebAi" :contacts="serviceContacts">
|
||||||
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')" @config-updated="handleConfigUpdated" />
|
<ConfigPage @go-to-browser="handleGoToBrowser" @logout="$emit('logout')"
|
||||||
|
@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" @stop-all="handleStopAll" />
|
<YoloBrowser v-bind="$attrs" :nav-sidebar-width="navSidebarWidth" @go-back="handleBackToConfig"
|
||||||
|
@stop-all="handleStopAll" />
|
||||||
</div>
|
</div>
|
||||||
</PermissionMask>
|
</PermissionMask>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-show="currentView === 'auto_dm_tk'" class="absolute inset-0 z-20 h-full w-full">
|
<div v-show="currentView === 'auto_dm_tk'" class="absolute inset-0 z-20 h-full w-full">
|
||||||
<PermissionMask
|
<PermissionMask permission-key="autotk" title="自动私信(TK版)工作台未开通" description="您当前没有使用自动私信(TK版)功能的权限"
|
||||||
permission-key="webAi"
|
:placeholder-image="placeholderWebAi" :contacts="serviceContacts">
|
||||||
title="自动私信(TK版)工作台未开通"
|
|
||||||
description="您当前没有使用自动私信(TK版)功能的权限"
|
|
||||||
:placeholder-image="placeholderWebAi"
|
|
||||||
:contacts="serviceContacts"
|
|
||||||
>
|
|
||||||
<AutoDmTkWorkbench :nav-sidebar-width="navSidebarWidth" />
|
<AutoDmTkWorkbench :nav-sidebar-width="navSidebarWidth" />
|
||||||
</PermissionMask>
|
</PermissionMask>
|
||||||
</div>
|
</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
|
<PermissionMask permission-key="crawl" title="TK工作台未开通" description="您当前没有使用TK工作台功能的权限"
|
||||||
permission-key="crawl"
|
:placeholder-image="placeholderTk" :contacts="serviceContacts">
|
||||||
title="TK工作台未开通"
|
|
||||||
description="您当前没有使用TK工作台功能的权限"
|
|
||||||
:placeholder-image="placeholderTk"
|
|
||||||
:contacts="serviceContacts"
|
|
||||||
>
|
|
||||||
<TkWorkbenches :key="tkWorkbenchKey" />
|
<TkWorkbenches :key="tkWorkbenchKey" />
|
||||||
</PermissionMask>
|
</PermissionMask>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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
|
<PermissionMask permission-key="crawl" title="主播列表未开通" description="您当前没有使用主播列表功能的权限"
|
||||||
permission-key="crawl"
|
:placeholder-image="placeholderHosts" :contacts="serviceContacts">
|
||||||
title="主播列表未开通"
|
|
||||||
description="您当前没有使用主播列表功能的权限"
|
|
||||||
:placeholder-image="placeholderHosts"
|
|
||||||
:contacts="serviceContacts"
|
|
||||||
>
|
|
||||||
<HostsList />
|
<HostsList />
|
||||||
</PermissionMask>
|
</PermissionMask>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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
|
<PermissionMask permission-key="bigBrother" title="大哥工作台未开通" description="您当前没有使用大哥工作台功能的权限"
|
||||||
permission-key="bigBrother"
|
:placeholder-image="placeholderBigBrother" :contacts="serviceContacts">
|
||||||
title="大哥工作台未开通"
|
|
||||||
description="您当前没有使用大哥工作台功能的权限"
|
|
||||||
:placeholder-image="placeholderBigBrother"
|
|
||||||
:contacts="serviceContacts"
|
|
||||||
>
|
|
||||||
<FanWorkbench />
|
<FanWorkbench />
|
||||||
</PermissionMask>
|
</PermissionMask>
|
||||||
</div>
|
</div>
|
||||||
@@ -172,16 +122,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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()" 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>
|
</div>
|
||||||
<iframe
|
<iframe v-else-if="adminLoaded" :src="shopUrl" class="w-full h-full border-0"
|
||||||
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"
|
sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-downloads" />
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -261,7 +208,7 @@ 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(() => { })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -403,6 +403,7 @@ const handleSubmit = async () => {
|
|||||||
bigBrother: result.user.bigBrother,
|
bigBrother: result.user.bigBrother,
|
||||||
crawl: result.user.crawl,
|
crawl: result.user.crawl,
|
||||||
webAi: result.user.webAi,
|
webAi: result.user.webAi,
|
||||||
|
autotk: result.user.autoTk
|
||||||
});
|
});
|
||||||
|
|
||||||
emit('loginSuccess')
|
emit('loginSuccess')
|
||||||
|
|||||||
4
src/types/electron.d.ts
vendored
4
src/types/electron.d.ts
vendored
@@ -79,7 +79,11 @@ export interface GreetingStats {
|
|||||||
|
|
||||||
export interface StandaloneTikTokAutomationOptions {
|
export interface StandaloneTikTokAutomationOptions {
|
||||||
greetingMessages?: string[]
|
greetingMessages?: string[]
|
||||||
|
prologueList?: Record<string, string[]>
|
||||||
|
needTranslate?: boolean
|
||||||
|
replyMessages?: string[]
|
||||||
replyUnreadMessages?: boolean
|
replyUnreadMessages?: boolean
|
||||||
|
dataPoolSource?: 'anchor_hosts' | 'brother_info' | '/api/anchor/hosts/getAll' | '/api/gifters/brotherInfo/getAll'
|
||||||
groupSwitchMinutes?: number
|
groupSwitchMinutes?: number
|
||||||
groupViewCounts?: number[]
|
groupViewCounts?: number[]
|
||||||
continuousMode?: boolean
|
continuousMode?: boolean
|
||||||
|
|||||||
@@ -58,26 +58,28 @@ const PERMISSIONS_KEY = 'user_permissions';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 存储权限信息
|
* 存储权限信息
|
||||||
* @param {Object} permissions - 权限对象 { bigBrother, crawl, webAi }
|
* @param {Object} permissions - 权限对象 { bigBrother, crawl, webAi, autotk }
|
||||||
*/
|
*/
|
||||||
export function setPermissions(permissions) {
|
export function setPermissions(permissions) {
|
||||||
|
const autotkValue = permissions.autotk ?? permissions.autoTK ?? 0;
|
||||||
localStorage.setItem(PERMISSIONS_KEY, JSON.stringify({
|
localStorage.setItem(PERMISSIONS_KEY, JSON.stringify({
|
||||||
bigBrother: permissions.bigBrother ?? 0,
|
bigBrother: permissions.bigBrother ?? 0,
|
||||||
crawl: permissions.crawl ?? 0,
|
crawl: permissions.crawl ?? 0,
|
||||||
webAi: permissions.webAi ?? 0,
|
webAi: permissions.webAi ?? 0,
|
||||||
|
autotk: autotkValue,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取权限信息
|
* 获取权限信息
|
||||||
* @returns {Object} 权限对象 { bigBrother, crawl, webAi }
|
* @returns {Object} 权限对象 { bigBrother, crawl, webAi, autotk }
|
||||||
*/
|
*/
|
||||||
export function getPermissions() {
|
export function getPermissions() {
|
||||||
try {
|
try {
|
||||||
const permissions = JSON.parse(localStorage.getItem(PERMISSIONS_KEY));
|
const permissions = JSON.parse(localStorage.getItem(PERMISSIONS_KEY));
|
||||||
return permissions || { bigBrother: 0, crawl: 0, webAi: 0 };
|
return permissions || { bigBrother: 0, crawl: 0, webAi: 0, autotk: 0 };
|
||||||
} catch {
|
} catch {
|
||||||
return { bigBrother: 0, crawl: 0, webAi: 0 };
|
return { bigBrother: 0, crawl: 0, webAi: 0, autotk: 0 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,61 +1,46 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full w-full overflow-hidden bg-gradient-to-br from-slate-100 to-slate-200">
|
<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 v-if="pageMode === 'config'" class="h-full overflow-auto p-6">
|
||||||
<div class="max-w-5xl mx-auto pb-8">
|
<div class="mx-auto max-w-5xl pb-8">
|
||||||
<div class="bg-white rounded-2xl shadow-xl p-8">
|
<div class="rounded-2xl bg-white p-8 shadow-xl">
|
||||||
<div class="flex items-end justify-between mb-4 gap-6">
|
<div class="mb-4 flex items-end justify-between gap-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-2xl font-semibold text-gray-900">自动私信工作台(TK版)</h1>
|
<h1 class="text-2xl font-semibold text-gray-900">自动私信工作台(TK版)</h1>
|
||||||
<p class="text-sm text-gray-500 mt-1">
|
|
||||||
先配置发送话术、AI 回复和分组视图,再进入浏览器视图页执行任务。
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span :class="statusChipClass">
|
<span :class="statusChipClass">{{ statusText }}</span>
|
||||||
{{ statusText }}
|
<button @click="handlePrepareViews" :disabled="isPreparing || !isElectronEnv" class="flex items-center gap-2 rounded-lg bg-gradient-to-r from-emerald-500 to-teal-500 px-4 py-2 text-sm text-white shadow-sm transition-all hover:from-emerald-600 hover:to-teal-600 disabled:cursor-not-allowed disabled:opacity-60">
|
||||||
</span>
|
|
||||||
<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>
|
<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>
|
||||||
<button @click="openBrowserView"
|
<button @click="openBrowserView" class="flex items-center gap-2 rounded-lg bg-gradient-to-r from-blue-500 to-cyan-500 px-4 py-2 text-sm text-white shadow-sm transition-all hover:from-blue-600 hover:to-cyan-600">
|
||||||
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>
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="mb-6 flex w-fit items-center gap-2 rounded-full border border-blue-200 bg-gradient-to-r from-blue-50 to-green-50 px-4 py-2 text-sm text-gray-700">
|
||||||
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="h-2 w-2 animate-pulse rounded-full bg-green-500" />
|
||||||
<span class="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
|
先填写配置,再预热视图,登录完成后再启动脚本。
|
||||||
`groupViewCounts` 固定传 3 个元素,分别对应第一组、第二组、第三组开启的视图数。
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-3 gap-6">
|
<div class="grid grid-cols-3 gap-6">
|
||||||
<div class="col-span-2 space-y-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">
|
<section class="rounded-xl border border-gray-200 bg-gradient-to-b from-white to-gray-50 p-6 shadow-sm">
|
||||||
<div class="flex items-center justify-between mb-6">
|
<div class="mb-6 flex items-center justify-between">
|
||||||
<div class="flex items-center gap-2">
|
<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="h-4 w-1 rounded-full bg-gradient-to-b from-blue-500 to-green-500" />
|
||||||
<span class="font-medium text-gray-900">运行参数</span>
|
<span class="font-medium text-gray-900">运行参数</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-3 gap-3 text-center">
|
<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="min-w-[88px] rounded-lg border border-gray-200 bg-white px-3 py-2">
|
||||||
<div class="text-[10px] text-gray-500">话术数</div>
|
<div class="text-[10px] text-gray-500">话术数</div>
|
||||||
<div class="mt-1 text-base font-semibold text-gray-800">{{ greetingCount }}</div>
|
<div class="mt-1 text-base font-semibold text-gray-800">{{ greetingCount }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-lg border border-gray-200 bg-white px-3 py-2 min-w-[88px]">
|
<div class="min-w-[88px] rounded-lg border border-gray-200 bg-white px-3 py-2">
|
||||||
<div class="text-[10px] text-gray-500">AI回复</div>
|
<div class="text-[10px] text-gray-500">数据池</div>
|
||||||
<div class="mt-1 text-base font-semibold text-gray-800">{{ configForm.replyUnreadMessages ? '开启' :
|
<div class="mt-1 text-base font-semibold text-gray-800">{{ dataPoolLabel }}</div>
|
||||||
'关闭' }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-lg border border-gray-200 bg-white px-3 py-2 min-w-[88px]">
|
<div class="min-w-[88px] rounded-lg border border-gray-200 bg-white px-3 py-2">
|
||||||
<div class="text-[10px] text-gray-500">开启视图</div>
|
<div class="text-[10px] text-gray-500">开启视图</div>
|
||||||
<div class="mt-1 text-base font-semibold text-gray-800">{{ totalEnabledViews }}</div>
|
<div class="mt-1 text-base font-semibold text-gray-800">{{ totalEnabledViews }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -63,237 +48,167 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-5">
|
<div class="space-y-5">
|
||||||
<div>
|
<div class="rounded-xl border border-gray-200 bg-white p-4">
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">发送话术</label>
|
<div class="flex items-center justify-between gap-4">
|
||||||
<textarea v-model="configForm.greetingText" :disabled="isStarting || !isElectronEnv" rows="6"
|
<div>
|
||||||
placeholder="一行一条话术"
|
<label class="mb-1 block text-sm font-medium text-gray-700">打招呼内容</label>
|
||||||
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"
|
</div>
|
||||||
@blur="handleGreetingBlur" />
|
<button @click="showGreetingDialog = true" :disabled="isStarting || !isElectronEnv" class="rounded-lg border border-blue-200 bg-blue-50 px-4 py-2 text-sm font-medium text-blue-600 transition-colors hover:bg-blue-100 disabled:cursor-not-allowed disabled:opacity-60">
|
||||||
<p class="mt-2 text-xs text-gray-500">`greetingMessages` 会按一行一条组装提交。</p>
|
打招呼内容配置
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 flex items-center gap-3 text-xs text-gray-500">
|
||||||
|
<span class="rounded-full border border-gray-200 bg-gray-100 px-3 py-1">基础话术 {{ greetingCount }} 条</span>
|
||||||
|
<span class="rounded-full border border-gray-200 bg-gray-100 px-3 py-1">翻译 {{ configForm.needTranslate ? '已开启' : '未开启' }}</span>
|
||||||
|
<span class="rounded-full border border-gray-200 bg-gray-100 px-3 py-1">语种 {{ languageCount }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-[minmax(0,1fr),220px] gap-4 items-end">
|
<div class="grid grid-cols-[minmax(0,1fr),220px] gap-4 items-end">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">分组轮换间隔(分钟)</label>
|
<label class="mb-2 block text-sm font-medium text-gray-700">分组轮换间隔(分钟)</label>
|
||||||
<input v-model.number="configForm.groupSwitchMinutes" :disabled="isStarting || !isElectronEnv"
|
<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" />
|
||||||
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>
|
</div>
|
||||||
<label
|
<label class="flex h-11 items-center gap-3 rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700">
|
||||||
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" />
|
||||||
<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>
|
<span>开启 AI 回复未读消息</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-xl border border-gray-200 bg-white p-4">
|
||||||
|
<div class="mb-4 flex items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-sm font-medium text-gray-700">数据池来源</label>
|
||||||
|
<p class="text-xs text-gray-500">启动时只需告诉后端用主播池还是大哥池。</p>
|
||||||
|
</div>
|
||||||
|
<span class="rounded-full border border-gray-200 bg-gray-100 px-3 py-1 text-xs text-gray-600">{{ dataPoolLabel }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<label class="flex items-center gap-3 rounded-lg border px-4 py-3 text-sm transition-colors" :class="configForm.dataPoolSource === 'anchor_hosts' ? 'border-blue-500 bg-blue-50 text-blue-700' : 'border-gray-200 bg-gray-50 text-gray-700'">
|
||||||
|
<input v-model="configForm.dataPoolSource" :disabled="isStarting || !isElectronEnv" type="radio" value="anchor_hosts" class="h-4 w-4 border-gray-300 text-blue-600 focus:ring-blue-500" />
|
||||||
|
<span>主播池</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-3 rounded-lg border px-4 py-3 text-sm transition-colors" :class="configForm.dataPoolSource === 'brother_info' ? 'border-purple-500 bg-purple-50 text-purple-700' : 'border-gray-200 bg-gray-50 text-gray-700'">
|
||||||
|
<input v-model="configForm.dataPoolSource" :disabled="isStarting || !isElectronEnv" type="radio" value="brother_info" class="h-4 w-4 border-gray-300 text-purple-600 focus:ring-purple-500" />
|
||||||
|
<span>大哥池</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div v-if="isBrotherInfoMode" class="mt-4">
|
||||||
|
<div class="mb-2 flex items-center justify-between gap-3">
|
||||||
|
<label class="block text-sm font-medium text-gray-700">大哥随机回复话术</label>
|
||||||
|
<span class="rounded-full border border-gray-200 bg-gray-100 px-3 py-1 text-xs text-gray-600">{{ replyMessageCount }} 条</span>
|
||||||
|
</div>
|
||||||
|
<textarea v-model="configForm.replyMessagesText" :disabled="isStarting || !isElectronEnv" rows="5" placeholder="一行一条回复话术,启动时传给后端 replyMessages" class="w-full rounded-lg border border-gray-300 bg-white px-4 py-3 text-sm text-gray-800 outline-none focus:border-purple-500 disabled:cursor-not-allowed disabled:opacity-60" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="bg-gradient-to-b from-white to-gray-50 rounded-xl border border-gray-200 shadow-sm p-6">
|
<section class="rounded-xl border border-gray-200 bg-gradient-to-b from-white to-gray-50 p-6 shadow-sm">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="mb-4 flex items-center justify-between">
|
||||||
<div class="flex items-center gap-2">
|
<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="h-4 w-1 rounded-full bg-gradient-to-b from-purple-500 to-blue-500" />
|
||||||
<span class="font-medium text-gray-900">视图分组</span>
|
<span class="font-medium text-gray-900">视图分组</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-xs text-gray-500">每组支持 0-3 个视图</span>
|
<span class="text-xs text-gray-500">每组支持 0-3 个视图</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-3 gap-4">
|
<div class="grid grid-cols-3 gap-4">
|
||||||
<div v-for="(count, index) in configForm.groupViewCounts" :key="index"
|
<div v-for="(count, index) in configForm.groupViewCounts" :key="index" class="rounded-xl border border-gray-200 bg-white p-4">
|
||||||
class="rounded-xl border border-gray-200 bg-white p-4">
|
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-sm font-medium text-gray-800">第 {{ index + 1 }} 组</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 class="mt-1 text-xs text-gray-500">{{ groupRangeLabel(index) }}</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="px-2 py-1 rounded-full text-[10px] bg-gray-100 text-gray-600">
|
<span class="rounded-full bg-gray-100 px-2 py-1 text-[10px] text-gray-600">{{ groupViewText(count) }}</span>
|
||||||
{{ groupViewText(count) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<input v-model.number="configForm.groupViewCounts[index]" :disabled="isStarting || !isElectronEnv"
|
<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)" />
|
||||||
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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<section class="bg-white rounded-xl border border-gray-200 shadow-sm p-5">
|
<section class="rounded-xl border border-gray-200 bg-white p-5 shadow-sm">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div><div class="text-sm font-medium text-gray-900">AI 人设</div></div>
|
||||||
<div class="text-sm font-medium text-gray-900">AI 人设</div>
|
<span :class="['rounded-full px-3 py-1 text-xs font-medium', aiConfigured ? 'bg-green-100 text-green-700' : 'bg-yellow-100 text-yellow-700']">{{ aiConfigured ? '已配置' : '未配置' }}</span>
|
||||||
<div 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>
|
</div>
|
||||||
<button @click="showAIDialog = true"
|
<button @click="showAIDialog = true" class="mt-4 w-full rounded-lg border border-blue-200 bg-blue-50 px-4 py-3 text-sm font-medium text-blue-600 transition-colors hover:bg-blue-100">配置 / 修改 AI 人设</button>
|
||||||
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>
|
||||||
|
|
||||||
<section class="bg-white rounded-xl border border-gray-200 shadow-sm p-5">
|
<section class="rounded-xl border border-gray-200 bg-white p-5 shadow-sm">
|
||||||
<div class="text-sm font-medium text-gray-900">执行主播库</div>
|
<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 rounded-lg border border-purple-200 bg-purple-50 px-4 py-3 text-sm font-medium text-purple-600 transition-colors hover:bg-purple-100">打开执行主播库</button>
|
||||||
<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>
|
||||||
|
|
||||||
<section class="bg-slate-950 rounded-xl shadow-sm p-5">
|
<section class="rounded-xl bg-slate-950 p-5 shadow-sm">
|
||||||
<div class="text-sm font-medium text-slate-100">本次提交参数</div>
|
<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>
|
<pre class="mt-3 overflow-x-auto text-xs leading-6 text-slate-300">{{ payloadPreview }}</pre>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!isElectronEnv"
|
<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>
|
||||||
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="flex h-full w-full bg-gradient-to-br from-gray-50 to-gray-100 animate-fadeIn">
|
<div v-else class="flex h-full w-full bg-gradient-to-br from-gray-50 to-gray-100 animate-fadeIn">
|
||||||
<aside :style="sidebarStyle"
|
<aside :style="sidebarStyle" class="h-full flex-shrink-0 border-r border-gray-200 bg-white shadow-sm">
|
||||||
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">
|
<div class="m-3 mb-0 flex gap-2">
|
||||||
<button @click="backToConfig"
|
<button @click="backToConfig" class="flex-1 rounded-lg border border-gray-200 bg-gray-100 px-3 py-2 text-left text-xs text-gray-700 transition-colors hover:bg-gray-200">返回配置</button>
|
||||||
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 @click="handleStop" :disabled="isStopping || !isElectronEnv" class="rounded-lg bg-red-500 px-3 py-2 text-xs text-white transition-colors hover:bg-red-600 disabled:cursor-not-allowed disabled:opacity-50">{{ isStopping ? '停止中' : '停止任务' }}</button>
|
||||||
返回配置
|
|
||||||
</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>
|
||||||
|
<div class="border-b border-gray-200 p-4">
|
||||||
<div class="p-4 border-b border-gray-200">
|
<h1 class="bg-gradient-to-r from-blue-600 to-cyan-500 bg-clip-text text-lg font-bold text-transparent">自动私信(TK版)</h1>
|
||||||
<h1 class="text-lg font-bold bg-gradient-to-r from-blue-600 to-cyan-500 bg-clip-text text-transparent">
|
<p class="mt-1 text-xs text-gray-500">浏览器视图页,位置与 BrowserView 完全对齐</p>
|
||||||
自动私信(TK版)
|
|
||||||
</h1>
|
|
||||||
<p class="text-xs text-gray-500 mt-1">浏览器视图页,位置与 BrowserView 完全对齐</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="space-y-4 overflow-auto p-4">
|
||||||
<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="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="mb-3 text-xs font-semibold uppercase tracking-wider text-gray-500">视图分组</div>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div v-for="group in browserViewGroups" :key="group.groupIndex"
|
<div v-for="group in browserViewGroups" :key="group.groupIndex" class="rounded-lg border border-gray-200 bg-white p-3">
|
||||||
class="rounded-lg border border-gray-200 bg-white p-3">
|
|
||||||
<div class="mb-2 flex items-center justify-between">
|
<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-sm font-medium text-gray-800">第 {{ group.groupIndex + 1 }} 组</span>
|
||||||
<span class="text-xs text-gray-500">{{ group.label }}</span>
|
<span class="text-xs text-gray-500">{{ group.label }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-3 gap-2">
|
<div class="grid grid-cols-3 gap-2">
|
||||||
<button v-for="view in group.views" :key="view.viewId"
|
<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>
|
||||||
: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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-xl border border-gray-200 bg-gray-50 p-4">
|
<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="mb-3 text-xs font-semibold uppercase tracking-wider text-gray-500">快速操作</div>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<label
|
<label class="flex items-center gap-3 rounded-lg border border-gray-200 bg-white px-3 py-3 text-sm text-gray-700">
|
||||||
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" />
|
||||||
<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>
|
<span>当前视图已完成手动登录</span>
|
||||||
</label>
|
</label>
|
||||||
<button
|
<button class="h-9 w-full rounded-lg border border-blue-600 bg-blue-600 text-sm text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60" :disabled="isStarting || !isElectronEnv" @click="handleStart">{{ isStarting ? '启动中...' : '启动任务' }}</button>
|
||||||
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"
|
<div class="text-xs leading-5 text-gray-500">先打开浏览器视图,在当前视图里手动登录账号;确认登录完成后,再勾选并启动脚本。</div>
|
||||||
:disabled="isStarting || !isElectronEnv || !loginConfirmed" @click="handleStart">
|
|
||||||
{{ isStarting ? '启动中...' : '启动任务' }}
|
|
||||||
</button>
|
|
||||||
<div class="text-xs leading-5 text-gray-500">
|
|
||||||
先打开浏览器视图,在当前视图里手动登录账号;确认登录完成后,再勾选并启动脚本。
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main class="flex-1 flex flex-col relative min-w-0">
|
<main class="relative flex min-w-0 flex-1 flex-col">
|
||||||
<div class="h-12 bg-white border-b border-gray-200 flex items-center px-4 gap-2 shadow-sm">
|
<div class="flex h-12 items-center gap-2 border-b border-gray-200 bg-white px-4 shadow-sm">
|
||||||
<span class="text-gray-500 text-sm mr-2">视图:</span>
|
<span class="mr-2 text-sm text-gray-500">视图:</span>
|
||||||
<button v-for="viewId in viewIds" :key="viewId" @click="handleSwitchView(viewId)"
|
<button v-for="viewId in viewIds" :key="viewId" @click="handleSwitchView(viewId)" :disabled="isSwitchingView || !isElectronEnv" :class="['rounded-lg px-3 py-1.5 text-sm font-medium transition-all disabled:cursor-not-allowed disabled:opacity-60', activeViewId === viewId ? 'bg-blue-500 text-white shadow-md' : 'border border-gray-200 bg-gray-100 text-gray-600 hover:bg-gray-200']">视图 {{ viewId }}</button>
|
||||||
: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" />
|
<div class="flex-1" />
|
||||||
|
<span class="rounded border border-gray-200 bg-gray-100 px-2 py-1 text-xs text-gray-500">{{ statusText }}</span>
|
||||||
<span class="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded border border-gray-200">
|
<span v-if="isSwitchingView" class="rounded border border-blue-200 bg-blue-50 px-2 py-1 text-xs text-blue-600">切换中...</span>
|
||||||
{{ 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>
|
||||||
|
<div class="relative flex-1">
|
||||||
<div class="flex-1 relative">
|
|
||||||
<ViewPlaceholder class="absolute inset-0" />
|
<ViewPlaceholder class="absolute inset-0" />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AIConfigDialog :visible="showAIDialog" :config="aiConfig" @close="showAIDialog = false" @save="handleSaveAIConfig"
|
<AIConfigDialog :visible="showAIDialog" :config="aiConfig" @close="showAIDialog = false" @save="handleSaveAIConfig" @change="(key, value) => aiConfig[key] = value" />
|
||||||
@change="(key, value) => aiConfig[key] = value" />
|
<HostListDialog :visible="showHostDialog" @close="showHostDialog = false" @save="() => {}" />
|
||||||
|
<GreetingDialog :visible="showGreetingDialog" @close="showGreetingDialog = false" @confirm="handleGreetingConfirm" />
|
||||||
<HostListDialog :visible="showHostDialog" @close="showHostDialog = false" @save="() => { }" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -304,182 +219,76 @@ import { isElectron } from '@/utils/electronBridge'
|
|||||||
import ViewPlaceholder from '@/components/ViewPlaceholder.vue'
|
import ViewPlaceholder from '@/components/ViewPlaceholder.vue'
|
||||||
import HostListDialog from '@/components/HostListDialog.vue'
|
import HostListDialog from '@/components/HostListDialog.vue'
|
||||||
import AIConfigDialog from '@/components/AIConfigDialog.vue'
|
import AIConfigDialog from '@/components/AIConfigDialog.vue'
|
||||||
|
import GreetingDialog from '@/components/GreetingDialog.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({ navSidebarWidth: { type: Number, default: 144 } })
|
||||||
navSidebarWidth: {
|
|
||||||
type: Number,
|
|
||||||
default: 144
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const TIKTOK_VIEW_IDS = Array.from({ length: 9 }, (_, index) => index + 10)
|
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 DEFAULT_GROUP_COUNTS = [3, 3, 3]
|
||||||
|
|
||||||
const isElectronEnv = isElectron()
|
const isElectronEnv = isElectron()
|
||||||
const pageMode = ref('config')
|
const pageMode = ref('config')
|
||||||
const activeViewId = ref(TIKTOK_VIEW_IDS[0])
|
const activeViewId = ref(TIKTOK_VIEW_IDS[0])
|
||||||
const statusText = ref('待启动')
|
const statusText = ref('待启动')
|
||||||
const showAIDialog = ref(false)
|
const showAIDialog = ref(false)
|
||||||
const showHostDialog = ref(false)
|
const showHostDialog = ref(false)
|
||||||
|
const showGreetingDialog = ref(false)
|
||||||
const aiConfigured = ref(false)
|
const aiConfigured = ref(false)
|
||||||
const loginConfirmed = ref(false)
|
const loginConfirmed = ref(false)
|
||||||
|
const preparedConfigKey = ref('')
|
||||||
const isPreparing = ref(false)
|
const isPreparing = ref(false)
|
||||||
const isStarting = ref(false)
|
const isStarting = ref(false)
|
||||||
const isStopping = ref(false)
|
const isStopping = ref(false)
|
||||||
const isSwitchingView = ref(false)
|
const isSwitchingView = ref(false)
|
||||||
|
const configForm = reactive({ prologueList: {}, needTranslate: false, dataPoolSource: 'anchor_hosts', replyMessagesText: '', replyUnreadMessages: true, groupSwitchMinutes: 10, groupViewCounts: [...DEFAULT_GROUP_COUNTS] })
|
||||||
const configForm = reactive({
|
const aiConfig = ref({ agentName: '', guildName: '', contactTool: '', contact: '' })
|
||||||
greetingText: DEFAULT_GREETING_TEXT,
|
const sidebarStyle = computed(() => ({ width: `${props.navSidebarWidth}px`, minWidth: '96px', maxWidth: '400px' }))
|
||||||
replyUnreadMessages: true,
|
|
||||||
groupSwitchMinutes: 10,
|
|
||||||
groupViewCounts: [...DEFAULT_GROUP_COUNTS]
|
|
||||||
})
|
|
||||||
|
|
||||||
const aiConfig = ref({
|
|
||||||
agentName: '',
|
|
||||||
guildName: '',
|
|
||||||
contactTool: '',
|
|
||||||
contact: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const sidebarStyle = computed(() => ({
|
|
||||||
width: `${props.navSidebarWidth}px`,
|
|
||||||
minWidth: '96px',
|
|
||||||
maxWidth: '400px'
|
|
||||||
}))
|
|
||||||
|
|
||||||
const greetingMessages = computed(() => {
|
const greetingMessages = computed(() => {
|
||||||
return configForm.greetingText
|
const baseMessages = Array.isArray(configForm.prologueList?.yolo) ? configForm.prologueList.yolo : []
|
||||||
.split(/\r?\n/)
|
return baseMessages.map(item => String(item || '').trim()).filter(Boolean)
|
||||||
.map(item => item.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
})
|
})
|
||||||
|
const replyMessages = computed(() => String(configForm.replyMessagesText || '').split(/\r?\n/).map(item => item.trim()).filter(Boolean))
|
||||||
const greetingCount = computed(() => greetingMessages.value.length)
|
const greetingCount = computed(() => greetingMessages.value.length)
|
||||||
|
const replyMessageCount = computed(() => replyMessages.value.length)
|
||||||
|
const languageCount = computed(() => Object.keys(configForm.prologueList || {}).length)
|
||||||
|
const isBrotherInfoMode = computed(() => configForm.dataPoolSource === 'brother_info')
|
||||||
|
const dataPoolLabel = computed(() => (isBrotherInfoMode.value ? '大哥池' : '主播池'))
|
||||||
const normalizedGroupSwitchMinutes = computed(() => clampMinutes(configForm.groupSwitchMinutes))
|
const normalizedGroupSwitchMinutes = computed(() => clampMinutes(configForm.groupSwitchMinutes))
|
||||||
const normalizedGroupViewCounts = computed(() => normalizeGroupViewCounts(configForm.groupViewCounts))
|
const normalizedGroupViewCounts = computed(() => normalizeGroupViewCounts(configForm.groupViewCounts))
|
||||||
const browserViewGroups = computed(() =>
|
const browserViewGroups = computed(() => normalizedGroupViewCounts.value.map((enabledCount, groupIndex) => {
|
||||||
normalizedGroupViewCounts.value.map((enabledCount, groupIndex) => {
|
const baseViewId = TIKTOK_VIEW_IDS[groupIndex * 3]
|
||||||
const baseViewId = TIKTOK_VIEW_IDS[groupIndex * 3]
|
return { groupIndex, label: `视图 ${baseViewId}-${baseViewId + 2}`, views: Array.from({ length: 3 }, (_, offset) => ({ viewId: baseViewId + offset, enabled: offset < enabledCount, active: activeViewId.value === baseViewId + offset })) }
|
||||||
return {
|
}))
|
||||||
groupIndex,
|
const viewIds = computed(() => browserViewGroups.value.flatMap(group => group.views.filter(view => view.enabled).map(view => view.viewId)))
|
||||||
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 totalEnabledViews = computed(() => normalizedGroupViewCounts.value.reduce((sum, count) => sum + count, 0))
|
||||||
const payloadPreview = computed(() => JSON.stringify(buildStartPayload(), null, 2))
|
const payloadPreview = computed(() => JSON.stringify(buildStartPayload(), null, 2))
|
||||||
const statusChipClass = computed(() => {
|
const currentPrepareKey = computed(() => JSON.stringify({ groupViewCounts: normalizedGroupViewCounts.value, groupSwitchMinutes: normalizedGroupSwitchMinutes.value, replyUnreadMessages: Boolean(configForm.replyUnreadMessages), dataPoolSource: configForm.dataPoolSource, replyMessages: replyMessages.value, prologueList: configForm.prologueList || {}, needTranslate: Boolean(configForm.needTranslate) }))
|
||||||
if (statusText.value.includes('运行')) {
|
const hasPreparedViews = computed(() => preparedConfigKey.value === currentPrepareKey.value)
|
||||||
return 'px-3 py-1 rounded-full text-xs font-medium border bg-emerald-50 text-emerald-700 border-emerald-200'
|
const statusChipClass = computed(() => statusText.value.includes('运行') ? 'rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1 text-xs font-medium text-emerald-700' : statusText.value.includes('失败') ? 'rounded-full border border-rose-200 bg-rose-50 px-3 py-1 text-xs font-medium text-rose-700' : 'rounded-full border border-amber-200 bg-amber-50 px-3 py-1 text-xs font-medium text-amber-700')
|
||||||
}
|
function clampMinutes(value) { const numericValue = Number(value); return !Number.isFinite(numericValue) || numericValue <= 0 ? 10 : Math.max(1, Math.round(numericValue)) }
|
||||||
if (statusText.value.includes('失败')) {
|
function clampGroupCount(value) { const numericValue = Number(value); return !Number.isFinite(numericValue) ? 0 : Math.min(3, Math.max(0, Math.round(numericValue))) }
|
||||||
return 'px-3 py-1 rounded-full text-xs font-medium border bg-rose-50 text-rose-700 border-rose-200'
|
function normalizeGroupViewCounts(values) { const safeValues = Array.isArray(values) ? values.slice(0, 3) : []; while (safeValues.length < 3) safeValues.push(0); return safeValues.map(clampGroupCount) }
|
||||||
}
|
|
||||||
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() {
|
function buildStartPayload() {
|
||||||
const payload = {
|
const payload = { dataPoolSource: configForm.dataPoolSource, replyUnreadMessages: Boolean(configForm.replyUnreadMessages), groupSwitchMinutes: normalizedGroupSwitchMinutes.value, groupViewCounts: normalizedGroupViewCounts.value, prologueList: JSON.parse(JSON.stringify(configForm.prologueList || {})), needTranslate: Boolean(configForm.needTranslate) }
|
||||||
replyUnreadMessages: Boolean(configForm.replyUnreadMessages),
|
if (greetingMessages.value.length > 0) payload.greetingMessages = greetingMessages.value
|
||||||
groupSwitchMinutes: normalizedGroupSwitchMinutes.value,
|
if (replyMessages.value.length > 0) payload.replyMessages = replyMessages.value
|
||||||
groupViewCounts: normalizedGroupViewCounts.value
|
|
||||||
}
|
|
||||||
|
|
||||||
if (greetingMessages.value.length > 0) {
|
|
||||||
payload.greetingMessages = greetingMessages.value
|
|
||||||
}
|
|
||||||
|
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
|
function ensureElectronCapability(methodName) { if (!isElectronEnv || !window.electronAPI?.[methodName]) { ElMessage.error('当前环境不支持该功能'); return false } return true }
|
||||||
function ensureElectronCapability(methodName) {
|
function invalidatePreparedState(nextStatus = '配置已变更,请重新预热视图') { preparedConfigKey.value = ''; loginConfirmed.value = false; if (!statusText.value.includes('运行')) statusText.value = nextStatus }
|
||||||
if (!isElectronEnv || !window.electronAPI?.[methodName]) {
|
function handleGreetingConfirm(data) { configForm.prologueList = { yolo: data.sentences || [], ...(data.translations || {}) }; configForm.needTranslate = Boolean(data.needTranslate); showGreetingDialog.value = false; invalidatePreparedState(); void saveSharedConfig() }
|
||||||
ElMessage.error('当前环境不支持该功能')
|
function handleSwitchMinutesBlur() { configForm.groupSwitchMinutes = clampMinutes(configForm.groupSwitchMinutes); invalidatePreparedState() }
|
||||||
return false
|
function handleGroupCountBlur(index) { configForm.groupViewCounts[index] = clampGroupCount(configForm.groupViewCounts[index]); invalidatePreparedState('视图配置已变更,请重新预热视图') }
|
||||||
}
|
function groupViewText(count) { return count <= 0 ? '不启用' : `开启 ${count} 个` }
|
||||||
return true
|
function groupRangeLabel(index) { const startViewId = TIKTOK_VIEW_IDS[index * 3]; const endViewId = TIKTOK_VIEW_IDS[index * 3 + 2]; return `视图 ${startViewId}-${endViewId}` }
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
async function checkAIConfig() {
|
||||||
if (!isElectronEnv || !window.electronAPI?.loadAIConfig) return
|
if (!isElectronEnv || !window.electronAPI?.loadAIConfig) return
|
||||||
try {
|
try {
|
||||||
const saved = await window.electronAPI.loadAIConfig()
|
const saved = await window.electronAPI.loadAIConfig()
|
||||||
if (saved && (saved.agentName || saved.guildName || saved.contactTool || saved.contact)) {
|
aiConfigured.value = Boolean(saved && (saved.agentName || saved.guildName || saved.contactTool || saved.contact))
|
||||||
aiConfig.value = saved
|
if (aiConfigured.value) aiConfig.value = saved
|
||||||
aiConfigured.value = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
aiConfigured.value = false
|
|
||||||
} catch {
|
} catch {
|
||||||
aiConfigured.value = false
|
aiConfigured.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSaveAIConfig() {
|
async function handleSaveAIConfig() {
|
||||||
if (isElectronEnv && window.electronAPI?.saveAIConfig) {
|
if (isElectronEnv && window.electronAPI?.saveAIConfig) {
|
||||||
await window.electronAPI.saveAIConfig(JSON.parse(JSON.stringify(aiConfig.value)))
|
await window.electronAPI.saveAIConfig(JSON.parse(JSON.stringify(aiConfig.value)))
|
||||||
@@ -487,26 +296,51 @@ async function handleSaveAIConfig() {
|
|||||||
aiConfigured.value = true
|
aiConfigured.value = true
|
||||||
showAIDialog.value = false
|
showAIDialog.value = false
|
||||||
}
|
}
|
||||||
|
async function loadSharedConfig() {
|
||||||
|
if (!isElectronEnv || !window.electronAPI?.loadRunConfig) return
|
||||||
|
try {
|
||||||
|
const saved = await window.electronAPI.loadRunConfig()
|
||||||
|
if (saved?.prologueList && typeof saved.prologueList === 'object') configForm.prologueList = JSON.parse(JSON.stringify(saved.prologueList))
|
||||||
|
if (typeof saved?.needTranslate === 'boolean') configForm.needTranslate = saved.needTranslate
|
||||||
|
if (saved?.standaloneTikTokDataPoolSource === 'anchor_hosts' || saved?.standaloneTikTokDataPoolSource === 'brother_info') configForm.dataPoolSource = saved.standaloneTikTokDataPoolSource
|
||||||
|
if (Array.isArray(saved?.standaloneTikTokReplyMessages)) configForm.replyMessagesText = saved.standaloneTikTokReplyMessages.join('\n')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('load shared config failed:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function saveSharedConfig() {
|
||||||
|
if (!isElectronEnv || !window.electronAPI?.saveRunConfig || !window.electronAPI?.loadRunConfig) return
|
||||||
|
try {
|
||||||
|
const saved = await window.electronAPI.loadRunConfig()
|
||||||
|
const nextConfig = {
|
||||||
|
...(saved || {}),
|
||||||
|
prologueList: JSON.parse(JSON.stringify(configForm.prologueList || {})),
|
||||||
|
needTranslate: Boolean(configForm.needTranslate),
|
||||||
|
standaloneTikTokDataPoolSource: configForm.dataPoolSource,
|
||||||
|
standaloneTikTokReplyMessages: replyMessages.value
|
||||||
|
}
|
||||||
|
await window.electronAPI.saveRunConfig(nextConfig)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('save shared config failed:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
async function handlePrepareViews() {
|
async function handlePrepareViews() {
|
||||||
if (!ensureElectronCapability('prepareStandaloneTikTokViews')) return
|
if (!ensureElectronCapability('prepareStandaloneTikTokViews')) return
|
||||||
|
if (viewIds.value.length === 0) {
|
||||||
|
ElMessage.warning('请至少启用一个视图后再预热')
|
||||||
|
return
|
||||||
|
}
|
||||||
loginConfirmed.value = false
|
loginConfirmed.value = false
|
||||||
isPreparing.value = true
|
isPreparing.value = true
|
||||||
try {
|
try {
|
||||||
const result = await window.electronAPI.prepareStandaloneTikTokViews({
|
const result = await window.electronAPI.prepareStandaloneTikTokViews({ groupViewCounts: normalizedGroupViewCounts.value, targetViewId: activeViewId.value })
|
||||||
groupViewCounts: normalizedGroupViewCounts.value,
|
if (!result?.success) throw new Error(result?.error || 'prepare standalone tiktok views failed')
|
||||||
targetViewId: activeViewId.value
|
if (result.currentViewId) activeViewId.value = result.currentViewId
|
||||||
})
|
preparedConfigKey.value = currentPrepareKey.value
|
||||||
if (!result?.success) {
|
await saveSharedConfig()
|
||||||
throw new Error(result?.error || 'prepare standalone tiktok views failed')
|
|
||||||
}
|
|
||||||
if (result.currentViewId) {
|
|
||||||
activeViewId.value = result.currentViewId
|
|
||||||
}
|
|
||||||
pageMode.value = 'browser'
|
pageMode.value = 'browser'
|
||||||
await window.electronAPI.showViews()
|
await window.electronAPI.showViews()
|
||||||
statusText.value = `请先在视图 ${activeViewId.value} 手动登录,完成后再启动脚本`
|
statusText.value = `视图已预热,请先在视图 ${activeViewId.value} 手动登录,再启动脚本`
|
||||||
ElMessage.success('视图预热完成,已切换到浏览器视图')
|
ElMessage.success('视图预热完成,已切换到浏览器视图')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : String(error)
|
const message = error instanceof Error ? error.message : String(error)
|
||||||
@@ -515,23 +349,20 @@ async function handlePrepareViews() {
|
|||||||
isPreparing.value = false
|
isPreparing.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openBrowserView() {
|
async function openBrowserView() {
|
||||||
if (!ensureElectronCapability('switchToView')) return
|
if (!ensureElectronCapability('switchToView')) return
|
||||||
|
|
||||||
pageMode.value = 'browser'
|
pageMode.value = 'browser'
|
||||||
try {
|
try {
|
||||||
await window.electronAPI.switchToView(activeViewId.value)
|
await window.electronAPI.switchToView(activeViewId.value)
|
||||||
await window.electronAPI.showViews()
|
await window.electronAPI.showViews()
|
||||||
if (!statusText.value.includes('运行')) {
|
if (!statusText.value.includes('运行')) {
|
||||||
statusText.value = `已打开视图 ${activeViewId.value},如未预热请先返回工作台执行预热视图`
|
statusText.value = hasPreparedViews.value ? `已打开预热视图 ${activeViewId.value},请手动登录后启动脚本` : '当前配置尚未预热,请先返回工作台执行预热视图'
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : String(error)
|
const message = error instanceof Error ? error.message : String(error)
|
||||||
ElMessage.error(`打开浏览器视图失败:${message}`)
|
ElMessage.error(`打开浏览器视图失败:${message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function backToConfig() {
|
async function backToConfig() {
|
||||||
loginConfirmed.value = false
|
loginConfirmed.value = false
|
||||||
pageMode.value = 'config'
|
pageMode.value = 'config'
|
||||||
@@ -542,10 +373,8 @@ async function backToConfig() {
|
|||||||
console.error('hide views failed:', error)
|
console.error('hide views failed:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSwitchView(viewId) {
|
async function handleSwitchView(viewId) {
|
||||||
if (!ensureElectronCapability('switchToView')) return
|
if (!ensureElectronCapability('switchToView')) return
|
||||||
|
|
||||||
isSwitchingView.value = true
|
isSwitchingView.value = true
|
||||||
try {
|
try {
|
||||||
await window.electronAPI.switchToView(viewId)
|
await window.electronAPI.switchToView(viewId)
|
||||||
@@ -560,16 +389,23 @@ async function handleSwitchView(viewId) {
|
|||||||
isSwitchingView.value = false
|
isSwitchingView.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleStart() {
|
async function handleStart() {
|
||||||
if (!ensureElectronCapability('startStandaloneTikTokAutomationAll')) return
|
if (!ensureElectronCapability('startStandaloneTikTokAutomationAll')) return
|
||||||
if (!loginConfirmed.value) {
|
if (!hasPreparedViews.value) {
|
||||||
ElMessage.warning('请先在当前视图手动登录,并勾选“已完成登录”后再启动脚本')
|
ElMessage.warning('请先完成当前配置对应的视图预热,再启动脚本')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (isBrotherInfoMode.value && replyMessages.value.length === 0) {
|
||||||
|
ElMessage.warning('选择大哥池后,请先填写大哥随机回复话术')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!loginConfirmed.value) {
|
||||||
|
ElMessage.warning('请先在当前视图手动登录,并勾选“当前视图已完成手动登录”后再启动脚本')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
isStarting.value = true
|
isStarting.value = true
|
||||||
try {
|
try {
|
||||||
|
await saveSharedConfig()
|
||||||
await window.electronAPI.switchToView(activeViewId.value)
|
await window.electronAPI.switchToView(activeViewId.value)
|
||||||
await window.electronAPI.showViews()
|
await window.electronAPI.showViews()
|
||||||
const result = await window.electronAPI.startStandaloneTikTokAutomationAll(buildStartPayload())
|
const result = await window.electronAPI.startStandaloneTikTokAutomationAll(buildStartPayload())
|
||||||
@@ -588,10 +424,8 @@ async function handleStart() {
|
|||||||
isStarting.value = false
|
isStarting.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleStop() {
|
async function handleStop() {
|
||||||
if (!ensureElectronCapability('stopStandaloneTikTokAutomationAll')) return
|
if (!ensureElectronCapability('stopStandaloneTikTokAutomationAll')) return
|
||||||
|
|
||||||
isStopping.value = true
|
isStopping.value = true
|
||||||
try {
|
try {
|
||||||
const result = await window.electronAPI.stopStandaloneTikTokAutomationAll()
|
const result = await window.electronAPI.stopStandaloneTikTokAutomationAll()
|
||||||
@@ -600,6 +434,8 @@ async function handleStop() {
|
|||||||
ElMessage.error(result?.error || '停止自动私信(TK版)失败')
|
ElMessage.error(result?.error || '停止自动私信(TK版)失败')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
preparedConfigKey.value = ''
|
||||||
|
loginConfirmed.value = false
|
||||||
statusText.value = '已停止'
|
statusText.value = '已停止'
|
||||||
ElMessage.success('自动私信(TK版)已停止')
|
ElMessage.success('自动私信(TK版)已停止')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -610,33 +446,37 @@ async function handleStop() {
|
|||||||
isStopping.value = false
|
isStopping.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
watch(currentPrepareKey, (nextKey, prevKey) => {
|
||||||
|
if (!prevKey) return
|
||||||
|
if (preparedConfigKey.value && preparedConfigKey.value !== nextKey) invalidatePreparedState()
|
||||||
|
})
|
||||||
|
watch(() => configForm.dataPoolSource, () => {
|
||||||
|
invalidatePreparedState()
|
||||||
|
void saveSharedConfig()
|
||||||
|
})
|
||||||
|
watch(() => configForm.replyMessagesText, () => {
|
||||||
|
if (!isBrotherInfoMode.value) return
|
||||||
|
invalidatePreparedState()
|
||||||
|
})
|
||||||
watch(viewIds, (nextViewIds) => {
|
watch(viewIds, (nextViewIds) => {
|
||||||
if (nextViewIds.length === 0) {
|
if (nextViewIds.length === 0) {
|
||||||
activeViewId.value = TIKTOK_VIEW_IDS[0]
|
activeViewId.value = TIKTOK_VIEW_IDS[0]
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!nextViewIds.includes(activeViewId.value)) activeViewId.value = nextViewIds[0]
|
||||||
if (!nextViewIds.includes(activeViewId.value)) {
|
|
||||||
activeViewId.value = nextViewIds[0]
|
|
||||||
}
|
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
watch(pageMode, async (newVal) => {
|
watch(pageMode, async (newVal) => {
|
||||||
if (!isElectronEnv) return
|
if (!isElectronEnv) return
|
||||||
if (newVal === 'config' && window.electronAPI?.hideViews) {
|
if (newVal === 'config' && window.electronAPI?.hideViews) await window.electronAPI.hideViews().catch(() => {})
|
||||||
await window.electronAPI.hideViews().catch(() => { })
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await checkAIConfig()
|
await checkAIConfig()
|
||||||
|
await loadSharedConfig()
|
||||||
if (!isElectronEnv || !window.electronAPI?.hideViews) return
|
if (!isElectronEnv || !window.electronAPI?.hideViews) return
|
||||||
await window.electronAPI.hideViews().catch(() => { })
|
await window.electronAPI.hideViews().catch(() => {})
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(async () => {
|
onUnmounted(async () => {
|
||||||
if (!isElectronEnv || !window.electronAPI?.hideViews) return
|
if (!isElectronEnv || !window.electronAPI?.hideViews) return
|
||||||
await window.electronAPI.hideViews().catch(() => { })
|
await window.electronAPI.hideViews().catch(() => {})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user