权限遮罩
This commit is contained in:
BIN
src/assets/placeholder-bigbrother.png
Normal file
BIN
src/assets/placeholder-bigbrother.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 98 KiB |
BIN
src/assets/placeholder-hosts.png
Normal file
BIN
src/assets/placeholder-hosts.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
BIN
src/assets/placeholder-tk.png
Normal file
BIN
src/assets/placeholder-tk.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 109 KiB |
BIN
src/assets/placeholder-webai.png
Normal file
BIN
src/assets/placeholder-webai.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 190 KiB |
279
src/components/PermissionMask.vue
Normal file
279
src/components/PermissionMask.vue
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
<template>
|
||||||
|
<div class="permission-mask-wrapper" ref="wrapperRef">
|
||||||
|
<!-- 有权限时才渲染原始内容 -->
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
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: ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const wrapperRef = ref(null)
|
||||||
|
const maskRef = ref(null)
|
||||||
|
|
||||||
|
// 生成唯一的守卫标识
|
||||||
|
const guardKey = computed(() => `guard_${props.permissionKey}_${Date.now()}`)
|
||||||
|
|
||||||
|
// 响应式权限检查
|
||||||
|
const permissionsData = ref(getPermissions())
|
||||||
|
|
||||||
|
const hasAccess = computed(() => {
|
||||||
|
return permissionsData.value[props.permissionKey] === 1
|
||||||
|
})
|
||||||
|
|
||||||
|
// 定时刷新权限状态(防止localStorage被篡改后状态不同步)
|
||||||
|
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)
|
||||||
|
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
|
||||||
|
console.warn('[PermissionMask] 检测到权限遮罩样式被非法修改,正在重载页面...')
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
observer.observe(wrapperRef.value, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['style', 'class']
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 定时检查权限(每2秒)
|
||||||
|
permissionCheckInterval = setInterval(refreshPermissions, 2000)
|
||||||
|
|
||||||
|
// 延迟设置DOM保护,确保元素已渲染
|
||||||
|
setTimeout(setupDOMProtection, 100)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (permissionCheckInterval) {
|
||||||
|
clearInterval(permissionCheckInterval)
|
||||||
|
}
|
||||||
|
if (observer) {
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 权限变化时重新设置保护
|
||||||
|
watch(hasAccess, (newVal) => {
|
||||||
|
if (observer) {
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
if (!newVal) {
|
||||||
|
setTimeout(setupDOMProtection, 100)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.permission-mask-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 占位背景 - 无权限时显示,防止删除遮罩后看到内容 */
|
||||||
|
.permission-placeholder {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: #f8fafc;
|
||||||
|
z-index: 99;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: top left;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-pattern {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(45deg, #e2e8f0 25%, transparent 25%),
|
||||||
|
linear-gradient(-45deg, #e2e8f0 25%, transparent 25%),
|
||||||
|
linear-gradient(45deg, transparent 75%, #e2e8f0 75%),
|
||||||
|
linear-gradient(-45deg, transparent 75%, #e2e8f0 75%);
|
||||||
|
background-size: 20px 20px;
|
||||||
|
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permission-mask {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(15, 23, 42, 0.75);
|
||||||
|
backdrop-filter: blur(1px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mask-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: 2.5rem;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 1.5rem;
|
||||||
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||||
|
max-width: 360px;
|
||||||
|
animation: slideUp 0.4s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lock-icon-wrapper {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lock-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mask-title {
|
||||||
|
font-size: 1.375rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mask-description {
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
color: #64748b;
|
||||||
|
margin: 0 0 1.5rem 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mask-hint {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.625rem 1rem;
|
||||||
|
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
|
||||||
|
border-radius: 9999px;
|
||||||
|
color: #3b82f6;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-icon {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -67,36 +67,64 @@
|
|||||||
<!-- Main Content Area -->
|
<!-- Main Content Area -->
|
||||||
<div class="flex-1 h-full relative">
|
<div class="flex-1 h-full relative">
|
||||||
|
|
||||||
<!-- Tab 1: Auto DM Workbench (Config + Browser) -->
|
<!-- Tab 1: Auto DM Workbench (Config + Browser) - webAi 权限 -->
|
||||||
<div v-show="currentView === 'auto_dm'" class="absolute inset-0 z-10 h-full w-full">
|
<div v-show="currentView === 'auto_dm'" class="absolute inset-0 z-10 h-full w-full">
|
||||||
<div v-if="autoDmMode === 'config'" class="h-full w-full bg-slate-50 overflow-auto">
|
<PermissionMask
|
||||||
<ConfigPage
|
permission-key="webAi"
|
||||||
|
title="自动私信工作台未开通"
|
||||||
|
description="您当前没有使用自动私信功能的权限"
|
||||||
|
:placeholder-image="placeholderWebAi"
|
||||||
|
>
|
||||||
|
<div v-if="autoDmMode === 'config'" class="h-full w-full bg-slate-50 overflow-auto">
|
||||||
|
<ConfigPage
|
||||||
@go-to-browser="handleGoToBrowser"
|
@go-to-browser="handleGoToBrowser"
|
||||||
@logout="$emit('logout')"
|
@logout="$emit('logout')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="autoDmMode === 'browser'" class="h-full w-full">
|
<div v-show="autoDmMode === 'browser'" class="h-full w-full">
|
||||||
<YoloBrowser
|
<YoloBrowser
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
@go-back="handleBackToConfig"
|
@go-back="handleBackToConfig"
|
||||||
@stop-all="handleStopAll"
|
@stop-all="handleStopAll"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</PermissionMask>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab 2: TK Workbench -->
|
<!-- Tab 2: TK Workbench - crawl 权限 -->
|
||||||
<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">
|
||||||
<TkWorkbenches />
|
<PermissionMask
|
||||||
|
permission-key="crawl"
|
||||||
|
title="TK工作台未开通"
|
||||||
|
description="您当前没有使用TK工作台功能的权限"
|
||||||
|
:placeholder-image="placeholderTk"
|
||||||
|
>
|
||||||
|
<TkWorkbenches />
|
||||||
|
</PermissionMask>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab 3: Hosts List -->
|
<!-- 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">
|
<div v-show="currentView === 'hosts'" class="absolute inset-0 z-20 bg-gray-50 h-full overflow-hidden p-4">
|
||||||
<HostsList />
|
<PermissionMask
|
||||||
|
permission-key="crawl"
|
||||||
|
title="主播列表未开通"
|
||||||
|
description="您当前没有使用主播列表功能的权限"
|
||||||
|
:placeholder-image="placeholderHosts"
|
||||||
|
>
|
||||||
|
<HostsList />
|
||||||
|
</PermissionMask>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab 4: Hosts List -->
|
<!-- Tab 4: Fan Workbench - bigBrother 权限 -->
|
||||||
<div v-show="currentView === 'FanWorkbench'" class="absolute inset-0 z-20 bg-gray-50 h-full overflow-hidden p-4">
|
<div v-show="currentView === 'FanWorkbench'" class="absolute inset-0 z-20 bg-gray-50 h-full overflow-hidden p-4">
|
||||||
<FanWorkbench />
|
<PermissionMask
|
||||||
|
permission-key="bigBrother"
|
||||||
|
title="大哥工作台未开通"
|
||||||
|
description="您当前没有使用大哥工作台功能的权限"
|
||||||
|
:placeholder-image="placeholderBigBrother"
|
||||||
|
>
|
||||||
|
<FanWorkbench />
|
||||||
|
</PermissionMask>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -109,7 +137,14 @@ import YoloBrowser from '@/views/YoloBrowser.vue'
|
|||||||
import TkWorkbenches from '@/views/tk/Workbenches.vue'
|
import TkWorkbenches from '@/views/tk/Workbenches.vue'
|
||||||
import HostsList from '@/views/tk/HostsList.vue'
|
import HostsList from '@/views/tk/HostsList.vue'
|
||||||
import ConfigPage from '@/pages/ConfigPage.vue'
|
import ConfigPage from '@/pages/ConfigPage.vue'
|
||||||
import FanWorkbench from '@/views/tk/FanWorkbench.vue' // Added import
|
import FanWorkbench from '@/views/tk/FanWorkbench.vue'
|
||||||
|
import PermissionMask from '@/components/PermissionMask.vue'
|
||||||
|
|
||||||
|
// 占位图片 - 无权限时显示的工作台截图
|
||||||
|
import placeholderTk from '@/assets/placeholder-tk.png'
|
||||||
|
import placeholderHosts from '@/assets/placeholder-hosts.png'
|
||||||
|
import placeholderWebAi from '@/assets/placeholder-webai.png'
|
||||||
|
import placeholderBigBrother from '@/assets/placeholder-bigbrother.png'
|
||||||
|
|
||||||
const emit = defineEmits(['logout', 'go-back', 'stop-all'])
|
const emit = defineEmits(['logout', 'go-back', 'stop-all'])
|
||||||
|
|
||||||
|
|||||||
@@ -159,7 +159,7 @@
|
|||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { isElectron, getAppVersion } from '../utils/electronBridge'
|
import { isElectron, getAppVersion } from '../utils/electronBridge'
|
||||||
import { setUser, setToken, setUserPass, getUserPass } from '@/utils/storage'
|
import { setUser, setToken, setUserPass, getUserPass, setPermissions } from '@/utils/storage'
|
||||||
import logo from '../assets/logo.png'
|
import logo from '../assets/logo.png'
|
||||||
import illustration from '../assets/illustration.png'
|
import illustration from '../assets/illustration.png'
|
||||||
|
|
||||||
@@ -233,6 +233,13 @@ const handleSubmit = async () => {
|
|||||||
setToken(result.user.tokenValue);
|
setToken(result.user.tokenValue);
|
||||||
setUser(result.user);
|
setUser(result.user);
|
||||||
|
|
||||||
|
// 保存权限信息
|
||||||
|
setPermissions({
|
||||||
|
bigBrother: result.user.bigBrother,
|
||||||
|
crawl: result.user.crawl,
|
||||||
|
webAi: result.user.webAi,
|
||||||
|
});
|
||||||
|
|
||||||
emit('loginSuccess')
|
emit('loginSuccess')
|
||||||
} else {
|
} else {
|
||||||
error.value = result.error || '登录失败'
|
error.value = result.error || '登录失败'
|
||||||
|
|||||||
@@ -51,3 +51,49 @@ export function setSerch(data) {
|
|||||||
export function getSerch() {
|
export function getSerch() {
|
||||||
return JSON.parse(localStorage.getItem('Serch'));
|
return JSON.parse(localStorage.getItem('Serch'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 权限管理 ====================
|
||||||
|
|
||||||
|
const PERMISSIONS_KEY = 'user_permissions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 存储权限信息
|
||||||
|
* @param {Object} permissions - 权限对象 { bigBrother, crawl, webAi }
|
||||||
|
*/
|
||||||
|
export function setPermissions(permissions) {
|
||||||
|
localStorage.setItem(PERMISSIONS_KEY, JSON.stringify({
|
||||||
|
bigBrother: permissions.bigBrother ?? 0,
|
||||||
|
crawl: permissions.crawl ?? 0,
|
||||||
|
webAi: permissions.webAi ?? 0,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取权限信息
|
||||||
|
* @returns {Object} 权限对象 { bigBrother, crawl, webAi }
|
||||||
|
*/
|
||||||
|
export function getPermissions() {
|
||||||
|
try {
|
||||||
|
const permissions = JSON.parse(localStorage.getItem(PERMISSIONS_KEY));
|
||||||
|
return permissions || { bigBrother: 0, crawl: 0, webAi: 0 };
|
||||||
|
} catch {
|
||||||
|
return { bigBrother: 0, crawl: 0, webAi: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除权限信息
|
||||||
|
*/
|
||||||
|
export function clearPermissions() {
|
||||||
|
localStorage.removeItem(PERMISSIONS_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查特定权限
|
||||||
|
* @param {string} permissionKey - 'bigBrother' | 'crawl' | 'webAi'
|
||||||
|
* @returns {boolean} 是否有权限
|
||||||
|
*/
|
||||||
|
export function hasPermission(permissionKey) {
|
||||||
|
const permissions = getPermissions();
|
||||||
|
return permissions[permissionKey] === 1;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user