提交 2a5ddbf3 authored 作者: 朱政's avatar 朱政

Merge branch 'pre' into zz-dev

流水线 #426 已通过 于阶段
in 1 分 31 秒
......@@ -2,42 +2,42 @@ import request from "@/api/request.js";
// 规则限制-首页统计接口
export function getStatCount() {
return request({
method: 'GET',
url: `/api/ruleLimitInfo/statCount`
})
return request({
method: "GET",
url: `/api/ruleLimitInfo/statCount`
});
}
// 规则限制-查询最新动态接口
export function getLatestUpdates() {
return request({
method: 'GET',
url: `/api/ruleLimitInfo/getLatestUpdates`
})
return request({
method: "GET",
url: `/api/ruleLimitInfo/getLatestUpdates`
});
}
// 规则限制-风险信号
export function getRiskSignal(params) {
return request({
method: 'GET',
url: `/api/commonFeature/riskSignal/${params}`
})
return request({
method: "GET",
url: `/api/commonFeature/riskSignal/${params}`
});
}
// 规则限制-查询新闻资讯
export function getNews(params) {
return request({
method: 'GET',
url: `/api/commonFeature/news/${params}`
})
return request({
method: "GET",
url: `/api/commonFeature/news/${params}`
});
}
// 规则限制-查询社交媒体
export function getRemarks(params) {
return request({
method: 'GET',
url: `/api/commonFeature/remarks/${params}`
})
return request({
method: "GET",
url: `/api/commonFeature/remarks/${params}`
});
}
// 规则限制-限制领域分布情况
......@@ -47,11 +47,11 @@ export function getRemarks(params) {
* @header token
*/
export function getAreaDistribution(params) {
return request({
method: 'GET',
url: `/api/ruleLimitInfo/getAreaDistribution`,
params
})
return request({
method: "GET",
url: `/api/ruleLimitInfo/getAreaDistribution`,
params
});
}
// 规则限制-受限实体数量变化趋势
......@@ -62,11 +62,11 @@ export function getAreaDistribution(params) {
* @header token
*/
export function getEntityChangeTrend(params) {
return request({
method: 'GET',
url: `/api/ruleLimitInfo/getEntityChangeTrend`,
params
})
return request({
method: "GET",
url: `/api/ruleLimitInfo/getEntityChangeTrend`,
params
});
}
// 规则限制-规则限制政令列表查询接口
......@@ -82,11 +82,11 @@ export function getEntityChangeTrend(params) {
* @header token
*/
export function getRuleLimitList(params) {
return request({
method: 'GET',
url: `/api/ruleLimitInfo/getRuleLimitList`,
params
})
return request({
method: "GET",
url: `/api/ruleLimitInfo/getRuleLimitList`,
params
});
}
// 规则限制-排华科技联盟列表接口
......@@ -103,55 +103,52 @@ export function getRuleLimitList(params) {
* @header token
*/
export function getACTAList(params) {
return request({
method: 'GET',
url: `/api/ruleLimitInfo/getACTAList`,
params
})
return request({
method: "GET",
url: `/api/ruleLimitInfo/getACTAList`,
params
});
}
export function getAcTAAllcountry() {
return request({
method: 'GET',
url: `/api/ruleLimitInfo/getACTAAllCountry/`
})
return request({
method: "GET",
url: `/api/ruleLimitInfo/getACTAAllCountry/`
});
}
// 规则限制-规则限制基本详情
export function getSanctionOverview(params) {
return request({
method: 'GET',
url: `/api/ruleLimitInfo/getSanctionOverview/${params}`
})
return request({
method: "GET",
url: `/api/ruleLimitInfo/getSanctionOverview/${params}`
});
}
// 规则限制-背景分析
export function getBackGround(params) {
return request({
method: 'GET',
url: `/api/ruleLimitInfo/getBackGround/${params}`
})
return request({
method: "GET",
url: `/api/ruleLimitInfo/getBackGround/${params}`
});
}
// 规则限制-限制条款
export function getLimitClause(params) {
return request({
method: 'GET',
url: `/api/ruleLimitInfo/getLimitClause/${params}`
})
return request({
method: "GET",
url: `/api/ruleLimitInfo/getLimitClause/${params}`
});
}
// 规则限制-相关举措
export function getRelevantMeasures(params) {
return request({
method: 'GET',
url: `/api/ruleLimitInfo/getRelevantMeasures/${params}`
})
return request({
method: "GET",
url: `/api/ruleLimitInfo/getRelevantMeasures/${params}`
});
}
// // 实体清单-制裁概况-获取发布机构机构动态
// /**
// * @param {Object} data
......@@ -167,8 +164,26 @@ export function getRelevantMeasures(params) {
// }
export function getRuleOrg(params) {
return request({
method: 'POST',
url: `/api/organization/relate/ruleOrg`, data: params
})
}
\ No newline at end of file
return request({
method: "POST",
url: `/api/organization/relate/ruleOrg`,
data: params
});
}
// 排华联盟-联盟简介
export function getUnionIntroduction(unionId) {
return request({
method: "GET",
url: `/api/ruleLimitInfo/getUnionIntroduction/${unionId}`
});
}
// 排华联盟-联盟动态
export function getUnionDynamicList(params) {
return request({
method: "GET",
url: `/api/ruleLimitInfo/getUnionDynamicList/${params.unionId}`,
params: { currentPage: params.currentPage, pageSize: params.pageSize }
});
}
......@@ -499,7 +499,8 @@ onMounted(() => {
<style lang="scss" scoped>
.module-header-wrapper {
width: 100%;
// height: 64px;
position: relative;
z-index: 101;
border-bottom: 1px solid rgba(234, 236, 238, 1);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background: linear-gradient(180deg, rgba(246, 250, 255, 0.8) 0%, rgba(255, 255, 255, 0.8) 100%);
......
// 规则限制
const RuleRestriction = () => import('@/views/ruleRestriction/index.vue')
const RuleRestrictionDetail = () => import('@/views/ruleRestriction/detail/index.vue')
const RuleRestrictionsAlliance = () => import('@/views/ruleRestriction/alliance/index.vue')
const ruleRestrictionsRoutes = [
// 规则限制
{
path: "/ruleRestrictions",
name: "RuleRestrictions",
component: RuleRestriction,
meta: {
title: "规则限制概览",
isShowHeader: true
}
},
// 规则限制详情
{
path: "/ruleRestrictions/detail",
name: "RuleRestrictionsDetail",
component: RuleRestrictionDetail,
meta: {
title: "规则限制详情",
dynamicTitle: true
}
}, {
path: "/ruleRestrictions/alliance",
name: "RuleRestrictionsAlliance",
component: RuleRestrictionsAlliance,
meta: {
title: "规则限制联盟详情",
dynamicTitle: true
}
},
const RuleRestriction = () => import("@/views/ruleRestriction/index.vue");
const RuleRestrictionDetail = () => import("@/views/ruleRestriction/detail/index.vue");
const RuleRestrictionsAlliance = () => import("@/views/ruleRestriction/alliance/index.vue");
]
const ruleRestrictionsRoutes = [
// 规则限制
{
path: "/ruleRestrictions",
name: "RuleRestrictions",
component: RuleRestriction,
meta: {
title: "规则限制概览",
isShowHeader: true
}
},
// 规则限制详情
{
path: "/ruleRestrictions/detail",
name: "RuleRestrictionsDetail",
component: RuleRestrictionDetail,
meta: {
title: "规则限制详情",
dynamicTitle: true
}
},
{
path: "/ruleRestrictions/alliance",
name: "RuleRestrictionsAlliance",
component: RuleRestrictionsAlliance,
meta: {
title: "规则限制联盟详情",
isShowHeader: true,
dynamicTitle: true
}
}
];
export default ruleRestrictionsRoutes
export default ruleRestrictionsRoutes;
......@@ -54,41 +54,59 @@
}
/***tabs-bar左边悬浮***/
.left-float-nav-tabs,
.left-float-nav-tabs .el-tabs {
overflow: visible !important;
width: auto !important;
}
.left-float-nav-tabs {
position: relative;
.el-tabs__header.is-left {
position: absolute;
left: -140px;
top: 0px;
position: absolute !important;
left: -160px !important;
top: 0 !important;
width: auto !important;
display: flex;
flex-direction: column;
align-items: center;
overflow: visible !important;
.el-tabs__nav {
display: flex;
flex-direction: column;
gap: 16px;
align-items: center;
overflow: visible !important;
float: none !important;
}
.el-tabs__active-bar {
background-color: transparent;
right: 12px;
top: 11px;
height: 8px !important;
border-top: 6px solid transparent;
/* 顶部边框透明 */
border-bottom: 6px solid transparent;
/* 底部边框透明 */
border-left: 8px solid var(--bg-white-100);
/* 左侧边框有颜色 */
display: none;
}
}
.el-tabs__content {
display: none;
}
.el-tabs__item.is-left {
@extend .text-tip-1;
color: var(--text-primary-65-color);
height: 32px;
padding: 4px 26px 4px 28px;
border-radius: 16px 16px 16px 16px;
padding: 4px 16px;
border-radius: 16px;
justify-content: center;
color: var(--text-primary-65-color);
background-color: var(--bg-white-100);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
border: none !important;
width: auto;
}
.el-tabs__item.is-left.is-active {
color: var(--bg-white-100);
background-color: var(--color-primary-100);
color: var(--bg-white-100) !important;
background-color: var(--color-primary-100) !important;
box-shadow: none;
}
}
\ No newline at end of file
......@@ -270,6 +270,8 @@ $axis-width: 2px;
.axis-col {
width: $node-size;
flex-shrink: 0;
position: relative;
// 修复:轴线不穿刺圆点 - 改为 flex-start,圆点用负margin浮在边界上
display: flex;
flex-direction: column;
align-items: center;
......@@ -281,7 +283,10 @@ $axis-width: 2px;
min-height: 12px;
&.invisible {
background-color: transparent;
// 第一个节点上方无线:隐藏并占位,使圆点紧贴timeline起点
height: 0;
min-height: 0;
overflow: hidden;
}
}
......@@ -290,12 +295,28 @@ $axis-width: 2px;
height: $node-size;
border-radius: 50%;
flex-shrink: 0;
overflow: hidden;
overflow: visible; // 改为 visible,使负 margin 不被裁剪
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
// 第一个节点特殊处理:居顶,上方无线
&:first-child {
margin-top: 0;
}
// 后续节点使用负 margin 浮在轴线上
&:not(:first-child) {
margin-top: -$node-size / 2;
}
// 最后一个节点特殊处理:下方无线
&:last-child {
margin-bottom: 0;
}
// 非首尾节点使用负 margin
&:not(:first-child):not(:last-child) {
margin-bottom: -$node-size / 2;
}
&.highlight {
background-color: rgba(245, 34, 45, 1);
......
<template>
<div class="bill-list">
<div class="bill-list">
<div v-if="loading" class="bill-list-loading">
<div v-for="i in 3" :key="i" class="bill-list-skeleton">
<div class="skeleton-left" />
<div class="skeleton-right">
<div class="skeleton-line w30" />
<div class="skeleton-line w50" />
<div class="skeleton-divider" />
<div class="skeleton-line w60" />
<div class="skeleton-line w40" />
<div v-for="i in 3" :key="i" class="right-main-box skeleton">
<div class="bill-cover skeleton-cover" />
<div class="bill-content">
<div class="header">
<div class="skeleton-line w50" />
<div class="skeleton-line w60" />
</div>
<div class="main">
<div class="skeleton-line w30" />
<div class="skeleton-line w40" />
<div class="skeleton-line w50" />
<div class="skeleton-line w35" />
</div>
</div>
</div>
</div>
<div v-else-if="bills.length === 0" class="bill-list-empty">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#bfbfbf" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<p>暂无相关法案</p>
</div>
<template v-else>
<BillCard
v-for="bill in bills"
:key="bill.billId"
:bill="bill"
:progress-stages="progressStages"
@click="handleBillMoreClick(bill.billId)"
/>
<div v-if="total > 0" class="bill-pagination">
<span class="bill-pagination-total">{{ '\u5171' + total + '\u9879\u6cd5\u6848' }}</span>
<el-pagination
:current-page="currentPage"
:page-size="pageSize"
:total="total"
:pager-count="5"
layout="prev, pager, next"
background
@current-change="$emit('page-change', $event)"
/>
<div class="right-main-box" v-for="item in mappedBills" :key="item.billId">
<div v-if="item.riskSignal" class="risk-tag" :class="getRiskTagClass(item.riskSignal)">{{ item.riskSignal }}</div>
<div class="bill-cover">
<img class="bill-image" :src="item.imageUrl || defaultBillImage" alt="" @error="e => { e.target.src = defaultBillImage }" />
<div class="bill-id" :title="item.billId">{{ item.billId || "-" }}</div>
</div>
<div class="bill-content">
<div class="header">
<div class="title" @click="handleBillMoreClick(item.billId)" :title="item.name">{{ item.name }}</div>
<div class="en-title" :title="item.eName">{{ item.eName }}</div>
</div>
<div class="main">
<div class="item"><div class="item-left">提案人:</div><div class="item-right">{{ item.tcr }}</div></div>
<div class="item"><div class="item-left">委员会:</div><div class="item-right">{{ item.wyh }}</div></div>
<div class="item"><div class="item-left">相关领域:</div><div class="item-right1"><AreaTag v-for="(val, idx) in item.areaList" :key="`${item.billId}-${val}-${idx}`" :tagName="val" /></div></div>
<div class="item"><div class="item-left">最新动议:</div><div class="item-right"><CommonPrompt :content="item.zxdy" /></div></div>
<div class="item">
<div class="item-left">法案进展:</div>
<div class="item-right2">
<div class="tag" v-for="(val, idx) in getReversedProgress(item.progress)" :key="`${item.billId}-${val}-${idx}`" :style="{ zIndex: item.progress.length - idx }">{{ val }}</div>
</div>
</div>
</div>
</div>
</div>
<div v-if="total > 0" class="right-footer">
<div class="footer-left">{{ `共 ${total} 项` }}</div>
<div class="footer-right">
<el-pagination
@current-change="$emit('page-change', $event)"
:page-size="pageSize"
:current-page="currentPage"
background
layout="prev, pager, next"
:total="total"
/>
</div>
</div>
</template>
</div>
</template>
<script setup>
import BillCard from './BillCard.vue'
import { useRouter } from "vue-router";
const router = useRouter();
import { useRoute } from "vue-router";
const route = useRoute();
defineProps({
import { computed } from 'vue'
import { useRouter } from "vue-router"
import AreaTag from '@/components/base/AreaTag/index.vue'
import CommonPrompt from '@/views/bill/commonPrompt/index.vue'
import DocumentPreview from './DocumentPreview.vue'
import defaultBillImage from '@/views/bill/assets/images/image1.png'
const router = useRouter()
const props = defineProps({
bills: { type: Array, required: true },
loading: { type: Boolean, default: false },
total: { type: Number, default: 0 },
currentPage: { type: Number, default: 1 },
pageSize: { type: Number, default: 10 },
progressStages: { type: Array, default: () => ['提出', '众议院通过', '参议院通过', '分歧协调', '提交总统', '法案通过'] },
progressStages: { type: Array, default: () => [] },
})
defineEmits(['page-change'])
/** 政策建议关联法案:新标签页打开法案介绍页,billId 随接口 id 变化 */
const handleBillMoreClick = (bill) => {
const billId = bill;
// debugger
if (!billId) {
return;
}
const route = router.resolve({
// 将 historyBill 接口数据映射为 right-main 模板需要的格式
const mappedBills = computed(() => {
return props.bills.map(bill => ({
billId: bill.billId,
name: bill.name,
eName: bill.ename,
tcr: bill.sponsorList?.[0] || '',
wyh: bill.congressName || '',
areaList: bill.industryList?.map(i => i.industryName) || [],
zxdy: bill.latestAction?.replace(/\bnull\b/gi, '至今').trim() || '',
progress: bill.stageList?.map(s => s.name || s) || [],
riskSignal: bill.riskSignal || '',
imageUrl: bill.imageUrl || '',
}))
})
const handleBillMoreClick = (billId) => {
if (!billId) return
const routeData = router.resolve({
path: "/billLayout/bill/introduction",
query: { billId: String(billId) }
});
window.open(route.href, "_blank");
};
})
window.open(routeData.href, "_blank")
}
const getRiskTagClass = (riskSignal) => {
if (riskSignal?.includes('高')) return 'risk-tag-critical'
if (riskSignal?.includes('中')) return 'risk-tag-high'
if (riskSignal?.includes('低')) return 'risk-tag-medium'
return 'risk-tag-medium'
}
const getReversedProgress = (progress) => Array.isArray(progress) ? [...progress].reverse() : []
</script>
<style scoped>
/* Bill List */
.bill-list {
display: flex;
flex-direction: column;
gap: 16px;
gap: 0;
}
.bill-list-loading {
......@@ -90,92 +137,350 @@ const handleBillMoreClick = (bill) => {
gap: 16px;
}
.bill-list-skeleton {
display: flex;
gap: 24px;
.bill-list-empty {
background: var(--bg-white-100);
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
padding: 24px;
}
.skeleton-left {
width: 120px;
height: 190px;
background: #f0f0f0;
border-radius: 4px;
animation: pulse 1.5s ease-in-out infinite;
}
.skeleton-right {
flex: 1;
padding: 48px;
text-align: center;
color: #bfbfbf;
font-size: 13px;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.skeleton-line {
/* Skeleton */
.skeleton .skeleton-line {
height: 16px;
background: #f0f0f0;
border-radius: 4px;
animation: pulse 1.5s ease-in-out infinite;
}
.skeleton .skeleton-cover {
background: #f0f0f0;
animation: pulse 1.5s ease-in-out infinite;
}
.skeleton-line.w30 { width: 30%; }
.skeleton-line.w35 { width: 35%; }
.skeleton-line.w40 { width: 40%; }
.skeleton-line.w50 { width: 50%; }
.skeleton-line.w60 { width: 60%; }
.skeleton-divider {
height: 1px;
background: #eaeced;
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.bill-list-empty {
background: var(--bg-white-100);
/* ============ right-main-box 样式(与 billHome ResourceLibrarySection 完全一致) ============ */
.right-main-box {
position: relative;
width: 100%;
height: 320px;
padding: 12px 16px;
box-sizing: border-box;
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
padding: 48px;
text-align: center;
color: #bfbfbf;
font-size: 13px;
background: var(--bg-white-100);
margin-bottom: 16px;
overflow: hidden;
display: flex;
flex-direction: column;
gap: 16px;
align-items: center;
gap: 12px;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
.right-main-box .risk-tag {
position: absolute;
top: 12px;
right: 16px;
height: 28px;
border-radius: 20px;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 0 10px;
box-sizing: border-box;
font-family: "Microsoft YaHei";
font-size: 14px;
font-weight: 500;
line-height: 28px;
white-space: nowrap;
}
.right-main-box .risk-tag::before {
content: "";
width: 5px;
height: 5px;
border-radius: 50%;
background: currentColor;
flex-shrink: 0;
}
.risk-tag-critical {
background: var(--color-red-10);
color: var(--color-red-100);
}
.risk-tag-high {
background: var(--color-orange-10);
color: var(--color-orange-100);
}
.risk-tag-medium {
background: var(--color-yellow-10);
color: var(--color-yellow-100);
}
.right-main-box .bill-cover {
width: 240px;
flex-shrink: 0;
position: relative;
height: 100%;
margin: 0;
overflow: hidden;
}
/* Pagination */
.bill-pagination {
.right-main-box .bill-cover::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 33.333%;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.92) 60%, var(--bg-white-100));
pointer-events: none;
}
.right-main-box .bill-image {
width: 240px;
height: 100%;
object-fit: none;
display: block;
}
.right-main-box .bill-id {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 33.333%;
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 0 0;
justify-content: center;
padding: 0 12px;
box-sizing: border-box;
color: var(--text-primary-80-color);
font-family: "Source Han Sans CN";
font-size: 20px;
font-weight: 700;
line-height: 26px;
letter-spacing: 0px;
text-align: center;
max-width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
z-index: 1;
}
.bill-pagination-total {
font-size: 14px;
font-family: 'Microsoft YaHei', sans-serif;
.right-main-box .bill-content {
flex: 1;
min-width: 0;
overflow: hidden;
box-sizing: border-box;
}
.right-main-box .header {
width: 100%;
border-bottom: 1px solid rgba(234, 236, 238, 1);
padding-bottom: 14px;
padding-right: 120px;
}
.right-main-box .header .title {
cursor: pointer;
height: 26px;
color: var(--text-primary-80-color);
font-family: Microsoft YaHei;
font-size: 20px;
font-weight: 700;
line-height: 26px;
letter-spacing: 0px;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.right-main-box .header .title:hover {
color: #1459bb;
}
.right-main-box .header .en-title {
margin-top: 8px;
height: 24px;
color: var(--text-primary-65-color);
font-family: Microsoft YaHei;
font-size: var(--font-size-base);
font-weight: 400;
color: rgba(95, 101, 108, 1);
line-height: 24px;
letter-spacing: 0px;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Override el-pagination to match design */
.bill-pagination :deep(.el-pagination) {
.right-main-box .main {
width: 100%;
margin-top: 10px;
}
.right-main-box .main .item {
margin-top: 12px;
display: flex;
align-items: flex-start;
}
.right-main-box .main .item .item-left {
width: 88px;
color: var(--text-primary-80-color);
font-family: "Microsoft YaHei";
font-size: var(--font-size-base);
font-weight: 700;
line-height: 24px;
letter-spacing: 1px;
text-align: left;
}
.right-main-box .main .item .item-right {
max-width: 1000px;
margin-left: 10px;
color: var(--text-primary-65-color);
font-family: "Microsoft YaHei";
font-size: var(--font-size-base);
font-weight: 400;
line-height: 24px;
letter-spacing: 0px;
text-align: left;
}
.right-main-box .main .item .item-right1 {
margin-left: 10px;
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.right-main-box .main .item .item-right1 .tag {
height: 24px;
line-height: 24px;
padding: 0 8px;
border-radius: 4px;
background: var(--color-primary-10);
color: var(--color-main-active);
font-family: "Microsoft YaHei";
font-size: 14px;
font-weight: 400;
}
.right-main-box .main .item .item-right2 {
margin-left: 10px;
display: flex;
align-items: center;
flex-wrap: wrap;
}
.right-main-box .main .item .item-right2 .tag {
height: 24px;
line-height: 22px;
padding: 0 10px 0 30px;
background: var(--bg-white-100);
color: var(--text-primary-65-color);
border-top: 1px solid rgb(234, 236, 238);
border-bottom: 1px solid rgb(234, 236, 238);
position: relative;
margin-left: -10px;
font-family: "Microsoft YaHei";
font-size: 14px;
font-weight: 400;
}
.right-main-box .main .item .item-right2 .tag::after {
content: "";
position: absolute;
top: 50%;
right: -8.485px;
width: 16.97px;
height: 16.97px;
background: inherit;
border-top: 1px solid rgb(234, 236, 238);
border-right: 1px solid rgb(234, 236, 238);
transform: translateY(-50%) rotate(45deg);
z-index: 1;
box-shadow: 2px -2px 2px rgba(0, 0, 0, 0.05);
box-sizing: border-box;
}
.right-main-box .main .item .item-right2 .tag:first-child {
margin-left: 0;
padding-left: 10px;
border-left: 1px solid rgb(234, 236, 238);
border-radius: 4px 0 0 4px;
}
.right-main-box .main .item .item-right2 .tag:last-child {
background: var(--text-primary-80-color);
color: var(--bg-white-100);
border-color: var(--text-primary-80-color);
padding-right: 10px;
border-radius: 0;
border-right: none;
}
.right-main-box .main .item .item-right2 .tag:last-child::after {
display: block;
border-color: var(--text-primary-80-color);
box-shadow: none;
}
.right-main-box .main .item .item-right2 .tag:first-child:last-child {
margin-left: 0;
padding: 0 10px;
border-radius: 4px 0 0 4px;
background: var(--text-primary-80-color);
color: var(--bg-white-100);
border: 1px solid var(--text-primary-80-color);
border-right: none;
}
/* Footer */
.right-footer {
height: 85px;
display: flex;
justify-content: space-between;
box-sizing: border-box;
padding-top: 12px;
}
.right-footer .footer-left {
color: var(--text-primary-80-color);
font-family: "Microsoft YaHei";
font-size: var(--font-size-base);
font-weight: 400;
line-height: 32px;
}
.right-footer :deep(.el-pagination) {
--el-pagination-bg-color: var(--bg-white-100);
--el-pagination-hover-color: rgba(5, 95, 194, 1);
padding: 0;
}
.bill-pagination :deep(.el-pagination.is-background .btn-prev),
.bill-pagination :deep(.el-pagination.is-background .btn-next),
.bill-pagination :deep(.el-pagination.is-background .el-pager li) {
.right-footer :deep(.el-pagination.is-background .btn-prev),
.right-footer :deep(.el-pagination.is-background .btn-next),
.right-footer :deep(.el-pagination.is-background .el-pager li) {
min-width: 32px;
height: 32px;
line-height: 32px;
......@@ -188,18 +493,18 @@ const handleBillMoreClick = (bill) => {
margin: 0 3px;
}
.bill-pagination :deep(.el-pagination.is-background .el-pager li:not(.is-disabled).is-active) {
.right-footer :deep(.el-pagination.is-background .el-pager li:not(.is-disabled).is-active) {
background: var(--bg-white-100);
color: rgba(5, 95, 194, 1);
border-color: rgba(5, 95, 194, 1);
}
.bill-pagination :deep(.el-pagination.is-background .el-pager li:not(.is-disabled):hover) {
.right-footer :deep(.el-pagination.is-background .el-pager li:not(.is-disabled):hover) {
color: rgba(5, 95, 194, 1);
}
.bill-pagination :deep(.el-pagination.is-background .btn-prev:hover),
.bill-pagination :deep(.el-pagination.is-background .btn-next:hover) {
.right-footer :deep(.el-pagination.is-background .btn-prev:hover),
.right-footer :deep(.el-pagination.is-background .btn-next:hover) {
color: rgba(5, 95, 194, 1);
}
</style>
......@@ -23,7 +23,10 @@
</div>
<!-- ECharts word cloud -->
<div ref="chartRef" class="pna-cloud"></div>
<div v-if="keywords.length > 0" ref="chartRef" class="pna-cloud"></div>
<div v-else class="pna-cloud">
<el-empty description="暂无数据" :image-size="80" />
</div>
</div>
</template>
......@@ -172,5 +175,8 @@ onBeforeUnmount(() => {
.pna-cloud {
flex: 1;
min-height: 400px;
display: flex;
align-items: center;
justify-content: center;
}
</style>
......@@ -32,11 +32,12 @@
</div>
</div>
<div class="pn-rows" :class="{ 'pn-rows-loading': loading }">
<div class="pn-rows" :class="{ 'pn-rows-loading': loading || newsList.length === 0 }">
<div v-if="loading" class="pn-rows-spinner">
<div class="pn-spinner-icon"></div>
<span class="pn-spinner-text">加载中...</span>
</div>
<el-empty v-else-if="newsList.length === 0" description="暂无数据" :image-size="80" />
<div
v-for="(item, index) in newsList"
v-show="!loading"
......@@ -52,7 +53,7 @@
</div>
</div>
<div class="pn-footer">
<div class="pn-footer" v-if="newsList.length > 0">
<span class="pn-footer-total">{{ total }}条关键新闻</span>
<div class="pn-pagination">
<button
......
......@@ -95,7 +95,11 @@
</div>
</template>
<div class="main">
<div v-for="item in CharacterLatestDynamic" :key="item" class="main-item">
<template v-if="CharacterLatestDynamic.length === 0">
<el-empty description="暂无数据" :image-size="80" />
</template>
<template v-else>
<div v-for="item in CharacterLatestDynamic" :key="item" class="main-item">
<div class="time">
<div class="year">{{ item.time.split("-")[0] }}</div>
<div class="date">{{ item.time.split("-")[1] + "月" + item.time.split("-")[2] + "日" }}
......@@ -127,9 +131,9 @@
</div>
</div>
</div>
<!-- <div class="line-test"></div> -->
</template>
</div>
<div class="pagination">
<div v-if="CharacterLatestDynamic.length > 0" class="pagination">
<div class="total">{{ `共 ${total} 项` }}</div>
<el-pagination @current-change="handleCurrentChange" :page-size="pageSize"
:current-page="currentPage" background layout="prev, pager, next" :total="total"
......@@ -233,29 +237,25 @@
</div>
</div>
<!-- 历史提案 -->
<!-- 在 member-of-congress 同级的左侧添加标签栏 -->
<!-- 历史提案 tab 对应的内容区 -->
<div v-if="infoActive === '历史提案'" class="proposal-wrapper">
<div v-if="infoActive === '历史提案'" class="proposal-wrapper">
<div class="proposal-tab-switcher">
<button :class="['proposal-tab', { active: newsTab === 'history' }]" @click="newsTab = 'history'">
<span>历史提案</span>
<svg v-if="newsTab === 'history'" class="proposal-tab-arrow" width="12" height="12"
viewBox="0 0 12 12" fill="currentColor">
<path d="M4 2l5 4-5 4V2z" />
<svg v-if="newsTab === 'history'" class="proposal-tab-arrow" width="8" height="8" viewBox="0 0 8 8" fill="currentColor">
<path d="M4 0L8 4L4 8V0Z" />
</svg>
</button>
<button :class="['proposal-tab', { active: newsTab === 'potential' }]" @click="newsTab = 'potential'">
<span>潜在提案</span>
<svg v-if="newsTab === 'potential'" class="proposal-tab-arrow" width="12" height="12"
viewBox="0 0 12 12" fill="currentColor">
<path d="M4 2l5 4-5 4V2z" />
<svg v-if="newsTab === 'potential'" class="proposal-tab-arrow" width="8" height="8" viewBox="0 0 8 8" fill="currentColor">
<path d="M4 0L8 4L4 8V0Z" />
</svg>
</button>
</div>
<HistoricalProposal v-if="newsTab === 'history'" />
<PotentialNews v-else />
</div>
</div>
<CharacterRelationships v-if="infoActive === '人物关系'" />
<RelevantSituation v-if="infoActive === '相关情况'" />
<!-- 弹框 -->
......@@ -1837,45 +1837,40 @@ const handleClickTag = async (tag) => {
/* 作为定位参考 */
}
.proposal-tab.active {
background: #055FC2;
color: #fff;
}
.proposal-tab-switcher {
position: absolute;
right: calc(100% + 24px);
left: 24px;
top: 0;
display: flex;
flex-direction: column;
gap: 12px;
z-index: 1;
gap: 16px;
align-items: center;
}
.proposal-tab {
width: 120px;
width: 112px;
height: 32px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 12px;
font-family: 'Source Han Sans CN', 'Noto Sans SC', sans-serif;
justify-content: center;
gap: 4px;
padding: 0 16px;
border-radius: 16px;
border: none;
font-size: 16px;
font-family: 'Microsoft YaHei', sans-serif;
font-weight: 400;
color: #8c8c8c;
background: none;
border: none;
border-radius: 20px;
color: var(--text-primary-65-color);
background-color: var(--bg-white-100);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
cursor: pointer;
white-space: nowrap;
box-sizing: border-box;
}
.proposal-tab.active {
background: #055FC2;
color: #fff;
&.active {
color: var(--bg-white-100);
background-color: var(--color-primary-100);
box-shadow: none;
}
}
.proposal-tab-arrow {
......
/**
* 人物主页 - 类型配置
* type 1: 科技领袖, type 2: 国会议员, type 3: 智库研究人员
*/
export const CHARACTER_CONFIG = {
1: {
// 科技领袖
rootClass: "tech-leader",
tabs: ["人物详情", "人物关系"],
tabWidth: "50%",
useImageProxy: false,
headerTagType: "areaTag",
wordCloudTitle: "科技观点",
yearDefault: "全部时间",
yearBuildMode: "dynamic",
yearStaticOptions: [],
showFundSource: false,
resumeMode: "inline",
resumeTitle: "职业履历",
resumeHeight: "1336px",
companySectionTitle: "实体信息",
basicInfoFields: [
{ label: "出生日期:", key: "birthday", type: "text" },
{ label: "国籍:", key: "country", type: "text" },
{ label: "教育背景:", key: "educationList", type: "education", format: "school(major)" },
{ label: "净资产:", key: "assets", type: "text" },
{ label: "职业:", key: "positionTitle", type: "text" },
{ label: "婚姻状况:", key: "marital", type: "text" },
{ label: "出生地:", key: "birthPlace", type: "text" }
],
boxHeightRules: [
{ condition: "undefined", value: "605px" },
{ condition: "empty", value: "405px" },
{ condition: "lte2", value: "505px" },
{ condition: "default", value: "625px" }
],
useAreaTypeApi: true,
fieldViewMode: "areaList",
dialogTagPrefix: "#",
dialogTagSuffix: " 相关领域标签",
showRelevantSituation: false,
historicalProposalType: null
},
2: {
// 国会议员
rootClass: "member-of-congress",
tabs: ["人物详情", "历史提案", "人物关系"],
tabWidth: "auto",
useImageProxy: true,
headerTagType: "inline",
wordCloudTitle: "科技观点",
yearDefault: "全部",
yearBuildMode: "static",
yearStaticOptions: ["全部", "2025", "2024", "2023", "2022", "2021", "2020"],
showFundSource: true,
resumeMode: "inline",
resumeTitle: "职业履历",
resumeHeight: "1556px",
companySectionTitle: "社交媒体",
basicInfoFields: [
{ label: "出生日期:", key: "birthday", type: "text" },
{ label: "现任职位:", key: "positionTitle", type: "text" },
{ label: "党派归属:", key: "party", type: "text" },
{ label: "教育背景:", key: "educationList", type: "education", format: "school+major" },
{ label: "代表州/选区:", key: "state", type: "text", titleClass: "address" },
{ label: "政治立场:", key: "political", type: "text", contentClass: "long" },
{ label: "出生地:", key: "birthPlace", type: "text" }
],
boxHeightRules: [
{ condition: "undefined", value: "545px" },
{ condition: "empty", value: "495px" },
{ condition: "lte2", value: "445px" },
{ condition: "default", value: "545px" }
],
useAreaTypeApi: false,
fieldViewMode: "sessionStorage",
dialogTagPrefix: "#",
dialogTagSuffix: "相关领域标签",
showRelevantSituation: false,
historicalProposalType: "bill"
},
3: {
// 智库研究人员
rootClass: "think-tank-person",
tabs: ["人物详情", "成果报告", "人物关系"],
tabWidth: "auto",
useImageProxy: false,
headerTagType: "areaTag",
wordCloudTitle: "核心观点",
yearDefault: "全部",
yearBuildMode: "static",
yearStaticOptions: ["全部", "2026", "2025", "2024", "2023", "2022", "2021"],
showFundSource: false,
resumeMode: "card",
resumeTitle: "政治履历",
resumeHeight: null,
companySectionTitle: "社交媒体",
basicInfoFields: [
{ label: "出生日期:", key: "birthday", type: "text" },
{ label: "现任职位:", key: "positionTitle", type: "text" },
{ label: "兼任职位:", key: "sideJob", type: "text" },
{ label: "政策倾向:", key: "political", type: "text" },
{ label: "国籍:", key: "country", type: "text" },
{ label: "教育背景:", key: "educationList", type: "education", format: "school(major)" },
{ label: "研究领域:", key: "industryList", type: "industry" }
],
boxHeightRules: [
{ condition: "undefined", value: "625px" },
{ condition: "empty", value: "425px" },
{ condition: "lte2", value: "525px" },
{ condition: "default", value: "625px" }
],
useAreaTypeApi: false,
fieldViewMode: "sessionStorage",
dialogTagPrefix: "",
dialogTagSuffix: "",
showRelevantSituation: false,
historicalProposalType: "news"
}
};
<template>
<div :class="config.rootClass">
<!-- 人物基础 -->
<div class="header">
<div class="avatar">
<el-avatar :size="160" shape="circle" :src="avatarUrl" />
</div>
<div class="info">
<p class="name-cn">{{ characterInfo.name }}</p>
<p class="name-en">{{ characterInfo.ename }}</p>
<div class="introduction">
<p>{{ characterInfo.description }}</p>
</div>
<div class="domain">
<template v-if="config.headerTagType === 'areaTag'">
<AreaTag v-for="tag in characterInfo.industryList" :key="tag"
:tag-name="tag.industryName" />
</template>
<template v-else>
<p v-for="item in characterInfo.industryList" :key="item" class="cl1" :class="{
cl1: item.status === '1',
cl2: item.status === '2',
cl3: item.status === '3',
cl4: item.status === '4',
cl5: item.status === '5',
cl6: item.status === '6'
}">
{{ item.industryName }}
</p>
</template>
</div>
</div>
</div>
<!-- 信息区分 -->
<div class="info-divide">
<div v-for="item in config.tabs" :key="item"
:class="{ active: infoActive === item }"
:style="config.tabWidth === '50%' ? { width: '50%' } : {}"
@click="infoActive = item">
{{ item }}
</div>
</div>
<!-- 人物详情 -->
<div class="info-content" v-show="infoActive === '人物详情'">
<div class="left">
<!-- 科技观点 / 核心观点 - 统一使用 WordCloudChart -->
<AnalysisBox :title="config.wordCloudTitle" width="1064px" height="300px" :show-all-btn="false" class="left-top">
<template #header-btn>
<el-select v-model="numActive" class="tab-select" :teleported="true" @change="handleChangeYear">
<el-option v-for="item in yearOptions" :key="item" :label="item" :value="item" />
</el-select>
</template>
<div class="chart-box">
<WordCloudChart v-if="characterView.length > 0" :key="wordCloudKey" :data="characterView" />
<el-empty v-else description="暂无数据" :image-size="80" />
</div>
</AnalysisBox>
<!-- 资金来源 (仅国会议员) -->
<template v-if="config.showFundSource">
<AnalysisBox title=" 资金来源" width="1064px" :show-all-btn="false" class="left-center auto-height-box">
<template #header-btn>
<el-select v-model="selectedOption" placeholder="请选择" class="tab-select" :teleported="true"
@change="handleChangeYearList">
<el-option v-for="item in yearList" :key="item.value" :label="item.label"
:value="item.value" />
</el-select>
</template>
<div class="fund-table-main">
<el-table :data="CharacterFundSource" style="width: 100%" :header-cell-style="{
background: 'transparent',
color: 'rgba(59, 65, 75, 1)',
fontWeight: 700,
fontSize: '16px',
borderBottom: '1px solid rgba(234, 236, 238, 1)'
}" :cell-style="{
fontSize: '16px',
fontWeight: 400,
color: 'rgba(59, 65, 75, 1)'
}" :row-class-name="tableRowClassName" :row-style="{ height: '50px' }" size="large">
<el-table-column prop="rank" label="排名" width="80" align="center" />
<el-table-column prop="contributor" label="贡献者" min-width="300" />
<el-table-column prop="totalAmount" label="总捐款" width="150" align="right" />
<el-table-column prop="individualAmount" label="个人捐款" width="150" align="right" />
<el-table-column prop="pacsAmount" label="PACs捐款" width="150" align="right" />
</el-table>
<div class="table-pagination">
<span class="table-pagination-total">共{{ fundTotal }}项</span>
<el-pagination v-if="fundTotal / fundPageSize >= 2" :current-page="fundCurrentPage"
:page-size="fundPageSize" :total="fundTotal" :pager-count="4" layout="prev, pager, next"
background @current-change="handleFundPageChange" />
</div>
</div>
</AnalysisBox>
</template>
<!-- 最新动态 -->
<AnalysisBox title="最新动态" width="1064px" :show-all-btn="false" class="left-bottom auto-height-box">
<template #header-btn>
<div class="input">
<input type="checkbox" v-model="isChecked" @change="handleChange"
style="padding-top: 2px;" /><span style="margin-left: 8px">只看涉华动态</span>
</div>
</template>
<div class="dynamic-main">
<template v-if="CharacterLatestDynamic.length === 0">
<el-empty description="暂无数据" :image-size="80" style="margin-bottom: 125px;" />
</template>
<template v-else>
<div v-for="(item, idx) in CharacterLatestDynamic" :key="item" class="main-item">
<div class="time">
<div class="year">{{ item.time.split("-")[0] }}</div>
<div class="date">{{ item.time.split("-")[1] + "月" + item.time.split("-")[2] + "日" }}</div>
</div>
<div class="timeline-col">
<div class="timeline-line-top" v-if="idx !== 0"></div>
<div class="timeline-icon">
<img :src="typeIcon1" alt="" v-if="item.remarks === true" />
<img :src="typeIcon2" alt="" v-else />
</div>
<div class="timeline-line-bottom" v-if="idx !== CharacterLatestDynamic.length - 1"></div>
</div>
<div class="content">
<div :class="{ 'content-type1': item.remarks === true, 'content-type2': item.remarks === false }">
<p v-if="item.remarks === true" class="content-title1">{{ item.content }}</p>
<p v-else class="content-title2">{{ item.title }}</p>
<p v-if="item.remarks === true" class="content-title-en">{{ item.econtent }}</p>
</div>
<p v-if="item.remarks === false" class="content-contentcontent">{{ item.content }}</p>
<div class="content-tag">
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
<AreaTag v-for="tag in item.industryList" :key="tag"
:tag-name="tag.industryName" @click="handleClickTag(tag.industryName)" />
</div>
<div class="origin">来源:{{ item.orgName }}</div>
</div>
</div>
</div>
</template>
</div>
<div v-if="CharacterLatestDynamic.length > 0" class="pagination">
<div class="total">{{ `共 ${total} 项` }}</div>
<el-pagination @current-change="handleCurrentChange" :page-size="pageSize"
:current-page="currentPage" background layout="prev, pager, next" :total="total"
class="custom-pagination" />
</div>
</AnalysisBox>
</div>
<div class="right">
<!-- 基本信息 -->
<AnalysisBox title="基本信息" width="520px" :height="boxHeight" :show-all-btn="false" class="right-top" v-if="characterBasicInfo">
<div class="main-content">
<div class="baseInfo">
<div v-for="field in config.basicInfoFields" :key="field.key" class="baseInfo-item">
<div class="baseInfo-item-title" :class="field.titleClass || ''">{{ field.label }}</div>
<div class="baseInfo-item-content" :class="field.contentClass || ''">
<template v-if="field.type === 'text'">
{{ characterBasicInfo[field.key] }}
</template>
<template v-else-if="field.type === 'education'">
<template v-if="field.format === 'school+major'">
<div v-for="item in characterBasicInfo.educationList" :key="item.school">
{{ item.school + item.major }}
</div>
</template>
<template v-else>
<span class="education-item"
v-for="value in characterBasicInfo.educationList"
:key="value.school">
{{ value.school + "(" + value.major + ")" }}
</span>
</template>
</template>
<template v-else-if="field.type === 'industry'">
<span class="span" v-for="item in characterBasicInfo[field.key]"
:key="item">{{ item.industryName }}</span>
</template>
</div>
</div>
</div>
<div class="company">
<div class="company-title">{{ config.companySectionTitle }}</div>
<div class="company-content">
<div v-for="item in characterBasicInfo.organizationList" :key="item"
class="company-item">
<img :src="item.imageUrl ? item.imageUrl : DefaultIcon2" alt="" />
<div>
<div class="company-cn">{{ item.name }}</div>
<div class="company-name">{{ item.ename }}</div>
</div>
</div>
</div>
</div>
</div>
</AnalysisBox>
<!-- 履历 - inline 方式 -->
<template v-if="config.resumeMode === 'inline'">
<AnalysisBox :title="config.resumeTitle" width="520px" :show-all-btn="false" class="right-bottom auto-height-box">
<template #header-btn>
<div class="resume-tabs">
<button class="resume-tab" :class="{ active: resumeType === 'career' }"
@click="switchResumeType('career')">职业履历</button>
<button class="resume-tab" :class="{ active: resumeType === 'education' }"
@click="switchResumeType('education')">教育履历</button>
</div>
</template>
<div class="content-main">
<template v-if="resumeType === 'career'">
<template v-if="currentResumeList && currentResumeList.length > 0">
<div v-for="item in currentResumeList" :key="item.startTime" class="content-item">
<img :src="resumeIcon01" alt="" class="image01" />
<div class="content-item-time">{{ item.startTime.split('-')[0] + '-' + getendTime(item) }}</div>
<div class="content-item-title"><span>{{ item.orgName }}</span><span style="margin-left: 5px;margin-right: 5px;">|</span><span>{{ item.jobName }}</span></div>
<div class="content-item-content">{{ item.content }}</div>
<div class="content-item-door" v-if="item.door">
<img :src="resumeIcon02" alt="" />
<span>{{ item.door }}</span>
</div>
</div>
</template>
<el-empty v-else :image-size="80" description="暂无职业履历" />
</template>
<template v-else>
<template v-if="currentResumeList && currentResumeList.length > 0">
<div v-for="(item, index) in currentResumeList" :key="index" class="content-item">
<img :src="resumeIcon01" alt="" class="image01" />
<div class="content-item-time">{{ item.startTime.split('-')[0] + '-' + getendTime(item) }}</div>
<div class="content-item-title">{{ item.school + '(' + item.country + ')' }}</div>
<div class="content-item-content">{{ item.description }}</div>
</div>
</template>
<el-empty v-else :image-size="80" description="暂无教育履历" />
</template>
</div>
</AnalysisBox>
</template>
<!-- 履历 - card 方式 -->
<template v-else>
<ResumeCard :title="config.resumeTitle" :list="CharacterResume"
@download="handleDownload" @collect="handleCollect" />
</template>
</div>
</div>
<!-- 历史提案 (国会议员) -->
<template v-if="config.historicalProposalType === 'bill'">
<div v-if="infoActive === '历史提案'" class="proposal-wrapper">
<div class="proposal-tab-switcher">
<button :class="['proposal-tab', { active: newsTab === 'history' }]" @click="newsTab = 'history'">
<span>历史提案</span>
<svg v-if="newsTab === 'history'" class="proposal-tab-arrow" width="8" height="8" viewBox="0 0 8 8" fill="currentColor">
<path d="M4 0L8 4L4 8V0Z" />
</svg>
</button>
<button :class="['proposal-tab', { active: newsTab === 'potential' }]" @click="newsTab = 'potential'">
<span>潜在提案</span>
<svg v-if="newsTab === 'potential'" class="proposal-tab-arrow" width="8" height="8" viewBox="0 0 8 8" fill="currentColor">
<path d="M4 0L8 4L4 8V0Z" />
</svg>
</button>
</div>
<BillTracker v-if="newsTab === 'history'" />
<PotentialNews v-else />
</div>
</template>
<!-- 成果报告 (智库) -->
<template v-if="config.historicalProposalType === 'news'">
<NewsTracker v-if="infoActive === '成果报告'" />
</template>
<!-- 人物关系 - 传递 personId 确保 graph 能获取数据 -->
<CharacterRelationships v-if="infoActive === '人物关系'" />
<!-- 相关情况 -->
<RelevantSituation v-if="config.showRelevantSituation && infoActive === '相关情况'" />
<!-- 弹框 -->
<el-dialog v-model="dialogVisible" width="761px" class="viewpoint-dialog" :modal="false" :draggable="true"
:lock-scroll="false" :show-close="config.rootClass === 'member-of-congress' ? false : undefined">
<template #header>
<div class="viewpoint-header">
<div class="viewpoint-title">
<span v-if="config.dialogTagPrefix" class="viewpoint-tag">{{ config.dialogTagPrefix }}{{ currentTag }}</span>
<span class="viewpoint-label">{{ config.dialogTagSuffix }}</span>
</div>
<template v-if="config.rootClass === 'member-of-congress'">
<div class="viewpoint-close" @click="dialogVisible = false">
<el-icon :size="16"><Close /></el-icon>
</div>
</template>
</div>
</template>
<div class="viewpoint-body">
<div v-for="item in CharacterFieldView" :key="item.id" class="viewpoint-item">
<div class="viewpoint-avatar">
<el-avatar :size="48" shape="circle"
:src="item.imageUrl ? getProxyUrl(item.imageUrl) : DefaultIcon1" />
</div>
<div class="viewpoint-content">
<div class="viewpoint-arrow"></div>
<div class="viewpoint-card">
<div class="viewpoint-card-header">
<span class="viewpoint-name">{{ item.name }}</span>
<span class="viewpoint-job">{{ item.jobName }}</span>
</div>
<div class="viewpoint-desc">{{ item.remarks }}</div>
</div>
</div>
</div>
<div v-if="!CharacterFieldView || CharacterFieldView.length === 0" class="viewpoint-empty">暂无数据</div>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed, onMounted, nextTick } from "vue";
import { useRoute } from 'vue-router';
import WordCloudChart from "@/components/base/WordCloundChart/index.vue";
import AnalysisBox from '@/components/base/boxBackground/analysisBox.vue';
import AreaTag from '@/components/base/AreaTag/index.vue';
import DefaultIcon1 from '@/assets/icons/default-icon1.png';
import DefaultIcon2 from '@/assets/icons/default-icon2.png';
import { Close } from '@element-plus/icons-vue';
import { ElEmpty } from 'element-plus';
import {
getCharacterGlobalInfo,
getCharacterBasicInfo,
getCharacterView,
getCharacterLatestDynamic,
getCharacterResume,
getCharacterFieldView,
getCharacterFundSource,
getCharacterReducationResume,
getareaType
} from "@/api/characterPage/characterPage.js";
import { CHARACTER_CONFIG } from './config.js';
const props = defineProps({
type: { type: [Number, String], required: true },
personId: { type: String, default: '' }
});
const route = useRoute();
const config = computed(() => CHARACTER_CONFIG[Number(props.type)] || CHARACTER_CONFIG[1]);
const personIdRef = ref(props.personId || route.query.personId || "Y000064");
// 类型特定资源 - 动态导入
const typeAssets = ref({});
const loadTypeAssets = async () => {
const t = Number(props.type);
if (t === 1) {
const assets = await import('../techLeader/assets/type1.png');
const assets2 = await import('../techLeader/assets/type2.png');
const icon01 = await import('../techLeader/assets/icon01.png');
const icon02 = await import('../techLeader/assets/icon02.png');
typeAssets.value = { type1: assets.default, type2: assets2.default, icon01: icon01.default, icon02: icon02.default };
} else if (t === 2) {
const assets = await import('../memberOfCongress/assets/type1.png');
const assets2 = await import('../memberOfCongress/assets/type2.png');
const icon01 = await import('../memberOfCongress/assets/icon01.png');
const icon02 = await import('../memberOfCongress/assets/icon02.png');
typeAssets.value = { type1: assets.default, type2: assets2.default, icon01: icon01.default, icon02: icon02.default };
} else {
const assets = await import('../thinkTankPerson/assets/type1.png');
const assets2 = await import('../thinkTankPerson/assets/type2.png');
const icon01 = await import('../thinkTankPerson/assets/icon01.png');
const icon02 = await import('../thinkTankPerson/assets/icon02.png');
typeAssets.value = { type1: assets.default, type2: assets2.default, icon01: icon01.default, icon02: icon02.default };
}
};
const typeIcon1 = computed(() => typeAssets.value.type1 || '');
const typeIcon2 = computed(() => typeAssets.value.type2 || '');
const resumeIcon01 = computed(() => typeAssets.value.icon01 || '');
const resumeIcon02 = computed(() => typeAssets.value.icon02 || '');
// 子组件
import CharacterRelationships from '../techLeader/components/characterRelationships/index.vue';
import RelevantSituation from '../techLeader/components/relevantSituation/index.vue';
import BillTracker from '../memberOfCongress/components/historicalProposal/components/BillTracker.vue';
import PotentialNews from '../memberOfCongress/components/historicalProposal/components/PotentialNews.vue';
import NewsTracker from '../thinkTankPerson/components/historicalProposal/components/NewsTracker.vue';
import ResumeCard from '../thinkTankPerson/components/Resume.vue';
// 图片代理
const getProxyUrl = (url) => {
if (!url) return "";
const urlStr = String(url);
if (!urlStr.startsWith('http') || urlStr.includes('images.weserv.nl') || urlStr.includes('localhost') || urlStr.includes('127.0.0.1')) {
return url;
}
const cleanUrl = urlStr.replace(/^https?:\/\//i, '');
return `https://images.weserv.nl/?url=${encodeURIComponent(cleanUrl)}`;
};
const avatarUrl = computed(() => {
const url = characterInfo.value.imageUrl;
if (!url) return '';
return config.value.useImageProxy ? getProxyUrl(url) : url;
});
// ============ 数据 ============
const characterInfo = ref({});
const characterBasicInfo = ref({});
const characterView = ref([]);
const wordCloudKey = ref(0);
const CharacterLatestDynamic = ref([]);
const CharacterResume = ref({});
const CharacterFieldView = ref([]);
const CharacterFundSource = ref([]);
const CharacterEducationResume = ref([]);
// ============ 状态 ============
const infoActive = ref("人物详情");
const resumeType = ref('career');
const newsTab = ref('history');
const numActive = ref('');
const yearOptions = ref([]);
// 分页 - 动态
const currentPage = ref(1);
const total = ref(0);
const pageSize = ref(7);
const loading = ref(false);
const abortController = ref(null);
// 资金来源
const fundCurrentPage = ref(1);
const fundPageSize = ref(4);
const fundTotal = ref(0);
const selectedOption = ref("all");
const yearList = ref([
{ label: "全部", value: "all" },
{ label: "2025", value: 2025 },
{ label: "2024", value: 2024 },
{ label: "2023", value: 2023 }
]);
const isChecked = ref(false);
const related = ref('N');
const dialogVisible = ref(false);
const currentTag = ref(null);
const areaList = ref([]);
// ============ 计算属性 ============
const boxHeight = computed(() => {
const orgList = characterBasicInfo.value.organizationList;
const rules = config.value.boxHeightRules;
if (orgList === undefined) return rules.find(r => r.condition === 'undefined')?.value || '600px';
if (orgList.length === 0) return rules.find(r => r.condition === 'empty')?.value || '400px';
if (orgList.length <= 2) return rules.find(r => r.condition === 'lte2')?.value || '500px';
return rules.find(r => r.condition === 'default')?.value || '600px';
});
const currentResumeList = computed(() => {
return resumeType.value === 'career' ? CharacterResume.value : CharacterEducationResume.value;
});
// ============ 方法 ============
const getendTime = (item) => {
if (item.endTime == null) {
return item.endTimeStatus == 0 ? '至今' : '未知';
} else {
return item.endTime.split('-')[0];
}
};
const switchResumeType = (type) => {
resumeType.value = type;
if (type === 'education' && CharacterEducationResume.value.length === 0) {
getCharacterEducationResumeFn();
}
};
// API 调用
const getCharacterGlobalInfoFn = async () => {
const params = { personId: personIdRef.value };
try {
const res = await getCharacterGlobalInfo(params);
if (res.code === 200 && res.data) {
characterInfo.value = res.data;
}
} catch (error) { }
};
const getCharacterBasicInfoFn = async () => {
const params = { personId: personIdRef.value };
try {
const res = await getCharacterBasicInfo(params);
if (res.code === 200 && res.data) {
characterBasicInfo.value = res.data;
}
} catch (error) { }
};
const getCharacterViewFn = async () => {
const params = { personId: personIdRef.value };
if (numActive.value !== config.value.yearDefault && numActive.value !== '全部' && numActive.value !== '全部时间') {
params.year = numActive.value;
}
try {
const res = await getCharacterView(params);
if (res.code === 200 && res.data) {
characterView.value = res.data.map(item => ({
name: item.option,
value: item.count
}));
} else {
characterView.value = [];
}
} catch (error) {
characterView.value = [];
}
// 强制刷新 WordCloudChart 组件
wordCloudKey.value++;
};
const handleChangeYear = () => {
characterView.value = [];
getCharacterViewFn();
};
const getCharacterFieldViewFn = async (tagname) => {
let areaId = window.sessionStorage.getItem("areaId") || "20";
if (config.value.fieldViewMode === 'areaList' && tagname) {
const areaItem = areaList.value.find(item => item.name === tagname);
areaId = areaItem?.id || "20";
}
const params = { areaId };
try {
const res = await getCharacterFieldView(params);
if (res.code === 200 && res.data) {
CharacterFieldView.value = res.data;
}
} catch (error) { }
};
const handleChange = () => {
related.value = isChecked.value ? 'Y' : 'N';
getCharacterLatestDynamicFn();
};
const handleCurrentChange = (page) => {
currentPage.value = page;
getCharacterLatestDynamicFn();
};
const getCharacterLatestDynamicFn = async () => {
if (abortController.value) {
abortController.value.abort();
}
abortController.value = new AbortController();
loading.value = true;
const params = {
personId: personIdRef.value,
cRelated: related.value,
currentPage: currentPage.value - 1,
pageSize: pageSize.value
};
try {
const res = await getCharacterLatestDynamic(params, abortController.value.signal);
if (res.code === 200) {
if (res.data && res.data.content) {
CharacterLatestDynamic.value = res.data.content.map(item => ({
title: item.title,
content: item.content,
econtent: item.econtent,
time: item.time,
industryList: item.industryList || ["人工智能"],
orgName: item.orgName,
remarks: item.remarks
}));
total.value = res.data.totalElements;
} else {
CharacterLatestDynamic.value = [];
total.value = 0;
}
} else {
CharacterLatestDynamic.value = [];
total.value = 0;
}
loading.value = false;
} catch (error) {
if (error.name !== "AbortError") {
console.error(error);
loading.value = false;
}
}
};
const getCharacterResumeFn = async () => {
const params = { personId: personIdRef.value };
try {
const res = await getCharacterResume(params);
if (res.code === 200 && res.data) {
CharacterResume.value = res.data;
}
} catch (error) { }
};
const getCharacterEducationResumeFn = async () => {
const params = { personId: personIdRef.value };
try {
const res = await getCharacterReducationResume(params);
if (res.code === 200 && res.data) {
CharacterEducationResume.value = res.data;
}
} catch (error) { }
};
const getCharacterFundSourceFn = async () => {
const params = {
personId: personIdRef.value,
pageSize: 4,
currentPage: fundCurrentPage.value - 1,
};
if (selectedOption.value !== 'all') {
params.year = selectedOption.value;
}
try {
const res = await getCharacterFundSource(params);
if (res.code === 200 && res.data) {
fundTotal.value = res.data.totalElements;
CharacterFundSource.value = res.data.content.map((item, index) => ({
rank: index + 1,
contributor: item.orgName,
totalAmount: item.totalDonation,
individualAmount: item.personalDonation,
pacsAmount: item.pacsDonation
}));
}
} catch (error) { }
};
const handleFundPageChange = (page) => {
fundCurrentPage.value = page;
getCharacterFundSourceFn();
};
const handleChangeYearList = () => {
getCharacterFundSourceFn();
};
const handleGetAreaType = async () => {
try {
const res = await getareaType();
if (res.code === 200 && res.data) {
areaList.value = res.data;
}
} catch (error) { }
};
const handleClickTag = async (tag) => {
currentTag.value = tag;
dialogVisible.value = true;
await getCharacterFieldViewFn(tag);
};
const tableRowClassName = ({ row, rowIndex }) => {
if (rowIndex % 2 === 0) return "highlight-row";
return "";
};
const handleDownload = () => { };
const handleCollect = () => { };
const buildYearOptions = () => {
if (config.value.yearBuildMode === 'dynamic') {
const currentYear = new Date().getFullYear();
const years = [];
for (let i = 0; i < 5; i++) {
years.push(String(currentYear - i));
}
yearOptions.value = [config.value.yearDefault, ...years];
} else {
yearOptions.value = config.value.yearStaticOptions;
}
numActive.value = config.value.yearDefault;
};
// ============ 初始化 ============
const init = async () => {
await loadTypeAssets();
buildYearOptions();
// 共用 API
getCharacterGlobalInfoFn();
getCharacterBasicInfoFn();
getCharacterLatestDynamicFn();
getCharacterResumeFn();
getCharacterViewFn();
// 类型特有
if (config.value.useAreaTypeApi) {
handleGetAreaType();
}
if (config.value.showFundSource) {
getCharacterFundSourceFn();
}
if (config.value.fieldViewMode === 'sessionStorage') {
getCharacterFieldViewFn();
}
};
onMounted(() => {
init();
});
</script>
<style lang="scss" scoped>
* {
margin: 0;
padding: 0;
}
:deep(.wrapper-main) {
margin-top: 15px;
}
:deep(.el-select-dropdown__item) {
text-align: center !important;
justify-content: center;
}
/* ============ 通用布局 ============ */
.tech-leader,
.member-of-congress,
.think-tank-person {
width: 1600px;
margin: 0 auto;
padding-bottom: 50px;
.header {
width: 1600px;
height: 200px;
margin: 16px auto;
background-color: rgba(255, 255, 255, 0.8);
border-radius: 10px;
box-shadow: 0px 0px 20px rgba(25, 69, 130, 0.1);
padding: 20px;
display: flex;
align-items: center;
.avatar {
width: 160px;
height: 160px;
margin-right: 24px;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.info {
flex: 1;
.name-cn {
font-size: 32px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 42px;
color: rgb(59, 65, 75);
margin-bottom: 8px;
}
.name-en {
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(59, 65, 75);
margin-bottom: 6px;
}
.introduction {
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(59, 65, 75);
margin-bottom: 6px;
}
.domain {
font-size: 14px;
display: flex;
flex-wrap: wrap;
gap: 8px;
p {
display: inline-block;
padding: 1px 8px;
border-radius: 4px;
margin-right: 8px;
border: 1px solid;
}
.cl1 { border-color: rgba(186, 224, 255, 1); background-color: rgba(230, 244, 255, 1); color: rgba(22, 119, 255, 1); }
.cl2 { border-color: rgba(217, 247, 190, 1); background-color: rgba(246, 255, 237, 1); color: rgba(82, 196, 26, 1); }
.cl3 { border-color: rgba(255, 241, 184, 1); background-color: rgba(255, 251, 230, 1); color: rgba(250, 173, 20, 1); }
.cl4 { border-color: rgba(255, 204, 199, 1); background-color: rgba(255, 241, 240, 1); color: rgba(255, 77, 79, 1); }
.cl5 { border-color: rgba(255, 241, 184, 1); background-color: rgba(255, 251, 230, 1); color: rgba(250, 173, 20, 1); }
.cl6 { border-color: rgba(255, 204, 199, 1); background-color: rgba(255, 241, 240, 1); color: rgba(255, 77, 79, 1); }
}
}
}
.info-divide {
width: 1600px;
height: 64px;
margin: 16px auto;
background-color: rgba(255, 255, 255, 0.65);
border-radius: 10px;
box-shadow: 0px 0px 20px rgba(25, 69, 130, 0.1);
padding: 5px;
display: flex;
justify-content: space-between;
align-items: center;
div {
width: 530px;
height: 54px;
text-align: center;
font-size: 20px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 54px;
color: rgb(59, 65, 75);
border-radius: 10px;
cursor: pointer;
&:hover {
background: rgba(231, 243, 255, 1);
}
}
.active {
font-size: 24px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 54px;
color: rgb(5, 95, 194);
background-color: rgba(231, 243, 255, 1);
border: 2px solid rgba(174, 214, 255, 1);
}
}
.info-content {
width: 1600px;
margin: 16px auto;
display: flex;
.left {
width: 1064px;
margin-right: 16px;
.left-top {
margin-bottom: 16px;
.chart-box {
width: 100%;
height: 200px;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
}
}
.left-center {
margin-bottom: 16px;
}
/* ============ 最新动态 ============ */
.left-bottom {
display: flex;
flex-direction: column;
:deep(.wrapper-header) {
position: relative;
display: flex;
align-items: center;
height: 56px;
padding: 0 24px;
flex-shrink: 0;
}
:deep(.header-btn) {
top: 16px;
}
.input {
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgba(132, 136, 142, 1);
display: flex;
align-items: center;
input[type="checkbox"] {
width: 14px;
height: 14px;
accent-color: #1890ff;
cursor: pointer;
margin: 0;
}
}
.dynamic-main {
padding: 0 22px;
.main-item {
display: flex;
flex-direction: row;
gap: 20px;
align-items: flex-start;
.time {
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: flex-end;
padding-top: 18px;
.year {
font-size: 16px;
font-weight: 700;
font-family: "Microsoft YaHei";
color: rgba(5, 95, 194, 1);
line-height: 24px;
letter-spacing: 1px;
}
.date {
font-size: 16px;
font-weight: 700;
font-family: "Microsoft YaHei";
color: rgba(5, 95, 194, 1);
line-height: 24px;
letter-spacing: 1px;
}
}
/* 时间轴列:上线段 + 图标 + 下线段 */
.timeline-col {
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: center;
align-self: stretch;
.timeline-line-top {
width: 2px;
height: 28px;
background-color: rgba(234, 236, 238, 1);
flex-shrink: 0;
}
.timeline-icon {
width: 24px;
height: 24px;
flex-shrink: 0;
img {
width: 24px;
height: 24px;
}
}
.timeline-line-bottom {
width: 2px;
flex: 1;
background-color: rgba(234, 236, 238, 1);
}
}
.content {
flex: 1;
min-width: 0;
padding: 16px 0;
display: flex;
flex-direction: column;
gap: 12px;
/* remarks=true 涉华观点卡片 */
.content-type1 {
background-color: rgba(246, 250, 255, 1);
border-radius: 4px;
border: 1px solid rgba(231, 243, 255, 1);
padding: 12px 16px;
display: flex;
flex-direction: column;
gap: 8px;
.content-title1 {
font-size: 16px;
font-weight: 700;
font-family: "Microsoft YaHei";
color: rgba(59, 65, 75, 1);
line-height: 24px;
cursor: pointer;
}
.content-title-en {
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
color: rgba(59, 65, 75, 1);
line-height: 24px;
cursor: pointer;
text-align: justify;
}
}
/* remarks=false 新闻 */
.content-type2 {
.content-title2 {
font-size: 18px;
font-weight: 700;
font-family: "Microsoft YaHei";
color: rgba(59, 65, 75, 1);
line-height: 24px;
cursor: pointer;
}
}
.content-contentcontent {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
text-overflow: ellipsis;
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
color: rgba(95, 101, 108, 1);
line-height: 24px;
cursor: pointer;
}
/* 标签 + 来源行 */
.content-tag {
display: flex;
justify-content: space-between;
align-items: center;
.origin {
font-size: 14px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 22px;
color: rgba(132, 136, 142, 1);
cursor: pointer;
white-space: nowrap;
flex-shrink: 0;
}
}
}
}
}
.pagination {
width: 100%;
height: 76px;
margin: 0;
display: flex;
padding: 0 31px 0 36px;
justify-content: space-between;
align-items: center;
border-top: 1px solid rgba(234, 236, 238, 1);
position: relative;
z-index: 3;
background: #fff;
flex-shrink: 0;
.total {
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
color: rgba(59, 65, 75, 1);
}
:deep(.custom-pagination) {
display: flex;
align-items: center;
}
:deep(.custom-pagination.el-pagination.is-background .el-pager li) {
min-width: 32px;
height: 32px;
line-height: 32px;
border-radius: 6px;
margin: 0 6px;
border: 1px solid rgba(0, 0, 0, 0.15);
background-color: #fff;
color: rgb(95, 101, 108);
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
}
:deep(.custom-pagination.el-pagination.is-background .el-pager li.is-active) {
background-color: #fff;
color: rgba(22, 119, 255, 1);
border-color: rgba(22, 119, 255, 1);
}
:deep(.custom-pagination.el-pagination.is-background .el-pager li.is-ellipsis) {
border: none;
background-color: transparent;
color: rgb(95, 101, 108);
min-width: 16px;
margin: 0 6px;
}
:deep(.custom-pagination.el-pagination.is-background .btn-prev),
:deep(.custom-pagination.el-pagination.is-background .btn-next) {
min-width: 32px;
height: 32px;
border-radius: 6px;
border: 1px solid rgba(0, 0, 0, 0.15);
background-color: #fff;
color: rgb(95, 101, 108);
font-size: 16px;
font-family: "Microsoft YaHei";
margin: 0 6px;
}
:deep(.custom-pagination.el-pagination.is-background .btn-prev.is-disabled),
:deep(.custom-pagination.el-pagination.is-background .btn-next.is-disabled) {
color: rgba(95, 101, 108, 0.45);
border-color: rgb(235, 238, 242);
background-color: #fff;
}
}
}
}
.right {
width: 520px;
.right-top {
margin-bottom: 16px;
.main-content {
width: 520px;
padding: 0 48px 50px 34px;
margin-top: 16px;
.baseInfo {
width: 438px;
padding-bottom: 12px;
border-bottom: 1px solid rgb(234, 236, 238);
.baseInfo-item {
display: flex;
margin-bottom: 12px;
.baseInfo-item-title {
flex: 0 0 110px;
width: 110px;
font-size: 16px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(59, 65, 75);
white-space: nowrap;
}
.baseInfo-item-content {
margin-left: 14px;
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(59, 65, 75);
}
.long {
width: 306px;
}
.longlong {
width: 306px;
display: flex;
flex-wrap: wrap;
gap: 4px;
.span {
padding: 2px 8px;
border-radius: 4px;
background-color: rgba(230, 244, 255, 1);
border: 1px solid rgba(186, 224, 255, 1);
color: rgba(22, 119, 255, 1);
font-size: 14px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 20px;
}
}
}
}
.company {
width: 438px;
padding-top: 19px;
.company-title {
font-size: 16px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(59, 65, 75);
margin-bottom: 19px;
}
.company-content {
width: 409px;
max-height: 114px;
display: flex;
flex-wrap: wrap;
overflow-y: auto;
scrollbar-width: none;
-ms-overflow-style: none;
.company-item {
width: 180px;
height: 49px;
margin-bottom: 16px;
display: flex;
align-items: center;
cursor: pointer;
img {
width: 48px;
height: 48px;
margin-right: 8px;
}
.company-cn {
width: 130px;
font-size: 16px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(59, 65, 75);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.company-name {
width: 130px;
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(95, 101, 108);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.company-item:nth-child(2n-1) {
margin-right: 39px;
}
}
&::-webkit-scrollbar {
display: none;
}
}
}
}
.right-bottom {
.content-main {
width: 480px;
margin-left: 16px;
.content-item {
width: 454px;
margin-bottom: 16px;
margin-left: 26px;
position: relative;
.image01 {
width: 14px;
height: 12.13px;
position: absolute;
top: 8px;
left: -26px;
}
.content-item-time {
font-size: 16px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 30px;
color: rgb(5, 95, 194);
margin-bottom: 8px;
}
.content-item-title {
font-size: 16px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 30px;
color: rgb(59, 65, 75);
margin-bottom: 8px;
}
.content-item-content {
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(95, 101, 108);
margin-bottom: 8px;
}
.content-item-door {
width: 300px;
height: 32px;
display: flex;
align-items: center;
padding: 4px 0 4px 11px;
border-radius: 4px;
background-color: rgba(255, 246, 240, 1);
border: 1px solid rgba(250, 140, 22, 0.4);
img {
width: 20px;
height: 24px;
margin-right: 10px;
}
span {
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgba(255, 149, 77, 1);
}
}
}
:deep(.el-empty) {
padding-bottom: 30px;
}
}
}
}
}
}
/* ============ 自适应高度覆盖 ============ */
.auto-height-box {
height: auto !important;
:deep(.wrapper-main) {
height: auto !important;
overflow: visible !important;
}
}
/* ============ 科技领袖特有 ============ */
.tech-leader {
.info-content {
height: auto;
}
}
/* ============ 国会议员特有 ============ */
.member-of-congress {
.select {
width: 120px;
}
.fund-table-main {
padding: 0 24px;
}
.table-pagination {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 16px;
padding-bottom: 16px;
.table-pagination-total {
font-size: 16px;
color: rgba(59, 65, 75, 1);
}
}
:deep(.el-table) {
--el-table-border-color: rgba(234, 236, 238, 1);
--el-table-header-bg-color: transparent;
tr {
background-color: rgba(255, 255, 255, 1);
}
.highlight-row {
background-color: rgba(247, 248, 249, 1) !important;
}
th.el-table__cell {
border-bottom: 1px solid rgba(234, 236, 238, 1);
}
td.el-table__cell {
border-bottom: none;
}
}
}
/* ============ 智库特有 ============ */
.think-tank-person {
.info-content {
height: auto;
}
}
/* ============ 弹框样式 ============ */
.viewpoint-dialog {
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
}
.viewpoint-dialog.el-dialog {
width: 761px !important;
height: 669px !important;
max-height: 669px !important;
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
overflow: hidden;
display: flex;
flex-direction: column;
}
.viewpoint-dialog .el-dialog__header {
padding: 0;
margin: 0;
height: 52px;
flex-shrink: 0;
}
.viewpoint-dialog .el-dialog__body {
padding: 0;
flex: 1;
overflow: hidden;
}
.viewpoint-header {
display: flex;
justify-content: space-between;
align-items: center;
height: 52px;
padding: 0 24px;
border-bottom: 1px solid rgba(231, 243, 255, 1);
box-sizing: border-box;
}
.viewpoint-title {
display: flex;
align-items: center;
}
.viewpoint-tag {
font-size: 18px;
font-family: 'Source Han Sans CN', 'Microsoft YaHei', sans-serif;
font-weight: 700;
color: rgba(206, 79, 81, 1);
line-height: 24px;
}
.viewpoint-label {
font-size: 18px;
font-family: 'Source Han Sans CN', 'Microsoft YaHei', sans-serif;
font-weight: 700;
color: rgba(59, 65, 75, 1);
line-height: 24px;
margin-left: 2px;
}
.viewpoint-close {
width: 32px;
height: 32px;
cursor: pointer;
}
.viewpoint-body {
padding: 18px 24px 24px;
height: calc(669px - 52px);
max-height: calc(669px - 52px);
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 12px;
box-sizing: border-box;
}
.viewpoint-item {
display: flex;
gap: 8px;
align-items: flex-start;
}
.viewpoint-avatar {
flex-shrink: 0;
padding-top: 12px;
}
.viewpoint-content {
flex: 1;
display: flex;
align-items: flex-start;
}
.viewpoint-arrow {
width: 9px;
height: 72px;
flex-shrink: 0;
background: url('../../techLeader/assets/arrow-icon.png') no-repeat center / 100%;
}
.member-of-congress .viewpoint-arrow {
background: url('../../memberOfCongress/assets/arrow-icon.png') no-repeat center / 100%;
}
.viewpoint-card {
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
padding: 8px 12px;
background-color: rgba(246, 250, 255, 1);
border: 1px solid rgba(231, 243, 255, 1);
border-radius: 4px;
}
.viewpoint-card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.viewpoint-name {
font-size: 16px;
font-family: 'Source Han Sans CN', 'Microsoft YaHei', sans-serif;
font-weight: 700;
color: rgba(59, 65, 75, 1);
line-height: 24px;
}
.viewpoint-job {
font-size: 16px;
font-family: 'Source Han Sans CN', 'Microsoft YaHei', sans-serif;
font-weight: 400;
color: rgba(95, 101, 108, 1);
line-height: 30px;
}
.viewpoint-desc {
font-size: 16px;
font-family: 'Source Han Sans CN', 'Microsoft YaHei', sans-serif;
font-weight: 400;
color: rgba(59, 65, 75, 1);
line-height: 24px;
text-align: justify;
}
.viewpoint-empty {
text-align: center;
padding: 40px 0;
color: rgba(170, 173, 177, 1);
font-size: 14px;
}
/* ============ 履历 tabs ============ */
.resume-tabs {
display: flex;
flex-direction: row;
gap: 8px;
align-items: flex-start;
}
.resume-tab {
width: 90px;
height: 28px;
display: flex;
justify-content: center;
align-items: center;
padding: 1px 8px;
border-radius: 4px;
border: 1px solid rgba(230, 231, 232, 1);
background: rgba(255, 255, 255, 1);
color: rgba(59, 65, 75, 1);
font-size: 16px;
font-family: 'Microsoft YaHei', sans-serif;
font-weight: 400;
line-height: 30px;
cursor: pointer;
box-sizing: border-box;
}
.resume-tab.active {
background: rgba(231, 243, 255, 1);
border-color: rgba(5, 95, 194, 1);
color: rgba(5, 95, 194, 1);
}
.tab-select {
width: 120px;
:deep(.el-input__wrapper) {
box-shadow: none;
border-radius: 10px;
height: 28px;
}
:deep(.el-input__wrapper:hover),
:deep(.el-input__wrapper.is-focus) {
box-shadow: none !important;
}
:deep(.el-select__placeholder) {
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 14px;
font-weight: 400;
line-height: 22px;
}
:deep(.el-select-dropdown__item) {
text-align: center;
justify-content: center;
}
:deep(.el-popper.is-light) {
border-radius: 10px;
}
}
/* ============ 基本信息通用样式 ============ */
.baseInfo-item {
display: flex;
align-items: flex-start;
}
.baseInfo-item-title {
flex: 0 0 110px;
font-size: 16px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(59, 65, 75);
white-space: nowrap;
}
.baseInfo-item-content {
flex: 1;
min-width: 0;
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(59, 65, 75);
display: flex;
flex-wrap: wrap;
gap: 8px 12px;
}
.education-item {
white-space: nowrap;
}
/* ============ 历史提案 tab ============ */
.proposal-wrapper {
width: 1600px;
margin: 16px auto;
position: relative;
}
.proposal-tab-switcher {
position: absolute;
left: -124px;
top: 0;
display: flex;
flex-direction: column;
gap: 16px;
align-items: center;
}
.proposal-tab {
width: 112px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
padding: 0 16px;
border-radius: 16px;
border: none;
font-size: 16px;
font-family: 'Microsoft YaHei', sans-serif;
font-weight: 400;
color: var(--text-primary-65-color);
background-color: var(--bg-white-100);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
cursor: pointer;
white-space: nowrap;
&.active {
color: var(--bg-white-100);
background-color: var(--color-primary-100);
box-shadow: none;
}
}
.proposal-tab-arrow {
flex-shrink: 0;
}
</style>
<style lang="scss">
/* 下拉面板 teleported 到 body,需要非 scoped 样式 */
.el-select__popper .el-select-dropdown__item {
text-align: center;
justify-content: center;
}
</style>
<template>
<div class="character-page">
<img src="./assets/images/background.png" alt="" class="bg" />
<!-- 主要内容 -->
<ModuleHeader />
<!-- 主要内容 -->
<div class="main">
<!-- 科技领袖 -->
<tech-leader v-if="type == 1"/>
<!-- 国会议员 -->
<member-of-congress v-if="type == 2" />
<!-- 智库研究人员 -->
<think-tank-person v-if="type == 3" />
<unified-character :type="type" :person-id="personId" />
</div>
</div>
</template>
......@@ -16,9 +12,8 @@
<script setup>
import { ref, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import TechLeader from './components/techLeader/index.vue';
import MemberOfCongress from './components/memberOfCongress/index.vue';
import ThinkTankPerson from './components/thinkTankPerson/index.vue';
import UnifiedCharacter from './components/unified/index.vue';
import ModuleHeader from '@/components/base/moduleHeader/index.vue';
import { getCharacterGlobalInfo } from "@/api/characterPage/characterPage.js";
......@@ -57,7 +52,7 @@ const getCharacterGlobalInfoFn = async () => {
}catch(error){
}
};
onMounted(() => {
......
......@@ -77,6 +77,7 @@ import SchoolDetail from './tabs/SchoolDetail.vue'
import ResearchStrength from './tabs/ResearchStrength.vue'
import Cooperation from './tabs/Cooperation.vue'
import OtherInfo from './tabs/OtherInfo.vue'
import bgImg from './assets/background.png'
// 从路由获取 orgId
const route = useRoute()
......@@ -122,24 +123,24 @@ const basicInfo = ref({
// 重点人物
const keyPeople = ref([
{
name: '艾伦·M·加伯',
title: '第30任校长',
{
name: '艾伦·M·加伯',
title: '第30任校长',
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=100&h=100&fit=crop&crop=face'
},
{
name: '迈克尔·桑德尔',
title: '政府学教授',
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop&crop=face'
},
{
name: '拉凯什·库拉纳',
title: '哈佛学院院长',
{
name: '拉凯什·库拉纳',
title: '哈佛学院院长',
avatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=100&h=100&fit=crop&crop=face'
},
{
name: '乔治·丘吉',
title: '哈佛医学院遗传学教授',
{
name: '迈克尔·桑德尔',
title: '政府学教授',
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop&crop=face'
},
{
name: '乔治·丘吉',
title: '哈佛医学院遗传学教授',
avatar: 'https://images.unsplash.com/photo-1560250097-0b93528c311a?w=100&h=100&fit=crop&crop=face'
}
])
......@@ -147,9 +148,9 @@ const keyPeople = ref([
// 统计数据
const statistics = ref([
{ label: '诺贝尔奖', value: '161', color: 'rgba(5, 95, 194, 1)' },
{ label: '图灵奖', value: '18', color: 'rgba(250, 173, 20, 1)' },
{ label: '图灵奖', value: '18', color: 'rgba(5, 95, 194, 1)' },
{ label: '菲尔兹奖', value: '14', color: 'rgba(5, 95, 194, 1)' },
{ label: '美国院士', value: '273', color: 'rgba(250, 173, 20, 1)' }
{ label: '美国院士', value: '273', color: 'rgba(5, 95, 194, 1)' }
])
// 历史事件
......@@ -198,7 +199,7 @@ const latestDynamics = ref([
date: '10月10日',
title: '哈佛大学再遭电话钓鱼攻击致数据泄露',
content: '一名未授权人员通过社会工程手段入侵校友数据库,泄露联系方式、捐赠记录等信息。这是该校2025年内第二次重大数据泄露事件。',
tags: [{ name: '数安治理', type: 'tag9' }],
tags: [{ name: '数据泄露', type: 'tag9' }],
isHighlight: true
},
{
......@@ -213,7 +214,7 @@ const latestDynamics = ref([
year: '2025',
date: '9月29日',
title: '常春藤盟校联合加强网络安全联盟',
content: '哈佛与普林斯顿、耶鲁、斯坦福比亚等校宣布成立"常春藤网络安全协作体",应对日益频繁的恶意网络攻击。',
content: '哈佛与普林斯顿、耶鲁、斯坦福等校宣布成立"常春藤网络安全协作体",应对日益频繁的恶意网络攻击。',
tags: [{ name: '生物科技', type: 'tag2' }],
isHighlight: true
},
......@@ -230,77 +231,61 @@ const latestDynamics = ref([
date: '9月15日',
title: '艾伦·M·加伯接任临时校长',
content: '原教务长艾伦·加伯(Alan M. Garber)出任哈佛第30任校长(临时),并完整复学术诚信与校园团结。',
tags: [{ name: '集成电路', type: 'tag6' }],
isHighlight: true
tags: [{ name: '集成电路', type: 'tag6' }]
},
{
year: '2025',
date: '9月11日',
title: '联邦法官叫停特朗普政府"禁招国际学生"令',
content: '2025年5月29日,马萨诸塞州联邦法官艾莉森·伯恩斯顿发布临时限制令,阻止国土安全部驱逐哈佛招收国际学生资质。',
tags: [{ name: '集成电路', type: 'tag6' }],
isHighlight: true
tags: [{ name: '集成电路', type: 'tag6' }]
},
{
year: '2025',
date: '8月28日',
title: '哈佛启动"AI与伦理"研究中心',
content: '投资500万美元成立新的跨学科研究中心,旨在探索人工智能技术的伦理应用和社会影响,汇聚全球顶尖学者。',
date: '9月10日',
title: '特朗普政府要求哈佛30天内提交"改革证据"',
content: '美国教育部正式致函哈佛大学,要求在30天内提交针对招生公平、言论自由和反犹主义处理的整改方案与证据。',
tags: [{ name: '人工智能', type: 'tag1' }]
},
{
year: '2025',
date: '8月15日',
title: '哈佛医学院宣布重大癌症研究突破',
content: '研究团队开发出新型免疫疗法,在临床试验中显示出对晚期肺癌的显著疗效,成果已发表于《自然》杂志。',
tags: [{ name: '生物医学', type: 'tag2' }]
},
{
year: '2025',
date: '7月30日',
title: '哈佛大学与MIT联合建立量子计算实验室',
content: '两校联合投资1亿美元,共同建立量子计算与信息研究实验室,推进量子技术的商业化应用。',
tags: [{ name: '前沿科技', type: 'tag11' }]
},
{
year: '2025',
date: '7月10日',
title: '2025年学年入学录取率创历史新低',
content: '2025年度本科生申请录取率仅为3.2%,创造了美国高等教育历史新低,共录取1,380名学生。',
tags: [{ name: '教育新闻', type: 'tag12' }]
date: '9月1日',
title: '哈佛2025届毕业典礼成政治声援现场',
content: '毕业典礼上多名学生举起声援巴勒斯坦标语,部分毕业生在演讲环节集体转身背对校方领导,表达对学校处理巴以议题方式的不满。',
tags: [{ name: '集成电路', type: 'tag6' }]
},
{
year: '2025',
date: '6月20日',
title: '哈佛成立"气候变化研究全球中心"',
content: '新成立的中心将汇聚来自20个国家的气候科学家,共同应对全球气候变化挑战,年度预算达3000万美元。',
tags: [{ name: '气候变化', type: 'tag7' }]
date: '8月25日',
title: '哈佛教授警告:国际博士生面临系统性驱逐风险',
content: '多位哈佛教授联名发表公开信,警告当前移民政策下国际博士生面临签证被撤销和驱逐出境的系统性风险,呼吁联邦政府保障学术自由。',
tags: [{ name: '能源', type: 'tag7' }, { name: '先进制造', type: 'tag10' }]
},
{
year: '2025',
date: '6月5日',
title: '哈佛建筑学院获普利兹克建筑学奖',
content: '著名建筑师与哈佛建筑学院合作的项目获得2025年普利兹克建筑学奖,该奖项被誉为建筑领域的"诺贝尔奖"。',
tags: [{ name: '艺术建筑', type: 'tag8' }]
date: '8月19日',
title: '巴以冲突激化哈佛校园对立',
content: '哈佛校园内亲以色列和亲巴勒斯坦学生团体多次发生激烈对峙,校方被迫加强安保措施并设立"自由表达区"。',
tags: [{ name: '先进制造', type: 'tag10' }]
},
{
year: '2025',
date: '5月15日',
title: '哈佛捐赠基金规模首次突破500亿美元',
content: '截至2024年6月30日,哈佛捐赠基金达501亿美元,创历史新高,位列全球大学捐赠基金首位。',
tags: [{ name: '财务报告', type: 'tag13' }]
date: '8月19日',
title: '联邦法院裁定哈佛招生不构成种族歧视',
content: '联邦上诉法院驳回原告上诉,维持原判认定哈佛大学在本科招生中考虑种族因素的做法不构成非法歧视。',
tags: [{ name: '先进制造', type: 'tag10' }]
},
{
year: '2025',
date: '4月28日',
title: '哈佛法学院宣布取消LSAT考试要求',
content: '从2025年秋季开始,申请人可无需提交LSAT成绩,法学院将采用更全面的综合评估方式审核申请。',
tags: [{ name: '招生政策', type: 'tag14' }]
date: '8月19日',
title: '哈佛捐赠基金规模达509亿美元,全球第一',
content: '截至2025财年末,哈佛大学捐赠基金规模达到509亿美元,继续稳居全球高校捐赠基金榜首,年均回报率达9.8%。',
tags: [{ name: '先进制造', type: 'tag10' }]
}
])
// 分页
const totalDynamics = ref(256)
const totalDynamics = ref(153)
const handlePageChange = (page) => {
console.log('Page changed to:', page)
......@@ -316,7 +301,7 @@ const handleVisitWebsite = () => {
.university-detail {
width: 100%;
height: 100vh;
background: rgba(247, 248, 249, 1);
background: url('/src/views/innovationSubject/innovativeInstitutions/assets/background.png') no-repeat center center;
position: relative;
overflow: hidden;
}
......@@ -327,13 +312,12 @@ const handleVisitWebsite = () => {
position: absolute;
top: 0;
left: 0;
background: linear-gradient(135deg, #1e5799 0%, #207cca 50%, #2989d8 100%);
z-index: 1;
.bg-overlay {
width: 100%;
height: 100%;
background: linear-gradient(to bottom, rgba(30, 87, 153, 0.8), rgba(32, 124, 202, 0.6));
background: linear-gradient(to bottom, rgba(30, 87, 153, 0.5), rgba(32, 124, 202, 0.3));
}
}
......@@ -347,87 +331,104 @@ const handleVisitWebsite = () => {
}
.header-section {
padding: 80px;
padding: 80px 160px;
display: flex;
justify-content: center;
padding-bottom: 16px;
.header-content {
width: 1600px;
height: 160px;
display: flex;
align-items: flex-start;
gap: 24px;
background: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(255, 255, 255, 1);
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
padding: 16px 19px;
position: relative;
.logo {
width: 110px;
height: 130px;
width: 128px;
height: 128px;
background: rgba(255, 255, 255, 0.9);
border-radius: 8px;
padding: 10px;
border-radius: 100px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
object-fit: cover;
border-radius: 100px;
}
}
.info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
.name-row {
display: flex;
align-items: baseline;
gap: 16px;
margin-bottom: 8px;
flex-direction: column;
gap: 4px;
.name-cn {
font-size: 32px;
font-size: 24px;
font-weight: 700;
font-family: 'Microsoft YaHei', sans-serif;
color: white;
font-family: 'Source Han Sans CN', 'Microsoft YaHei', sans-serif;
color: rgba(59, 65, 75, 1);
margin: 0;
}
.name-en {
font-size: 16px;
font-weight: 400;
font-family: 'Microsoft YaHei', sans-serif;
color: rgba(255, 255, 255, 0.9);
font-family: 'Source Han Sans CN', 'Microsoft YaHei', sans-serif;
color: rgba(59, 65, 75, 1);
line-height: 24px;
}
}
.description {
font-size: 16px;
font-weight: 400;
font-family: 'Microsoft YaHei', sans-serif;
font-family: 'Source Han Sans CN', 'Microsoft YaHei', sans-serif;
line-height: 24px;
color: rgba(255, 255, 255, 0.9);
margin-bottom: 12px;
color: rgba(59, 65, 75, 1);
text-align: justify;
margin: 0;
}
.tags {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
}
.visit-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
gap: 8px;
padding: 5px 16px;
background: rgba(5, 95, 194, 1);
border-radius: 4px;
border-radius: 6px;
color: white;
font-size: 14px;
font-size: 16px;
font-family: 'Microsoft YaHei', sans-serif;
cursor: pointer;
transition: opacity 0.2s;
flex-shrink: 0;
line-height: 22px;
align-self: flex-start;
margin-top: 16px;
&:hover {
opacity: 0.9;
}
......
......@@ -21,28 +21,31 @@
<div class="flex-display content-row">
<div class="left-col">
<AnalysisBox title="打压动态" :showAllBtn="false">
<AnalysisBox title="动态" :showAllBtn="false">
<div class="news-list-wrap">
<div class="news-scroll-area">
<div
v-for="item in newsList"
:key="item.id"
class="news-item flex-display mouse-hover"
@click="handleNewsClick(item)"
>
<div class="item-thumb flex-display-center">
<span class="item-thumb-text">新闻资讯</span>
</div>
<div class="item-body">
<p class="text-tip-1-bold text-primary-80-clor item-title">{{ item.title }}</p>
<p class="text-tip-2 text-primary-65-clor item-content">{{ item.content }}</p>
</div>
<div class="item-meta flex-display">
<span class="text-tip-2 text-primary-50-clor">{{ item.date }}</span>
<span class="text-tip-2 text-primary-50-clor meta-sep">·</span>
<span class="text-tip-2 text-primary-50-clor">{{ item.source }}</span>
<template v-for="(item, idx) in newsList" :key="item.id">
<div
class="news-item flex-display mouse-hover"
@click="handleNewsClick(item)"
>
<div class="item-thumb flex-display-center">
<span class="item-thumb-text">新闻资讯</span>
</div>
<div class="item-body">
<div class="item-top flex-display">
<p class="text-tip-1-bold text-primary-80-clor item-title">{{ item.title }}</p>
<div class="item-meta flex-display">
<span class="text-tip-2 text-primary-65-clor">{{ item.date }}</span>
<span class="text-tip-2 text-primary-65-clor meta-sep">·</span>
<span class="text-tip-2 text-primary-65-clor">{{ item.source }}</span>
</div>
</div>
<p class="text-tip-2 text-primary-65-clor item-content">{{ item.content }}</p>
</div>
</div>
</div>
<div v-if="idx < newsList.length - 1" class="news-divider"></div>
</template>
</div>
<div class="pagination-row flex-display">
......@@ -59,7 +62,7 @@
@click="changePage(p)"
>{{ p }}</button>
<span v-if="pagination.page < pagination.totalPages - 2" class="page-ellipsis text-primary-50-clor">...</span>
<button class="page-btn" :class="{ active: pagination.page === pagination.totalPages }" @click="changePage(pagination.totalPages)">{{ pagination.totalPages }}</button>
<button v-if="pagination.totalPages > 1" class="page-btn" :class="{ active: pagination.page === pagination.totalPages }" @click="changePage(pagination.totalPages)">{{ pagination.totalPages }}</button>
<button class="page-btn" :disabled="pagination.page === pagination.totalPages" @click="changePage(pagination.page + 1)">&gt;</button>
</div>
</div>
......@@ -68,7 +71,7 @@
</div>
<div class="right-col">
<AnalysisBox title="联盟简介" :showAllBtn="false">
<AnalysisBox title="简介" :showAllBtn="false">
<div class="intro-wrap">
<div class="intro-row flex-display">
<span class="text-tip-2-bold text-primary-80-clor intro-label">时间:</span>
......@@ -104,7 +107,7 @@ import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import AnalysisBox from '@/components/base/boxBackground/analysisBox.vue'
import AreaTag from '@/components/base/AreaTag/index.vue'
import { getNewsDetail, getNewsList } from './mock/newsDetail'
import { getUnionIntroduction, getUnionDynamicList } from '@/api/ruleRestriction/index.js'
const route = useRoute()
const router = useRouter()
......@@ -125,10 +128,10 @@ const newsDetail = ref({
const newsList = ref([])
const pagination = ref({
page: 5,
page: 1,
pageSize: 10,
total: 153,
totalPages: 16
total: 0,
totalPages: 1
})
const visiblePages = computed(() => {
......@@ -145,16 +148,48 @@ const visiblePages = computed(() => {
return pages
})
const loadNewsDetail = () => {
newsDetail.value = getNewsDetail(newsId.value)
const loadNewsDetail = async () => {
try {
const res = await getUnionIntroduction(newsId.value)
if (res.code === 200) {
const data = res.data
newsDetail.value = {
title: data.unionNameZh || '',
subTitle: data.unionName || '',
tags: data.domains || [],
time: data.time || '',
background: data.background || '',
countries: (data.memberCountries || []).map(c => ({ name: c.countryName }))
}
}
} catch (error) {
console.error('获取联盟简介失败:', error)
}
}
const loadNewsList = (page = 1) => {
const data = getNewsList(page, pagination.value.pageSize)
newsList.value = data.list
pagination.value.page = data.page
pagination.value.total = data.total
pagination.value.totalPages = data.totalPages
const loadNewsList = async (page = 1) => {
try {
const res = await getUnionDynamicList({
unionId: newsId.value,
currentPage: page,
pageSize: pagination.value.pageSize
})
if (res.code === 200) {
const data = res.data
newsList.value = (data.content || []).map(item => ({
id: item.newsId,
title: item.title || '',
content: item.summary || '',
date: item.publishedTime || '',
source: item.source || ''
}))
pagination.value.page = page
pagination.value.total = data.totalElements || 0
pagination.value.totalPages = data.totalPages || 1
}
} catch (error) {
console.error('获取联盟动态失败:', error)
}
}
const changePage = (page) => {
......@@ -168,11 +203,14 @@ const handleNewsClick = (item) => {
onMounted(() => {
loadNewsDetail()
loadNewsList(pagination.value.page)
loadNewsList(1)
})
</script>
<style lang="scss" scoped>
:deep(.main-container1){
overflow: auto;
}
.page-wrap {
width: 100%;
min-height: 100vh;
......@@ -228,7 +266,6 @@ onMounted(() => {
.left-col {
width: 1064px;
height: 1284px;
flex-shrink: 0;
}
......@@ -241,33 +278,32 @@ onMounted(() => {
.news-list-wrap {
display: flex;
flex-direction: column;
height: calc(100% - 48px);
padding: 0 20px;
}
.news-scroll-area {
flex: 1;
min-height: 0;
overflow-y: auto;
}
.news-item {
gap: 16px;
padding: 16px 0;
border-bottom: 1px solid #eaecee;
align-items: flex-start;
gap: 20px;
padding: 0;
align-items: center;
cursor: pointer;
transition: background 0.15s;
}
&:last-child {
border-bottom: none;
}
.news-divider {
width: 100%;
height: 1px;
background: #f0f2f4;
margin: 12px 0;
}
.item-thumb {
width: 100px;
height: 70px;
border-radius: 6px;
width: 97px;
height: 72px;
border-radius: 4px;
background: linear-gradient(135deg, #4a7bf7 0%, #6b8cff 100%);
flex-shrink: 0;
}
......@@ -280,13 +316,24 @@ onMounted(() => {
.item-body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.item-top {
align-items: center;
width: 100%;
gap: 0;
}
.item-title {
margin: 0 0 6px 0;
margin: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
flex: 1;
min-width: 0;
}
.item-content {
......
......@@ -224,6 +224,19 @@ export default {
:deep(.el-timeline) {
max-width: 688px !important;
padding: 0;
overflow: hidden;
}
:deep(.el-timeline-item) {
padding-bottom: 4px;
}
:deep(.el-timeline-item:last-child) {
padding-bottom: 0;
}
:deep(.el-timeline-item:last-child .el-timeline-item__wrapper) {
padding-bottom: 0;
}
:deep(.el-timeline-item__wrapper) {
......@@ -253,4 +266,9 @@ export default {
text-align: justify;
}
:deep(.el-timeline-item:last-child .el-timeline-item__tail) {
visibility: hidden;
display: none;
}
</style>
......@@ -28,9 +28,9 @@
<!-- 涉华观点次数列 -->
<div class="row-col col-count">
<el-progress :percentage="getProgress(item.mediaQuoteCount)" :show-text="false"
style="/* 矩形 244 */ width: 82px; height: 8px" :status="getStatus(getProgress(item.mediaQuoteCount))" />
<span class="count-text">{{ item.mediaQuoteCount }}</span>
<el-progress :percentage="getProgress(item.chinaRelatedCount)" :show-text="false"
style="/* 矩形 244 */ width: 82px; height: 8px" :status="getStatus(getProgress(item.chinaRelatedCount))" />
<span class="count-text">{{ item.chinaRelatedCount }}</span>
</div>
<!-- 媒体引用次数列 -->
......@@ -46,7 +46,6 @@
</template>
<script setup>
import { ref,onMounted,defineProps,watch } from "vue";
import personData from "../json/personData.json"; // 引入JSON数据
import {getMainCharactersView } from "@/api/technologyFigures/technologyFigures";
import { useCharacterNav } from "../utils/useCharacterNav";
const { handleClickToCharacter } = useCharacterNav();
......@@ -99,14 +98,16 @@ const handlegetMainCharactersViewFn = async () => {
});
}
} catch (error) {}
} catch (error) {
console.error("获取主要人物涉华观点统计失败:", error);
}
};
onMounted(async () => {
handlegetMainCharactersViewFn();
});
const personList = ref();
const personList = ref([]);
// 进度条状态
const getStatus = _percent => {
const percent = _percent;
......@@ -120,8 +121,9 @@ const getStatus = _percent => {
};
// 计算进度条宽度(基于最大值的百分比)
const getMaxCount = () => {
const counts = personList.value.flatMap(item => [item.chinaRelatedCount, item.mediaQuoteCount]);
return Math.max(...counts);
const counts = personList.value.flatMap(item => [item.chinaRelatedCount || 0, item.mediaQuoteCount || 0]);
if (counts.length === 0) return 1;
return Math.max(...counts, 1);
};
const getProgress = count => (count / getMaxCount()) * 100;
</script>
......
......@@ -12,7 +12,7 @@
<img :src="item.avatar" alt="" class="source-library-avatar" />
</div>
<div class="source-library-text-content">
<div class="card-main" :class="{ 'no-title': !item.title }">
<div class="card-main" ">
<h3 class="source-library-name">{{ item.name }}</h3>
<p class="source-library-title" v-if="item.title">{{ item.title }}</p>
<div class="taglist">
......@@ -177,7 +177,11 @@ const handleClcikToCharacter = async id => {
ElMessage.warning("找不到当前人员的类型值!");
return;
}
} catch (error) {}
} catch (error) {
if (error.name !== "AbortError") {
console.error("获取人物资源库失败:", error);
}
}
};
onMounted(async () => {
......@@ -209,7 +213,7 @@ const handlePageChange = p => {
box-sizing: border-box;
display: flex;
border: 1px solid rgba(234, 236, 238, 1);
border-radius: var(---10, 10px);
border-radius: 10px;
padding: 20px 18px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background: rgba(255, 255, 255, 1);
......@@ -249,9 +253,9 @@ const handlePageChange = p => {
.source-library-avatar {
/* 椭圆 142 */
width: 88px;
height: 88px;
border-radius: 50%;
width: 92px;
height: 121px;
// border-radius: 50%;
}
.source-library-text-content {
......
......@@ -81,11 +81,6 @@
import * as echarts from "echarts";
import worldJson from "@/assets/json/world.json";
import { getCharacterTrends } from "@/api/technologyFigures/technologyFigures";
import { ref } from "vue";
const CharacterTrends = ref([]);
const time = ref("");
const date = ref("");
const handlegetCharacterTrendsFn = async () => {
const params = {
......@@ -104,7 +99,9 @@ const handlegetCharacterTrendsFn = async () => {
};
});
}
} catch (error) {}
} catch (error) {
console.error("获取人物动向失败:", error);
}
};
function getDateDaysAgo(days) {
......@@ -169,6 +166,9 @@ export default {
tickWidth: 10,
viewStartIndex: 0,
viewDays: 120,
CharacterTrends: [],
time: "",
date: "",
};
},
computed: {
......
......@@ -12,7 +12,7 @@
<p class="speech-stance-title">{{ item.positionTitle }}</p>
</div>
<p class="speech-stance-content" :title="item.remarks">{{ item.remarks }}</p>
<p class="speech-stance-content" :title="item.remarks"><span class="speech-stance-text">{{ item.remarks }}</span></p>
</div>
</div>
......@@ -95,11 +95,11 @@ const handleClcikToCharacter = async id => {
const aId = ref();
const params = ref({});
watch(() => props.fieldSelected, (val) => {
aId.value = props.areaId;
if(val !== "全部领域"){
params.value.industryId = aId.value;
watch(() => [props.fieldSelected, props.areaId], (val) => {
const [fieldVal, areaVal] = val;
aId.value = areaVal;
if(fieldVal !== "全部领域"){
params.value.industryId = areaVal;
}else{
params.value = {};
}
......@@ -117,7 +117,9 @@ const handlegetPersonRelationFn = async () => {
if (res.code === 200) {
PersonRelation.value = res.data;
}
} catch (error) {}
} catch (error) {
console.error("获取重要人物言论及立场失败:", error);
}
};
onMounted(async () => {
......@@ -129,15 +131,13 @@ onMounted(async () => {
<style scoped>
.speech-stance-container {
width: 1530px;
/* max-width: 900px; */
margin: 0 auto;
margin: 5px 35px;
margin: 15px 35px;
}
.speech-stance-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 30px 9px;
gap: 28px 9px;
}
/* 响应式:小屏变一列 */
......@@ -204,8 +204,16 @@ onMounted(async () => {
line-height: 30px;
letter-spacing: 0px;
text-align: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
/* 宽度保持不变,ellipsis 移至内部 span */
}
.speech-stance-text {
display: inline-block;
max-width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-right: 24px;
vertical-align: top;
}
</style>
\ No newline at end of file
......@@ -81,10 +81,10 @@
<OverviewMainBox title="人物新闻动态" width="1064px" height="460px"
@to-detail="handleClcikToCharacter(curnews.personId, curnews.name)">
<!-- 自定义左上角图标插槽 -->
<template #headerIcon>
<template #header-icon>
<img src="./assets/images/personNewIcon.svg " alt="" />
</template>
<!-- 主内容区域 -->
<div class="box1-wrapper">
<div class="box1-left" @click="handleSwithCurBill('left')">
......@@ -95,7 +95,7 @@
</div>
<div class="box1-main">
<el-carousel ref="carouselRef" height="354px" :autoplay="true" :interval="3000"
<el-carousel ref="carouselRef" height="352px" :autoplay="true" :interval="3000"
arrow="never" indicator-position="none" @change="handleCarouselChange">
<el-carousel-item v-for="person in newsDynamics" :key="person.personId">
<div class="carousel-content">
......@@ -104,8 +104,11 @@
<img :src="person.imageUrl" alt="人物头像" />
</div>
<!-- 竖线分隔 -->
<!-- <div class="vertical-divider"></div> -->
<!-- 事件列表 -->
<div class="events">
<div class="events" style="height: 64px;">
<!-- 头部区域 -->
<div class="header">
<div class="info">
......@@ -156,14 +159,15 @@
postDate="signalTime" name="signalTitle" riskLevel="signalLevel" />
<RiskSignalOverviewDetailDialog v-model="isRiskOverviewDetailOpen" />
</div>
<DivideHeader id="position2" class="divide-header" :titleText="'言论动态'"></DivideHeader>
<DivideHeader id="position2" class="divide-header" :titleText="'言论动态'" v-if="false"></DivideHeader>
<div class="center-center">
<div class="box3">
<div class="box3" v-if="false">
<div class="box3-header">
<div class="box3-header-left">
<div class="box3-header-icon">
<img src="./assets/images/TechnologyFigures-icon3.png" alt="" />
</div>
<!-- <div class="box3-header-title">{{ "人物动向" }}</div> -->
<div class="header-title"
style="width: 1560px; display: flex; justify-content: space-between; margin-top: 10px">
......@@ -235,7 +239,14 @@
</div>
</div>
</div>
<div class="box5-main" ><WordCloudChart width="790px" v-if="WordLoading" :data="CharacterOpinionWordCloud" /></div>
<div class="box5-main">
<WordCloudChart v-if="CharacterOpinionWordCloud.length > 0" :key="wordCloudKey" width="790px" :data="CharacterOpinionWordCloud" />
<el-empty v-else description="暂无数据" />
</div>
<div class="data-origin-box">
<div class="data-origin-icon"><img :src="tipsIcon" alt="" /></div>
<div class="data-origin-text">科技人物观点词云,数据来源:公开新闻报道及社交媒体数据</div>
</div>
</div>
<div class="box6">
<div class="box6-header" style="width: 790px">
......@@ -255,6 +266,10 @@
</div>
</div>
<div class="box6-main" id="box6Chart"></div>
<div class="data-origin-box" style="margin-top: 18px;">
<div class="data-origin-icon"><img :src="tipsIcon" alt="" /></div>
<div class="data-origin-text">科技人物观点涉及领域变化趋势,数据来源:公开新闻报道及社交媒体数据</div>
</div>
</div>
</div>
<div class="center-footer1">
......@@ -268,6 +283,10 @@
</div>
</div>
<div class="box7-main" id="box7Chart"></div>
<div class="data-origin-box" style="padding-top: 3px;">
<div class="data-origin-icon"><img :src="tipsIcon" alt="" /></div>
<div class="data-origin-text">科技人物类型,数据来源:公开新闻报道及社交媒体数据</div>
</div>
</div>
<div class="box8">
<div class="box8-header">
......@@ -294,6 +313,10 @@
<div class="box8-main">
<PersonTable :persontypeid="persontypeid" :yearSelect="yearSelect" />
</div>
<div class="data-origin-box" style="margin-top:14px">
<div class="data-origin-icon"><img :src="tipsIcon" alt="" /></div>
<div class="data-origin-text">观点统计,数据来源:公开新闻报道及社交媒体数据</div>
</div>
</div>
</div>
</div>
......@@ -343,7 +366,6 @@ import getDonutChart from "./utils/donutChart.js";
// 组件
import TimelineMap from "./component/TimelineMap.vue";
import PersonNewsCard from "./component/PersonNewsCard.vue";
import SpeechStance from "./component/speechStance.vue";
import PersonTable from "./component/PersonTable.vue";
import SourceLibrary from "./component/SourceLibrary.vue";
......@@ -448,6 +470,7 @@ const handlegetareaTypeFn = async () => {
};
const WordLoading=ref(false)
const wordCloudKey = ref(0)
// 获取科技人物观点词云
const CharacterOpinionWordCloud = ref([]);
......@@ -470,9 +493,9 @@ const handlegetCharacterOpinionWordCloudFn = async () => {
name: item.option,
value: item.count
}
});
WordLoading.value=true
wordCloudKey.value++
}
} catch (error) { }
};
......@@ -655,7 +678,7 @@ const handleClcikToCharacter = async (id, name) => {
personTypeName = arr[0].typeName;
console.log("personTypeName", personTypeName);
if (personTypeName === "科技企业领袖") {
if (personTypeName === "科技企业领袖" || personTypeName === "政府官员") {
type = 1;
} else if (personTypeName === "国会议员") {
type = 2;
......@@ -1277,7 +1300,7 @@ onMounted(async () => {
.box1-main {
width: 1009px;
height: 354px;
height: 352px;
margin-top: 28px;
margin-left: 40px;
}
......@@ -1556,7 +1579,7 @@ onMounted(async () => {
.box4-main {
height: 402px;
overflow-y: auto;
overflow-y: hidden;
box-sizing: border-box;
padding-top: 8px;
}
......@@ -1564,15 +1587,14 @@ onMounted(async () => {
}
.center-footer {
margin-top: 21px;
height: 460px;
margin-top: 16px;
display: flex;
justify-content: center;
gap: 15px;
gap: 16px;
.box5 {
width: 792px;
height: 460px;
height: 500px;
box-sizing: border-box;
border: 1px solid rgba(234, 236, 238, 1);
border-radius: 10px;
......@@ -1619,7 +1641,7 @@ onMounted(async () => {
.box6 {
width: 792px;
height: 460px;
height: 500px;
box-sizing: border-box;
border: 1px solid rgba(234, 236, 238, 1);
border-radius: 10px;
......@@ -1669,11 +1691,11 @@ onMounted(async () => {
margin-top: 16px;
display: flex;
justify-content: center;
gap: 15px;
gap: 16px;
.box7 {
width: 792px;
height: 460px;
height: 500px;
box-sizing: border-box;
border: 1px solid rgba(234, 236, 238, 1);
border-radius: 10px;
......@@ -1719,13 +1741,13 @@ onMounted(async () => {
.box7-main {
height: 405px;
margin: 0 59px 0 59px;
margin: 0 24px 0 24px;
}
}
.box8 {
width: 792px;
height: 460px;
height: 500px;
box-sizing: border-box;
border: 1px solid rgba(234, 236, 238, 1);
border-radius: 10px;
......@@ -1895,13 +1917,13 @@ onMounted(async () => {
padding: 1px 8px 1px 8px;
color: rgba(59, 65, 75, 1);
box-sizing: border-box;
border: 1px solid rgba(230, 231, 232, 1);
border: 1px solid rgba(234, 236, 238, 1);
border-radius: 4px;
background: rgba(255, 255, 255, 1);
font-family: Microsoft YaHei;
font-size: 14px;
font-weight: 400;
line-height: 30px;
line-height: 22px;
letter-spacing: 0px;
text-align: left;
cursor: pointer;
......@@ -1923,11 +1945,11 @@ onMounted(async () => {
box-sizing: border-box;
border: 1px solid rgba(5, 95, 194, 1);
border-radius: 4px;
background: rgba(231, 243, 255, 1);
background: rgba(246, 251, 255, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-size: 14px;
font-weight: 400;
line-height: 30px;
line-height: 22px;
letter-spacing: 0px;
text-align: left;
cursor: pointer;
......@@ -2149,23 +2171,34 @@ onMounted(async () => {
.avatar {
width: 280px;
height: 354px;
width: 266px;
height: 352px;
flex-shrink: 0;
img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 4px;
}
}
.vertical-divider {
width: 2px;
align-self: center;
height: 54%;
background: rgba(230, 231, 232, 1);
border-radius: 1.5px;
flex-shrink: 0;
margin: 0 10px;
}
.events {
flex: 1;
padding: 0 20px;
.header {
padding-top: 16px;
padding-top: 0;
.info {
display: flex;
......@@ -2173,7 +2206,7 @@ onMounted(async () => {
justify-content: space-between;
h2 {
color: rgba(20, 89, 187, 1);
color: #055fc2;
font-family: Microsoft YaHei;
font-size: 24px;
font-weight: 700;
......@@ -2181,9 +2214,10 @@ onMounted(async () => {
}
.newtitle {
color: rgba(102, 102, 102, 1);
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 14px;
font-size: 16px;
line-height: 24px;
margin-top: 8px;
}
}
......@@ -2209,18 +2243,14 @@ onMounted(async () => {
}
.line {
border-bottom: 1px solid rgba(240, 242, 244, 1);
border-bottom: 1px solid rgba(234, 236, 238, 1);
margin-top: 16px;
}
}
.timeline-wrapper {
height: 289px;
width: 688px;
overflow-y: auto;
overflow: auto;
scrollbar-width: none;
-ms-overflow-style: none;
overflow: hidden;
:deep(.el-timeline) {
margin-top: 14px;
......@@ -2235,18 +2265,36 @@ onMounted(async () => {
left: 4px;
}
// 修复:首尾节点时间线穿点问题
:deep(.el-timeline-item) {
&:first-child {
.el-timeline-item__tail {
top: 10px; // 从圆点中心对齐
height: calc(100% - 10px);
}
}
&:last-child {
.el-timeline-item__tail {
display: none !important;
}
}
&:nth-last-child(2) {
.el-timeline-item__tail {
height: calc(100% + 10px); // 衔接到最后一个圆点中心
}
}
}
:deep(.el-timeline-item__content) {
color: rgba(102, 102, 102, 1);
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 14px;
font-size: 16px;
font-weight: 400;
line-height: 24px;
text-align: justify;
}
}
.timeline-wrapper::-webkit-scrollbar {
display: none;
}
.timeline-dot {
display: flex;
justify-content: space-between;
......@@ -2267,7 +2315,6 @@ onMounted(async () => {
.dot-date {
margin-left: 16px;
margin-top: -6px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 16px;
......@@ -2320,7 +2367,7 @@ onMounted(async () => {
.box1-main {
width: 1009px;
height: 354px;
height: 352px;
margin-top: 28px;
margin-left: 40px;
}
......@@ -2331,14 +2378,15 @@ onMounted(async () => {
}
.avatar {
width: 280px;
height: 354px;
width: 266px;
height: 352px;
flex-shrink: 0;
img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 4px;
}
}
......@@ -2355,7 +2403,7 @@ onMounted(async () => {
h2 {
margin: 0;
color: rgba(20, 89, 187, 1);
color: #055fc2;
font-family: Microsoft YaHei;
font-size: 24px;
font-weight: 700;
......@@ -2363,9 +2411,11 @@ onMounted(async () => {
.newtitle {
margin-top: 8px;
color: rgba(102, 102, 102, 1);
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 14px;
font-size: 16px;
line-height: 24px;
font-weight: 400;
}
}
}
......@@ -2390,15 +2440,14 @@ onMounted(async () => {
}
.line {
border-bottom: 1px solid rgba(240, 242, 244, 1);
border-bottom: 1px solid rgba(234, 236, 238, 1);
margin-top: 16px;
}
}
.timeline-wrapper {
height: 289px;
width: 688px;
overflow-y: auto;
overflow: hidden;
:deep(.el-timeline) {
margin-top: 14px;
......@@ -2413,11 +2462,34 @@ onMounted(async () => {
left: 4px;
}
// 修复:首尾节点时间线穿点问题
:deep(.el-timeline-item) {
&:first-child {
.el-timeline-item__tail {
top: 10px; // 从圆点中心对齐
height: calc(100% - 10px);
}
}
&:last-child {
.el-timeline-item__tail {
display: none !important;
}
}
&:nth-last-child(2) {
.el-timeline-item__tail {
height: calc(100% + 10px); // 衔接到最后一个圆点中心
}
}
}
:deep(.el-timeline-item__content) {
color: rgba(102, 102, 102, 1);
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 14px;
font-size: 16px;
font-weight: 400;
line-height: 24px;
text-align: justify;
margin-top: 4px;
}
}
......@@ -2491,7 +2563,7 @@ onMounted(async () => {
.box1-main {
width: 1009px;
height: 354px;
height: 352px;
margin-top: 28px;
margin-left: 40px;
}
......@@ -2502,14 +2574,15 @@ onMounted(async () => {
}
.avatar {
width: 280px;
height: 354px;
width: 266px;
height: 352px;
flex-shrink: 0;
img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 4px;
}
}
......@@ -2526,7 +2599,7 @@ onMounted(async () => {
h2 {
margin: 0;
color: rgba(20, 89, 187, 1);
color: #055fc2;
font-family: Microsoft YaHei;
font-size: 24px;
font-weight: 700;
......@@ -2534,9 +2607,10 @@ onMounted(async () => {
.newtitle {
margin-top: 8px;
color: rgba(102, 102, 102, 1);
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 14px;
font-size: 16px;
line-height: 24px;
}
}
}
......@@ -2547,15 +2621,14 @@ onMounted(async () => {
}
.line {
border-bottom: 1px solid rgba(240, 242, 244, 1);
border-bottom: 1px solid rgba(234, 236, 238, 1);
margin-top: 16px;
}
}
.timeline-wrapper {
height: 289px;
width: 688px;
overflow-y: auto;
overflow: hidden;
:deep(.el-timeline) {
margin-top: 14px;
......@@ -2563,18 +2636,41 @@ onMounted(async () => {
}
:deep(.el-timeline-item__wrapper) {
padding-left: 40px;
padding-left: 16px;
}
:deep(.el-timeline-item__tail) {
left: 4px;
}
// 修复:首尾节点时间线穿点问题
:deep(.el-timeline-item) {
&:first-child {
.el-timeline-item__tail {
top: 10px; // 从圆点中心对齐
height: calc(100% - 10px);
}
}
&:last-child {
.el-timeline-item__tail {
display: none !important;
}
}
&:nth-last-child(2) {
.el-timeline-item__tail {
height: calc(100% + 10px); // 衔接到最后一个圆点中心
}
}
}
:deep(.el-timeline-item__content) {
color: rgba(102, 102, 102, 1);
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 14px;
font-size: 16px;
font-weight: 400;
line-height: 24px;
text-align: justify;
}
}
......@@ -2609,4 +2705,31 @@ onMounted(async () => {
height: 18px;
}
}
.data-origin-box {
display: flex;
align-items: center;
padding: 8px 24px;
box-sizing: border-box;
.data-origin-icon {
width: 14px;
height: 14px;
font-size: 0;
margin-right: 6px;
flex-shrink: 0;
img {
width: 100%;
height: 100%;
}
}
.data-origin-text {
color: rgba(153, 153, 153, 1);
font-family: Microsoft YaHei;
font-size: 12px;
line-height: 16px;
}
}
</style>
import * as echarts from "echarts";
const getMultiLineChart = (dataX, dataY1, dataY2, dataY3, dataY4, dataY5, dataY6) => {
return {
tooltip: {
trigger: "axis",
axisPointer: {
type: "cross",
label: {
backgroundColor: "#6a7985"
}
}
},
grid: {
top: "50px",
right: "50px",
bottom: "0px",
left: "40px",
containLabel: true
},
legend: {
data: ["人工智能", "集成电路", "量子科技", "生物科技", "通信网络", "能源"],
show: true,
top: "0px",
left: "center",
textStyle: {
color: "rgba(95, 101, 108, 1)",
fontFamily: "Microsoft YaHei",
fontSize: "16px"
}, // 设置图例标记为圆形
icon: "circle",
itemWidth: 10, // 圆的直径
itemHeight: 10, // 圆的直径
itemGap: 20 // 图例项之间的间距
},
color: [
"rgba(10, 87, 166, 1)", // 人工智能(深蓝色)
"rgba(64, 196, 181, 1)", // 集成电路(青绿色)
"rgba(114, 46, 209, 1)", // 量子科技(紫色)
"rgba(245, 141, 46, 1)", // 生物科技(橙色)
"rgba(111, 180, 255, 1)", // 通信网络(浅蓝色)
"rgba(196, 92, 68, 1)" // 能源(红褐色)
],
xAxis: [
{
type: "category",
boundaryGap: false,
data: dataX,
axisLabel: {
fontSize: 14 // 设置X轴字体大小
}
}
],
yAxis: [
{
type: "value",
splitLine: {
show: true,
lineStyle: {
type: "dashed",
color: "#E7F3FF"
}
},
axisLabel: {
fontSize: 14 // 设置X轴字体大小
},
}
],
series: [
{
name: "人工智能",
type: "line",
lineStyle: {
width: 1.5 // 单位:px,数值越大线条越粗
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: "rgba(10, 87, 166, 0.2)" // 起始颜色
},
{
offset: 1,
color: "rgba(10, 87, 166, 0)" // 结束颜色
}
])
},
emphasis: {
focus: "series"
},
data: dataY1
},
{
name: "集成电路",
type: "line",
lineStyle: {
width: 1.5 // 单位:px,数值越大线条越粗
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: "rgba(64, 196, 181, 0.2)" // 起始颜色
},
{
offset: 1,
color: "rgba(64, 196, 181, 0)" // 结束颜色
}
])
},
emphasis: {
focus: "series"
},
data: dataY2
},
{
name: "量子科技",
type: "line",
lineStyle: {
width: 1.5 // 单位:px,数值越大线条越粗
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: "rgba(114, 46, 209, 1)" // 起始颜色
},
{
offset: 1,
color: "rgba(114, 46, 209, 0)" // 结束颜色
}
])
},
emphasis: {
focus: "series"
},
data: dataY3
},
{
name: "生物科技",
type: "line",
lineStyle: {
width: 1.5 // 单位:px,数值越大线条越粗
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: "rgba(245, 141, 46, 0.2)" // 起始颜色
},
{
offset: 1,
color: "rgba(245, 141, 46, 0)" // 结束颜色
}
])
},
emphasis: {
focus: "series"
},
data: dataY4
},
{
name: "通信网络",
type: "line",
lineStyle: {
width: 1.5 // 单位:px,数值越大线条越粗
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: "rgba(111, 180, 255, 0.2)" // 起始颜色
},
{
offset: 1,
color: "rgba(111, 180, 255, 0)" // 结束颜色
}
])
},
emphasis: {
focus: "series"
},
data: dataY5
},
{
name: "能源",
type: "line",
lineStyle: {
width: 1.5 // 单位:px,数值越大线条越粗
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: "rgba(196, 92, 68, 0.2)" // 起始颜色
},
{
offset: 1,
color: "rgba(196, 92, 68, 0)" // 结束颜色
}
])
},
emphasis: {
focus: "series"
},
data: dataY6
}
]
};
return {
tooltip: {
trigger: "axis",
axisPointer: {
type: "cross",
label: {
backgroundColor: "#6a7985"
}
}
},
grid: {
top: "50px",
right: "24px",
bottom: "0px",
left: "24px",
containLabel: true
},
legend: {
data: ["人工智能", "集成电路", "量子科技", "生物科技", "通信网络", "能源"],
show: true,
top: "0px",
left: "center",
textStyle: {
color: "rgba(95, 101, 108, 1)",
fontFamily: "Microsoft YaHei",
fontSize: "16px"
}, // 设置图例标记为圆形
icon: "circle",
itemWidth: 10, // 圆的直径
itemHeight: 10, // 圆的直径
itemGap: 20 // 图例项之间的间距
},
color: [
"rgba(10, 87, 166, 1)", // 人工智能(深蓝色)
"rgba(64, 196, 181, 1)", // 集成电路(青绿色)
"rgba(114, 46, 209, 1)", // 量子科技(紫色)
"rgba(245, 141, 46, 1)", // 生物科技(橙色)
"rgba(111, 180, 255, 1)", // 通信网络(浅蓝色)
"rgba(196, 92, 68, 1)" // 能源(红褐色)
],
xAxis: [
{
type: "category",
boundaryGap: false,
data: dataX,
axisLabel: {
fontSize: 14,
color: "rgba(59, 65, 75, 1)"
},
axisLine: {
lineStyle: {
color: "#E7F3FF"
}
}
}
],
yAxis: [
{
type: "value",
splitLine: {
show: true,
lineStyle: {
type: "dashed",
color: "#E7F3FF"
}
},
axisLabel: {
fontSize: 14 // 设置X轴字体大小
}
}
],
series: [
{
name: "人工智能",
type: "line",
lineStyle: {
width: 1.5 // 单位:px,数值越大线条越粗
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: "rgba(10, 87, 166, 0.2)" // 起始颜色
},
{
offset: 1,
color: "rgba(10, 87, 166, 0)" // 结束颜色
}
])
},
emphasis: {
focus: "series"
},
data: dataY1
},
{
name: "集成电路",
type: "line",
lineStyle: {
width: 1.5 // 单位:px,数值越大线条越粗
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: "rgba(64, 196, 181, 0.2)" // 起始颜色
},
{
offset: 1,
color: "rgba(64, 196, 181, 0)" // 结束颜色
}
])
},
emphasis: {
focus: "series"
},
data: dataY2
},
{
name: "量子科技",
type: "line",
lineStyle: {
width: 1.5 // 单位:px,数值越大线条越粗
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: "rgba(114, 46, 209, 1)" // 起始颜色
},
{
offset: 1,
color: "rgba(114, 46, 209, 0)" // 结束颜色
}
])
},
emphasis: {
focus: "series"
},
data: dataY3
},
{
name: "生物科技",
type: "line",
lineStyle: {
width: 1.5 // 单位:px,数值越大线条越粗
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: "rgba(245, 141, 46, 0.2)" // 起始颜色
},
{
offset: 1,
color: "rgba(245, 141, 46, 0)" // 结束颜色
}
])
},
emphasis: {
focus: "series"
},
data: dataY4
},
{
name: "通信网络",
type: "line",
lineStyle: {
width: 1.5 // 单位:px,数值越大线条越粗
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: "rgba(111, 180, 255, 0.2)" // 起始颜色
},
{
offset: 1,
color: "rgba(111, 180, 255, 0)" // 结束颜色
}
])
},
emphasis: {
focus: "series"
},
data: dataY5
},
{
name: "能源",
type: "line",
lineStyle: {
width: 1.5 // 单位:px,数值越大线条越粗
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: "rgba(196, 92, 68, 0.2)" // 起始颜色
},
{
offset: 1,
color: "rgba(196, 92, 68, 0)" // 结束颜色
}
])
},
emphasis: {
focus: "series"
},
data: dataY6
}
]
};
};
export default getMultiLineChart;
......@@ -13,6 +13,7 @@ import { getPersonSummaryInfo } from "@/api/technologyFigures/technologyFigures
// 人物类型名称 -> type 映射
const PERSON_TYPE_MAP = {
"科技企业领袖": 1,
"政府官员": 1,
"国会议员": 2,
"智库研究人员": 3,
};
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论