-
+
+
+
+
+
{{ title }}
+
{{ description }}
+
+ info
+ 请联系管理员开通权限
+
-
{{ title }}
-
{{ description }}
-
- info
- 请联系管理员开通权限
+
+
+
+
@@ -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)
})
@@ -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;
+}
diff --git a/src/components/Sidebar.vue b/src/components/Sidebar.vue
index b5e28f9..bacd992 100644
--- a/src/components/Sidebar.vue
+++ b/src/components/Sidebar.vue
@@ -1,5 +1,5 @@
-