提交 40ce6cb6 authored 作者: 刘宇琪's avatar 刘宇琪

提交master分支的改动:创新主题详情页 词云组件更新 刘宇琪

上级 bd4bf45d
......@@ -42,8 +42,8 @@
<el-option v-for="item in num" :key="item" :label="item" :value="item" />
</el-select>
</template>
<div class="echarts" id="wordCloudChart">
</div>
<div class="echarts" ><WordCloudChart v-if="wordLoading" :data="characterView"/></div>
</AnalysisBox>
<AnalysisBox title=" 金钱来源" width="1064px" height="512px" :show-all-btn="false" class="left-center">
......@@ -305,6 +305,7 @@ import HistoricalProposal from "./components/historicalProposal/components/BillT
import PotentialNews from './components/historicalProposal/components/PotentialNews.vue'
import getWordCloudChart from "../../utils/worldCloudChart";
import setChart from "@/utils/setChart";
import WordCloudChart from "@/components/base/WordCloundChart/index.vue"
import {
getCharacterGlobalInfo,
getCharacterBasicInfo,
......@@ -434,7 +435,7 @@ const getCharacterBasicInfoFn = async () => {
console.error(error);
}
};
const wordLoading=ref(false)
// 获取人物观点
const characterView = ref({});
const getCharacterViewFn = async () => {
......@@ -444,6 +445,7 @@ const getCharacterViewFn = async () => {
if (numActive.value !== '全部') {
params.year = numActive.value;
}
wordLoading.value=false
try {
const res = await getCharacterView(params);
if (res.code === 200) {
......@@ -456,6 +458,7 @@ const getCharacterViewFn = async () => {
};
});
}
wordLoading.value=true
}
} catch (error) {
......@@ -465,18 +468,15 @@ const getCharacterViewFn = async () => {
const handleCharacterView = async () => {
await getCharacterViewFn();
const wordCloudChart = getWordCloudChart(characterView.value);
setChart(wordCloudChart, "wordCloudChart");
// const wordCloudChart = getWordCloudChart(characterView.value);
// setChart(wordCloudChart, "wordCloudChart");
};
const handleChangeYear = () => {
characterView.value = []
handleCharacterView()
}
}
const yearList = ref([
{
label: "全部",
......
......@@ -24,16 +24,16 @@
<div class="info-content" v-show="infoActive === '人物详情'">
<div class="left">
<AnalysisBox title="核心观点" width="1064px" height="300px" :show-all-btn="false" class="left-top">
<template #headerBtn>
<template #header-btn>
<el-select v-model="numActive" class="tab-select" :teleported="true" @change="handleChangeYear">
<el-option label="全部" value="全部" />
<el-option v-for="item in num" :key="item" :label="item" :value="item" />
</el-select>
</template>
<div class="echarts" id="wordCloudChart"></div>
<div class="echarts" > <WordCloudChart v-if="wordLoading" :data="characterView" /></div>
</AnalysisBox>
<AnalysisBox title="最新动态" width="1064px" height="1617px" :show-all-btn="false" class="left-bottom">
<template #headerBtn>
<template #header-btn>
<div class="input"><input type="checkbox" v-model="isChecked" @change="handleChange" />只看涉华动态
</div>
</template>
......@@ -168,6 +168,7 @@ import CharacterRelationships from "./components/characterRelationships/index.vu
import RelevantSituation from "./components/relevantSituation/index.vue";
import HistoricalProposal from "./components/historicalProposal/components/NewsTracker.vue";
import getWordCloudChart from "../../utils/worldCloudChart";
import WordCloudChart from "@/components/base/WordCloundChart/index.vue"
import {
getCharacterGlobalInfo,
getCharacterBasicInfo,
......@@ -249,7 +250,7 @@ const getCharacterBasicInfoFn = async () => {
console.error(error);
}
};
const wordLoading=ref(false)
const characterView = ref({});
const getCharacterViewFn = async () => {
const params = {
......@@ -259,6 +260,7 @@ const getCharacterViewFn = async () => {
if(numActive.value!='全部'){
params['year']=numActive.value
}
wordLoading.value=false
try {
const res = await getCharacterView(params);
if (res.code === 200) {
......@@ -271,6 +273,7 @@ const getCharacterViewFn = async () => {
};
});
}
wordLoading.value=true
}
} catch (error) {
......@@ -280,8 +283,8 @@ const getCharacterViewFn = async () => {
const handleCharacterView = async () => {
await getCharacterViewFn();
const wordCloudChart = getWordCloudChart(characterView.value);
setChart(wordCloudChart, "wordCloudChart");
// const wordCloudChart = getWordCloudChart(characterView.value);
// setChart(wordCloudChart, "wordCloudChart");
};
const handleChangeYear = (value) => {
......@@ -415,7 +418,7 @@ onMounted(() => {
const info = ref(["人物详情", "成果报告", "人物关系" ]);
// const info = ref(["人物详情", "成果报告", "人物关系", "相关情况"]);
const infoActive = ref("人物详情");
const num = ref(["2025", "2024", "2023", "2022", "2021", "2020"]);
const num = ref(["2026","2025", "2024", "2023", "2022", "2021" ]);
const numActive = ref("全部");
......
......@@ -175,10 +175,43 @@
</div>
</div>
</div> -->
<NewsList :newsList="newsList" @item-click="handleNewsInfoClick" @more-click="handleToMoreNews"
<NewsList style="margin-right:16px" :newsList="newsList" @item-click="handleNewsInfoClick" @more-click="handleToMoreNews"
img="newsImage" title="newsTitle" content="newsContent" />
<MessageBubble :messageList="messageList" imageUrl="personImage" @more-click="handleToSocialDetail"
name="personName" content="remarks" source="orgName" />
<div class="social-media-wrapper">
<!-- 翻页按钮 - 绝对定位到右上角 -->
<div class="pagination-btns">
<button
class="page-btn"
:class="{ disabled: currentPage <= 1 }"
:disabled="currentPage <= 1"
@click="prevPage"
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M10 12L6 8L10 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<button
class="page-btn"
:class="{ disabled: currentPage >= totalPages }"
:disabled="currentPage >= totalPages"
@click="nextPage"
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M6 4L10 8L6 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div>
<!-- MessageBubble 组件 -->
<MessageBubble
:messageList="paginatedList"
imageUrl="personImage"
@more-click="handleToSocialDetail"
name="personName"
content="remarks"
source="orgName"
/>
</div>
<!-- <div class="box4">
<div class="box4-header">
<div class="header-icon">
......@@ -356,7 +389,31 @@ const handleCurrentChange = page => {
currentPage.value = page;
handleFindListBySubjectTypeId();
};
// 计算总页数
const totalPages = computed(() => {
return Math.ceil(messageList.value.length / pageSize.value) || 1;
});
// 当前页的数据
const paginatedList = computed(() => {
const start = (currentPage.value - 1) * pageSize.value;
const end = start + pageSize.value;
return messageList.value.slice(start, end);
});
// 上一页
const prevPage = () => {
if (currentPage.value > 1) {
currentPage.value--;
}
};
// 下一页
const nextPage = () => {
if (currentPage.value < totalPages.value) {
currentPage.value++;
}
};
// 地理分布
const distributionList = ref([
{
......@@ -568,38 +625,22 @@ const handleGetNews = async () => {
// 社交媒体
const messageList = ref([
{
img: Message1,
name: "贾森·史密斯",
time: "15:23 · 发布于真实社交",
content: `埃隆·马斯克在强力支持我竞选总统之前,早就知道我强烈反对‘电动汽车强制令’。这太荒谬了,这一直是我竞选活动的主要部分。电动汽车没问题,但不应该强迫每个人都拥有一辆。埃隆获得的补贴可能远远超过历史上任何一个人。如果没有补贴,埃隆可能不得不关门大吉,回到南非老家。`
},
{
img: Message2,
name: "詹姆斯·布莱尔",
time: "14:49 · 发布于X",
content: `如果这个疯狂的支出法案获得通过,‘美国党’将在第二天成立。`
},
{
img: Message3,
name: "塞巴斯蒂安·马拉比",
time: "11:05 · 发布于X",
content: `提出特朗普政府的AI政策强调技术开放与快速应用,但可能以牺牲安全防范为代价,开启了“潘多拉魔盒”。`
}
]);
const handleGetSocialMediaInfo = async () => {
try {
const res = await getSocialMediaInfo();
console.log("社交媒体", res);
if (res.code === 200 && res.data) {
messageList.value = res.data.map(item => ({
...item,
time: item.time.replace(/T/, " "),
}));
}
} catch (error) {
console.error("获取社交媒体error", error);
}
try {
const res = await getSocialMediaInfo();
console.log("社交媒体", res);
if (res.code === 200 && res.data) {
messageList.value = res.data.map(item => ({
...item,
time: item.time.replace(/T/, " "),
}));
currentPage.value = 1;
}
} catch (error) {
console.error("获取社交媒体error", error);
}
};
// 政令涉及领域
......@@ -2382,4 +2423,45 @@ onMounted(async () => {
:deep(.table-row) {
height: 64px;
}
.social-media-wrapper {
position: relative;
display: inline-block;
}
.pagination-btns {
position: absolute;
top: 10px;
right: 20px;
display: flex;
align-items: center;
gap: 8px;
z-index: 10;
}
.page-btn {
width: 28px;
height: 28px;
border: 1px solid #055fc2;
border-radius: 4px;
background: #fff;
color: #055fc2;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
padding: 0;
&:hover:not(.disabled) {
background: #055fc2;
color: #fff;
}
&.disabled {
cursor: not-allowed;
opacity: 0.4;
border-color: #c0c4cc;
color: #c0c4cc;
}
}
</style>
\ No newline at end of file
<template>
<div class="chart-summary">
<svg class="summary-icon" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="10" cy="10" r="9" stroke="#055FC2" stroke-width="2" fill="none"/>
<path d="M7 10L9 12L13 8" stroke="#055FC2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<p class="summary-text">{{ text }}</p>
<svg class="arrow-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"
@click="$emit('more')">
<path d="M9 18L15 12L9 6" stroke="#055FC2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
</template>
<script setup>
defineProps({
text: {
type: String,
required: true
}
})
defineEmits(['more'])
</script>
<style lang="scss" scoped>
.chart-summary {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px 16px;
margin: 0 16px 16px;
background: #F6FAFF;
border: 1px solid #E7F3FF;
border-radius: 4px;
.summary-icon {
flex-shrink: 0;
margin-top: 2px;
}
.summary-text {
flex: 1;
font-size: 16px;
line-height: 24px;
color: #055FC2;
margin: 0;
}
.arrow-icon {
flex-shrink: 0;
cursor: pointer;
transition: opacity 0.2s;
&:hover {
opacity: 0.7;
}
}
}
</style>
<template>
<div class="cases-timeline">
<div class="timeline-line-row">
<div class="top-line">
<div class="top-line1"></div>
</div>
</div>
<div class="cases-carousel">
<LeftBtn @click="prevCases" />
<div class="cases-items">
<div v-for="(item, index) in visibleCases" :key="index" class="case-item">
<div class="case-date">{{ item.date }}</div>
<div class="node-dot"></div>
<div class="connector-line"></div>
<div class="case-card">
<div class="case-tag" :class="item.tagType">{{ item.tag }}</div>
<div class="case-partner">合作主体:{{ item.partner }}</div>
<div class="case-title">{{ item.title }}</div>
</div>
</div>
</div>
<RightBtn @click="nextCases" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import LeftBtn from '@/components/base/PageBtn/LeftBtn.vue'
import RightBtn from '@/components/base/PageBtn/RightBtn.vue'
interface CaseItem {
date: string
tag: string
tagType: 'project' | 'patent' | 'paper'
partner: string
title: string
}
const props = withDefaults(defineProps<{
data?: CaseItem[]
pageSize?: number
}>(), {
pageSize: 4
})
const defaultData: CaseItem[] = [
{ date: '2025年3月12日', tag: '项目合作', tagType: 'project', partner: '清华大学', title: '"气候变化、碳中和与能源智联"(CNEST)研讨会' },
{ date: '2025年5月7日', tag: '专利合作', tagType: 'patent', partner: '北京航空航天大学', title: '北航-哈佛 Wood 实验室联合研制鲤鱼仿生吸盘机器人' },
{ date: '2025年6月23日', tag: '论文合作', tagType: 'paper', partner: '山西省古建筑与彩塑壁画保护研究院', title: '山西古建院-哈佛中国艺术实验室合作' },
{ date: '2025年10月19日', tag: '项目合作', tagType: 'project', partner: '复旦大学、四川大学', title: '哈佛中国健康伙伴计划多机构合作' },
{ date: '2024年12月5日', tag: '论文合作', tagType: 'paper', partner: '北京大学', title: '人工智能在医疗诊断中的应用研究' },
{ date: '2024年9月18日', tag: '专利合作', tagType: 'patent', partner: '中国科学院', title: '新型纳米材料在能源存储中的应用专利' },
{ date: '2024年7月22日', tag: '项目合作', tagType: 'project', partner: '上海交通大学', title: '智能制造与工业4.0联合研究项目' },
{ date: '2024年4月10日', tag: '论文合作', tagType: 'paper', partner: '浙江大学', title: '量子计算在密码学中的突破性进展' },
{ date: '2024年2月28日', tag: '专利合作', tagType: 'patent', partner: '南京大学', title: '生物降解塑料新型合成技术专利' },
{ date: '2023年11月15日', tag: '项目合作', tagType: 'project', partner: '武汉大学', title: '长江流域生态环境保护联合研究' }
]
const casesData = computed(() => props.data && props.data.length > 0 ? props.data : defaultData)
const currentPage = ref(0)
const visibleCases = computed(() => {
const start = currentPage.value * props.pageSize
return casesData.value.slice(start, start + props.pageSize)
})
const totalPages = computed(() => Math.ceil(casesData.value.length / props.pageSize))
// 上一页
const prevCases = () => {
if (currentPage.value > 0) {
currentPage.value--
}
}
// 下一页
const nextCases = () => {
if (currentPage.value < totalPages.value - 1) {
currentPage.value++
}
}
</script>
<style lang="scss" scoped>
.cases-timeline {
padding: 24px 16px;
position: relative;
.timeline-line-row {
position: absolute;
left: 48px;
right: 48px;
top: calc(24px + 22px + 8px + 11px - 4px);
height: 8px;
z-index: 0;
.top-line {
width: 100%;
height: 8px;
background: url('../assets/top-line-icon.png') repeat-x;
background-size: auto 8px;
.top-line1 {
display: none;
}
}
}
.cases-carousel {
display: flex;
align-items: center;
gap: 60px;
.cases-items {
flex: 1;
display: flex;
gap: 60px;
overflow: hidden;
margin-top: 9px;
.case-item {
flex-shrink: 0;
width: 273px;
display: flex;
flex-direction: column;
align-items: flex-start;
position: relative;
z-index: 1;
.case-date {
font-size: 14px;
font-weight: 700;
color: #055FC2;
margin-bottom: 8px;
padding-left: 2px;
}
.node-dot {
width: 11px;
height: 11px;
border-radius: 50%;
background: #fff;
border: 4px solid #1677FF;
box-sizing: border-box;
position: relative;
z-index: 2;
}
.connector-line {
width: 1px;
height: 20px;
background: #1677FF;
margin-left: 5px;
}
.case-card {
width: 273px;
min-height: 210px;
padding: 16px;
background: #FFFFFF;
border: 1px solid #E5E7EB;
border-radius: 4px;
display: flex;
flex-direction: column;
gap: 8px;
.case-tag {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
width: fit-content;
&.project {
background: #FFF1F0;
color: #F5222D;
border: 1px solid #FFA39E;
}
&.patent {
background: #FFFBE6;
color: #FAAD14;
border: 1px solid #FFE58F;
}
&.paper {
background: #F0F5FF;
color: #2F54EB;
border: 1px solid #ADC6FF;
}
}
.case-partner {
font-size: 14px;
color: #5F656C;
}
.case-title {
font-size: 16px;
font-weight: 700;
color: #3B414B;
line-height: 24px;
}
}
}
}
}
}
</style>
<template>
<div class="history-timeline-wrapper">
<div class="timeline-list">
<div v-for="(event, index) in events" :key="index" class="timeline-item">
<!-- 左侧:实心三角箭头 + 年份,上下布局 -->
<div class="item-label">
<!-- 实心右三角 SVG,与设计图一致 -->
<svg class="arrow-icon" width="12" height="14" viewBox="0 0 12 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0L12 7L0 14V0Z" fill="#F97316"/>
</svg>
<span class="year-text">{{ event.year }}</span>
</div>
<!-- 右侧:标题 + 描述,上下布局 -->
<div class="item-content">
<div class="item-title">{{ event.title }}</div>
<div class="item-desc" v-if="event.description">{{ event.description }}</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
defineProps({
events: {
type: Array,
default: () => [],
// 每项结构:{ year: string, title: string, description?: string }
}
})
</script>
<style lang="scss" scoped>
.history-timeline-wrapper {
width: 100%;
padding: 16px 24px;
.timeline-list {
display: flex;
flex-direction: column;
gap: 20px;
.timeline-item {
display: flex;
flex-direction: column;
gap: 8px;
// 第一行:实心三角 + 年份横排
.item-label {
display: flex;
align-items: center;
gap: 8px;
.arrow-icon {
flex-shrink: 0;
margin-top: 1px;
}
.year-text {
font-size: 18px;
font-weight: 700;
font-family: 'Source Han Sans CN', 'Microsoft YaHei', sans-serif;
color: rgb(5, 95, 194);
line-height: 24px;
}
}
// 第二行起:标题 + 描述,左侧缩进与年份对齐
.item-content {
padding-left: 20px; // 12px箭头 + 8px gap
.item-title {
font-size: 16px;
font-weight: 700;
font-family: 'Source Han Sans CN', 'Microsoft YaHei', sans-serif;
color: rgb(59, 65, 75);
line-height: 24px;
}
.item-desc {
font-size: 14px;
font-weight: 400;
font-family: 'Source Han Sans CN', 'Microsoft YaHei', sans-serif;
color: rgb(95, 101, 108);
line-height: 22px;
margin-top: 6px;
}
}
}
}
}
</style>
<template>
<div class="semi-donut-chart-container">
<div ref="chartRef" class="chart"></div>
<div class="chart-legend">
<div v-for="(item, index) in legendData" :key="index" class="legend-item">
<span class="legend-dot" :style="{ background: colors[index] }"></span>
<span class="legend-label">{{ item.name }}</span>
<span class="legend-value">{{ item.value }}%</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, shallowRef, computed } from 'vue'
import * as echarts from 'echarts'
import { semiDonutOption } from './bindEcharts'
const props = defineProps<{
data: {
names: string[]
values: number[]
}
}>()
const chartRef = ref<HTMLDivElement>()
const chartInstance = shallowRef<echarts.ECharts>()
const colors = ['rgba(105, 177, 255, 1)', 'rgba(255, 192, 105, 1)', 'rgba(135, 232, 222, 1)']
const legendData = computed(() => {
return props.data.names.map((name, index) => ({
name,
value: props.data.values[index]
}))
})
function initChart() {
if (!chartRef.value) return
chartInstance.value = echarts.init(chartRef.value)
updateChart()
}
function updateChart() {
if (!chartInstance.value) return
if (!props.data?.names?.length) return
const option = semiDonutOption(props.data)
chartInstance.value.setOption(option)
}
function handleResize() {
chartInstance.value?.resize()
}
onMounted(() => {
initChart()
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
chartInstance.value?.dispose()
})
watch(() => props.data, updateChart, { deep: true })
</script>
<style scoped>
.semi-donut-chart-container {
display: flex;
align-items: center;
justify-content: center;
gap: 40px;
padding: 24px 40px;
height: 320px;
.chart {
width: 240px;
height: 240px;
flex-shrink: 0;
}
.chart-legend {
display: flex;
flex-direction: column;
gap: 16px;
flex-shrink: 0;
.legend-item {
display: flex;
align-items: center;
gap: 8px;
white-space: nowrap;
.legend-dot {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
}
.legend-label {
font-size: 14px;
color: #3B414B;
}
.legend-value {
font-size: 14px;
font-weight: 700;
color: #3B414B;
margin-left: 8px;
}
}
}
}
</style>
// echarts 图表配置
export const pieOption = (data: { names: string[]; values: number[]; total: number }) => {
const seriesData = data.names.map((name, index) => ({
value: data.values[index],
name: name
}))
return {
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 'right',
data: data.names
},
series: [
{
name: '合作类型',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: [8, 8, 0, 0]
},
label: {
show: false
},
emphasis: {
label: {
show: true
}
},
labelLine: {
show: false
},
data: seriesData,
color: ['rgba(105, 177, 255, 1)', 'rgba(255, 192, 105, 1)', 'rgba(135, 232, 222, 1)']
}
]
}
}
// 完整环形饼图配置
export const semiDonutOption = (data: { names: string[]; values: number[] }) => {
const seriesData = data.names.map((name, index) => ({
value: data.values[index],
name: name
}))
return {
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
show: false
},
series: [
{
name: '合作类型',
type: 'pie',
center: ['50%', '50%'],
radius: ['55%', '80%'],
avoidLabelOverlap: false,
itemStyle: {
borderWidth: 2,
borderColor: '#fff'
},
label: {
show: false
},
emphasis: {
label: {
show: false
}
},
labelLine: {
show: false
},
data: seriesData,
color: ['rgba(105, 177, 255, 1)', 'rgba(255, 192, 105, 1)', 'rgba(135, 232, 222, 1)']
}
]
}
}
<template>
<div class="timeline-wrapper">
<button class="arrow left" :disabled="index <= 0" @click="index--">
{{ '<' }} </button>
<div class="timeline-box">
<div class="line"></div>
<div v-for="(item, i) in showList" :key="item[idKey]" class="node" :style="leftOffset(i)">
<div class="node" :style="leftOffset(i)">
<div class="time">
{{ item.cooperateDate }}
</div>
<!-- 圆环 -->
<div class="dot" :class="linePos(i, flip)"></div>
<!-- 卡片:放到线右侧 -->
<div class="card" :class="[cardPos(i, flip), 'right-side']" @click="$emit('click-card', item)">
<div class="tag">
{{ item.typeName }}
</div>
<div class="title">
{{ '合作主体:' + item.subjectlist.join(',') }}
<img class="item-header-icon" src="@/assets/images/icon/copy.png" style="cursor: pointer;" />
</div>
<div class="content">
{{ item.cooperateName }}
</div>
</div>
</div>
</div>
</div>
<button class="arrow right" :disabled="index >= total - 5" @click="index++">
{{ '>' }}
</button>
</div>
</template>
<script>
export default {
name: 'TimeLine',
props: {
data: { // 父组件传入的数组
type: Array,
required: true
},
textKey: { // 要显示的文本字段
type: String,
default: 'text'
},
idKey: { // 唯一标识字段
type: String,
default: 'id'
}
},
data() {
return { index: 0 };
},
computed: {
total() {
return this.data.length;
},
showList() {
return this.data.slice(this.index, this.index + 5);
},
flip() { return this.index % 2 === 1; }
},
methods: {
leftOffset(i) {
return { left: `${(i * 100) / 5}%` };
},
/* 上下层翻转(保留上次逻辑) */
cardPos(i, flip = false) {
// return (i % 2) ^ flip ? 'down' : 'up';
return 'down';
},
/* 线延伸方向 = 卡片出现方向 */
linePos(i, flip = false) {
return this.cardPos(i, flip); // up / down
}
}
};
</script>
<style scoped>
/* 样式与之前完全一致,不再重复 */
.timeline-wrapper {
display: flex;
align-items: center;
width: 100%;
position: relative;
padding: 0 40px;
}
.arrow {
position: absolute;
top: 45px;
/* 左右切换按钮 */
width: 24px;
height: 48px;
font-size: 24px;
border-color: #E7F3FF;
border: 0;
background: #E7F3FF;
cursor: pointer;
z-index: 10;
color: #3E84D1;
}
.arrow:disabled {
color: #c0c4cc;
cursor: not-allowed;
}
.left {
left: 0;
border-radius: 0px 4px 4px 0px;
}
.right {
right: 0;
border-radius: 4px 0px 0px 4px;
}
.timeline-box {
flex: 1;
height: 100%;
position: relative;
}
.line {
position: absolute;
left: 0;
right: 0;
top: 50%;
height: 6px;
background-image: url("@/assets/images/bg/timeLine-bg.jpg");
transform: translateY(-50%);
background-size: auto 100%;
}
.node {
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
z-index: 2;
}
/* ===== 圆环基础 ===== */
.dot {
top: -30px;
width: 14px;
height: 14px;
border-radius: 50%;
border: 3px solid #409eff;
background: #fff;
position: relative;
margin: 0 auto;
z-index: 2;
}
/* ===== 延伸线 ===== */
.dot::after {
content: '';
position: absolute;
left: 50%;
transform: translateX(-1px);
/* 居中细线 */
width: 1px;
background: #409eff;
}
/* 向上节点:线往下伸 */
.dot.up::after {
bottom: 100%;
height: 165px;
/* 圆环底部 → 卡片顶 */
}
/* 向下节点:线往上伸 */
.dot.down::after {
top: 100%;
height: 165px;
}
.card {
position: absolute;
padding: 8px 12px;
text-align: left;
cursor: pointer;
font-size: 14px;
/* 容器 299 */
width: 273px;
height: 210px;
border-radius: 4px;
/* 业务系统/模块阴影 */
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background: var(--主色/白色主色, rgba(255, 255, 255, 1));
}
.time {
width: 125px;
color: rgba(5, 95, 194, 1);
font-family: Microsoft YaHei;
font-size: 20px;
font-weight: 700;
line-height: 26px;
letter-spacing: 0px;
text-align: justify;
margin-bottom: 35px;
margin-left: 125px;
}
.title {
color: rgba(59, 65, 75, 1);
height: 85px;
font-family: Microsoft YaHei;
font-style: Regular;
font-size: 18px;
font-weight: 400;
line-height: 24px;
letter-spacing: 0px;
text-align: left;
}
.content {
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-style: Bold;
font-size: 18px;
font-weight: 700;
line-height: 24px;
letter-spacing: 0px;
text-align: left;
}
.card.up {
bottom: 20px;
}
.card.down {
top: 55px;
left: 115px;
}
</style>
\ No newline at end of file
差异被折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论