提交 d0365709 authored 作者: yanpeng's avatar yanpeng

Merge branch 'pre' of http://8.140.26.4:10003/caijian/risk-monitor into yp-dev

流水线 #463 已失败 于阶段
in 45 秒
......@@ -113,7 +113,7 @@ service.interceptors.response.use(
}
// 特殊处理:风险信号管理页面接口偶发 500,不弹出提示
// 覆盖接口:/api/riskSignal/getCountInfo | /api/riskSignal/getDailyCount | /api/riskSignal/pageQuery
// 覆盖接口:/api/riskSignal/baseInfo | /api/riskSignal/pageQuery
try {
const errUrl = String(error?.config?.url || '')
if (error?.response?.status === 500 && errUrl.includes('/api/riskSignal/')) {
......
import request from "@/api/request.js";
// 基本统计信息
export function getCountInfo() {
return request({
method: 'GET',
url: `/api/riskSignal/getCountInfo`,
})
}
// 每日统计信息
export function getDailyCount() {
return request({
method: 'GET',
url: `/api/riskSignal/getDailyCount`,
})
/** 风险信号管理页:统计 + 日历热力数据(原 getCountInfo + getDailyCount) */
export function getRiskSignalBaseInfo() {
return request({
method: "GET",
url: `/api/riskSignal/baseInfo`
});
}
// 按条件分页查询风险信号信息
export function getPageQuery(data) {
return request({
method: 'POST',
url: `/api/riskSignal/pageQuery`,
url: `/api/riskSignal/PageLimit`,
data: data
})
}
......@@ -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 }
});
}
......@@ -53,6 +53,25 @@ export function getThinkTankPolicyIndustryChange(params) {
});
}
/**
* 智库概览-数量变化趋势(按领域统计)
* GET /thinkTankReport/domainStats
* @param {{ startDate: string, endDate: string }} params
*/
export function getThinkTankReportDomainStats(params) {
return request({
method: "GET",
url: `/api/thinkTankReport/domainStats`,
params: {
startDate: params.startDate,
endDate: params.endDate
},
// 与 policyIndustryChange 一致:无数据年份可能返回 400/500,避免走全局错误提示
validateStatus: (status) =>
(status >= 200 && status < 300) || status === 400 || status === 500
});
}
// 政策建议领域分布
export function getThinkTankPolicyIndustry(params) {
return request({
......
<template>
<el-dialog
v-model="visible"
class="risk-signal-detail-dialog"
modal-class="risk-signal-detail-modal"
width="1280px"
align-center
:z-index="zIndex"
:show-close="true"
destroy-on-close
@closed="handleClosed"
>
<template #header>
<img class="header-icon" src="@/views/viewRiskSignal/assets/images/risk-icon.png" alt="" />
<span class="risk-signal-detail-dialog__level risk-signal-detail-dialog__level--lv1">特别重大风险</span>
</template>
<div class="risk-signal-detail-dialog__body">
<span class="risk-signal-detail-dialog__title">扩大实体清单制裁范围,对中企子公司实施同等管制</span>
<div class="risk-signal-detail-dialog__origin">政策法规打压类风险</div>
<div class="risk-signal-detail-dialog__meta">
<span>2025年11月10日 16:14·美国商务部</span>
<div class="risk-signal-detail-dialog__tags">
<AreaTag :key="'overview-risk-dialog-tag-bio'" tagName="生物科技">生物科技</AreaTag>
<AreaTag :key="'overview-risk-dialog-tag-ai'" tagName="人工智能">人工智能</AreaTag>
</div>
</div>
</div>
<div class="risk-signal-detail-dialog_relation">
<div class="relation">
<div class="logo">
<img src="@/views/viewRiskSignal/assets/images/logo.png" alt="" />
</div>
<div class="name-text">总统行政令——</div>
<div class="content-text">关于调整进口木材、锯材及其衍生产品进入美国的相关修正案</div>
</div>
<div class="right-arrow">
<img src="@/views/viewRiskSignal/assets/images/right-arrow.png" alt="" />
</div>
</div>
<div class="risk-signal-detail-dialog__desc">
<p class="risk-signal-detail-dialog__desc-p">
任何被列入美国出口管制“实体清单”或“军事最终用户清单”的企业,如果其直接或间接持有另一家公司50%或以上的股权,那么这家被控股的公司也将自动受到与清单上母公司同等的出口管制限制
</p>
<p class="risk-signal-detail-dialog__desc-p">
任何被列入美国出口管制“实体清单”或“军事最终用户清单”的企业,如果其直接或间接持有另一家公司50%或以上的股权,那么这家被控股的公司也将自动受到与清单上母公司同等的出口管制限制
</p>
</div>
<template #footer>
<el-button type="primary" class="risk-signal-detail-dialog__action-btn" @click="visible = false">
确定风险
</el-button>
</template>
</el-dialog>
</template>
<script setup>
import AreaTag from "@/components/base/AreaTag/index.vue";
defineProps({
/** 与遮罩、弹窗层级一致,避免被大屏/轮播盖住 */
zIndex: {
type: Number,
default: 20000
}
});
const emit = defineEmits(["closed"]);
const visible = defineModel({ type: Boolean, default: false });
function handleClosed() {
emit("closed");
}
</script>
<style lang="scss" src="./risk-signal-overview-detail-dialog.scss"></style>
/* 概览页风险信号写死详情弹窗(teleport 到 body,需全局样式;与 viewRiskSignal 管理页 dialog 视觉对齐) */
.risk-signal-detail-modal.el-overlay {
display: flex;
align-items: center;
justify-content: center;
overflow: hidden !important;
z-index: 20000 !important;
background-color: rgba(0, 0, 0, 0.25) !important;
backdrop-filter: blur(30px);
-webkit-backdrop-filter: blur(30px);
}
.risk-signal-detail-modal .el-overlay-dialog {
display: flex;
align-items: center;
justify-content: center;
overflow: hidden !important;
}
.el-dialog.is-align-center.risk-signal-detail-dialog,
.risk-signal-detail-dialog.el-dialog {
position: relative;
width: 1280px !important;
max-width: calc(100vw - 32px);
height: 750px;
max-height: calc(100vh - 32px);
border-radius: 10px;
display: flex;
flex-direction: column;
overflow: hidden !important;
box-sizing: border-box;
padding: 0 12px 12px 12px !important;
}
.risk-signal-detail-dialog.el-dialog::-webkit-scrollbar,
.risk-signal-detail-dialog .el-dialog__body::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
.risk-signal-detail-dialog.el-dialog,
.risk-signal-detail-dialog .el-dialog__body {
scrollbar-width: none;
-ms-overflow-style: none;
}
.el-dialog.is-align-center.risk-signal-detail-dialog::after,
.risk-signal-detail-dialog.el-dialog::after {
content: "";
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 88px;
width: calc(1278px + 24px);
max-width: none;
height: 1px;
background-color: rgb(234, 236, 238);
pointer-events: none;
z-index: 5;
}
.risk-signal-detail-dialog .el-dialog__header .risk-signal-detail-dialog__level,
.risk-signal-detail-dialog__level {
font-family: "YouSheBiaoTiHei", sans-serif;
font-weight: 400;
font-size: 30px;
line-height: 39px;
letter-spacing: 0;
text-align: left;
}
.risk-signal-detail-dialog__level--lv1 {
color: rgb(206, 79, 81) !important;
}
.risk-signal-detail-dialog__level--lv2 {
color: rgba(250, 140, 22, 1) !important;
}
.risk-signal-detail-dialog__level--lv3 {
color: rgba(212, 177, 6, 1) !important;
}
.risk-signal-detail-dialog__level--lv4 {
color: rgba(82, 196, 26, 1) !important;
}
.risk-signal-detail-dialog__level--lv5 {
color: rgba(22, 119, 255, 1) !important;
}
.risk-signal-detail-dialog .el-dialog__footer {
padding: 0 !important;
margin: 0 !important;
border-top: none !important;
height: 0;
min-height: 0;
overflow: visible;
position: static;
}
.risk-signal-detail-dialog__action-btn {
position: absolute;
right: 53px;
bottom: 26px;
width: 360px !important;
height: 36px !important;
margin: 0 !important;
padding: 0 !important;
border-radius: 6px !important;
background-color: rgb(5, 95, 194) !important;
border-color: rgb(5, 95, 194) !important;
color: rgb(255, 255, 255) !important;
font-family: "Source Han Sans CN", sans-serif;
font-weight: 400 !important;
font-size: 16px !important;
line-height: 24px !important;
letter-spacing: 0 !important;
text-align: center !important;
z-index: 6;
}
.risk-signal-detail-dialog .el-dialog__header {
flex-shrink: 0;
display: flex;
align-items: center;
margin: 0;
padding: 0;
border-bottom: none;
padding: 8px 0;
position: relative;
}
.risk-signal-detail-dialog .el-dialog__header::after {
content: "";
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 0;
width: calc(1278px + 32px);
max-width: none;
height: 1px;
background-color: rgb(234, 236, 238);
pointer-events: none;
z-index: 1;
}
.risk-signal-detail-dialog .el-dialog__body {
flex: 1;
min-height: 0;
min-width: 0;
overflow: hidden !important;
box-sizing: border-box;
padding-left: 22px;
padding-right: 22px;
padding-top: 34px;
}
.risk-signal-detail-dialog .risk-signal-detail-dialog__title,
.risk-signal-detail-dialog .risk-signal-detail-dialog__desc,
.risk-signal-detail-dialog .risk-signal-detail-dialog__meta,
.risk-signal-detail-dialog .risk-signal-detail-dialog__body {
max-width: 100%;
overflow-wrap: anywhere;
word-break: break-word;
}
.risk-signal-detail-dialog .risk-signal-detail-dialog_relation {
max-width: 100%;
width: 100% !important;
box-sizing: border-box;
}
.risk-signal-detail-dialog .risk-signal-detail-dialog__title {
font-family: "Source Han Sans CN", sans-serif;
font-weight: 700;
font-size: 24px;
line-height: 36px;
letter-spacing: 0;
text-align: justify;
color: rgba(59, 65, 75, 1);
}
.risk-signal-detail-dialog .risk-signal-detail-dialog__origin {
font-family: "Source Han Sans CN", sans-serif;
font-weight: 400;
font-size: 16px;
line-height: 30px;
letter-spacing: 0;
text-align: justify;
color: rgb(206, 79, 81);
background-color: rgb(250, 237, 237);
width: fit-content;
padding: 0 12px;
border-radius: 20px;
}
.risk-signal-detail-dialog .risk-signal-detail-dialog__body {
display: flex;
flex-direction: column;
gap: 8px;
margin-left: 12px;
}
.risk-signal-detail-dialog .risk-signal-detail-dialog__meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
font-family: "Source Han Sans CN", sans-serif;
font-weight: 400;
font-size: 16px;
line-height: 24px;
letter-spacing: 0;
text-align: left;
color: rgb(59, 65, 75);
justify-content: space-between;
}
.risk-signal-detail-dialog .header-icon {
width: 32px;
height: 32px;
margin-right: 6px;
}
.risk-signal-detail-dialog .header-icon img {
width: 100%;
height: 100%;
display: block;
}
.risk-signal-detail-dialog .risk-signal-detail-dialog_relation {
width: 100%;
max-width: 100%;
height: 48px;
box-sizing: border-box;
margin-top: 24px;
background-color: rgb(246, 250, 255);
border: 1px solid rgb(231, 243, 255);
border-radius: 50px;
display: flex;
justify-content: space-between;
cursor: pointer;
}
.risk-signal-detail-dialog .risk-signal-detail-dialog_relation .relation {
height: 36px;
display: flex;
flex-direction: row;
}
.risk-signal-detail-dialog .risk-signal-detail-dialog_relation .logo {
width: 36px;
height: 36px;
margin-top: 6px;
margin-left: 7px;
display: block;
}
.risk-signal-detail-dialog .risk-signal-detail-dialog_relation .logo img {
width: 100%;
height: 100%;
display: block;
}
.risk-signal-detail-dialog .risk-signal-detail-dialog_relation .name-text {
color: rgb(5, 95, 194);
font-family: "Source Han Sans CN", sans-serif;
font-weight: 700;
font-size: 16px;
line-height: 24px;
letter-spacing: 1px;
text-align: left;
margin-top: 12px;
margin-left: 8px;
}
.risk-signal-detail-dialog .risk-signal-detail-dialog_relation .content-text {
color: rgb(5, 95, 194);
font-family: "Source Han Sans CN", sans-serif;
font-weight: 400;
font-size: 16px;
line-height: 24px;
letter-spacing: 0;
text-align: left;
margin-top: 12px;
}
.risk-signal-detail-dialog .risk-signal-detail-dialog_relation .right-arrow {
width: 12px;
height: 11px;
margin-top: 19px;
margin-right: 18px;
}
.risk-signal-detail-dialog .risk-signal-detail-dialog_relation .right-arrow img {
width: 100%;
height: 100%;
display: block;
}
.risk-signal-detail-dialog .risk-signal-detail-dialog__desc {
color: rgb(95, 101, 108);
font-family: "Source Han Sans CN", sans-serif;
font-weight: 400;
font-size: 16px;
line-height: 30px;
letter-spacing: 0;
text-align: justify;
margin-top: 24px;
padding-left: 12px;
padding-right: 12px;
height: 310px;
}
.risk-signal-detail-dialog .risk-signal-detail-dialog__desc-p {
margin: 0;
}
.risk-signal-detail-dialog .risk-signal-detail-dialog__desc-p + .risk-signal-detail-dialog__desc-p {
margin-top: 12px;
}
.risk-signal-detail-dialog .risk-signal-detail-dialog__tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
<template>
<div class="normal-box" :class="{ 'box-glow': isBorderActive }">
<div class="content-box">
<slot></slot>
</div>
</div>
</template>
<script setup>
const props = defineProps({
isBorderActive: {
type: Boolean,
default: false,
}
})
</script>
<style lang="scss" scoped>
.normal-box {
position: relative;
width: 100%;
height: 100%;
border-radius: 16px;
// border: 2px solid var(--bg-black-5);
background: var(--color-primary-2);
.content-box {
position: absolute;
z-index: 2;
top: 10px;
left: 10px;
width: calc(100% - 20px);
height: calc(100% - 20px);
}
}
/* 霓虹发光效果变体 */
.box-glow {
width: 100%;
height: 100%;
position: relative;
border-radius: 16px;
// background: #1a1a1a;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
background: var(--bg-black-5);
// border: 2px solid transparent !important;
border: none;
.content-box {
position: absolute;
z-index: 2;
top: 10px;
left: 10px;
width: calc(100% - 20px);
height: calc(100% - 20px);
}
}
.box-glow::before {
content: '';
position: absolute;
width: 150%;
height: 250%;
background: conic-gradient(from 0deg,
#85B2FF 0deg 180deg,
#B685FF 180deg 360deg);
animation: rotate 3s linear infinite;
filter: blur(10px);
}
.box-glow::after {
content: '';
position: absolute;
inset: 3px;
background: #fff;
border-radius: 12px;
}
// .glow-layer {
// position: absolute;
// inset: -2px;
// background: conic-gradient(from 0deg,
// #85B2FF 0deg 180deg,
// #B685FF 180deg 360deg);
// animation: rotate 3s linear infinite;
// filter: blur(20px);
// opacity: 0.5;
// }
</style>
......@@ -10,8 +10,8 @@
</div>
<!-- <div class="more" @click="handleToMoreNews">{{ "更多 +" }}</div> -->
</div>
<div class="msg-bubble-main" ref="scrollContainer">
<div class="message-bubble" v-for="(item, index) in displayList" :key="index" @click="handleClickPerson(item)">
<div class="msg-bubble-main" ref="scrollContainer" @mouseenter="pauseScroll" @mouseleave="resumeScroll">
<div class="message-bubble" v-for="(item, index) in infiniteList" :key="index" @click="handleClickPerson(item)">
<div class="avatar-container">
<img :src="item[props.imageUrl] || avatarUser" :alt="item[props.name]" class="avatar" />
<div class="avatar-containerOne" v-if="isRepublicanParty">
......@@ -39,7 +39,7 @@
</template>
<script setup>
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount, onUnmounted } from "vue";
import avatarUser from "@/assets/images/avatar_user.png";
const emit = defineEmits(["click", "info-click"]);
......@@ -135,31 +135,165 @@ const displayList = computed(() => {
return list
})
const scrollSpeed = ref(30)
const autoStart = ref(true)
const copyCount = ref(3)
// 创建无限列表(复制多份)
const infiniteList = computed(() => {
if (!displayList.value || displayList.value.length === 0) return []
const result = []
for (let i = 0; i < copyCount.value; i++) {
result.push(...displayList.value)
}
return result
})
// 状态
let animationId = null
let lastTimestamp = 0
let isPaused = ref(false)
let isScrolling = ref(false)
// 滚动动画函数(使用 requestAnimationFrame)
const scrollAnimation = (timestamp) => {
if (!scrollContainer.value || isPaused.value) {
if (!isPaused.value) {
animationId = requestAnimationFrame(scrollAnimation)
}
return
}
if (!lastTimestamp) {
lastTimestamp = timestamp
animationId = requestAnimationFrame(scrollAnimation)
return
}
// 计算时间差(秒)
const deltaTime = Math.min(0.033, (timestamp - lastTimestamp) / 1000) // 限制最大33ms
if (deltaTime <= 0) {
lastTimestamp = timestamp
animationId = requestAnimationFrame(scrollAnimation)
return
}
const container = scrollContainer.value
const scrollHeight = container.scrollHeight
const clientHeight = container.clientHeight
// 计算滚动步长
const step = scrollSpeed.value * deltaTime
let newScrollTop = container.scrollTop + step
// 检查是否滚动到底部
const maxScrollTop = scrollHeight - clientHeight
if (newScrollTop >= maxScrollTop - 1) {
// 到达底部,无缝跳转到顶部
container.scrollTop = 0
// 重置时间戳避免跳跃
lastTimestamp = timestamp
} else {
container.scrollTop = newScrollTop
}
lastTimestamp = timestamp
animationId = requestAnimationFrame(scrollAnimation)
}
// 开始滚动
const startScroll = () => {
if (timer) clearInterval(timer)
timer = setInterval(() => {
currentIndex.value = (currentIndex.value + 1) % props.messageList.length
}, 2000) // 每秒滚动一条
if (animationId) {
cancelAnimationFrame(animationId)
animationId = null
}
if (isPaused.value) return
lastTimestamp = 0
isScrolling.value = true
animationId = requestAnimationFrame(scrollAnimation)
}
// 停止滚动
const stopScroll = () => {
if (timer) {
clearInterval(timer)
timer = null
}
if (animationId) {
cancelAnimationFrame(animationId)
animationId = null
}
lastTimestamp = 0
isScrolling.value = false
}
onMounted(() => {
if (props.messageList.length > 3) {
startScroll()
}
// 暂停滚动
const pauseScroll = () => {
if (isPaused.value) return
isPaused.value = true
stopScroll()
}
// 恢复滚动
const resumeScroll = () => {
if (!isPaused.value) return
isPaused.value = false
if (autoStart.value) {
startScroll()
}
}
// 重置滚动位置(可选方法)
const resetScrollPosition = () => {
if (scrollContainer.value) {
scrollContainer.value.scrollTop = 0
}
}
// 监听 displayList 变化
watch(
() => displayList.value,
async (newVal, oldVal) => {
if (newVal && newVal.length > 0) {
await nextTick()
// 如果列表内容变化,重置滚动位置并重新开始
if (scrollContainer.value) {
const wasPaused = isPaused.value
stopScroll()
scrollContainer.value.scrollTop = 0
if (!wasPaused && autoStart.value) {
isPaused.value = false
startScroll()
}
}
}
},
{ deep: true, immediate: false }
)
// 监听 scrollSpeed 变化
watch(
() => scrollSpeed.value,
() => {
if (isScrolling.value && !isPaused.value) {
// 重新启动滚动以应用新速度
stopScroll()
startScroll()
}
}
)
// 组件挂载
onMounted(async () => {
await nextTick()
if (autoStart.value && displayList.value && displayList.value.length > 0) {
startScroll()
}
})
onBeforeUnmount(() => {
stopScroll()
// 组件卸载
onUnmounted(() => {
stopScroll()
})
</script>
......
<template>
<div class="module-header-wrapper">
<div class="nav-content">
<div class="nav-content" :class="{ 'nav-content-library': isShowDataLibrary }">
<div class="nav-left" :class="{ 'flex-start': isShowSearchBar }">
<div class="icon">
<img v-show="!isShowSearchBar" src="@/assets/icons/overview/logo.png" alt="" />
......@@ -85,6 +85,7 @@ import { useRouter } from "vue-router";
import { useRoute } from "vue-router";
import { getPersonType } from "@/api/common/index";
import request, { removeToken } from "@/api/request.js";
import { broadcastAuthLogout } from "@/utils/authCrossTabLogout.js";
import SearchBar from "@/components/layout/SearchBar.vue";
import Menu1 from "@/assets/icons/overview/menu1.png";
......@@ -478,6 +479,7 @@ const handleUserCommand = async (command) => {
} catch {
// ignore
}
broadcastAuthLogout();
try {
window.sessionStorage.removeItem("auth_token");
} catch {
......@@ -487,6 +489,11 @@ const handleUserCommand = async (command) => {
}
};
const isShowDataLibrary = computed(() => {
const isShowLibrary = route.fullPath.includes('/dataLibrary')
return isShowLibrary
})
onMounted(() => {
handleGetPersonType();
handleGetSsoUserInfo();
......@@ -497,12 +504,18 @@ 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%);
padding: 12px 0;
.nav-content-library {
width: calc(100% - 24px - 37px) !important;
margin: 0 37px 0 24px !important;
}
.nav-content {
width: 1600px;
margin: 0 auto;
......
import { createRouter, createWebHistory } from "vue-router";
import { setToken, removeToken, getToken } from "@/api/request.js";
import { AUTH_LOGOUT_CHANNEL } from "@/utils/authCrossTabLogout.js";
/** localStorage:跨标签页记录当前前端的 bootId(与 vite define 的 __APP_BOOT_ID__ 对齐) */
const VITE_BOOT_STORAGE_KEY = "app_vite_boot_id";
......@@ -241,5 +242,59 @@ router.beforeEach((to, from, next) => {
next();
});
/**
* 在其它标签页/窗口退出登录时,本页立即跳转登录页(不依赖用户再点一次路由)。
* 1)storage:监听同源其它文档对 `force_login` 的写入
* 2)BroadcastChannel:与登出时的 `broadcastAuthLogout()` 配对,减少仅依赖 storage 的时序问题
*/
function installCrossTabLogoutRedirect() {
if (typeof window === "undefined") {
return;
}
const redirectToLoginAfterRemoteLogout = () => {
try {
window.localStorage.setItem(FORCE_LOGIN_KEY, "1");
} catch {
// ignore
}
try {
removeToken();
window.localStorage.removeItem("auth_token");
window.sessionStorage.removeItem("auth_token");
} catch {
// ignore
}
try {
if (router.currentRoute.value.path !== "/login") {
router.replace({ path: "/login", replace: true }).catch(() => {});
}
} catch {
// ignore
}
};
window.addEventListener("storage", (e) => {
if (e.key !== FORCE_LOGIN_KEY || e.newValue !== "1") {
return;
}
redirectToLoginAfterRemoteLogout();
});
try {
if (typeof BroadcastChannel !== "undefined") {
const authBroadcast = new BroadcastChannel(AUTH_LOGOUT_CHANNEL);
authBroadcast.addEventListener("message", (ev) => {
if (ev?.data?.type === "force_logout") {
redirectToLoginAfterRemoteLogout();
}
});
}
} catch {
// ignore
}
}
installCrossTabLogoutRedirect();
export default router;
// 规则限制
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;
......@@ -12,7 +12,7 @@ const useTagsViewStore = defineStore('tags-view', {
this.addCachedView(view)
},
addVisitedView(view) {
this.visitedViews = localStorage.getItem('visitedViews') && JSON.parse(localStorage.getItem('visitedViews')) || []
this.visitedViews = sessionStorage.getItem('visitedViews') && JSON.parse(sessionStorage.getItem('visitedViews')) || []
this.visitedViews.forEach(item => {
item.active = false
})
......@@ -24,7 +24,7 @@ const useTagsViewStore = defineStore('tags-view', {
...view,
title: view.meta?.title || '未命名'
})
localStorage.setItem('visitedViews', JSON.stringify(this.visitedViews))
sessionStorage.setItem('visitedViews', JSON.stringify(this.visitedViews))
} else {
this.visitedViews.forEach(v => {
if (v.path === view.path) {
......@@ -50,7 +50,7 @@ const useTagsViewStore = defineStore('tags-view', {
if (index !== -1) {
this.visitedViews.splice(index, 1)
}
localStorage.setItem('visitedViews', JSON.stringify(this.visitedViews))
sessionStorage.setItem('visitedViews', JSON.stringify(this.visitedViews))
resolve([...this.visitedViews])
})
},
......@@ -66,7 +66,7 @@ const useTagsViewStore = defineStore('tags-view', {
},
loadVisitedViewFromLocalStorage() {
const saved = localStorage.getItem('visitedViews')
const saved = sessionStorage.getItem('visitedViews')
if (saved) {
this.items = JSON.parse(saved)
}
......
<template>
<el-row class="wrapper layout-grid-line">
<el-col :span="span">
<pre>
{{
`
import WorkingBox from '@/components/base/WorkingBox/index.vue'
<div class="outer-box">
<WorkingBox>
<div class="content">我是内容区域</div>
</WorkingBox>
</div>
`
}}
</pre>
<div class="outer-box">
<WorkingBox :is-border-active="isBorderActive">
<div class="content">我是内容区域</div>
</WorkingBox>
<div class="btn-box">
<el-button type="primary" @click="handleChangeBorderActiveStatus(true)">激活</el-button>
<el-button type="plain" @click="handleChangeBorderActiveStatus(false)">恢复</el-button>
</div>
</div>
</el-col>
</el-row>
</template>
<script setup>
import { ref } from 'vue'
import '@/styles/common.scss'
import WorkingBox from '@/components/base/WorkingBox/index.vue'
const span = 12
const isBorderActive = ref(false)
const handleChangeBorderActiveStatus = (isActive) => {
isBorderActive.value = isActive
}
</script>
<style lang="scss" scoped>
.outer-box {
width: 600px;
height: 300px;
margin: 50px 20px;
.content {
width: 100%;
height: 100%;
background: #e3e3e3;
}
.btn-box{
margin-top: 10px;
}
}
</style>
\ No newline at end of file
......@@ -9,7 +9,7 @@
<TextStyle />
<div class="text-title-1-show">通用样式/组件</div>
<div style="position: relative; min-height: 700px;">
<el-tabs tabPosition="left" class="tabs-nav-no-wrap left-float-nav-tabs">
<el-tabs tabPosition="left" class="tabs-nav-no-wrap left-float-nav-tabs dev-style-tabs">
<el-tab-pane label="通用" lazy>
<common-page />
</el-tab-pane>
......@@ -70,6 +70,9 @@
<el-tab-pane label="引力关系图" lazy>
<RelationForceChart />
</el-tab-pane>
<el-tab-pane label="激活工作框" lazy>
<WorkingBox />
</el-tab-pane>
</el-tabs>
</div>
</el-space>
......@@ -103,10 +106,17 @@ import AiSummary from './Ai/AiSummary/index.vue'
import RelationChart from './RelationChart/index.vue'
import RelationCenterChart from './RelationCenterChart/index.vue'
import RelationForceChart from './RelationForceChart/index.vue'
import WorkingBox from './WorkingBox/index.vue'
</script>
<style lang="scss" scoped>
.box {
padding-bottom: 20px;
}
.dev-style-tabs {
:deep(.el-tabs__content) {
display: block !important;
}
}
</style>
\ No newline at end of file
......@@ -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
/** 与 `router/index.js` 中 BroadcastChannel 监听使用同一频道名 */
export const AUTH_LOGOUT_CHANNEL = "risk_monitor_auth";
/**
* 通知同源其它标签页/窗口:用户已退出,应立即进入登录页。
* 与 localStorage `force_login=1` 配合;发起退出的标签页在写入后调用一次即可。
*/
export function broadcastAuthLogout() {
try {
if (typeof BroadcastChannel === "undefined") {
return;
}
const ch = new BroadcastChannel(AUTH_LOGOUT_CHANNEL);
ch.postMessage({ type: "force_logout" });
ch.close();
} catch {
// ignore
}
}
......@@ -115,6 +115,7 @@
</div>
</div>
</div>
<RiskSignalOverviewDetailDialog v-model="isRiskOverviewDetailOpen" />
</div>
</template>
......@@ -123,8 +124,7 @@ import { color } from "echarts";
import { onMounted, ref, computed } from "vue";
import WaveBall from "./WaveBall.vue";
import { getBillRiskSignal } from "@/api/bill/billHome";
import router from "@/router";
import { navigateToViewRiskSignalOpenFirstDetail } from "@/utils/riskSignalOverviewNavigate";
import RiskSignalOverviewDetailDialog from "@/components/base/RiskSignalOverviewDetailDialog/index.vue";
const sectionTab = [
{
textColor: "rgba(9, 88, 217, 1)",
......@@ -291,8 +291,10 @@ const handleSwithCurNews = name => {
}
};
const isRiskOverviewDetailOpen = ref(false);
const handleRiskSignalItemToManage = () => {
navigateToViewRiskSignalOpenFirstDetail(router);
isRiskOverviewDetailOpen.value = true;
};
const highlightedEventType = ref("");
......
......@@ -125,6 +125,7 @@
</div>
</div>
</div>
<RiskSignalOverviewDetailDialog v-model="isRiskOverviewDetailOpen" />
</div>
</template>
......@@ -134,7 +135,8 @@ import { onMounted, ref, onUnmounted, computed } from "vue";
import WaveBall from "./WaveBall.vue";
import { getLatestRiskUpdates, getLatestRisks } from "@/api/zmOverview/risk/index.js";
import router from "@/router/index";
import { navigateToViewRiskSignal, navigateToViewRiskSignalOpenFirstDetail } from "@/utils/riskSignalOverviewNavigate";
import { navigateToViewRiskSignal } from "@/utils/riskSignalOverviewNavigate";
import RiskSignalOverviewDetailDialog from "@/components/base/RiskSignalOverviewDetailDialog/index.vue";
import icon1 from "./icon/title-1.svg";
import icon2 from "./icon/title-2.svg";
import icon3 from "./icon/title-3.svg";
......@@ -652,8 +654,10 @@ const filteredHotNewsList = computed(() => {
return hotNewsList.value.filter(newsItem => newsItem.signalId === currentHoveredSignalId.value);
});
const isRiskOverviewDetailOpen = ref(false);
const handleRiskSignalRowToManage = () => {
navigateToViewRiskSignalOpenFirstDetail(router);
isRiskOverviewDetailOpen.value = true;
};
const handleToRiskManage = () => {
......
......@@ -238,16 +238,18 @@
<DivideHeader id="position4" class="divide4" :titleText="'资源库'"></DivideHeader>
<ResourceLibrarySection :on-click-to-detail="handleClickToDetailO" :on-after-page-change="handlePageChange" />
</div>
<RiskSignalOverviewDetailDialog v-model="isRiskOverviewDetailOpen" />
</div>
</div>
</template>
<script setup>
import RiskSignal from "@/components/base/riskSignal/index.vue";
import RiskSignalOverviewDetailDialog from "@/components/base/RiskSignalOverviewDetailDialog/index.vue";
import SummaryCardsPanel from "@/components/base/SummaryCardsPanel/index.vue";
import { onMounted, ref, onUnmounted, nextTick, watch, computed } from "vue";
import router from "@/router/index";
import { navigateToViewRiskSignal, navigateToViewRiskSignalOpenFirstDetail } from "@/utils/riskSignalOverviewNavigate";
import { navigateToViewRiskSignal } from "@/utils/riskSignalOverviewNavigate";
import setChart from "@/utils/setChart";
import {
getBillIndustry,
......@@ -466,8 +468,10 @@ const handleClickToDetailO = item => {
// router.push("/billLayout?billId=" + item.billId)
};
const isRiskOverviewDetailOpen = ref(false);
const handleRiskSignalItemToManage = () => {
navigateToViewRiskSignalOpenFirstDetail(router);
isRiskOverviewDetailOpen.value = true;
};
// 查看更多风险信号
......
......@@ -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);
......
......@@ -17,7 +17,8 @@
<img src="./assets/icon-star.svg" alt="" class="btn-icon" />
</div>
<div class="mainBox">
<div class="graph" id="relGraph"></div>
<div v-show="activeIndex !== '关系图'" class="graph" id="relGraph"></div>
<RelationChart v-if="activeIndex === '关系图' && graphData.rootId" :graph-data="graphData" />
</div>
</div>
</template>
......@@ -27,6 +28,7 @@ import { ref, onMounted, onBeforeUnmount, watch } from "vue";
import * as echarts from "echarts";
import { getCharacterGlobalInfo, getCharacterRelation } from "@/api/characterPage/characterPage.js";
import "default-passive-events";
import RelationChart from '@/components/base/RelationChart/index.vue';
import { useRoute } from "vue-router";
const route = useRoute();
......@@ -95,6 +97,7 @@ const nodes = ref([]);
const links = ref([]);
const characterInfo = ref({});
const CharacterRelation = ref([]);
const graphData = ref({ rootId: '', nodes: [], lines: [] });
const list = ref([
{
......@@ -108,8 +111,8 @@ const list = ref([
icon: new URL("./assets/icon-force.svg", import.meta.url).href,
},
{
value: "树形布局",
label: "树形布局",
value: "关系图",
label: "关系图",
icon: new URL("./assets/icon-tree.svg", import.meta.url).href,
},
]);
......@@ -204,6 +207,15 @@ const getCharacterRelationFn = async () => {
nodes.value = [centerNode];
links.value = [];
}
// 构建关系图数据
const gNodes = [{ id: 'center', text: characterInfo.value.name || '' }];
const gLines = [];
CharacterRelation.value.forEach((item, index) => {
gNodes.push({ id: `node_${index}`, text: item.name });
gLines.push({ from: 'center', to: `node_${index}`, text: item.relation || '' });
});
graphData.value = { rootId: 'center', nodes: gNodes, lines: gLines };
};
// ========== 渲染图表 ==========
......
......@@ -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
......
......@@ -84,7 +84,7 @@ export function useBills() {
}
if (filters.selectedTimeRanges.length > 0 && !filters.selectedTimeRanges.includes('all')) {
params.years = filters.selectedTimeRanges
params.years = filters.selectedTimeRanges.map(Number)
}
params.isCN = filters.isCN
......
......@@ -68,10 +68,9 @@
color: 'rgba(59, 65, 75, 1)'
}" :row-class-name="tableRowClassName" :row-style="{ height: '60px' }" size="large">
<el-table-column prop="rank" label="排名" width="100" align="center" />
<el-table-column prop="contributor" label="贡献者" min-width="300" />
<el-table-column prop="contributor" label="贡献者" min-width="200" />
<el-table-column prop="totalAmount" label="总捐款" width="150" />
<el-table-column prop="individualAmount" label="个人捐款" width="150" />
<el-table-column prop="pacsAmount" label="PACs捐款" width="150" />
<el-table-column prop="donationYear" label="捐款年份" width="150" />
</el-table>
<div class="table-pagination">
<span class="table-pagination-total">共{{ fundTotal }}项</span>
......@@ -95,7 +94,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] + "日" }}
......@@ -105,7 +108,7 @@
<img src="./assets/type1.png" alt="" v-if="item.remarks === true" /><img
src="./assets/type2.png" alt="" v-else />
</div>
<div class="content">
<div class="content" style="cursor: pointer;" @click="gotoNewsDetail(item.newsId)">
<div
:class="{ 'content-type1': item.remarks === true, 'content-type2': item.remarks === false }">
......@@ -127,9 +130,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 +236,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 === '相关情况'" />
<!-- 弹框 -->
......@@ -389,7 +388,9 @@ const handleBillMoreClick = (bill) => {
};
import { useRoute } from 'vue-router';
import { useGotoNewsDetail } from '@/router/modules/news.js';
const route = useRoute();
const gotoNewsDetail = useGotoNewsDetail();
const personId = ref(route.query.personId || "Y000064");
const newsTab = ref('history')
// 处理图片代理
......@@ -490,24 +491,17 @@ const handleChangeYear = () => {
const yearList = ref([
{
label: "全部",
value: 'all'
},
{
label: "2025",
value: 2025
},
{
label: "2024",
value: 2024
},
{
label: "2023",
value: 2023
const yearList = ref([]);
const generateYearList = () => {
const currentYear = new Date().getFullYear();
const list = [{ label: "全部", value: 'all' }];
for (let i = 0; i < 5; i++) {
const year = currentYear - i;
list.push({ label: String(year), value: year });
}
]);
yearList.value = list;
};
generateYearList();
const CharacterFundSource = ref([]);
const getCharacterFundSourceFn = async () => {
......@@ -516,7 +510,7 @@ const getCharacterFundSourceFn = async () => {
pageSize: 4,
currentPage: fundCurrentPage.value - 1,
};
if (selectedOption.value !== '全部') {
if (selectedOption.value !== 'all') {
params.year = selectedOption.value;
}
......@@ -528,10 +522,9 @@ const getCharacterFundSourceFn = async () => {
CharacterFundSource.value = res.data.content.map((item, index) => {
return {
rank: index + 1,
contributor: item.orgName,
contributor: item.companyName,
totalAmount: item.totalDonation,
individualAmount: item.personalDonation,
pacsAmount: item.pacsDonation
donationYear: item.donationYear
}
});
......@@ -627,7 +620,8 @@ const getCharacterLatestDynamicFn = async () => {
time: item.time,
industryList: item.industryList || ["人工智能"],
orgName: item.orgName,
remarks: item.remarks
remarks: item.remarks,
newsId: item.newsId
}));
total.value = res.data.totalElements;
} else {
......@@ -730,7 +724,7 @@ const info = ref(["人物详情", "历史提案", "人物关系" ]);
const infoActive = ref("人物详情");
const num = ref(['全部', "2025", "2024", "2023", "2022", "2021", "2020"]);
const numActive = ref("全部");
const selectedOption = ref("全部");
const selectedOption = ref("all");
const dialogVisible = ref(false);
const currentTag = ref(null)
......@@ -1837,45 +1831,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 {
......
......@@ -17,7 +17,8 @@
<img src="./assets/icon-star.svg" alt="" class="btn-icon" />
</div>
<div class="mainBox">
<div class="graph" id="relGraph"></div>
<div v-show="activeIndex !== '关系图'" class="graph" id="relGraph"></div>
<RelationChart v-if="activeIndex === '关系图' && graphData.rootId" :graph-data="graphData" />
</div>
</div>
</template>
......@@ -27,6 +28,7 @@ import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
import * as echarts from 'echarts'
import { getCharacterGlobalInfo, getCharacterRelation } from '@/api/characterPage/characterPage.js'
import { useRoute } from 'vue-router'
import RelationChart from '@/components/base/RelationChart/index.vue'
const route = useRoute()
const personId = ref(route.query.personId || 'Y000064')
......@@ -79,6 +81,7 @@ const nodes = ref([])
const links = ref([])
const characterInfo = ref({})
const CharacterRelation = ref([])
const graphData = ref({ rootId: '', nodes: [], lines: [] })
const list = ref([
{
......@@ -91,9 +94,9 @@ const list = ref([
label: '力导向布局',
icon: new URL('./assets/icon-force.svg', import.meta.url).href,
},
{
value: '树形布局',
label: '树形布局',
{
value: '关系图',
label: '关系图',
icon: new URL('./assets/icon-tree.svg', import.meta.url).href,
},
])
......@@ -193,6 +196,15 @@ const getCharacterRelationFn = async () => {
nodes.value = [centerNode]
links.value = []
}
// 构建关系图数据
const gNodes = [{ id: 'center', text: characterInfo.value.name || '' }]
const gLines = []
CharacterRelation.value.forEach((item, index) => {
gNodes.push({ id: `node_${index}`, text: item.name })
gLines.push({ from: 'center', to: `node_${index}`, text: item.relation || '' })
})
graphData.value = { rootId: 'center', nodes: gNodes, lines: gLines }
}
const handleChangeLayout = (value) => {
......
......@@ -67,7 +67,7 @@
<img src="./assets/type1.png" alt="" v-if="item.remarks === true" />
<img src="./assets/type2.png" alt="" v-else />
</div>
<div class="content">
<div class="content" style="cursor: pointer;" @click="gotoNewsDetail(item.newsId)">
<div
:class="{ 'content-type1': item.remarks === true, 'content-type2': item.remarks === false }">
<p v-if="item.remarks === true" class="content-title1">{{ item.content }}</p>
......@@ -273,7 +273,9 @@ import DefaultIcon1 from '@/assets/icons/default-icon1.png'
import DefaultIcon2 from '@/assets/icons/default-icon2.png'
import { Close } from '@element-plus/icons-vue'
import { useRoute } from 'vue-router';
import { useGotoNewsDetail } from '@/router/modules/news.js';
const route = useRoute();
const gotoNewsDetail = useGotoNewsDetail();
const areaList = ref([])
const personId = ref(route.query.personId || "Y000064");
const wordloading=ref(false)
......@@ -545,7 +547,8 @@ const getCharacterLatestDynamicFn = async () => {
time: item.time,
industryList: item.industryList || ["人工智能"],
orgName: item.orgName,
remarks: item.remarks
remarks: item.remarks,
newsId: item.newsId
}));
total.value = res.data.totalElements;
} else {
......
......@@ -20,11 +20,15 @@
<button
:class="['news-tab', { active: activeTab === 'local' }]"
@click="$emit('update:activeTab', 'local')"
> 报告</button>
> 智库报告</button>
<button
:class="['news-tab', { active: activeTab === 'capital' }]"
@click="$emit('update:activeTab', 'capital')"
> 项目</button>
> 调查项目</button>
<button
:class="['news-tab', { active: activeTab === 'hearing' }]"
@click="$emit('update:activeTab', 'hearing')"
> 听证会</button>
</div>
<div class="news-sort" ref="sortDropdownRef">
......
......@@ -48,7 +48,7 @@
<img src="./assets/type1.png" alt="" v-if="item.remarks === true" /><img
src="./assets/type2.png" alt="" v-else />
</div>
<div class="content">
<div class="content" style="cursor: pointer;" @click="gotoNewsDetail(item.newsId)">
<div
:class="{ 'content-type1': item.remarks === true, 'content-type2': item.remarks === false }">
<p v-if="item.remarks === true" class="content-title1">
......@@ -202,7 +202,9 @@ import DefaultIcon1 from '@/assets/icons/default-icon1.png'
import DefaultIcon2 from '@/assets/icons/default-icon2.png'
import { useRoute } from 'vue-router';
import { useGotoNewsDetail } from '@/router/modules/news.js';
const route = useRoute();
const gotoNewsDetail = useGotoNewsDetail();
const personId = ref(route.query.personId || "Y000064");
const boxHeight = computed(() => {
if(characterBasicInfo.value.organizationList==undefined) return '625px'
......@@ -370,7 +372,8 @@ const getCharacterLatestDynamicFn = async () => {
time: item.time,
industryList: item.industryList || ["人工智能"],
orgName: item.orgName,
remarks: item.remarks
remarks: item.remarks,
newsId: item.newsId
}));
total.value = res.data.totalElements;
} else {
......
/**
* 人物主页 - 类型配置
* 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: "dynamic",
yearStaticOptions: [],
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: "dynamic",
yearStaticOptions: [],
showFundSource: false,
resumeMode: "inline",
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"
}
};
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论