销售名片
@@ -4,6 +4,6 @@ VITE_API_BASE_URL=https://newclient.api.yolozs.com
|
||||
# 注册地址
|
||||
VITE_REGISTER_API_URL=https://backstageapi.yolozs.com
|
||||
# pk api地址
|
||||
VITE_PK_MINI_API_URL=https://pk.hanxiaokj.cn
|
||||
VITE_PK_MINI_API_URL=https://pk.yolozs.com
|
||||
# 商店地址
|
||||
VITE_SHOP_URL=https://ck.ninisc.cn/
|
||||
VITE_SHOP_URL=https://www.tkzyw.com
|
||||
|
||||
@@ -84,3 +84,8 @@ export function liveHostDetail(data) {
|
||||
export function revenueStats(hostId) {
|
||||
return getAxios({ url: 'api/save_data/revenue_stats?displayId=' + hostId })
|
||||
}
|
||||
|
||||
// 获取客服名片列表
|
||||
export function getCustomServiceInfo() {
|
||||
return getAxios({ url: '/api/common/custom_service_info' })
|
||||
}
|
||||
|
||||
BIN
src/assets/nav/back.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src/assets/nav/nav1.png
Normal file
|
After Width: | Height: | Size: 642 B |
BIN
src/assets/nav/nav11.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/assets/nav/nav2.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
src/assets/nav/nav22.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src/assets/nav/nav3.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src/assets/nav/nav33.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src/assets/nav/nav4.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src/assets/nav/nav44.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src/assets/nav/nav5.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src/assets/nav/nav55.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src/assets/nav/nav6.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src/assets/nav/nav66.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
src/assets/nav/yolo.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
@@ -4,29 +4,62 @@
|
||||
<template v-if="hasAccess">
|
||||
<slot></slot>
|
||||
</template>
|
||||
|
||||
|
||||
<!-- 无权限时显示遮罩和占位内容 -->
|
||||
<template v-else>
|
||||
<!-- 占位背景 - 显示工作台截图作为假界面 -->
|
||||
<!-- 占位背景 -->
|
||||
<div class="permission-placeholder">
|
||||
<img v-if="placeholderImage" :src="placeholderImage" alt="" class="placeholder-image" />
|
||||
<div v-else class="placeholder-pattern"></div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 权限遮罩层 -->
|
||||
<div class="permission-mask" ref="maskRef" :data-permission-guard="guardKey">
|
||||
<div class="mask-content">
|
||||
<div class="lock-icon-wrapper">
|
||||
<svg class="lock-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 11H5C3.89543 11 3 11.8954 3 13V20C3 21.1046 3.89543 22 5 22H19C20.1046 22 21 21.1046 21 20V13C21 11.8954 20.1046 11 19 11Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7 11V7C7 5.67392 7.52678 4.40215 8.46447 3.46447C9.40215 2.52678 10.6739 2 12 2C13.3261 2 14.5979 2.52678 15.5355 3.46447C16.4732 4.40215 17 5.67392 17 7V11" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<!-- 上方:锁提示区域 -->
|
||||
<div class="mask-top">
|
||||
<div class="mask-content">
|
||||
<div class="lock-icon-wrapper">
|
||||
<svg class="lock-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 11H5C3.89543 11 3 11.8954 3 13V20C3 21.1046 3.89543 22 5 22H19C20.1046 22 21 21.1046 21 20V13C21 11.8954 20.1046 11 19 11Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7 11V7C7 5.67392 7.52678 4.40215 8.46447 3.46447C9.40215 2.52678 10.6739 2 12 2C13.3261 2 14.5979 2.52678 15.5355 3.46447C16.4732 4.40215 17 5.67392 17 7V11" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="mask-title">{{ title }}</h3>
|
||||
<p class="mask-description">{{ description }}</p>
|
||||
<div class="mask-hint">
|
||||
<span class="material-icons-round hint-icon">info</span>
|
||||
<span>请联系管理员开通权限</span>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="mask-title">{{ title }}</h3>
|
||||
<p class="mask-description">{{ description }}</p>
|
||||
<div class="mask-hint">
|
||||
<span class="material-icons-round hint-icon">info</span>
|
||||
<span>请联系管理员开通权限</span>
|
||||
</div>
|
||||
|
||||
<!-- 下方:名片区域 -->
|
||||
<div v-if="contacts && contacts.length" class="cards-area">
|
||||
<div class="cards-header">
|
||||
<button class="refresh-btn" @click="shuffleContacts">
|
||||
<span class="material-icons-round" style="font-size:18px;">refresh</span>
|
||||
换一批
|
||||
</button>
|
||||
</div>
|
||||
<div class="cards-row">
|
||||
<div
|
||||
v-for="(contact, index) in visibleContacts"
|
||||
:key="index"
|
||||
class="contact-card"
|
||||
>
|
||||
<div class="card-avatar-wrapper">
|
||||
<img :src="contact.avatar" class="card-avatar" alt="" />
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="card-name">{{ contact.name }}</div>
|
||||
<div class="card-desc">{{ contact.desc }}</div>
|
||||
<img :src="contact.qrcode" class="card-qrcode" alt="二维码" />
|
||||
<div class="card-phone">
|
||||
<span class="material-icons-round phone-icon">phone</span>
|
||||
{{ contact.phone }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -39,75 +72,74 @@ import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { getPermissions } from '@/utils/storage'
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 权限类型: 'bigBrother' | 'crawl' | 'webAi'
|
||||
*/
|
||||
permissionKey: {
|
||||
type: String,
|
||||
required: true,
|
||||
validator: (value) => ['bigBrother', 'crawl', 'webAi'].includes(value)
|
||||
},
|
||||
/**
|
||||
* 遮罩标题
|
||||
*/
|
||||
title: {
|
||||
type: String,
|
||||
default: '功能未开通'
|
||||
},
|
||||
/**
|
||||
* 遮罩描述
|
||||
*/
|
||||
description: {
|
||||
type: String,
|
||||
default: '您当前没有使用此功能的权限'
|
||||
},
|
||||
/**
|
||||
* 占位图片路径(工作台截图)
|
||||
*/
|
||||
placeholderImage: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 名片数据,每项: { avatar, name, desc, qrcode, phone }
|
||||
contacts: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const wrapperRef = ref(null)
|
||||
const maskRef = ref(null)
|
||||
const contactOffset = ref(0)
|
||||
|
||||
// 生成唯一的守卫标识
|
||||
const guardKey = computed(() => `guard_${props.permissionKey}_${Date.now()}`)
|
||||
|
||||
// 响应式权限检查
|
||||
const permissionsData = ref(getPermissions())
|
||||
|
||||
const hasAccess = computed(() => {
|
||||
return permissionsData.value[props.permissionKey] === 1
|
||||
})
|
||||
|
||||
// 定时刷新权限状态(防止localStorage被篡改后状态不同步)
|
||||
// 每次显示3张,换一批向后轮转
|
||||
const visibleContacts = computed(() => {
|
||||
if (!props.contacts.length) return []
|
||||
const total = props.contacts.length
|
||||
return [0, 1, 2].map(i => props.contacts[(contactOffset.value + i) % total])
|
||||
})
|
||||
|
||||
const shuffleContacts = () => {
|
||||
if (props.contacts.length <= 3) return
|
||||
contactOffset.value = (contactOffset.value + 3) % props.contacts.length
|
||||
}
|
||||
|
||||
let permissionCheckInterval = null
|
||||
|
||||
const refreshPermissions = () => {
|
||||
permissionsData.value = getPermissions()
|
||||
}
|
||||
|
||||
// MutationObserver 监测DOM篡改
|
||||
let observer = null
|
||||
|
||||
const setupDOMProtection = () => {
|
||||
if (hasAccess.value || !wrapperRef.value) return
|
||||
|
||||
|
||||
observer = new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
// 检测遮罩是否被删除
|
||||
if (mutation.type === 'childList') {
|
||||
const maskExists = wrapperRef.value?.querySelector('.permission-mask')
|
||||
if (!maskExists && !hasAccess.value) {
|
||||
console.warn('[PermissionMask] 检测到权限遮罩被非法移除,正在重载页面...')
|
||||
// 强制刷新页面
|
||||
window.location.reload()
|
||||
}
|
||||
}
|
||||
// 检测遮罩样式是否被修改(如display:none, visibility:hidden等)
|
||||
if (mutation.type === 'attributes' && mutation.target.classList?.contains('permission-mask')) {
|
||||
const mask = mutation.target
|
||||
const style = window.getComputedStyle(mask)
|
||||
@@ -118,7 +150,7 @@ const setupDOMProtection = () => {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
observer.observe(wrapperRef.value, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
@@ -128,30 +160,20 @@ const setupDOMProtection = () => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 定时检查权限(每2秒)
|
||||
permissionCheckInterval = setInterval(refreshPermissions, 2000)
|
||||
console.log("获取名片",props.contacts)
|
||||
|
||||
// 延迟设置DOM保护,确保元素已渲染
|
||||
permissionCheckInterval = setInterval(refreshPermissions, 2000)
|
||||
setTimeout(setupDOMProtection, 100)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (permissionCheckInterval) {
|
||||
clearInterval(permissionCheckInterval)
|
||||
}
|
||||
if (observer) {
|
||||
observer.disconnect()
|
||||
}
|
||||
if (permissionCheckInterval) clearInterval(permissionCheckInterval)
|
||||
if (observer) observer.disconnect()
|
||||
})
|
||||
|
||||
// 权限变化时重新设置保护
|
||||
watch(hasAccess, (newVal) => {
|
||||
if (observer) {
|
||||
observer.disconnect()
|
||||
}
|
||||
if (!newVal) {
|
||||
setTimeout(setupDOMProtection, 100)
|
||||
}
|
||||
if (observer) observer.disconnect()
|
||||
if (!newVal) setTimeout(setupDOMProtection, 100)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -162,7 +184,6 @@ watch(hasAccess, (newVal) => {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 占位背景 - 无权限时显示,防止删除遮罩后看到内容 */
|
||||
.permission-placeholder {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
@@ -183,7 +204,7 @@ watch(hasAccess, (newVal) => {
|
||||
.placeholder-pattern {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image:
|
||||
background-image:
|
||||
linear-gradient(45deg, #e2e8f0 25%, transparent 25%),
|
||||
linear-gradient(-45deg, #e2e8f0 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, #e2e8f0 75%),
|
||||
@@ -198,11 +219,21 @@ watch(hasAccess, (newVal) => {
|
||||
inset: 0;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-content: flex-start;
|
||||
background: rgba(15, 23, 42, 0.75);
|
||||
backdrop-filter: blur(1px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 上方锁提示区域 */
|
||||
.mask-top {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 6vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mask-content {
|
||||
@@ -219,14 +250,8 @@ watch(hasAccess, (newVal) => {
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.lock-icon-wrapper {
|
||||
@@ -276,4 +301,119 @@ watch(hasAccess, (newVal) => {
|
||||
.hint-icon {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
/* 名片区域 */
|
||||
.cards-area {
|
||||
width: 100%;
|
||||
padding: 3vh 4rem 2rem;
|
||||
animation: slideUp 0.4s ease-out 0.1s both;
|
||||
}
|
||||
|
||||
.cards-header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0.5rem 1.25rem;
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.refresh-btn:hover {
|
||||
background: #1d4ed8;
|
||||
}
|
||||
|
||||
.cards-row {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
/* 单张名片 */
|
||||
.contact-card {
|
||||
flex: 1;
|
||||
max-width: 280px;
|
||||
background: #fff;
|
||||
border-radius: 1.25rem;
|
||||
padding-top: 52px;
|
||||
position: relative;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-avatar-wrapper {
|
||||
position: absolute;
|
||||
top: -44px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
border-radius: 50%;
|
||||
border: 4px solid #fff;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
overflow: hidden;
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
.card-avatar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0.5rem 1.5rem 1.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-name {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 0.8125rem;
|
||||
color: #64748b;
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.card-qrcode {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
object-fit: contain;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.card-phone {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 1rem;
|
||||
color: #2563eb;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.phone-icon {
|
||||
font-size: 1.1rem;
|
||||
color: #2563eb;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<aside class="w-[200px] h-full bg-gradient-to-b from-white to-gray-50 border-r border-gray-200 flex flex-col shadow-sm">
|
||||
<aside :style="{ width: sidebarWidth + 'px', minWidth: '96px', maxWidth: '400px' }" class="h-full bg-gradient-to-b from-white to-gray-50 border-r border-gray-200 flex flex-col shadow-sm flex-shrink-0">
|
||||
<!-- 返回和停止按钮 -->
|
||||
<div class="m-3 mb-0 flex gap-2">
|
||||
<button @click="onGoBack"
|
||||
@@ -154,7 +154,8 @@ const props = defineProps({
|
||||
type: Object,
|
||||
default: () => ({ greetingCount: 0, inviteCount: 0 })
|
||||
},
|
||||
automationLogs: { type: Array, default: () => [] }
|
||||
automationLogs: { type: Array, default: () => [] },
|
||||
sidebarWidth: { type: Number, default: 144 }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['tabSwitch', 'goBack', 'stopAll'])
|
||||
|
||||
@@ -11,8 +11,8 @@ export const PK_MINI_CONFIG = {
|
||||
|
||||
// GoEasy 配置
|
||||
GOEASY: {
|
||||
HOST: 'hangzhou.goeasy.io',
|
||||
APP_KEY: 'PC-a88037e060ed4753bb316ac7239e62d9',
|
||||
HOST: 'singapore.goeasy.io',
|
||||
APP_KEY: 'PC-8b16068a70024e27a17f93e36080afcd',
|
||||
},
|
||||
|
||||
// API 基础地址(从中心配置读取,随环境自动切换)
|
||||
|
||||
@@ -1,71 +1,77 @@
|
||||
<template>
|
||||
<div class="flex h-screen w-screen overflow-hidden bg-white">
|
||||
<!-- Left Navigation Sidebar -->
|
||||
<div class="w-36 flex flex-col items-center py-4 bg-slate-900 border-r border-slate-800 z-50">
|
||||
<div class="mb-6">
|
||||
<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="w-10 h-10 bg-blue-600 rounded-xl flex items-center justify-center shadow-lg shadow-blue-900/50">
|
||||
<span class="material-icons-round text-white">grid_view</span>
|
||||
<div class="" >
|
||||
<img :src="yoloIcon" class="yolo-logo" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 flex flex-col gap-3 w-full px-2">
|
||||
<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"
|
||||
:class="currentView === 'tk' ? 'bg-blue-600 text-white shadow-lg shadow-blue-900/30' : 'text-slate-400 hover:bg-slate-800 hover:text-white'">
|
||||
<span class="material-icons-round text-xl">tiktok</span>
|
||||
<span class="text-xs font-medium truncate">TK 工作台</span>
|
||||
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"
|
||||
:class="currentView === 'hosts' ? 'bg-blue-600 text-white shadow-lg shadow-blue-900/30' : 'text-slate-400 hover:bg-slate-800 hover:text-white'">
|
||||
<span class="material-icons-round text-xl">people</span>
|
||||
<span class="text-xs font-medium truncate">主播列表</span>
|
||||
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"
|
||||
:class="currentView === 'auto_dm' ? 'bg-blue-600 text-white shadow-lg shadow-blue-900/30' : 'text-slate-400 hover:bg-slate-800 hover:text-white'">
|
||||
<span class="material-icons-round text-xl">chat</span>
|
||||
<span class="text-xs font-medium truncate">自动私信</span>
|
||||
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"
|
||||
:class="currentView === 'FanWorkbench' ? 'bg-blue-600 text-white shadow-lg shadow-blue-900/30' : 'text-slate-400 hover:bg-slate-800 hover:text-white'">
|
||||
<span class="material-icons-round text-xl">supervised_user_circle</span>
|
||||
<span class="text-xs font-medium truncate">大哥工作台</span>
|
||||
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"
|
||||
:class="currentView === 'pk_mini' ? 'bg-blue-600 text-white shadow-lg shadow-blue-900/30' : 'text-slate-400 hover:bg-slate-800 hover:text-white'">
|
||||
<span class="material-icons-round text-xl">sports_esports</span>
|
||||
<span class="text-xs font-medium truncate">PK 工作台</span>
|
||||
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"
|
||||
:class="currentView === 'shop' ? 'bg-blue-600 text-white shadow-lg shadow-blue-900/30' : 'text-slate-400 hover:bg-slate-800 hover:text-white'">
|
||||
<span class="material-icons-round text-xl">admin_panel_settings</span>
|
||||
<span class="text-xs font-medium truncate">YOLO商店</span>
|
||||
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>
|
||||
</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 hover:bg-slate-800 hover:text-red-400 transition-all">
|
||||
<span class="material-icons-round text-xl">logout</span>
|
||||
<span class="text-xs font-medium">退出登录</span>
|
||||
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>
|
||||
@@ -75,11 +81,12 @@
|
||||
|
||||
<!-- 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="自动私信工作台未开通"
|
||||
<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
|
||||
@@ -88,10 +95,11 @@
|
||||
/>
|
||||
</div>
|
||||
<div v-show="autoDmMode === 'browser'" class="h-full w-full">
|
||||
<YoloBrowser
|
||||
v-bind="$attrs"
|
||||
@go-back="handleBackToConfig"
|
||||
@stop-all="handleStopAll"
|
||||
<YoloBrowser
|
||||
v-bind="$attrs"
|
||||
:nav-sidebar-width="navSidebarWidth"
|
||||
@go-back="handleBackToConfig"
|
||||
@stop-all="handleStopAll"
|
||||
/>
|
||||
</div>
|
||||
</PermissionMask>
|
||||
@@ -99,11 +107,12 @@
|
||||
|
||||
<!-- Tab 2: TK Workbench - crawl 权限 -->
|
||||
<div v-show="currentView === 'tk'" class="absolute inset-0 z-20 bg-gray-50 h-full overflow-hidden">
|
||||
<PermissionMask
|
||||
permission-key="crawl"
|
||||
title="TK工作台未开通"
|
||||
<PermissionMask
|
||||
permission-key="crawl"
|
||||
title="TK工作台未开通"
|
||||
description="您当前没有使用TK工作台功能的权限"
|
||||
:placeholder-image="placeholderTk"
|
||||
:contacts="serviceContacts"
|
||||
>
|
||||
<TkWorkbenches />
|
||||
</PermissionMask>
|
||||
@@ -111,11 +120,12 @@
|
||||
|
||||
<!-- Tab 3: Hosts List - crawl 权限 -->
|
||||
<div v-show="currentView === 'hosts'" class="absolute inset-0 z-20 bg-gray-50 h-full overflow-hidden p-4">
|
||||
<PermissionMask
|
||||
permission-key="crawl"
|
||||
title="主播列表未开通"
|
||||
<PermissionMask
|
||||
permission-key="crawl"
|
||||
title="主播列表未开通"
|
||||
description="您当前没有使用主播列表功能的权限"
|
||||
:placeholder-image="placeholderHosts"
|
||||
:contacts="serviceContacts"
|
||||
>
|
||||
<HostsList />
|
||||
</PermissionMask>
|
||||
@@ -128,6 +138,7 @@
|
||||
title="大哥工作台未开通"
|
||||
description="您当前没有使用大哥工作台功能的权限"
|
||||
:placeholder-image="placeholderBigBrother"
|
||||
:contacts="serviceContacts"
|
||||
>
|
||||
<FanWorkbench />
|
||||
</PermissionMask>
|
||||
@@ -140,7 +151,7 @@
|
||||
|
||||
<!-- 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-sm 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
|
||||
@@ -156,7 +167,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { ref, watch, onMounted, onUnmounted, useTemplateRef } from 'vue'
|
||||
import { isElectron } from '@/utils/electronBridge'
|
||||
import YoloBrowser from '@/views/YoloBrowser.vue'
|
||||
import TkWorkbenches from '@/views/tk/Workbenches.vue'
|
||||
@@ -166,6 +177,23 @@ import FanWorkbench from '@/views/tk/FanWorkbench.vue'
|
||||
import PkMiniWorkbench from '@/views/pk-mini/PkMiniWorkbench.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'
|
||||
import nav2 from '@/assets/nav/nav2.png'
|
||||
import nav22 from '@/assets/nav/nav22.png'
|
||||
import nav3 from '@/assets/nav/nav3.png'
|
||||
import nav33 from '@/assets/nav/nav33.png'
|
||||
import nav4 from '@/assets/nav/nav4.png'
|
||||
import nav44 from '@/assets/nav/nav44.png'
|
||||
import nav5 from '@/assets/nav/nav5.png'
|
||||
import nav55 from '@/assets/nav/nav55.png'
|
||||
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'
|
||||
@@ -180,6 +208,53 @@ const autoDmMode = ref('config') // Default Sub-state: 'config' or 'browser'
|
||||
const adminLoaded = ref(false) // Web iframe 懒加载(仅非 Electron)
|
||||
const shopOpened = ref(false) // Electron 只首开加载一次
|
||||
const shopUrl = ENV.SHOP_URL
|
||||
const sidebarRef = useTemplateRef('sidebarRef')
|
||||
const navSidebarWidth = ref(200) // 左侧导航菜单的实际宽度(px),传给 YoloBrowser/Sidebar 使用
|
||||
|
||||
// 客服名片
|
||||
const serviceContacts = ref([])
|
||||
const loadServiceContacts = async () => {
|
||||
try {
|
||||
const res = await getCustomServiceInfo()
|
||||
console.log("获取名片",res)
|
||||
if (res) {
|
||||
serviceContacts.value = res.map(item => ({
|
||||
avatar: item.avater,
|
||||
name: item.name,
|
||||
desc: item.description,
|
||||
qrcode: item.concat,
|
||||
phone: item.phone
|
||||
}))
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取客服名片失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 监听菜单栏实际宽度,通知后端更新 BrowserView 定位
|
||||
let resizeObserver = null
|
||||
const notifySidebarWidth = (width) => {
|
||||
navSidebarWidth.value = Math.round(width)
|
||||
if (isElectron()) {
|
||||
window.electronAPI.setSidebarWidth(Math.round(width)).catch(() => {})
|
||||
}
|
||||
}
|
||||
onMounted(() => {
|
||||
loadServiceContacts()
|
||||
if (!isElectron()) return
|
||||
resizeObserver = new ResizeObserver((entries) => {
|
||||
const width = entries[0]?.contentRect.width
|
||||
if (width) notifySidebarWidth(width)
|
||||
})
|
||||
if (sidebarRef.value) {
|
||||
resizeObserver.observe(sidebarRef.value)
|
||||
// 立即上报初始宽度
|
||||
notifySidebarWidth(sidebarRef.value.getBoundingClientRect().width)
|
||||
}
|
||||
})
|
||||
onUnmounted(() => {
|
||||
resizeObserver?.disconnect()
|
||||
})
|
||||
|
||||
const handleGoToBrowser = async () => {
|
||||
autoDmMode.value = 'browser'
|
||||
@@ -263,6 +338,10 @@ watch(autoDmMode, async (newVal) => {
|
||||
<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{
|
||||
width: 70%;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
3
src/types/electron.d.ts
vendored
@@ -112,6 +112,9 @@ export interface ElectronAPI {
|
||||
loadAIConfig: () => Promise<Record<string, unknown>>
|
||||
loadAnchorData: () => Promise<unknown[]>
|
||||
saveAnchorData: (data: unknown[]) => Promise<{ success: boolean }>
|
||||
openShop: (url: string) => Promise<{ success: boolean; error?: string }>
|
||||
hideShop: () => Promise<{ success: boolean; error?: string }>
|
||||
setSidebarWidth: (width: number) => Promise<{ success: boolean }>
|
||||
saveRunConfig: (config: Record<string, unknown>) => Promise<{ success: boolean }>
|
||||
loadRunConfig: () => Promise<Record<string, unknown> | null>
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
<!-- 侧边栏 -->
|
||||
<Sidebar :tabs="tabs" :current-tab="currentTab" @tab-switch="handleTabSwitch" @go-back="handleGoToConfig"
|
||||
@stop-all="handleStopAll" :is-loading="isLoading" :account-groups="accountGroups"
|
||||
:rotation-status="rotationStatus" :greeting-stats="greetingStats" :automation-logs="automationLogs" />
|
||||
:rotation-status="rotationStatus" :greeting-stats="greetingStats" :automation-logs="automationLogs"
|
||||
:sidebar-width="navSidebarWidth" />
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<main class="flex-1 flex flex-col relative">
|
||||
@@ -64,7 +65,7 @@ import ViewPlaceholder from '@/components/ViewPlaceholder.vue'
|
||||
// Wait, in App.vue these were local state. I needs to move the logic here or keep it in App.vue and pass via props.
|
||||
// To keep valid functionality, I will copy the logic 1:1 here.
|
||||
|
||||
const props = defineProps(['accountGroups', 'rotationStatus', 'greetingStats', 'automationLogs'])
|
||||
const props = defineProps(['accountGroups', 'rotationStatus', 'greetingStats', 'automationLogs', 'navSidebarWidth'])
|
||||
const emit = defineEmits(['go-back', 'stop-all', 'request-config-load'])
|
||||
|
||||
// Constants
|
||||
|
||||