2025-07-22 16:40:46 +08:00
|
|
|
<template>
|
|
|
|
|
<div class="word-cloud-container">
|
|
|
|
|
<div class="header">
|
|
|
|
|
<img src="@/assets/images/words.png" alt="" style="margin-top: -8px; margin-left: -2px" />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="globe-center">
|
|
|
|
|
<div
|
|
|
|
|
class="ring-dashed"
|
|
|
|
|
:style="{ transform: `translate(-50%, -50%) rotate(${scanAngle}deg)` }"
|
|
|
|
|
></div>
|
|
|
|
|
<div class="ring-solid"></div>
|
|
|
|
|
<div class="globe-icon">
|
|
|
|
|
<div class="globe-land"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
v-for="word in words"
|
|
|
|
|
:key="word.text"
|
|
|
|
|
class="word-item"
|
|
|
|
|
:style="[
|
|
|
|
|
getWordStyle(word),
|
|
|
|
|
{ opacity: word.visible ? word.opacity : 0, transition: 'opacity 0.1s ease-in' }
|
|
|
|
|
]"
|
|
|
|
|
>
|
|
|
|
|
<span class="dot"></span>
|
|
|
|
|
<span class="text">{{ word.text }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
import { ref, onMounted, onBeforeUnmount, defineProps } from "vue";
|
|
|
|
|
|
|
|
|
|
const props = defineProps({
|
|
|
|
|
wordsCloudList: {
|
|
|
|
|
type: Array,
|
|
|
|
|
default: () => []
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const scanAngle = ref(0);
|
|
|
|
|
const scanTimer = ref(null);
|
2025-07-28 16:02:19 +08:00
|
|
|
const containerWidth = 350;
|
2025-07-22 16:40:46 +08:00
|
|
|
const containerHeight = 276;
|
|
|
|
|
|
|
|
|
|
const words = ref(props.wordsCloudList);
|
|
|
|
|
|
|
|
|
|
const prepareWords = () => {
|
|
|
|
|
const centerX = containerWidth / 2;
|
|
|
|
|
const centerY = containerHeight / 2;
|
|
|
|
|
words.value = words.value.map((word) => {
|
|
|
|
|
const wordCenterX = word.left + word.width / 2;
|
|
|
|
|
const wordCenterY = word.top + word.height / 2;
|
|
|
|
|
const vecX = wordCenterX - centerX;
|
|
|
|
|
const vecY = wordCenterY - centerY;
|
|
|
|
|
let angleRad = Math.atan2(vecY, vecX);
|
|
|
|
|
let angleDeg = angleRad * (180 / Math.PI);
|
|
|
|
|
let scanAngle = angleDeg + 90;
|
|
|
|
|
if (scanAngle < 0) {
|
|
|
|
|
scanAngle += 360;
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
...word,
|
|
|
|
|
angle: scanAngle,
|
|
|
|
|
visible: false
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const startScan = () => {
|
|
|
|
|
const duration = 20000;
|
|
|
|
|
let startTime = null;
|
|
|
|
|
|
|
|
|
|
const animate = (timestamp) => {
|
|
|
|
|
if (!startTime) {
|
|
|
|
|
startTime = timestamp;
|
|
|
|
|
}
|
|
|
|
|
const elapsedTime = timestamp - startTime;
|
|
|
|
|
const progress = (elapsedTime / duration) % 1;
|
|
|
|
|
const newScanAngle = progress * 360;
|
|
|
|
|
|
|
|
|
|
if (newScanAngle < scanAngle.value) {
|
|
|
|
|
words.value.forEach((w) => (w.visible = false));
|
|
|
|
|
startTime = timestamp;
|
|
|
|
|
}
|
|
|
|
|
scanAngle.value = newScanAngle;
|
|
|
|
|
|
|
|
|
|
words.value.forEach((word) => {
|
|
|
|
|
if (!word.visible && scanAngle.value >= word.angle) {
|
|
|
|
|
word.visible = true;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
scanTimer.value = requestAnimationFrame(animate);
|
|
|
|
|
};
|
|
|
|
|
scanTimer.value = requestAnimationFrame(animate);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getWordStyle = (word) => {
|
|
|
|
|
return {
|
|
|
|
|
top: `${word.top}px`,
|
|
|
|
|
left: `${word.left}px`,
|
|
|
|
|
width: `${word.width}px`,
|
|
|
|
|
height: `${word.height}px`,
|
|
|
|
|
borderRadius: `${word.height / 2}px`,
|
|
|
|
|
fontSize: `${word.fontSize}px`,
|
|
|
|
|
paddingLeft: `${word.fontSize * 0.8}px`,
|
|
|
|
|
paddingRight: "10px"
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
prepareWords();
|
|
|
|
|
startScan();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
if (scanTimer.value) cancelAnimationFrame(scanTimer.value);
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.word-cloud-container {
|
|
|
|
|
position: relative;
|
2025-07-28 16:02:19 +08:00
|
|
|
width: 350px;
|
2025-07-22 16:40:46 +08:00
|
|
|
height: 100%;
|
|
|
|
|
background-color: rgba(4, 20, 33, 0.4);
|
|
|
|
|
background-image: linear-gradient(to right, rgba(6, 61, 113, 0.2), rgba(8, 30, 56, 0.8));
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
box-shadow: inset 0 0 18px rgba(6, 45, 84, 1);
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
font-family: "Microsoft YaHei", sans-serif;
|
|
|
|
|
right: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-bg {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.title-text {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 6px;
|
|
|
|
|
left: 15px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.globe-center {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 50%;
|
|
|
|
|
left: 50%;
|
|
|
|
|
transform: translate(-50%, -50%);
|
|
|
|
|
width: 200px;
|
|
|
|
|
height: 200px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.globe-center > div {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 50%;
|
|
|
|
|
left: 50%;
|
|
|
|
|
transform: translate(-50%, -50%);
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ring-dashed {
|
|
|
|
|
width: 125px;
|
|
|
|
|
height: 125px;
|
|
|
|
|
background: conic-gradient(
|
|
|
|
|
from 0deg,
|
|
|
|
|
#3fa9f5 0.5deg,
|
|
|
|
|
transparent 0.5deg 3.6deg,
|
|
|
|
|
#3fa9f5 3.6deg 4.1deg,
|
|
|
|
|
transparent 4.1deg 7.2deg,
|
|
|
|
|
#3fa9f5 7.2deg 7.7deg,
|
|
|
|
|
transparent 7.7deg 10.8deg,
|
|
|
|
|
#3fa9f5 10.8deg 11.3deg,
|
|
|
|
|
transparent 11.3deg 14.4deg,
|
|
|
|
|
#3fa9f5 14.4deg 14.9deg,
|
|
|
|
|
transparent 14.9deg 18deg,
|
|
|
|
|
#3fa9f5 18deg 18.5deg,
|
|
|
|
|
transparent 18.5deg 21.6deg,
|
|
|
|
|
#3fa9f5 21.6deg 22.1deg,
|
|
|
|
|
transparent 22.1deg 25.2deg,
|
|
|
|
|
#3fa9f5 25.2deg 25.7deg,
|
|
|
|
|
transparent 25.7deg 28.8deg,
|
|
|
|
|
#3fa9f5 28.8deg 29.3deg,
|
|
|
|
|
transparent 29.3deg 32.4deg,
|
|
|
|
|
#3fa9f5 32.4deg 32.9deg,
|
|
|
|
|
transparent 32.9deg 36deg,
|
|
|
|
|
#3fa9f5 36deg 36.5deg,
|
|
|
|
|
transparent 36.5deg 360deg
|
|
|
|
|
);
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
/* animation: spin 20s linear infinite; */ /* We now control this with JS */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ring-solid {
|
|
|
|
|
width: 108px;
|
|
|
|
|
height: 108px;
|
|
|
|
|
background: radial-gradient(circle, rgba(0, 151, 225, 0.1) 0%, rgba(0, 151, 225, 0.5) 100%);
|
|
|
|
|
opacity: 0.2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.globe-icon {
|
|
|
|
|
width: 70px;
|
|
|
|
|
height: 70px;
|
|
|
|
|
border: 1px solid #2974b3;
|
|
|
|
|
background: linear-gradient(135deg, #22629e09, #2975b534 38%, #3693da72 75%, #3fa9f5 100%);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.globe-land {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
background: linear-gradient(180deg, #ffffff, rgba(255, 255, 255, 0.1));
|
|
|
|
|
-webkit-mask-image: url('data:image/svg+xml;utf8,<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12.006 18.317C11.488 17.53 11.115 16.962 9.693 17.187C7.008 17.613 6.71 18.084 6.582 18.856L6.545 19.091L6.509 19.34C6.362 20.364 6.368 20.752 6.838 21.246C8.736 23.237 9.873 24.673 10.217 25.512C10.386 25.923 10.818 27.163 10.52 28.389C12.356 27.66 13.973 26.5 15.248 25.034C15.413 24.473 15.533 23.775 15.533 22.928V22.771C15.533 21.387 15.533 20.756 14.555 20.197C14.142 19.963 13.834 19.822 13.587 19.71C13.037 19.459 12.672 19.295 12.18 18.575C12.121 18.489 12.064 18.404 12.006 18.317ZM10 4.75C6.524 4.75 3.386 6.198 1.156 8.523C1.422 8.708 1.653 8.967 1.812 9.324C2.118 10.01 2.118 10.717 2.118 11.341C2.117 11.834 2.117 12.301 2.275 12.64C2.492 13.102 3.425 13.298 4.248 13.471C4.542 13.533 4.846 13.596 5.122 13.673C5.881 13.883 6.468 14.565 6.939 15.112C7.134 15.34 7.423 15.674 7.567 15.758C7.643 15.703 7.885 15.441 8.005 15.01C8.096 14.682 8.07 14.39 7.936 14.231C7.097 13.242 7.143 11.335 7.402 10.632C7.81 9.524 9.086 9.606 10.018 9.666C10.366 9.689 10.694 9.71 10.939 9.68C11.872 9.562 12.159 8.143 12.362 7.865C12.801 7.265 14.141 6.361 14.974 5.802C13.454 5.126 11.771 4.75 10 4.75Z" fill="black"/></svg>');
|
|
|
|
|
-webkit-mask-size: contain;
|
|
|
|
|
mask-size: contain;
|
|
|
|
|
-webkit-mask-repeat: no-repeat;
|
|
|
|
|
mask-repeat: no-repeat;
|
|
|
|
|
-webkit-mask-position: center;
|
|
|
|
|
mask-position: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.word-item {
|
|
|
|
|
position: absolute;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
color: #def6ff;
|
|
|
|
|
background: linear-gradient(
|
|
|
|
|
135deg,
|
|
|
|
|
rgba(47, 101, 195, 0) 0%,
|
|
|
|
|
rgba(45, 102, 196, 0.02) 15%,
|
|
|
|
|
rgba(40, 110, 200, 0.08) 33%,
|
|
|
|
|
rgba(32, 116, 204, 0.19) 48%,
|
|
|
|
|
rgba(6, 143, 220, 0.51) 92%,
|
|
|
|
|
rgba(0, 151, 225, 0.6) 100%
|
|
|
|
|
);
|
2025-07-28 16:02:19 +08:00
|
|
|
/* border: 1px solid transparent; */
|
2025-07-22 16:40:46 +08:00
|
|
|
border-image-source: linear-gradient(
|
|
|
|
|
135deg,
|
|
|
|
|
rgba(63, 169, 245, 0) 0%,
|
|
|
|
|
rgba(63, 169, 245, 0.04) 49%,
|
|
|
|
|
rgba(61, 169, 246, 0.08) 64%,
|
|
|
|
|
rgba(49, 174, 247, 0.15) 71%,
|
|
|
|
|
rgba(0, 192, 255, 0.7) 100%
|
|
|
|
|
);
|
|
|
|
|
border-image-slice: 1;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.word-item .dot {
|
|
|
|
|
width: 3px;
|
|
|
|
|
height: 3px;
|
|
|
|
|
background: #d9e021;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
box-shadow: 0 0 3px 1.5px rgba(217, 224, 33, 0.4);
|
|
|
|
|
margin-right: 5px;
|
|
|
|
|
}
|
|
|
|
|
.word-item .text {
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.bottom-decor {
|
|
|
|
|
position: absolute;
|
|
|
|
|
bottom: -3px;
|
|
|
|
|
left: 50%;
|
|
|
|
|
transform: translateX(-50%);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes spin {
|
|
|
|
|
from {
|
|
|
|
|
transform: translate(-50%, -50%) rotate(0deg);
|
|
|
|
|
}
|
|
|
|
|
to {
|
|
|
|
|
transform: translate(-50%, -50%) rotate(360deg);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|