2026-02-04 21:24:11 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="permission-mask-wrapper" ref="wrapperRef">
|
|
|
|
|
|
<!-- 有权限时才渲染原始内容 -->
|
|
|
|
|
|
<template v-if="hasAccess">
|
|
|
|
|
|
<slot></slot>
|
|
|
|
|
|
</template>
|
2026-03-03 21:57:18 +08:00
|
|
|
|
|
2026-02-04 21:24:11 +08:00
|
|
|
|
<!-- 无权限时显示遮罩和占位内容 -->
|
|
|
|
|
|
<template v-else>
|
2026-03-03 21:57:18 +08:00
|
|
|
|
<!-- 占位背景 -->
|
2026-02-04 21:24:11 +08:00
|
|
|
|
<div class="permission-placeholder">
|
|
|
|
|
|
<img v-if="placeholderImage" :src="placeholderImage" alt="" class="placeholder-image" />
|
|
|
|
|
|
<div v-else class="placeholder-pattern"></div>
|
|
|
|
|
|
</div>
|
2026-03-03 21:57:18 +08:00
|
|
|
|
|
2026-02-04 21:24:11 +08:00
|
|
|
|
<!-- 权限遮罩层 -->
|
|
|
|
|
|
<div class="permission-mask" ref="maskRef" :data-permission-guard="guardKey">
|
2026-03-03 21:57:18 +08:00
|
|
|
|
<!-- 上方:锁提示区域 -->
|
|
|
|
|
|
<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>
|
2026-03-05 14:47:02 +08:00
|
|
|
|
<span>请联系下方客服开通权限</span>
|
2026-03-03 21:57:18 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 下方:名片区域 -->
|
|
|
|
|
|
<div v-if="contacts && contacts.length" class="cards-area">
|
|
|
|
|
|
<div class="cards-header">
|
2026-03-05 14:47:02 +08:00
|
|
|
|
<img :src="exchangeIcon" class="refresh-btn" @click="shuffleContacts" alt="换一批" />
|
2026-02-04 21:24:11 +08:00
|
|
|
|
</div>
|
2026-03-03 21:57:18 +08:00
|
|
|
|
<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>
|
2026-03-05 14:47:02 +08:00
|
|
|
|
<img :src="cardBg" class="card-bg" alt="" />
|
2026-03-03 21:57:18 +08:00
|
|
|
|
<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">
|
2026-03-05 14:47:02 +08:00
|
|
|
|
<img :src="phoneIcon" class="phone-icon" alt="" />
|
2026-03-03 21:57:18 +08:00
|
|
|
|
{{ contact.phone }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-02-04 21:24:11 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
|
|
|
|
|
import { getPermissions } from '@/utils/storage'
|
2026-03-05 14:47:02 +08:00
|
|
|
|
import cardBg from '@/assets/nav/card.png'
|
|
|
|
|
|
import phoneIcon from '@/assets/nav/phone.png'
|
|
|
|
|
|
import exchangeIcon from '@/assets/nav/exchange.png'
|
2026-02-04 21:24:11 +08:00
|
|
|
|
|
|
|
|
|
|
const props = defineProps({
|
|
|
|
|
|
permissionKey: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
required: true,
|
|
|
|
|
|
validator: (value) => ['bigBrother', 'crawl', 'webAi'].includes(value)
|
|
|
|
|
|
},
|
|
|
|
|
|
title: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: '功能未开通'
|
|
|
|
|
|
},
|
|
|
|
|
|
description: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: '您当前没有使用此功能的权限'
|
|
|
|
|
|
},
|
|
|
|
|
|
placeholderImage: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: ''
|
2026-03-03 21:57:18 +08:00
|
|
|
|
},
|
|
|
|
|
|
// 名片数据,每项: { avatar, name, desc, qrcode, phone }
|
|
|
|
|
|
contacts: {
|
|
|
|
|
|
type: Array,
|
|
|
|
|
|
default: () => []
|
2026-02-04 21:24:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const wrapperRef = ref(null)
|
|
|
|
|
|
const maskRef = ref(null)
|
2026-03-05 14:47:02 +08:00
|
|
|
|
const visibleIndices = ref([])
|
|
|
|
|
|
const visibleContacts = ref([])
|
2026-02-04 21:24:11 +08:00
|
|
|
|
|
|
|
|
|
|
const guardKey = computed(() => `guard_${props.permissionKey}_${Date.now()}`)
|
|
|
|
|
|
|
|
|
|
|
|
const permissionsData = ref(getPermissions())
|
|
|
|
|
|
|
|
|
|
|
|
const hasAccess = computed(() => {
|
|
|
|
|
|
return permissionsData.value[props.permissionKey] === 1
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-05 14:47:02 +08:00
|
|
|
|
const pickRandomIndices = (sourceIndices, count) => {
|
|
|
|
|
|
const arr = [...sourceIndices]
|
|
|
|
|
|
for (let i = arr.length - 1; i > 0; i--) {
|
|
|
|
|
|
const j = Math.floor(Math.random() * (i + 1))
|
|
|
|
|
|
;[arr[i], arr[j]] = [arr[j], arr[i]]
|
|
|
|
|
|
}
|
|
|
|
|
|
return arr.slice(0, count)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const updateVisibleContacts = (excludeCurrent = false) => {
|
2026-03-03 21:57:18 +08:00
|
|
|
|
const total = props.contacts.length
|
2026-03-05 14:47:02 +08:00
|
|
|
|
|
|
|
|
|
|
if (total === 0) {
|
|
|
|
|
|
visibleIndices.value = []
|
|
|
|
|
|
visibleContacts.value = []
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (total <= 3) {
|
|
|
|
|
|
const all = [...Array(total).keys()]
|
|
|
|
|
|
visibleIndices.value = pickRandomIndices(all, total)
|
|
|
|
|
|
visibleContacts.value = visibleIndices.value.map(i => props.contacts[i])
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const all = [...Array(total).keys()]
|
|
|
|
|
|
let candidates = all
|
|
|
|
|
|
|
|
|
|
|
|
if (excludeCurrent && visibleIndices.value.length) {
|
|
|
|
|
|
const current = new Set(visibleIndices.value)
|
|
|
|
|
|
candidates = all.filter(i => !current.has(i))
|
|
|
|
|
|
if (candidates.length < 3) {
|
|
|
|
|
|
candidates = all
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
visibleIndices.value = pickRandomIndices(candidates, 3)
|
|
|
|
|
|
visibleContacts.value = visibleIndices.value.map(i => props.contacts[i])
|
|
|
|
|
|
}
|
2026-03-03 21:57:18 +08:00
|
|
|
|
|
|
|
|
|
|
const shuffleContacts = () => {
|
2026-03-05 14:47:02 +08:00
|
|
|
|
updateVisibleContacts(true)
|
2026-03-03 21:57:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-04 21:24:11 +08:00
|
|
|
|
let permissionCheckInterval = null
|
|
|
|
|
|
|
|
|
|
|
|
const refreshPermissions = () => {
|
|
|
|
|
|
permissionsData.value = getPermissions()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let observer = null
|
|
|
|
|
|
|
|
|
|
|
|
const setupDOMProtection = () => {
|
|
|
|
|
|
if (hasAccess.value || !wrapperRef.value) return
|
2026-03-03 21:57:18 +08:00
|
|
|
|
|
2026-02-04 21:24:11 +08:00
|
|
|
|
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()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
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()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2026-03-03 21:57:18 +08:00
|
|
|
|
|
2026-02-04 21:24:11 +08:00
|
|
|
|
observer.observe(wrapperRef.value, {
|
|
|
|
|
|
childList: true,
|
|
|
|
|
|
subtree: true,
|
|
|
|
|
|
attributes: true,
|
|
|
|
|
|
attributeFilter: ['style', 'class']
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
2026-03-05 14:47:02 +08:00
|
|
|
|
updateVisibleContacts(false)
|
2026-03-03 21:57:18 +08:00
|
|
|
|
permissionCheckInterval = setInterval(refreshPermissions, 2000)
|
2026-02-04 21:24:11 +08:00
|
|
|
|
setTimeout(setupDOMProtection, 100)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
2026-03-03 21:57:18 +08:00
|
|
|
|
if (permissionCheckInterval) clearInterval(permissionCheckInterval)
|
|
|
|
|
|
if (observer) observer.disconnect()
|
2026-02-04 21:24:11 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-05 14:47:02 +08:00
|
|
|
|
watch(
|
|
|
|
|
|
() => props.contacts,
|
|
|
|
|
|
() => {
|
|
|
|
|
|
updateVisibleContacts(false)
|
|
|
|
|
|
},
|
|
|
|
|
|
{ deep: true }
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-02-04 21:24:11 +08:00
|
|
|
|
watch(hasAccess, (newVal) => {
|
2026-03-03 21:57:18 +08:00
|
|
|
|
if (observer) observer.disconnect()
|
|
|
|
|
|
if (!newVal) setTimeout(setupDOMProtection, 100)
|
2026-02-04 21:24:11 +08:00
|
|
|
|
})
|
|
|
|
|
|
</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%;
|
2026-03-03 21:57:18 +08:00
|
|
|
|
background-image:
|
2026-02-04 21:24:11 +08:00
|
|
|
|
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;
|
2026-03-03 21:57:18 +08:00
|
|
|
|
flex-direction: column;
|
2026-02-04 21:24:11 +08:00
|
|
|
|
align-items: center;
|
2026-03-03 21:57:18 +08:00
|
|
|
|
justify-content: flex-start;
|
2026-02-04 21:24:11 +08:00
|
|
|
|
background: rgba(15, 23, 42, 0.75);
|
|
|
|
|
|
backdrop-filter: blur(1px);
|
|
|
|
|
|
-webkit-backdrop-filter: blur(8px);
|
2026-03-03 21:57:18 +08:00
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 上方锁提示区域 */
|
|
|
|
|
|
.mask-top {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
padding-top: 6vh;
|
|
|
|
|
|
width: 100%;
|
2026-02-04 21:24:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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 {
|
2026-03-03 21:57:18 +08:00
|
|
|
|
from { opacity: 0; transform: translateY(20px); }
|
|
|
|
|
|
to { opacity: 1; transform: translateY(0); }
|
2026-02-04 21:24:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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;
|
|
|
|
|
|
}
|
2026-03-03 21:57:18 +08:00
|
|
|
|
|
|
|
|
|
|
/* 名片区域 */
|
|
|
|
|
|
.cards-area {
|
|
|
|
|
|
width: 100%;
|
2026-03-05 14:47:02 +08:00
|
|
|
|
padding: 3vh 0 2rem;
|
2026-03-03 21:57:18 +08:00
|
|
|
|
animation: slideUp 0.4s ease-out 0.1s both;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.cards-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
margin-bottom: 1rem;
|
2026-03-05 14:47:02 +08:00
|
|
|
|
padding-right: 10%;
|
2026-03-03 21:57:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.refresh-btn {
|
2026-03-05 14:47:02 +08:00
|
|
|
|
height: 2.2vw;
|
2026-03-03 21:57:18 +08:00
|
|
|
|
cursor: pointer;
|
2026-03-05 14:47:02 +08:00
|
|
|
|
display: block;
|
|
|
|
|
|
transition: opacity 0.2s;
|
2026-03-03 21:57:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.refresh-btn:hover {
|
2026-03-05 14:47:02 +08:00
|
|
|
|
opacity: 0.8;
|
2026-03-03 21:57:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.cards-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-evenly;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 单张名片 */
|
|
|
|
|
|
.contact-card {
|
2026-03-05 14:47:02 +08:00
|
|
|
|
width: 17%;
|
|
|
|
|
|
flex: none;
|
|
|
|
|
|
max-width: unset;
|
|
|
|
|
|
aspect-ratio: 2 / 3;
|
|
|
|
|
|
background: transparent;
|
2026-03-03 21:57:18 +08:00
|
|
|
|
border-radius: 1.25rem;
|
2026-03-05 14:47:02 +08:00
|
|
|
|
padding-top: 0;
|
2026-03-03 21:57:18 +08:00
|
|
|
|
position: relative;
|
2026-03-05 14:47:02 +08:00
|
|
|
|
box-shadow: none;
|
2026-03-03 21:57:18 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 14:47:02 +08:00
|
|
|
|
.contact-card::before {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-bg {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
inset: 0;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
object-fit: fill;
|
|
|
|
|
|
border-radius: 1.25rem;
|
|
|
|
|
|
z-index: 0;
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-03 21:57:18 +08:00
|
|
|
|
.card-avatar-wrapper {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
transform: translateX(-50%);
|
2026-03-05 14:47:02 +08:00
|
|
|
|
width: 40%;
|
|
|
|
|
|
aspect-ratio: 1;
|
2026-03-03 21:57:18 +08:00
|
|
|
|
border-radius: 50%;
|
2026-03-05 14:47:02 +08:00
|
|
|
|
border: 3px solid #fff;
|
2026-03-03 21:57:18 +08:00
|
|
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
background: #e2e8f0;
|
2026-03-05 14:47:02 +08:00
|
|
|
|
z-index: 2;
|
2026-03-03 21:57:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-avatar {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-body {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
2026-03-05 14:47:02 +08:00
|
|
|
|
padding: 50% 8% 6%;
|
2026-03-03 21:57:18 +08:00
|
|
|
|
width: 100%;
|
2026-03-05 14:47:02 +08:00
|
|
|
|
height: 100%;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
z-index: 1;
|
|
|
|
|
|
justify-content: flex-start;
|
|
|
|
|
|
box-sizing: border-box;
|
2026-03-03 21:57:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-name {
|
2026-03-05 14:47:02 +08:00
|
|
|
|
font-size: 1.1vw;
|
2026-03-03 21:57:18 +08:00
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: #1e293b;
|
2026-03-05 14:47:02 +08:00
|
|
|
|
margin-bottom: 0.3vw;
|
|
|
|
|
|
text-align: center;
|
2026-03-03 21:57:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-desc {
|
2026-03-05 14:47:02 +08:00
|
|
|
|
font-size: 0.8vw;
|
2026-03-03 21:57:18 +08:00
|
|
|
|
color: #64748b;
|
|
|
|
|
|
text-align: center;
|
2026-03-05 14:47:02 +08:00
|
|
|
|
margin-bottom: 0.5vw;
|
|
|
|
|
|
line-height: 1.4;
|
2026-03-03 21:57:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-qrcode {
|
2026-03-05 14:47:02 +08:00
|
|
|
|
width: 60%;
|
|
|
|
|
|
aspect-ratio: 1;
|
2026-03-03 21:57:18 +08:00
|
|
|
|
object-fit: contain;
|
2026-03-05 14:47:02 +08:00
|
|
|
|
margin-bottom: 0.5vw;
|
2026-03-03 21:57:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-phone {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-03-05 14:47:02 +08:00
|
|
|
|
gap: 0.3vw;
|
|
|
|
|
|
font-size: 0.85vw;
|
2026-03-03 21:57:18 +08:00
|
|
|
|
color: #2563eb;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.phone-icon {
|
2026-03-05 14:47:02 +08:00
|
|
|
|
width: 0.9vw;
|
|
|
|
|
|
height: 0.9vw;
|
|
|
|
|
|
object-fit: contain;
|
2026-03-03 21:57:18 +08:00
|
|
|
|
}
|
2026-02-04 21:24:11 +08:00
|
|
|
|
</style>
|