Signed-off-by: changyunju <2743319061@qq.com>

This commit is contained in:
changyunju 2025-06-24 09:31:34 +08:00
parent 9f39908deb
commit 288a17892c

View File

@ -1,64 +1,68 @@
<template> <template>
<div class="main-container"> <div class="opinion-leader-layout">
<!-- ========================================================== --> <!-- 顶部描述 -->
<!-- 左侧区域: 意见领袖列表 (OpinionLeaders) --> <header class="main-header">
<!-- ========================================================== --> 评估高影响节点识别特色在于评估节点重要性时有机结合了节点微观影响力与全局影响力提升了意...
<div class="left-panel"> </header>
<h2 class="panel-title">意见领袖抽展示</h2>
<div class="tabs"> <div class="main-content">
<button <!-- ==================== 左侧面板: 意见领袖列表 ==================== -->
v-for="tab in tabs" <div class="left-panel">
:key="tab" <h2 class="panel-title">意见领袖抽展示</h2>
:class="{ active: activeTab === tab }" <div class="tabs">
@click="activeTab = tab" <button
> v-for="tab in tabs"
{{ tab }} :key="tab"
</button> :class="{ active: activeTab === tab }"
</div> @click="activeTab = tab"
<div class="leader-list"> >
<!-- v-for 循环的是根据 activeTab 筛选后的可见领袖 --> {{ tab }}
<div v-for="leader in filteredVisibleLeaders" :key="leader.id" class="leader-item"> </button>
<img :src="leader.avatar" :alt="leader.name" class="avatar"> </div>
<div class="info"> <div class="leader-list">
<div class="name"> <div v-for="leader in filteredVisibleLeaders" :key="leader.id" class="leader-item">
<span class="en-name">{{ leader.name }}</span> <img :src="leader.avatar" :alt="leader.name" class="avatar">
<span v-if="leader.chineseName" class="cn-name">{{ leader.chineseName }}</span> <div class="info">
</div> <div class="name">
<div class="stats"> <span class="en-name">{{ leader.name }}</span>
<span>粉丝数量: {{ leader.followers }}</span> <span v-if="leader.chineseName" class="cn-name">{{ leader.chineseName }}</span>
<span>发帖总数: {{ leader.posts }}</span> </div>
<div class="stats">
<span>粉丝数量: {{ leader.followers }}</span>
<span>发帖总数: {{ leader.posts }}</span>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- ========================================================== --> <!-- ==================== 右侧面板: 网络图与时间轴 ==================== -->
<!-- 右侧区域: 佩洛西图谱 (PelosiGraph) --> <div class="right-panel">
<!-- ========================================================== --> <!-- 这个组件现在被整合到右侧面板中 -->
<div class="right-panel"> <div class="key-node-recognition">
<div class="key-node-recognition"> <div class="background-svg-wrapper">
<div class="background-svg-wrapper"> <!-- 背景SVG与之前相同 -->
<svg width="100%" height="100%" viewBox="0 0 800 540" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg"> <svg width="100%" height="100%" viewBox="0 0 800 540" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
<defs><linearGradient id="paint0_linear_bg" x1="0" y1="167.1" x2="800" y2="167.1" gradientUnits="userSpaceOnUse"><stop stop-color="#063D71" stop-opacity="0.2" /><stop offset="1" stop-color="#081E38" stop-opacity="0.8" /></linearGradient><linearGradient id="paint1_linear_border" x1="400" y1="0" x2="400" y2="540" gradientUnits="userSpaceOnUse"><stop stop-color="#3AA1F8" /><stop offset="1" stop-color="#3AA1F8" stop-opacity="0.2" /></linearGradient></defs> <defs><linearGradient id="paint0_linear_bg" x1="0" y1="167.1" x2="800" y2="167.1" gradientUnits="userSpaceOnUse"><stop stop-color="#063D71" stop-opacity="0.2" /><stop offset="1" stop-color="#081E38" stop-opacity="0.8" /></linearGradient><linearGradient id="paint1_linear_border" x1="400" y1="0" x2="400" y2="540" gradientUnits="userSpaceOnUse"><stop stop-color="#3AA1F8" /><stop offset="1" stop-color="#3AA1F8" stop-opacity="0.2" /></linearGradient></defs>
<path d="M798 0.5H2C1.17159 0.5 0.500003 1.17158 0.5 2V538C0.5 538.828 1.17159 539.5 2 539.5H798C798.828 539.5 799.5 538.828 799.5 538V2C799.5 1.17157 798.828 0.5 798 0.5Z" fill="url(#paint0_linear_bg)" fill-opacity="0.48" stroke="url(#paint1_linear_border)" /> <path d="M798 0.5H2C1.17159 0.5 0.500003 1.17158 0.5 2V538C0.5 538.828 1.17159 539.5 2 539.5H798C798.828 539.5 799.5 538.828 799.5 538V2C799.5 1.17157 798.828 0.5 798 0.5Z" fill="url(#paint0_linear_bg)" fill-opacity="0.48" stroke="url(#paint1_linear_border)" />
</svg> </svg>
</div> </div>
<div class="content-wrapper"> <div class="content-wrapper">
<h1 class="graph-title">佩洛西系列事件</h1> <h1 class="main-title">佩洛西系列事件</h1>
<!-- ECharts 图表容器 --> <!-- 图表容器 -->
<div ref="chartContainer" class="chart-container"></div> <div ref="chartContainer" class="chart-container"></div>
<!-- 时间轴 --> <!-- 时间轴 -->
<div class="timeline-container"> <div class="timeline-container">
<span class="time-label">2022.07.31 00:00:00</span> <span class="time-label">2022.07.31 00:00:00</span>
<div class="timeline-track"> <div class="timeline-track">
<div v-for="point in timePoints" :key="point.id" class="timeline-point-wrapper" @click="onTimePointClick(point.id)"> <div v-for="point in timePoints" :key="point.id" class="timeline-point-wrapper" @click="activeTimePoint = point.id">
<div class="timeline-point" :class="{ active: activeTimePoint === point.id }"> <div class="timeline-point" :class="{ active: activeTimePoint === point.id }">
<div v-if="activeTimePoint === point.id" class="active-pin"></div> <div v-if="activeTimePoint === point.id" class="active-pin"></div>
</div>
</div> </div>
</div> </div>
<span class="time-label">2022.08.01 00:00:00</span>
</div> </div>
<span class="time-label">2022.08.01 00:00:00</span>
</div> </div>
</div> </div>
</div> </div>
@ -67,7 +71,7 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, onUnmounted, computed, watch } from 'vue'; import { ref, onMounted, onBeforeUnmount, watch, computed } from 'vue';
import * as echarts from 'echarts/core'; import * as echarts from 'echarts/core';
import { GraphChart } from 'echarts/charts'; import { GraphChart } from 'echarts/charts';
import { TitleComponent, TooltipComponent } from 'echarts/components'; import { TitleComponent, TooltipComponent } from 'echarts/components';
@ -75,32 +79,36 @@ import { CanvasRenderer } from 'echarts/renderers';
echarts.use([TitleComponent, TooltipComponent, GraphChart, CanvasRenderer]); echarts.use([TitleComponent, TooltipComponent, GraphChart, CanvasRenderer]);
// =================================================================== // --- ---
// () const chartContainer = ref(null);
// =================================================================== let myChart = null;
// 1.
const activeTimePoint = ref(1);
const timePoints = ref(Array.from({ length: 10 }, (_, i) => ({ id: i + 1 })));
// 2.
const allLeaderData = ref([
{ id: '5', name: 'Hu Xijin', chineseName: '胡锡进', followers: '53.8万', posts: '54', avatar: 'https://i.imgur.com/Y3vH2oP.png', category: '自媒体' },
{ id: 'bidishalolo', name: 'bidishalolo', followers: '2387', posts: '8380', avatar: 'https://i.imgur.com/6a5A466.png', category: '自媒体' },
{ id: 'indo-pacific', name: 'Indo-Pacific News', followers: '11.5万', posts: '11.3万', avatar: 'https://i.imgur.com/PkooCvB.png', category: '新闻媒体' },
{ id: '1', name: 'The Spectator Index', followers: '233.5万', posts: '56', avatar: 'https://i.imgur.com/rS2aP3s.png', category: '新闻媒体' },
{ id: 'mickwallace', name: 'Mick Wallace', followers: '24.8万', posts: '10259', avatar: 'https://i.imgur.com/gKk9p3j.png', category: '自媒体' },
//
]);
// 3.
const visibleLeaders = computed(() => {
return allLeaderData.value.slice(0, activeTimePoint.value);
});
// 4.
const tabs = ref(['全部', '新闻媒体', '自媒体', '政府官号']); const tabs = ref(['全部', '新闻媒体', '自媒体', '政府官号']);
const activeTab = ref('全部'); const activeTab = ref('全部');
const timePoints = ref(Array.from({ length: 10 }, (_, i) => ({ id: i + 1 })));
const activeTimePoint = ref(1); //
// --- ---
// 1.
const allLeaderData = [
{ id: '5', name: 'Hu Xijin', chineseName: '胡锡进', avatar: 'https://i.imgur.com/Y3vH2oP.png', followers: '53.8万', posts: '54', category: '自媒体' },
{ id: 'bidishalolo', name: 'bidishalolo', avatar: 'https://i.imgur.com/6a5A466.png', followers: '2387', posts: '8380', category: '自媒体' },
{ id: 'indo-pacific', name: 'Indo-Pacific News', avatar: 'https://i.imgur.com/PkooCvB.png', followers: '11.5万', posts: '11.3万', category: '新闻媒体' },
{ id: '1', name: 'The Spectator Index', avatar: 'https://i.imgur.com/rS2aP3s.png', followers: '233.5万', posts: '56', category: '新闻媒体' },
{ id: 'mickwallace', name: 'Mick Wallace', avatar: 'https://i.imgur.com/gKk9p3j.png', followers: '24.8万', posts: '10259', category: '自媒体' },
{ id: '0', name: 'China Coast Guard', chineseName: '中国海警', avatar: 'https://i.imgur.com/rN5V6fD.png', followers: '120万', posts: '88', category: '政府官号' },
{ id: '2', name: 'Nancy Pelosi', avatar: 'https://i.imgur.com/g0t6GqB.png', followers: '1780万', posts: '320', category: '政府官号' },
{ id: '6', name: 'Cat Avatar User', avatar: 'https://i.imgur.com/QhT8k5q.png', followers: '1.2万', posts: '950', category: '自媒体' },
{ id: '7', name: 'User C', avatar: 'https://i.imgur.com/7bO2A5a.png', followers: '8000', posts: '120', category: '自媒体' },
{ id: '8', name: 'Default User', avatar: 'https://i.imgur.com/tP2x2Jg.png', followers: '500', posts: '30', category: '自媒体' },
];
// 2.
const visibleLeaders = ref([]);
// 3. Tab
const filteredVisibleLeaders = computed(() => { const filteredVisibleLeaders = computed(() => {
if (activeTab.value === '全部') { if (activeTab.value === '全部') {
return visibleLeaders.value; return visibleLeaders.value;
@ -108,124 +116,289 @@ const filteredVisibleLeaders = computed(() => {
return visibleLeaders.value.filter(leader => leader.category === activeTab.value); return visibleLeaders.value.filter(leader => leader.category === activeTab.value);
}); });
// 5.
const chartContainer = ref(null); // 4. 线
let myChart = null; const allGraphNodes = [
// ...allLeaderData.map(leader => ({
const allGraphNodes = computed(() => [ id: leader.id,
...allLeaderData.value.map(leader => ({ name: leader.name,
id: leader.id, name: leader.name, symbol: `image://${leader.avatar}`, symbol: `image://${leader.avatar}`,
symbolSize: 50, category: leader.category === '新闻媒体' ? 1 : 0 symbolSize: 50,
category: leader.category === '政府官号' ? 0 : 1,
label: { show: leader.chineseName, color: '#fff' }
})), })),
]); //
const allGraphLinks = ref([ { source: 'bidishalolo', target: '5' }, { source: 'indo-pacific', target: '1' } ]); ...Array.from({ length: 30 }, (_, i) => ({ id: `n${i}`, name: `User ${i}`, symbolSize: 15, category: 2 })),
];
// =================================================================== const allGraphLinks = [
// //
// =================================================================== { source: '5', target: '1' }, { source: '1', target: '2' }, { source: 'indo-pacific', target: '1' },
{ source: '0', target: '2' }, { source: 'mickwallace', target: '5' },
//
{ source: '5', target: 'n0' }, { source: '5', target: 'n1' },
{ source: 'bidishalolo', target: 'n2' },
{ source: 'indo-pacific', target: 'n3' }, { source: 'indo-pacific', target: 'n4' },
{ source: '1', target: 'n5' }, { source: '1', target: 'n6' },
{ source: 'mickwallace', target: 'n7' },
{ source: '0', target: 'n8' }, { source: '0', target: 'n9' }, { source: '0', target: 'n10' },
{ source: '2', target: 'n11' },
{ source: '6', target: 'n12' }, { source: '7', target: 'n13' }, { source: '8', target: 'n14' },
];
// A. // 5. ECharts
const onTimePointClick = (pointId) => { const chartOptions = {
activeTimePoint.value = pointId; tooltip: {},
animationDurationUpdate: 1000,
animationEasingUpdate: 'quinticInOut',
series: [{
type: 'graph', layout: 'force', roam: true, draggable: true,
categories: [
{ name: '政府官号', itemStyle: { color: '#E06300', shadowBlur: 20, shadowColor: '#E06300', borderColor: '#FF8A2B', borderWidth: 2 } },
{ name: '新闻媒体/自媒体', itemStyle: { color: '#0A53B5', shadowBlur: 15, shadowColor: '#3AA1F8', borderColor: '#3AA1F8', borderWidth: 2 } },
{ name: '普通用户', itemStyle: { color: '#0B4B69', borderColor: '#1A8BFF', borderWidth: 1 } },
],
label: { position: 'right', formatter: '{b}', show: false },
lineStyle: { color: 'source', curveness: 0.1, opacity: 0.5 },
force: { repulsion: 120, edgeLength: 80, gravity: 0.1 },
emphasis: { focus: 'adjacency', lineStyle: { width: 5 } },
data: [], links: [],
}]
}; };
// B. // --- ---
const updateGraphData = () => { const updateViewForTimePoint = (timePoint) => {
// 1.
visibleLeaders.value = allLeaderData.slice(0, timePoint);
// 2.
if (!myChart) return; if (!myChart) return;
// `visibleLeaders` 线
const leadersSet = new Set(visibleLeaders.value.map(l => l.id));
const currentVisibleLinks = allGraphLinks.value.filter(link => leadersSet.has(link.source) && leadersSet.has(link.target));
const currentVisibleNodes = allGraphNodes.value.filter(node => leadersSet.has(node.id));
// 使 const leadersToShowIds = visibleLeaders.value.map(l => l.id);
myChart.setOption({ const leadersSet = new Set(leadersToShowIds);
series: [{
data: currentVisibleNodes, const visibleLinks = allGraphLinks.filter(link =>
links: currentVisibleLinks leadersSet.has(link.source) && leadersSet.has(link.target) // 线
}] );
const visibleNodeIds = new Set(leadersToShowIds);
//
// visibleLinks.forEach(link => {
// visibleNodeIds.add(link.source);
// visibleNodeIds.add(link.target);
// });
const visibleNodes = allGraphNodes.filter(node => visibleNodeIds.has(node.id));
myChart.setOption({
series: [{ data: visibleNodes, links: visibleLinks }]
}); });
}; };
// C. ECharts // --- ---
onMounted(() => { onMounted(() => {
if (chartContainer.value) { if (chartContainer.value) {
myChart = echarts.init(chartContainer.value); myChart = echarts.init(chartContainer.value);
myChart.setOption({ myChart.setOption(chartOptions);
tooltip: {}, animationDurationUpdate: 1000, updateViewForTimePoint(activeTimePoint.value); //
series: [{ window.addEventListener('resize', resizeChart);
type: 'graph', layout: 'force', roam: true,
categories: [
{ name: '自媒体/政府', itemStyle: { color: '#E06300', shadowBlur: 20, shadowColor: '#E06300', borderWidth: 2, borderColor: '#FF8A2B' } },
{ name: '新闻媒体', itemStyle: { color: '#0A53B5', shadowBlur: 15, shadowColor: '#3AA1F8', borderWidth: 2, borderColor: '#3AA1F8' } },
],
label: { show: false }, lineStyle: { color: 'source', curveness: 0.1, opacity: 0.5 },
force: { repulsion: 150, edgeLength: 100, gravity: 0.1 },
data: [], links: [],
}]
});
updateGraphData(); //
window.addEventListener('resize', myChart.resize);
} }
}); });
// D. onBeforeUnmount(() => {
watch(activeTimePoint, () => { window.removeEventListener('resize', resizeChart);
if (myChart) { if (myChart) myChart.dispose();
updateGraphData();
}
}); });
onUnmounted(() => { watch(activeTimePoint, (newTimePoint) => {
if (myChart) { updateViewForTimePoint(newTimePoint);
window.removeEventListener('resize', myChart.resize);
myChart.dispose();
}
}); });
const resizeChart = () => {
if (myChart) myChart.resize();
};
</script> </script>
<style scoped> <style scoped>
.main-container { /* 全局布局 */
display: flex; .opinion-leader-layout {
flex-direction: row; width: 1200px;
gap: 10px; height: 700px;
padding: 10px; background-color: #031024;
background-color: #031024; color: #cce7ff;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
color: #fff; display: flex;
flex-direction: column;
border: 1px solid #1a8bff;
} }
/* 左侧面板样式 (从 OpinionLeaders.vue 合并) */ .main-header {
.left-panel { width: 350px; flex-shrink: 0; background-color: rgba(6, 45, 90, 0.3); border: 1px solid #1a8bff; display: flex; flex-direction: column; padding: 15px; border-radius: 4px; } padding: 10px 20px;
.panel-title { font-size: 18px; font-weight: bold; text-align: center; padding: 10px; margin: 0 0 15px 0; background: linear-gradient(to right, rgba(58,161,248,0), rgba(58,161,248,0.3), rgba(58,161,248,0)); border-top: 1px solid #3aa1f8; border-bottom: 1px solid #3aa1f8; color: #fff; } font-size: 14px;
.tabs { display: flex; margin-bottom: 15px; border-bottom: 2px solid #1a5a9c; } background-color: rgba(10, 35, 68, 0.5);
.tabs button { background: none; border: none; color: #a9c2e0; padding: 8px 16px; font-size: 14px; cursor: pointer; transition: all 0.3s ease; position: relative; } border-bottom: 1px solid #1a8bff;
.tabs button.active { color: #fff; font-weight: bold; } }
.tabs button.active::after { content: ''; position: absolute; bottom: -2px; left: 0; width: 100%; height: 2px; background-color: #3aa1f8; }
.leader-list { flex-grow: 1; overflow-y: auto; color: #fff;} .main-content {
display: flex;
flex-grow: 1;
overflow: hidden;
}
/* 左侧面板 */
.left-panel {
width: 350px;
flex-shrink: 0;
background-color: rgba(6, 45, 90, 0.3);
border-right: 1px solid #1a8bff;
display: flex;
flex-direction: column;
padding: 15px;
}
.panel-title {
font-size: 18px;
font-weight: bold;
text-align: center;
padding: 10px;
margin: 0 0 15px 0;
background: linear-gradient(to right, rgba(58,161,248,0), rgba(58,161,248,0.3), rgba(58,161,248,0));
border-top: 1px solid #3aa1f8;
border-bottom: 1px solid #3aa1f8;
}
.tabs {
display: flex;
margin-bottom: 15px;
border-bottom: 2px solid #1a5a9c;
}
.tabs button {
background: none;
border: none;
color: #a9c2e0;
padding: 8px 16px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.tabs button.active {
color: #fff;
font-weight: bold;
}
.tabs button.active::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 100%;
height: 2px;
background-color: #3aa1f8;
}
.leader-list {
flex-grow: 1;
overflow-y: auto;
}
/* 滚动条样式 */
.leader-list::-webkit-scrollbar { width: 4px; } .leader-list::-webkit-scrollbar { width: 4px; }
.leader-list::-webkit-scrollbar-track { background: transparent; } .leader-list::-webkit-scrollbar-track { background: transparent; }
.leader-list::-webkit-scrollbar-thumb { background: #3aa1f8; border-radius: 2px; } .leader-list::-webkit-scrollbar-thumb { background: #3aa1f8; border-radius: 2px; }
.leader-item { display: flex; align-items: center; padding: 10px 5px; border-bottom: 1px solid rgba(58, 161, 248, 0.2); }
.avatar { width: 50px; height: 50px; border-radius: 50%; margin-right: 15px; flex-shrink: 0; }
.info { display: flex; flex-direction: column; gap: 5px; }
.name { display: flex; align-items: baseline; gap: 8px; }
.en-name { font-size: 16px; font-weight: bold; }
.cn-name { font-size: 14px; color: #a9c2e0; }
.stats { font-size: 12px; color: #a9c2e0; display: flex; gap: 20px; }
/* 右侧面板样式 (从 PelosiGraph.vue 合并) */ .leader-item {
.right-panel { flex-grow: 1; height: 540px; position: relative; } display: flex;
.key-node-recognition { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } align-items: center;
.background-svg-wrapper { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; } padding: 10px 5px;
.content-wrapper { position: relative; z-index: 2; display: flex; flex-direction: column; height: 100%; padding: 15px 20px; box-sizing: border-box; } border-bottom: 1px solid rgba(58, 161, 248, 0.2);
.graph-title { text-align: center; font-size: 20px; font-weight: bold; color: #cce7ff; letter-spacing: 2px; margin: 0 0 5px 0; text-shadow: 0 0 5px rgba(58, 161, 248, 0.5); } }
.chart-container { flex-grow: 1; width: 100%; height: calc(100% - 100px); }
.timeline-container { width: 100%; height: 50px; display: flex; align-items: center; justify-content: space-between; padding: 0 10px; box-sizing: border-box; } .avatar {
width: 50px;
height: 50px;
border-radius: 50%;
margin-right: 15px;
flex-shrink: 0;
}
.info {
display: flex;
flex-direction: column;
gap: 5px;
}
.name {
display: flex;
align-items: baseline;
gap: 8px;
}
.en-name {
font-size: 16px;
font-weight: bold;
}
.cn-name {
font-size: 14px;
color: #a9c2e0;
}
.stats {
font-size: 12px;
color: #a9c2e0;
display: flex;
gap: 20px;
}
/* 右侧面板 */
.right-panel {
flex-grow: 1;
position: relative;
}
.key-node-recognition {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.background-svg-wrapper {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
z-index: 1;
}
.content-wrapper {
position: relative; z-index: 2; display: flex; flex-direction: column;
height: 100%; padding: 15px 20px; box-sizing: border-box;
}
.main-title {
text-align: center; font-size: 20px; font-weight: bold; color: #cce7ff;
letter-spacing: 2px; margin: 0 0 5px 0; text-shadow: 0 0 5px rgba(58, 161, 248, 0.5);
}
.chart-container {
flex-grow: 1; width: 100%;
height: calc(100% - 100px);
}
.timeline-container, .time-label, .timeline-track, .timeline-point-wrapper, .timeline-point, .active-pin {
/* 时间轴样式与上一版完全相同 */
box-sizing: border-box;
}
.timeline-container { width: 100%; height: 50px; display: flex; align-items: center; justify-content: space-between; padding: 0 10px;}
.time-label { font-size: 12px; color: #a9c2e0; } .time-label { font-size: 12px; color: #a9c2e0; }
.timeline-track { flex-grow: 1; height: 4px; background: linear-gradient(90deg, #1b62a9, #3aa1f8, #1b62a9); margin: 0 15px; position: relative; display: flex; justify-content: space-between; align-items: center; } .timeline-track { flex-grow: 1; height: 4px; background: linear-gradient(90deg, #1b62a9, #3aa1f8, #1b62a9); margin: 0 15px; position: relative; display: flex; justify-content: space-between; align-items: center;}
.timeline-point-wrapper { display: flex; align-items: center; justify-content: center; height: 20px; cursor: pointer; } .timeline-point-wrapper { display: flex; align-items: center; justify-content: center; height: 20px; cursor: pointer;}
.timeline-point { width: 10px; height: 10px; background-color: #8dc5ff; border-radius: 50%; border: 1px solid #cce7ff; transition: transform 0.3s ease; position: relative; } .timeline-point { width: 10px; height: 10px; background-color: #8dc5ff; border-radius: 50%; border: 1px solid #cce7ff; transition: transform 0.3s ease; position: relative;}
.timeline-point-wrapper:hover .timeline-point { transform: scale(1.5); } .timeline-point-wrapper:hover .timeline-point { transform: scale(1.5); }
.timeline-point.active { background-color: #ffc94d; border-color: #fff; transform: scale(1.3); } .timeline-point.active { background-color: #ffc94d; border-color: #fff; transform: scale(1.3); }
.active-pin { width: 20px; height: 24px; background-image: url('data:image/svg+xml;utf8,<svg width="20" height="24" viewBox="0 0 20 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10 24L15 14H5L10 24Z" fill="%23FFC94D"/><path d="M1.5 8C1.5 4.41015 4.41015 1.5 8 1.5H12C15.5899 1.5 18.5 4.41015 18.5 8V11C18.5 12.6569 17.1569 14 15.5 14H4.5C2.84315 14 1.5 12.6569 1.5 11V8Z" fill="%23FFC94D" stroke="white"/></svg>'); position: absolute; bottom: 5px; left: 50%; transform: translateX(-50%); } .active-pin { width: 20px; height: 24px; background-image: url('data:image/svg+xml;utf8,<svg width="20" height="24" viewBox="0 0 20 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10 24L15 14H5L10 24Z" fill="%23FFC94D"/><path d="M1.5 8C1.5 4.41015 4.41015 1.5 8 1.5H12C15.5899 1.5 18.5 4.41015 18.5 8V11C18.5 12.6569 17.1569 14 15.5 14H4.5C2.84315 14 1.5 12.6569 1.5 11V8Z" fill="%23FFC94D" stroke="white"/></svg>'); position: absolute; bottom: 5px; left: 50%; transform: translateX(-50%);}
</style> </style>