完成相似列表,关系强图,贴文列表,词云图

This commit is contained in:
qumeng039@126.com 2025-07-17 17:36:45 +08:00
parent 83f5ae04d5
commit 650027a40c
12 changed files with 1263 additions and 28 deletions

View File

@ -1,9 +1,9 @@
{
"semi": true,
"singleQuote": false,
"printWidth":100,
"printWidth": 100,
"trailingComma": "none",
"tabWidth": 2,
"endOfLine": "auto",
"arrowParens": "always"
}
}

View File

@ -1,5 +0,0 @@
# Vue 3 + Vite
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).

9
jsconfig.json Normal file
View File

@ -0,0 +1,9 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"exclude": ["node_modules"]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -1,7 +1,290 @@
import { defineStore } from "pinia";
import testAvatar from "@/assets/anchorAvatar/aichuidunhaitao-tingenqifu.png";
export const useCharacterInteractionStore = defineStore("characterInteraction", {
state: () => ({}),
state: () => ({
userList: [
[
{ id: 1, name: "Polo Hun", avatar: testAvatar, fancy: 634, post: 32 },
{ id: 2, name: "楊政子", avatar: testAvatar, fancy: 5556, post: 23 }
],
[
{ id: 3, name: "吳政輝", avatar: testAvatar, fancy: 0, post: 0 },
{ id: 4, name: "錫聰 林", avatar: testAvatar, fancy: 564, post: 13 }
],
[
{ id: 5, name: "一人一讚 馬總統感恩有您 永遠支...", avatar: testAvatar, fancy: 0, post: 0 },
{
id: 6,
name: "一人一讚 馬總統感恩有您 永遠支...",
avatar: testAvatar,
fancy: 564,
post: 13
}
],
[
{ id: 7, name: "一人一讚 馬總統感恩有您 永遠支...", avatar: testAvatar, fancy: 0, post: 0 },
{
id: 8,
name: "一人一讚 馬總統感恩有您 永遠支...",
avatar: testAvatar,
fancy: 564,
post: 13
}
],
[
{ id: 9, name: "一人一讚 馬總統感恩有您 永遠支...", avatar: testAvatar, fancy: 0, post: 0 },
{
id: 10,
name: "一人一讚 馬總統感恩有您 永遠支...",
avatar: testAvatar,
fancy: 564,
post: 13
}
]
],
userChartList: [
{
id: 1,
group: [
{ id: 1, name: "Polo Hun", avatar: testAvatar, fancy: 634, post: 32 },
{ id: 2, name: "楊政子", avatar: testAvatar, fancy: 5556, post: 23 }
],
number: "0.60"
},
{
id: 2,
group: [
{
id: 7,
name: "一人一讚 馬總統感恩有您 永遠支...",
avatar: testAvatar,
fancy: 0,
post: 0
},
{
id: 8,
name: "一人一讚 馬總統感恩有您 永遠支...",
avatar: testAvatar,
fancy: 564,
post: 13
}
],
number: 0.45
},
{
id: 3,
group: [
{
id: 9,
name: "一人一讚 馬總統感恩有您 永遠支...",
avatar: testAvatar,
fancy: 0,
post: 0
},
{
id: 10,
name: "一人一讚 馬總統感恩有您 永遠支...",
avatar: testAvatar,
fancy: 564,
post: 13
}
],
number: 0.32
}
],
posts: [
{
id: 1,
timestamp: "2024-01-04 0:03:16",
author: "十八子91221",
influence: 2,
highlighted: false,
like: 1,
comment: 1,
transmit: 0,
content:
"转发微博【#南部战区南海海域例行巡航#】1月3日至4日中国人民解放军@南部战区 组织海空兵力位南海海域进行例行巡航。战区部队全时保持高度戒备,坚决捍卫国家主权安全和海洋权益,任何搅局南海、制造热点的军事活动尽在掌握之中。 "
},
{
id: 2,
timestamp: "2024-02-03 12:58:53",
author: "中国海警",
influence: 6636,
highlighted: false,
like: 5013,
comment: 247,
transmit: 1376,
content: `【#中国海警发声# 】2月2日菲方1艘小型民船对其非法"坐滩" 仁爱礁军舰运送生活物资,中国海警全程跟监管控。中国对包括仁爱礁在内的南沙群岛及其附近海域拥有无可争辩的主权,中国海警依法在中国管辖海域持续开展维权执法活动。`
},
{
id: 3,
timestamp: "2024-02-22 17:13:44",
author: "外贸发布BBS",
influence: 6919,
like: 5354,
comment: 155,
transmit: 1410,
highlighted: false,
content: "所以,这算张冠李戴,还是指桑骂槐?#南海救助局寻获2名罹难者遗体#"
},
{
id: 4,
timestamp: "2024-02-23 17:54:46",
author: "什么时候有江西舰",
influence: 1670,
like: 1310,
comment: 60,
transmit: 300,
highlighted: false,
content:
"转发微博【#中国海警局新闻发言人就菲律宾侵闯黄岩岛发表谈话# 】中国海警局新闻发言人甘羽表示2月22日至23日菲律宾渔业和水产资源局3002船不顾中方一再劝阻和警告执意侵闯中国黄岩岛邻近海域。"
},
{
id: 5,
timestamp: "2024-03-14 09:58:12",
author: "新浪军事",
influence: 5292,
highlighted: false,
like: 2827,
comment: 1350,
transmit: 1115,
content: `#大陆渔船金门外海翻覆2人不幸罹难#【又有大陆渔船在金门海域翻覆已致两人遇难两岸联合搜救】3月14日南都记者从金门海巡队获悉当日清晨一艘大陆渔船"闽龙渔61222"在金门海域遭浪袭翻覆船上6名船员落海行踪不明。据悉目前落海的6名船员已全部寻获4人送医、2人不幸罹难`
},
{
id: 6,
timestamp: "2024-03-23 21:20:40",
author: "空天砺剑",
influence: 150,
highlighted: false,
like: 130,
comment: 10,
transmit: 10,
content:
"#菲律宾船对中国海警举白旗# 菲律宾人这是感谢帮它们洗船挥手致谢,既然人家这么客气,那就加大水压吧!"
},
{
id: 7,
timestamp: "2024-03-23 21:42:36",
author: "大侠啊啊啊啊",
influence: 2473,
highlighted: false,
like: 1923,
comment: 124,
transmit: 426,
content: "#菲律宾船对中国海警举白旗#大快人心"
},
{
id: 8,
timestamp: "2024-04-06 18:17:30",
author: "人民日报",
influence: 8345,
highlighted: false,
like: 6238,
comment: 409,
transmit: 1698,
content: `【#中方正告菲方任何侵权伎俩都是徒劳#】中国海警局新闻发言人甘羽表示4月4日菲方组织多艘船只非法位中国南沙群岛鲎藤礁邻近海域活动中国海警依法依规处置现场操作专业规范。中国对包括鲎藤礁在内的南沙群岛及其邻近海域拥有无可争辩的主权菲公务船打着所谓"护渔"幌子非法侵权挑衅,组织媒体蓄意煽炒误导,持续破坏南海稳定。我们正告菲方,任何侵权伎俩都是徒劳。中国海警将依法在中国管辖海域常态维权执法,坚决维护领土主权和海洋权益。`
},
{
id: 9,
timestamp: "2024-04-06 19:19:50",
author: "如皋老猫",
influence: 433,
highlighted: false,
like: 355,
comment: 67,
transmit: 11,
content:
"#中国海警回应菲律宾侵闯鲎藤礁#马科斯已经吃了秤砣铁了心当美狗了,所以,希望中国海警除了文字回应外,也该用行动予以坚决回应了!"
},
{
id: 10,
timestamp: "2024-05-16 00:22:18",
author: "环球网",
influence: 2889,
highlighted: false,
like: 1786,
comment: 362,
transmit: 741,
content: `【#环球时报社评##中方对菲律宾的善意和耐心不是无止境的#】在菲律宾近来不断公开否认"君子协定"、后被中方以铁证打脸之后马尼拉又开始了新的政治表演。15日5艘商业渔船上约200人从菲律宾高调驶向黄岩岛海域"维权"预计16日到达黄岩岛。菲律宾官方派出海岸警卫队的船为这支所谓"民间船队"护航,一些西方媒体也迅速跟进。这一套配合相当熟练的操作,我们此前已经在菲方于南海生事的好几次事件当中看到。`
}
],
wordCloudData: [
{
text: "佩洛西",
top: 115.5,
left: 215.5,
width: 109,
height: 40,
fontSize: 28,
opacity: 1
},
{ text: "中国", top: 183.5, left: 69.5, width: 73, height: 35, fontSize: 22, opacity: 1 },
{
text: "中国人民解放军",
top: 72.5,
left: 132.5,
width: 123,
height: 22,
fontSize: 12,
opacity: 1
},
{
text: "中美关系",
top: 171.5,
left: 212.5,
width: 81,
height: 22,
fontSize: 14,
opacity: 0.8
},
{ text: "台独", top: 135.5, left: 42.5, width: 57, height: 24, fontSize: 16, opacity: 1 },
{
text: "台海和平",
top: 228.5,
left: 230.5,
width: 81,
height: 22,
fontSize: 14,
opacity: 0.8
},
{
text: "坚决反对",
top: 200.5,
left: 38.5,
width: 73,
height: 19,
fontSize: 12,
opacity: 0.7
},
{
text: "联合公报",
top: 241.5,
left: 130.5,
width: 73,
height: 19,
fontSize: 12,
opacity: 0.7
},
{
text: "有力反制",
top: 211.5,
left: 143.5,
width: 73,
height: 19,
fontSize: 12,
opacity: 1
},
{ text: "白宫", top: 176.5, left: 15.5, width: 49, height: 19, fontSize: 12, opacity: 0.8 },
{ text: "内政", top: 87.5, left: 36.5, width: 53, height: 22, fontSize: 14, opacity: 1 },
{ text: "访台", top: 103.5, left: 95.5, width: 49, height: 19, fontSize: 12, opacity: 0.8 },
{ text: "领土", top: 57.5, left: 72.5, width: 49, height: 19, fontSize: 12, opacity: 0.6 },
{ text: "原则", top: 77.5, left: 264.5, width: 49, height: 19, fontSize: 12, opacity: 0.7 },
{ text: "台湾", top: 195.5, left: 287.5, width: 49, height: 19, fontSize: 12, opacity: 0.8 }
]
}),
actions: {},
persist: true // 开启持久化
});

View File

@ -4,30 +4,147 @@
<div class="content">
<div class="left-container">
<div class="userPanel">
<UserPanel :title="userPanelTitleImg"></UserPanel>
<UserPanel
:title="userPanelTitleImg"
:userList="interactionStore.userList"
@click:selectedGroup="handleSelectedUserGroup"
></UserPanel>
</div>
<div class="userChart">
<UserChart
:title="userChartTitleImg"
:userChartList="interactionStore.userChartList"
></UserChart>
</div>
<div class="userChart"></div>
</div>
<div class="middle-container">
<div class="graph"></div>
<div class="timeShaft"></div>
<div class="graph">
<Graph :title="graphTitleImg"></Graph>
</div>
<div class="postList">
<PostList
:posts="interactionStore.posts"
@click:openDialog="handleOpenPostDialog"
></PostList>
</div>
</div>
<div class="right-container">
<div class="anlysisPanel"></div>
<div class="cloudWords"></div>
<div class="cloudWords">
<WordsCloud :wordsCloudList="interactionStore.wordCloudData"></WordsCloud>
</div>
</div>
</div>
<el-dialog v-model="postDialog" width="640" align-center class="custom-dialog">
<img src="@/assets/images/head/post-dialog-title.png" alt="" class="postTitleImage" />
<div class="dialog-content">
<div class="post-content">{{ currentPostPost.content }}</div>
<div class="heat">
<div class="item-heat-detail">
<div class="item-heat-like">
<Icon icon="ei:like" width="25" height="25" /> {{ currentPostPost.like }}
</div>
<div class="item-heat-comment">
<Icon icon="la:comment-dots" width="25" height="25" /> {{ currentPostPost.comment }}
</div>
<div class="item-heat-transmit">
<Icon icon="mdi:share-outline" width="25" height="25" /> {{
currentPostPost.transmit
}}
</div>
</div>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref } from "vue";
import UserPanel from "../components/userPanel.vue";
import UserChart from "../components/userChart.vue";
import PostList from "../components/postList.vue";
import Graph from "../components/graph.vue";
import WordsCloud from "../components/cloudWords.vue";
import { Icon } from "@iconify/vue";
import { useCharacterInteractionStore } from "@/store/llinkPrediction/index";
import userPanelTitleImg from "@/assets/images/linkPrediction/title/user-title.png";
import userChartTitleImg from "@/assets/images/linkPrediction/title/interaction-strenth-title.png";
import graphTitleImg from "@/assets/images/linkPrediction/title/graph1-title.png";
const interactionStore = useCharacterInteractionStore();
//
const postDialog = ref(false);
//
const currentPostPost = ref(null);
const handleSelectedUserGroup = (group) => {
console.log(group);
};
const handleOpenPostDialog = (post) => {
postDialog.value = true;
currentPostPost.value = post;
console.log(post);
};
</script>
<style scoped lang="less">
:deep(.custom-dialog) {
width: 640px;
border-width: 0px, 0px, 0px, 0px;
border-style: solid;
border-image-source: linear-gradient(180deg, #3aa1f8 0%, rgba(58, 161, 248, 0.2) 100%);
background-color: rgba(6, 45, 90, 1);
border: 1px solid #1a8bff;
border-radius: 2px;
padding: 0 0;
z-index: 1;
}
:deep(.post-dialog) {
height: 300px;
}
:deep(.custom-dialog) .postTitleImage {
margin-top: -24px;
margin-left: -2px;
}
:deep(.custom-dialog) .dialog-content {
width: 100%;
padding: 25px 20px;
}
:deep(.custom-dialog) .dialog-content .content {
color: rgba(255, 255, 255, 0.8);
font-family: "PingFang SC";
font-size: 16px;
font-style: normal;
font-weight: 400;
opacity: 0.7;
}
.post-content {
color: #fff;
font-size: 16px;
opacity: 0.7;
}
.heat {
display: flex;
justify-content: flex-end;
margin-top: 20px;
.item-heat-detail {
display: flex;
justify-content: space-between;
div {
display: flex;
align-items: center;
justify-content: center;
width: 70px;
color: #fff;
opacity: 0.7;
}
}
}
.layout-container {
width: 1544px;
display: flex;
@ -92,7 +209,7 @@ const interactionStore = useCharacterInteractionStore();
background-color: rgba(6, 45, 90, 0.3);
border: 1px solid rgba(0, 113, 188, 0.705);
}
.timeShaft {
.postList {
flex: 1;
border-radius: 4px;
background-color: rgba(6, 45, 90, 0.3);

View File

@ -1,7 +1,287 @@
<template>
<div class="cloudWords-component"></div>
<div class="word-cloud-container">
<div class="header">
<img src="@/assets/images/words.png" alt="" style="margin-top: -8px" />
</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></script>
<script setup>
import { ref, onMounted, onBeforeUnmount, defineProps } from "vue";
<style scoped lang="less"></style>
const props = defineProps({
wordsCloudList: {
type: Array,
default: () => []
}
});
const scanAngle = ref(0);
const scanTimer = ref(null);
const containerWidth = 370;
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;
width: 370px;
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%
);
border: 1px solid transparent;
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>

View File

@ -1,7 +1,46 @@
<template>
<div class="graph-component"></div>
<div class="graph-component">
<img :src="title" alt="" class="title" />
<div class="graph-container" id="container"></div>
<div class="time-axis"></div>
</div>
</template>
<script setup></script>
<script setup>
import { defineProps } from "vue";
const props = defineProps({
title: {
type: String,
default: ""
}
});
</script>
<style scoped lang="less"></style>
<style scoped lang="less">
.graph-component {
width: 100%;
height: 100%;
position: relative;
.title {
margin: 0 auto;
}
.graph-container {
width: 100%;
height: 93%;
}
.time-axis {
width: 95%;
height: 42px;
border: 1px solid #3aa1f8;
background: linear-gradient(270deg, rgba(0, 82, 125, 0.48) 0%, rgba(0, 200, 255, 0.23) 100%);
backdrop-filter: blur(3px);
position: absolute;
bottom: 0;
left: 20px;
bottom: 20px;
border-radius: 4px;
z-index: 1;
}
}
</style>

View File

@ -1,7 +1,268 @@
<template>
<div class="postList-component"></div>
<div class="postList-component">
<img
src="@/assets/images/head/ContextoftheincidentTitle.png"
alt=""
style="margin-top: -17px; margin-left: -11px"
/>
<div
class="post-list-wrapper"
ref="listRef"
@mouseenter="pauseScroll"
@mouseleave="resumeScroll"
>
<div class="scrolling-content">
<div
class="post-item"
v-for="(post, index) in posts"
:key="index"
:class="{ highlighted: post.highlighted }"
@click="handleLeaderPost(post)"
>
<div class="post-type">
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="icon"
>
<rect opacity="0.1" width="20" height="20" rx="2" fill="#2B88DD" />
<path d="M5 4V7C5 7.55228 5.44772 8 6 8H7" stroke="#8EFBFF" />
<path d="M15 4V7C15 7.55228 14.5523 8 14 8H13" stroke="#8EFBFF" />
<path d="M5 16V13C5 12.4477 5.44772 12 6 12H7" stroke="#8EFBFF" />
<path d="M15 16V13C15 12.4477 14.5523 12 14 12H13" stroke="#8EFBFF" />
</svg>
<span class="post-type-text">帖文</span>
</div>
<span class="timestamp">{{ post.timestamp }}</span>
<span class="author">{{ post.author }}发布了帖文</span>
<div class="influence-section">
<div class="influence-label">互动: {{ post.influence }}</div>
<div class="influence-bar">
<div class="bar-track"></div>
<div class="bar-fill" :style="{ width: getInfluenceWidth(post.influence) }"></div>
<div class="bar-handle-wrapper" :style="{ left: getInfluenceWidth(post.influence) }">
<div class="bar-handle"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup></script>
<script setup>
import { ref, computed, defineProps, defineEmits, onMounted, onBeforeUnmount } from "vue";
const props = defineProps({
posts: {
type: Array,
default: () => []
}
});
<style scoped lang="less"></style>
const emit = defineEmits(["click:openDialog"]);
const animationDuration = computed(() => `${props.posts.length * 3}s`);
const maxInfluence = 10000;
const getInfluenceWidth = (influence) => {
const percentage = (influence / maxInfluence) * 100;
return `${Math.min(percentage, 100)}%`;
};
const handleLeaderPost = (item) => {
emit("click:openDialog", item);
console.log(item);
};
const listRef = ref(null);
let scrollTimer = null;
let direction = 1; // 1: , -1:
const scrollStep = 1; //
const scrollInterval = 80; // 40ms
function startScroll() {
if (scrollTimer) return;
scrollTimer = setInterval(() => {
const el = listRef.value;
if (!el) return;
el.scrollTop += direction * scrollStep;
//
if (el.scrollTop + el.clientHeight >= el.scrollHeight) {
direction = -1;
}
//
if (el.scrollTop <= 0) {
direction = 1;
}
}, scrollInterval);
}
function pauseScroll() {
if (scrollTimer) {
clearInterval(scrollTimer);
scrollTimer = null;
}
}
function resumeScroll() {
startScroll();
}
onMounted(() => {
startScroll();
});
onBeforeUnmount(() => {
pauseScroll();
});
</script>
<style scoped lang="less">
.postList-component {
width: 100%;
height: 100%;
.post-list-wrapper {
width: 100%;
height: 80%;
overflow: auto;
}
/* 滚动条整体样式 - WebKit浏览器 */
.post-list-wrapper::-webkit-scrollbar {
width: 3px; /* 垂直滚动条宽度 */
height: 5px; /* 水平滚动条高度 */
}
/* 滚动条滑块 */
.post-list-wrapper::-webkit-scrollbar-thumb {
background: rgba(147, 210, 255, 0.3); /* 蓝色半透明滑块 */
border-radius: 4px;
}
/* 鼠标悬停在滑块上的效果 */
.post-list-wrapper::-webkit-scrollbar-thumb:hover {
background: rgba(147, 210, 255, 0.5); /* 更明显的蓝色 */
}
.scrolling-content {
display: flex;
flex-direction: column;
gap: 1px;
animation-name: scroll-up;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
.post-list-wrapper:hover .scrolling-content {
animation-play-state: paused;
}
.post-item {
display: flex;
align-items: center;
height: 36px;
padding: 0 16px;
background: linear-gradient(90deg, rgba(63, 169, 245, 0.16) 0%, rgba(0, 84, 187, 0) 100%);
border-left: 2px solid #3fa9f5;
color: white;
font-size: 14px;
gap: 12px;
flex-shrink: 0;
cursor: pointer;
margin: 8px 0;
}
.post-item.highlighted {
background: linear-gradient(90deg, rgba(63, 169, 245, 0.4) 0%, rgba(0, 84, 187, 0) 100%);
}
.post-type {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.post-type-text {
color: #8efbff;
font-size: 14px;
font-weight: 400;
text-shadow: 0px 1px 1px rgba(0, 0, 0, 0.25);
}
.timestamp {
flex-shrink: 0;
}
.author {
flex-grow: 1;
text-align: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.influence-section {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.influence-label {
width: 90px;
display: flex;
justify-content: flex-start;
}
.influence-bar {
width: 200px;
height: 10px;
position: relative;
display: flex;
align-items: center;
}
.bar-track {
width: 100%;
height: 2px;
background-color: rgba(208, 222, 238, 0.1);
position: absolute;
}
.bar-fill {
position: absolute;
height: 2px;
background: linear-gradient(270deg, #00f3ff 0%, #00527d 100%);
}
.bar-handle-wrapper {
position: absolute;
top: 50%;
transform: translateY(-50%);
height: 16px;
width: 16px;
display: flex;
justify-content: center;
align-items: center;
}
.bar-handle {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #e0f1ff;
position: relative;
box-shadow: 0 0 6px 0 rgba(13, 97, 255, 0.8);
}
.bar-handle::before {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 16px;
height: 16px;
border-radius: 50%;
border: 1px solid rgba(21, 154, 255, 0.3);
}
}
</style>

View File

@ -1,7 +1,146 @@
<template>
<div class="userChart-component"></div>
<div class="userChart-component">
<img :src="title" alt="" class="title" />
<div class="userChart-container">
<div class="baseLine-back">
<div class="baseline" v-for="(item, index) in baseLineList" :key="index">
<div class="baseline-head-number">{{ item }}</div>
<div class="measure-column-line"></div>
</div>
</div>
<div class="interaction-list">
<div class="interaction-item" v-for="item in userChartList" :key="item.id">
<div class="avatar-list">
<img
class="avatar-item"
v-for="child in item.group"
:key="child.id"
:src="child.avatar"
/>
</div>
<div class="proportion-line-item">
<div class="proportion-line">
<div class="reality-ratio-line" :style="`width: ${item.number * 100 - 9}%`">
<div class="ratio">{{ item.number }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup></script>
<script setup>
import { defineProps } from "vue";
const baseLineList = [0, 0.2, 0.4, 0.6, 0.8, 1, "数量"];
const props = defineProps({
title: {
type: String,
default: ""
},
userChartList: {
type: Array,
default: []
}
});
</script>
<style scoped lang="less"></style>
<style scoped lang="less">
.userChart-component {
width: 100%;
height: 100%;
.title {
margin-top: -8px;
margin-left: -2px;
}
.userChart-container {
padding: 0px 20px;
.baseLine-back {
width: 218px;
display: flex;
position: relative;
left: 93px;
display: flex;
justify-content: space-between;
top: 17px;
.baseline {
display: flex;
flex-direction: column;
align-items: center;
.baseline-head-number {
font-family: D-DIN;
font-size: 12px;
color: #94c1ec;
}
.measure-column-line {
position: absolute;
height: 170px;
width: 1px; /* 不建议 0.5px,兼容性差 */
background-image: repeating-linear-gradient(
to bottom,
rgba(57, 69, 106, 0.86) 0,
rgba(57, 69, 106, 0.86) 4px,
/* 实线段长度 */ transparent 4px,
transparent 8px /* 4+4=8px 为一个周期 */
);
top: 25px;
}
}
}
.interaction-list {
.interaction-item {
width: 100%;
height: 30px;
margin: 30px 0;
display: flex;
align-items: center;
.avatar-list {
display: flex;
.avatar-item {
width: 32px;
height: 32px;
margin-right: 10px;
border-radius: 4px;
}
}
.proportion-line-item {
width: 100%;
height: 50px;
margin-left: 38px;
overflow: hidden;
display: flex;
align-items: center;
.proportion-line {
width: 95%;
background-color: #1b2c3e;
height: 8px;
border-radius: 10px;
.reality-ratio-line {
width: 70px;
height: 100%;
background-color: #287fe2;
border-radius: 10px;
position: relative;
.ratio {
color: #2ab8fd;
font-family: D-DIN;
font-size: 16px;
font-style: normal;
font-weight: 700;
line-height: normal;
position: absolute;
right: -40px;
top: -10px;
}
}
}
}
}
}
}
}
</style>

View File

@ -1,12 +1,40 @@
<template>
<div class="userPanel-component">
<img :src="title" alt="" class="title" />
<div class="user-list">
<div class="a-pair-user-item" v-for="(group, index) in userList" :key="index">
<div
class="shadow-box"
@click="handleUserItem(index, group)"
:class="{ active: curUserGroupIndex == index }"
>
<div class="user-list-item" v-for="child in group" :key="child.id">
<img :src="child.avatar" alt="" class="avatar" />
<div class="user-info">
<div class="username">{{ child.name }}</div>
<div class="userState">
<div class="userState-fancy">
粉丝数:
<p>{{ child.fancy }}</p>
</div>
<div class="userState-monitor-count">
发帖数:
<p>{{ child.post }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps } from "vue";
import { defineProps, defineEmits, ref } from "vue";
const curUserGroupIndex = ref(0);
const emit = defineEmits(["click:selectedGroup"]);
const props = defineProps({
userList: {
type: Array,
@ -17,6 +45,11 @@ const props = defineProps({
default: ""
}
});
const handleUserItem = (index, group = {}) => {
curUserGroupIndex.value = index;
emit("click:selectedGroup", group);
};
</script>
<style scoped lang="less">
@ -27,5 +60,84 @@ const props = defineProps({
margin-top: -7px;
margin-left: -2px;
}
.user-list {
width: 100%;
height: 480px;
padding: 0px 20px;
overflow: auto;
.a-pair-user-item {
height: 155px;
border-bottom: 0.5px solid rgba(0, 113, 188, 0.5);
.shadow-box {
width: 100%;
cursor: pointer;
padding: 0px 10px;
&:hover {
border-radius: 4px;
background-image: linear-gradient(to right, #0876be, #0ea7d500);
}
}
}
&::-webkit-scrollbar {
width: 3px; /* 垂直滚动条宽度 */
height: 5px; /* 水平滚动条高度 */
}
&::-webkit-scrollbar-thumb {
background: rgba(147, 210, 255, 0.3); /* 蓝色半透明滑块 */
border-radius: 4px;
}
&::-webkit-scrollbar-thumb:hover {
background: rgba(147, 210, 255, 0.5); /* 更明显的蓝色 */
}
.user-list-item {
width: 100%;
height: 70px;
display: flex;
justify-content: space-between;
align-items: center;
.avatar {
width: 48px;
height: 48px;
border-radius: 5px;
}
.user-info {
flex: 1;
padding-left: 15px;
display: flex;
flex-direction: column;
justify-content: space-between;
.username {
color: #fff;
font-size: 16px;
font-family: "微软雅黑";
white-space: nowrap; /* 禁止换行 */
overflow: hidden; /* 隐藏溢出内容 */
text-overflow: ellipsis; /* 显示省略号 */
}
.userState {
display: flex;
justify-content: space-between;
margin-top: 8px;
font-size: 13px;
color: #cccccc9d;
.userState-monitor-count {
width: 90px;
}
div {
display: flex;
p {
color: #fff;
margin-left: 5px;
}
}
}
}
}
}
}
.active {
border-radius: 4px;
background-image: linear-gradient(to right, #0876be, #0ea7d500);
}
</style>