提交 5d43cdf1 authored 作者: 张烨's avatar 张烨

Merge remote-tracking branch 'origin/pre' into zy-dev

流水线 #424 已通过 于阶段
in 1 分 36 秒
import { getToken } from "@/api/request.js";
import { getToken, formatBearerAuthorization } from "@/api/request.js";
const CHART_INTERPRETATION_URL = "/aiAnalysis/chart_interpretation";
const API_KEY = "aircasKEY19491001";
......@@ -183,6 +183,8 @@ export function getChartAnalysis(data, options = {}) {
(async () => {
try {
const { fetchEventSource } = await import("@microsoft/fetch-event-source");
const sseRaw = getToken();
const sseToken = sseRaw ? String(sseRaw).trim() : "";
await fetchEventSource(CHART_INTERPRETATION_URL, {
method: "POST",
......@@ -191,8 +193,7 @@ export function getChartAnalysis(data, options = {}) {
Accept: "text/event-stream",
"Cache-Control": "no-cache",
"X-API-Key": API_KEY,
// 后端同项目其它接口使用 token 字段名(axios 拦截器里就是这样注入的)
token: getToken()
...(sseToken ? { token: sseToken } : {})
},
body: JSON.stringify(data),
signal: abortController.signal,
......
......@@ -12,11 +12,15 @@ export function search(data) {
})
}
/**
* @param { type } 4:智库列表 6:科技人物所属机构
*/
// 智库列表
export function getThinkTankList() {
export function getThinkTankList(params) {
return request({
method: 'GET',
url: `/temporarySearch/search-info/all-organization-names`,
params
})
}
......@@ -66,4 +70,20 @@ export function getControlReason() {
method: 'GET',
url: `/temporarySearch/search-info/all-controlReason`,
})
}
// 科技人物-人物类别
export function getPersonType() {
return request({
method: 'GET',
url: `/temporarySearch/search-info/all-personType`,
})
}
// 科技企业-企业类型
export function getBusinessType() {
return request({
method: 'GET',
url: `/temporarySearch/search-info/all-businessType`,
})
}
\ No newline at end of file
......@@ -498,14 +498,15 @@ export function getEntitiesUpdateCount(sanTypeId = 1) {
* @param {string} rule - 规则
* @param {string} type - 类型
*/
export function getSanDomainCount(rule, type) {
export function getSanDomainCount(rule, sanTypeIds, type) {
return request200(
request({
method: "GET",
url: "/api/entitiesDataCount/getSanDomainCount",
params: {
rule,
type
sanTypeIds
// type
}
})
);
......
......@@ -136,3 +136,35 @@ export function getRelateNews(sanRecordId) {
export function getReasonAndSan(sanRecordId) {
return http.get(`/api/sanctionList/invFin/getReasonAndSan?sanRecordId=${sanRecordId}`);
}
/**
* 制裁历程
* url:/entitiesDataCount/getSanRecord
*/
export function getSanRecord(params) {
return http.get("/api/entitiesDataCount/getSanRecord", params);
}
/**
* 限制关系分析-限制举措关系图
* url:/sanctionList/invFin/recordRelation
*/
export function getRecordRelation(sanRecordIds) {
return http.get(`/api/sanctionList/invFin/recordRelation?sanRecordIds=${sanRecordIds}`);
}
/**
* 查询投融资限制关联-图谱-节点详情
* url:/sanctionList/invFin/getVertexInfo
*/
export function getVertexInfo(sanRecordId) {
return http.get(`/api/sanctionList/invFin/getVertexInfo?sanRecordId=${sanRecordId}`);
}
/**
* 查询投融资限制关联-图谱-关系详情
* url:/sanctionList/invFin/getEdgeInfo
*/
export function getEdgeInfo(edgeId) {
return http.get(`/api/sanctionList/invFin/getEdgeInfo?edgeId=${edgeId}`);
}
......@@ -2,27 +2,8 @@
import axios from 'axios'
// 引入 element-plus 里面的消息提示
import { ElMessage } from 'element-plus'
import { getToken, setToken, removeToken, formatBearerAuthorization } from '@/api/request.js'
// Token 管理
const TOKEN_KEY = 'auth_token'
// 获取token
const getToken = () => {
return 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiIsImlzcyI6ImRhdGEtY2VudGVyIiwiZXhwIjozODI1ODM1NTkxLCJpYXQiOjE2NzgzNTE5NTMsImp0aSI6IjI4YmY1NTZjMTc0MDQ3YjJiNTExNWM3NzVhYjhlNWRmIiwidXNlcm5hbWUiOiJzdXBlcl91c2VyIn0.zHyVzsleX2lEqjDBYRpwluu_wy2nZKGl0dw3IUGnKNw'
// return localStorage.getItem(TOKEN_KEY)
}
// 设置 token
const setToken = (token) => {
localStorage.setItem(TOKEN_KEY, token)
}
// 移除 token
const removeToken = () => {
localStorage.removeItem(TOKEN_KEY)
}
// 导出 token 管理方法
export { getToken, setToken, removeToken }
// 创建 axios 实例
......@@ -30,19 +11,35 @@ const service = axios.create({
timeout: 300 * 1000 // 请求超时时间
})
// request 拦截器
// request 拦截器:与主 request 一致,就地改 config,避免 mergeConfig 破坏 url
service.interceptors.request.use(config => {
// 获取 token 并添加到请求头
const token = getToken()
const raw = getToken()
const token = raw ? String(raw).trim() : ""
if (!config.headers) {
config.headers = new axios.AxiosHeaders()
} else if (!(config.headers instanceof axios.AxiosHeaders)) {
config.headers = axios.AxiosHeaders.from(config.headers)
}
if (token) {
config.headers['token'] = token
config.headers.set('token', token, true)
} else {
config.headers.delete('token')
config.headers.delete('Token')
// ===== 旧逻辑保留(勿删):Authorization: Bearer <token> =====
// config.headers.delete('Authorization')
// config.headers.delete('authorization')
}
// 图表解读等 AI 分析服务(Vite 代理 /aiAnalysis)需要 X-API-Key
const reqUrl = String(config.url || '')
const reqUrl = String(config.url ?? '')
if (reqUrl.includes('aiAnalysis')) {
const aiApiKey = import.meta.env.VITE_AI_ANALYSIS_API_KEY
if (aiApiKey) {
config.headers['X-API-Key'] = aiApiKey
if (!config.headers) {
config.headers = new axios.AxiosHeaders()
} else if (!(config.headers instanceof axios.AxiosHeaders)) {
config.headers = axios.AxiosHeaders.from(config.headers)
}
config.headers.set('X-API-Key', aiApiKey)
}
}
return config
......@@ -54,7 +51,10 @@ service.interceptors.request.use(config => {
// response 拦截器
service.interceptors.response.use(
response => {
const res = response.data
const res = response?.data
if (!res) {
return Promise.reject(new Error('响应数据为空'))
}
// 根据需求:接口返回 code 不等于 200 的时候报错
if (res.code !== 200) {
ElMessage({
......@@ -90,10 +90,20 @@ service.interceptors.response.use(
type: 'error',
duration: 3 * 1000
})
removeToken()
const h = error.config?.headers
const hadToken =
h &&
(typeof h.get === 'function'
? Boolean(
h.get('token') || h.get('Token')
)
: Boolean(
h.token || h.Token
))
if (hadToken) removeToken()
} else {
ElMessage({
message: error.message,
message: typeof error?.message === 'string' ? error.message : '请求失败',
type: 'error',
duration: 3 * 1000
})
......
......@@ -8,8 +8,19 @@ import {
// Token管理
const TOKEN_KEY = 'auth_token'
// ===== 兼容导出(勿删):历史代码仍会 import formatBearerAuthorization =====
// 说明:当前线上版本后端用 `token` 头,不用 Authorization;但为了不影响其它模块编译/运行,这里保留该方法导出。
const formatBearerAuthorization = (raw) => {
const v = String(raw || "").trim()
if (!v) return ""
if (/^Bearer\s+/i.test(v)) return v
return `Bearer ${v}`
}
// ===== 兼容导出(勿删)=====
// 获取token
const getToken = () => {
// 固定 token(恢复原行为:所有请求都带这个 token)
return 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkYXRhLWNlbnRlciIsImF1ZCI6IndlYiIsImlzcyI6ImRhdGEtY2VudGVyIiwiZXhwIjozODI1ODM1NTkxLCJpYXQiOjE2NzgzNTE5NTMsImp0aSI6IjI4YmY1NTZjMTc0MDQ3YjJiNTExNWM3NzVhYjhlNWRmIiwidXNlcm5hbWUiOiJzdXBlcl91c2VyIn0.zHyVzsleX2lEqjDBYRpwluu_wy2nZKGl0dw3IUGnKNw'
// return localStorage.getItem(TOKEN_KEY)
}
......@@ -25,7 +36,7 @@ const removeToken = () => {
}
// 导出token管理方法
export { getToken, setToken, removeToken }
export { getToken, setToken, removeToken, formatBearerAuthorization }
// const BASE_API = import.meta.env.VITE_BASE_API
// 创建axios实例
......@@ -89,6 +100,28 @@ service.interceptors.response.use(
// 重复请求触发的取消不提示错误
if (isCanceledError) return Promise.reject(error)
// 特殊处理:用户信息接口可能在某些环境不存在(404),此时不弹出错误提示
// 需求:GET /api/sso/user/info 返回 404 时静默
try {
const errUrl = String(error?.config?.url || '')
if (error?.response?.status === 404 && errUrl.includes('/api/sso/user/info')) {
return Promise.reject(error)
}
} catch {
// ignore
}
// 特殊处理:风险信号管理页面接口偶发 500,不弹出提示
// 覆盖接口:/api/riskSignal/getCountInfo | /api/riskSignal/getDailyCount | /api/riskSignal/pageQuery
try {
const errUrl = String(error?.config?.url || '')
if (error?.response?.status === 500 && errUrl.includes('/api/riskSignal/')) {
return Promise.reject(error)
}
} catch {
// ignore
}
// 处理token过期或无效的情况(排除 AI 分析服务:其 401 多为 API Key 问题)
const errUrl = String(error.config?.url || '')
......@@ -118,4 +151,12 @@ service.interceptors.response.use(
}
)
export default service
\ No newline at end of file
export default service
/*
===========================
下面是“当前新版 request.js”代码备份(按你的要求:不删除,仅注释保留)
===========================
(此处保留了之前实现的 token 多来源读取、AxiosHeaders、默认头同步、401 防抖等逻辑)
*/
\ No newline at end of file
......@@ -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 }
});
}
// 智库概览信息
import request, { getToken } from "@/api/request.js";
import request, { getToken, formatBearerAuthorization } from "@/api/request.js";
// 智库列表
export function getThinkTankList() {
......@@ -257,7 +257,8 @@ export function postReportDomainViewAnalysis(data) {
*/
export async function postReportDomainViewAnalysisStream(data, handlers = {}) {
const { onReasoningChunk, onMessage } = handlers
const token = getToken()
const raw = getToken()
const token = raw ? String(raw).trim() : ""
const response = await fetch('/intelligent-api/report-domain-view-analysis', {
method: 'POST',
headers: {
......
/**
* 必须在 router、业务组件之前执行,且不能 import @/api/request(会提前加载 element-plus,易触发 Vue 初始化异常)。
* 与 request.js 中 TOKEN_KEY / TOKEN_SESSION_KEY 保持一致。
*/
const TOKEN_KEY = "auth_token";
const TOKEN_SESSION_KEY = "auth_token";
if (typeof window !== "undefined") {
try {
const q = new URLSearchParams(window.location.search || "");
const t = q.get("token");
if (t && String(t).trim()) {
const v = String(t).trim();
localStorage.setItem(TOKEN_KEY, v);
sessionStorage.setItem(TOKEN_SESSION_KEY, v);
}
} catch {
// ignore
}
}
<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;
}
.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;
}
.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="summary-cards-panel" :style="{ width: panelWidth }" v-loading="loading">
<div class="date-box">
<div class="date-icon" v-if="tipIcon">
<img :src="tipIcon" alt="" />
</div>
<div class="date-text">{{ descriptionText }}</div>
<TimeTabPane @time-click="onTimeClick" :activeTime="activeTime" />
</div>
<div class="cards-box" v-if="cards.length">
<div class="summary-item" v-for="item in cards" :key="item.id || item.name">
<div class="item-left">
<img :src="item.avatar || defaultAvatar" alt="" />
</div>
<div class="item-center" @click="emit('name-click', item)">
<div class="item-name one-line-ellipsis">{{ item.name }}</div>
<div v-if="item.subText" class="item-sub-text one-line-ellipsis">{{ item.subText }}</div>
</div>
<el-popover content="跳转至数据资源库" placement="top">
<template #reference>
<div class="item-total" @click="emit('count-click', item)">{{ item.count }}{{ countSuffix }}</div>
</template>
</el-popover>
<el-icon color="var(--color-primary-100)">
<ArrowRightBold />
</el-icon>
<div class="item-dot" v-if="item.delta">+{{ item.delta }}</div>
</div>
<div v-if="shouldShowMoreCard" class="summary-item" @click="emit('more-click')">
<div class="item-more">{{ moreText }} ({{ totalCount }}家)</div>
<el-icon color="var(--color-primary-100)">
<ArrowRightBold />
</el-icon>
</div>
</div>
<div v-else class="cards-empty">
<el-empty :description="emptyText" :image-size="80" />
</div>
</div>
</template>
<script setup>
import TimeTabPane from "@/components/base/TimeTabPane/index.vue";
import { ArrowRightBold } from "@element-plus/icons-vue";
import { computed } from "vue";
const props = defineProps({
descriptionText: {
type: String,
default: ""
},
cards: {
type: Array,
default: () => []
},
totalCount: {
type: Number,
default: 0
},
showMoreCard: {
type: Boolean,
default: true
},
moreCardMinCount: {
type: Number,
default: 8
},
emptyText: {
type: String,
default: "暂无数据"
},
activeTime: {
type: String,
default: "近一年"
},
tipIcon: {
type: String,
default: ""
},
defaultAvatar: {
type: String,
default: ""
},
countSuffix: {
type: String,
default: "项"
},
moreText: {
type: String,
default: "查看全部机构"
},
panelWidth: {
type: String,
default: "1600px"
},
loading: {
type: Boolean,
default: false
}
});
const emit = defineEmits(["time-click", "name-click", "count-click", "more-click"]);
const shouldShowMoreCard = computed(() => {
if (!props.showMoreCard) return false;
if (props.moreCardMinCount <= 0) return true;
return (props.cards?.length || 0) >= props.moreCardMinCount;
});
const onTimeClick = event => {
emit("time-click", event);
};
</script>
<style scoped lang="scss">
.summary-cards-panel {
margin: 48px auto 0;
.date-box {
display: flex;
align-items: center;
.date-icon {
width: 16px;
height: 16px;
font-size: 0;
margin-right: 6px;
img {
width: 100%;
height: 100%;
}
}
.date-text {
width: 20px;
flex: auto;
font-size: 18px;
line-height: 18px;
font-family: Source Han Sans CN;
color: var(--text-primary-80-color);
}
}
.cards-box {
margin: 20px 0 64px;
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 80px;
gap: 16px;
font-family: Microsoft YaHei;
.summary-item {
padding: 0 16px;
display: flex;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.65);
border: 1px solid rgba(255, 255, 255, 1);
border-radius: 10px;
box-shadow: 0 0 20px 0 rgba(25, 69, 130, 0.1);
align-items: center;
justify-content: center;
cursor: pointer;
transition: transform 0.3s ease, box-shadow 0.3s ease;
position: relative;
&:hover {
transform: translateY(-3px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
.item-left {
width: 48px;
height: 48px;
font-size: 0;
img {
width: 100%;
height: 100%;
}
}
.item-center {
width: 20px;
flex: auto;
margin: 0 16px;
cursor: pointer;
}
.item-name {
color: rgba(59, 65, 75, 1);
font-size: 20px;
font-weight: 700;
line-height: 20px;
&:hover {
color: var(--color-primary-100);
text-decoration: underline;
}
}
.item-sub-text {
margin-top: 6px;
color: var(--text-primary-50-color);
font-size: 14px;
line-height: 18px;
}
.item-total {
font-size: 20px;
margin-right: 2px;
white-space: nowrap;
font-weight: 700;
line-height: 20px;
color: var(--color-primary-100);
&:hover {
text-decoration: underline;
}
}
.item-more {
font-size: 16px;
margin-right: 12px;
white-space: nowrap;
font-weight: 700;
line-height: 16px;
color: var(--color-primary-100);
}
.item-dot {
position: absolute;
right: -13px;
top: -10px;
padding: 0 8px;
height: 26px;
background-color: #ff4d4f;
color: white;
font-size: 16px;
line-height: 26px;
font-family: Source Han Sans CN;
border-radius: 14px;
letter-spacing: 1px;
}
}
}
.cards-empty {
margin: 20px 0 64px;
height: 120px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.65);
border: 1px solid rgba(255, 255, 255, 1);
border-radius: 10px;
box-shadow: 0 0 20px 0 rgba(25, 69, 130, 0.1);
}
}
</style>
......@@ -20,14 +20,37 @@
</div>
<div class="nav-right">
<div class="info-box" @click="handleClickToolBox">
<div class="mail">
<div class="info-box">
<div class="mail" @click="handleClickToolBox">
<img src="@/assets/icons/overview/mail.png" alt="" />
</div>
<div class="user">
<img src="@/assets/icons/overview/user.png" alt="" />
<div class="user-trigger">
<div class="user" @click.stop="handleToggleUserPanel">
<img src="@/assets/icons/overview/user.png" alt="" />
</div>
</div>
<div v-if="isShowUserPanel" class="user-panel">
<div class="user-panel-row">
<div class="blue-solid"></div>
<span class="user-panel-value user-panel-value--nickname">{{ userNickname }}</span>
<div class="role-box">
<span class="user-panel-value user-panel-value--role">{{ roleName }}</span>
</div>
</div>
<div class="user-panel-row user-panel-row--single">
<span class="user-panel-value user-panel-value--organ">{{ organName }}</span>
</div>
<div class="solid"></div>
<div class="user-panel-logout" @click.stop="handleUserCommand('logout')"><img src="./back.png"
class="back-image" />{{ "退出登录" }}
</div>
</div>
<div class="name text-regular">{{ "管理员" }}</div>
</div>
</div>
<div class="menu-box" v-show="isShowMenu" @mouseenter="handleHoverMenu(true)"
......@@ -61,6 +84,7 @@ import { ref, computed, onMounted, watchEffect, onUnmounted } from "vue";
import { useRouter } from "vue-router";
import { useRoute } from "vue-router";
import { getPersonType } from "@/api/common/index";
import request, { removeToken } from "@/api/request.js";
import SearchBar from "@/components/layout/SearchBar.vue";
import Menu1 from "@/assets/icons/overview/menu1.png";
......@@ -87,6 +111,16 @@ const router = useRouter();
const route = useRoute()
const personTypeList = ref([]);
const ssoUserInfo = ref(null);
const isShowUserPanel = ref(false);
// 用户面板展示兜底:接口无数据时不显示 "-",改为默认文案
const userNickname = computed(() => ssoUserInfo.value?.userInfo?.userNickname || "管理员");
const organName = computed(() => ssoUserInfo.value?.organInfo?.organName || "开发部门");
const roleName = computed(() => {
const roles = ssoUserInfo.value?.roles;
return Array.isArray(roles) && roles.length ? (roles[0]?.roleName || "系统管理员") : "系统管理员";
});
// 获取人物类别
const handleGetPersonType = async () => {
......@@ -102,6 +136,21 @@ const handleGetPersonType = async () => {
} catch (error) { }
};
// 获取当前登录用户信息(不展示,仅缓存;Authorization 由全局 request 拦截器自动注入)
const handleGetSsoUserInfo = async () => {
try {
const res = await request({
method: "GET",
// 通过 Vite 代理:/api -> 172.19.21.228:28080(rewrite 去掉 /api,后端收到 /sso/user/info)
url: "/api/sso/user/info"
});
ssoUserInfo.value = res?.data ?? null;
console.log("用户信息", ssoUserInfo.value);
} catch {
ssoUserInfo.value = null;
}
};
// 概览页标题列表
const homeTitleList = ref([
......@@ -354,8 +403,93 @@ const handleClickToolBox = () => {
ElMessage.warning("当前功能正在开发中,敬请期待!");
};
const handleToggleUserPanel = () => {
isShowUserPanel.value = !isShowUserPanel.value;
};
const handleHideUserPanel = () => {
isShowUserPanel.value = false;
};
const handleUserCommand = async (command) => {
if (command === "logout") {
isShowUserPanel.value = false;
// ===== 退出登录(外部重定向版,切线上接口时先注释保留) =====
// const ssoOrigin = import.meta.env.VITE_SSO_GATEWAY_ORIGIN || "http://172.19.21.228:28080";
// try {
// // 走后端登出接口,返回统一登出页地址(能清 SSO Cookie,确保下次必须重新登录)
// const res = await request({
// method: "POST",
// // 通过 Vite 代理:/api -> 172.19.21.228:28080(rewrite 去掉 /api,后端收到 /sso/logout)
// url: "/api/sso/logout"
// });
//
// // 无论成功失败,都清掉本地 token(确保后续请求不再带 Authorization)
// removeToken();
// try {
// window.localStorage.removeItem("auth_token");
// } catch {
// // ignore
// }
// try {
// window.sessionStorage.removeItem("auth_token");
// } catch {
// // ignore
// }
// try {
// window.sessionStorage.setItem("force_sso_login", "1");
// } catch {
// // ignore
// }
//
// const redirectUrl = res && typeof res.data === "string" ? res.data : "";
// window.location.replace(redirectUrl || `${ssoOrigin}/api/v2`);
// } catch (e) {
// // 兜底:清本地并跳网关
// removeToken();
// try {
// window.localStorage.removeItem("auth_token");
// } catch {
// // ignore
// }
// try {
// window.sessionStorage.removeItem("auth_token");
// } catch {
// // ignore
// }
// try {
// window.sessionStorage.setItem("force_sso_login", "1");
// } catch {
// // ignore
// }
// window.location.replace(`${ssoOrigin}/api/v2`);
// }
// ===== 退出登录(站内 login 版:清 token 后进入 /login) =====
removeToken();
try {
window.localStorage.removeItem("auth_token");
} catch {
// ignore
}
// 退出后强制所有页面回到登录页(跨标签页/刷新生效)
try {
window.localStorage.setItem("force_login", "1");
} catch {
// ignore
}
try {
window.sessionStorage.removeItem("auth_token");
} catch {
// ignore
}
router.replace("/login");
}
};
onMounted(() => {
handleGetPersonType();
handleGetSsoUserInfo();
});
</script>
......@@ -363,7 +497,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%);
......@@ -469,6 +604,8 @@ onMounted(() => {
display: flex;
justify-content: flex-end;
align-items: center;
position: relative;
.mail {
width: 32px;
......@@ -499,6 +636,159 @@ onMounted(() => {
height: 30px;
color: var(--text-primary-80-color);
}
.user-trigger {
display: flex;
align-items: center;
cursor: pointer;
}
.user-panel {
position: absolute;
right: 0;
top: calc(32px + 21px);
width: 240px;
height: 141px;
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);
display: flex;
flex-direction: column;
// 高度固定 141px,需要把内容控制在容器内,避免分割线被裁剪
padding: 16px 15px 12px 24px;
box-sizing: border-box;
overflow: hidden;
z-index: 999999;
}
.user-panel-row {
display: flex;
align-items: center;
justify-content: space-between;
flex-direction: row;
gap: 12px;
position: relative;
padding-right: 3px;
}
.user-panel-row--single {
justify-content: flex-start;
}
.blue-solid {
position: absolute;
left: -24px;
width: 4px;
height: 48px;
background: rgb(5, 95, 194);
top: 0;
}
.solid {
width: 206px;
height: 1px;
background: rgb(234, 236, 238);
margin-top: 13px;
}
.user-panel-label {
color: rgba(59, 65, 75, 0.8);
font-size: 14px;
line-height: 20px;
flex-shrink: 0;
}
.user-panel-value {
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 14px;
line-height: 22px;
letter-spacing: 0px;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-panel-value--nickname {
font-family: "Source Han Sans CN";
font-weight: 700;
font-size: 18px;
line-height: 24px;
letter-spacing: 0px;
text-align: left;
color: rgb(59, 65, 75);
margin-left: 8px;
}
.role-box {
padding: 0px 8px;
background-color: rgb(231, 243, 255);
display: flex;
border-radius: 4px;
}
.user-panel-value--role {
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 14px;
line-height: 22px;
letter-spacing: 0px;
text-align: justify;
color: rgb(5, 95, 194);
}
.user-panel-value--organ {
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 14px;
line-height: 22px;
letter-spacing: 0px;
text-align: left;
color: rgb(95, 101, 108);
margin-top: 4px;
margin-left: 8px;
}
.user-panel-logout {
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
line-height: 24px;
letter-spacing: 0px;
text-align: justify;
color: rgb(59, 65, 75);
cursor: pointer;
height: 24px;
margin-top: 12px;
display: flex;
align-items: center;
margin-left: 8px;
.back-image {
width: 16px;
height: 16px;
margin-right: 12px;
img {
width: 100%;
height: 100%;
display: block;
}
}
}
}
}
......
......@@ -151,15 +151,15 @@ const handleToNewsAnalysis = (item, index) => {
&:hover {
background: var(--color-bg-hover);
.right-top .title {
.right-top > .title {
text-decoration: underline;
color: rgb(5, 95, 194) !important;
font-weight: 700;
}
.right-top .text-inner {
border-bottom-color: rgb(5, 95, 194) !important;
}
// .right-top .text-inner {
// border-bottom-color: rgb(5, 95, 194) !important;
// }
}
.left {
......
import "./bootstrapAuth.js";
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
......
// 企业资源库
const DataCompany = () => import('@/views/dataLibrary/company/index.vue')
const dataCompanyRoutes = [
// 企业资源库路由
{
path: "/dataLibrary/dataCompany",
name: "DataCompany",
component: DataCompany,
meta: {
title: '科技企业', // 显示在tag上的文字
affix: false, // 是否为固定tag(首页通常设置为true,不可关闭)
keepAlive: true // 是否需要缓存
}
},
]
export default dataCompanyRoutes
\ No newline at end of file
// 机构资源库
const DataIns = () => import('@/views/dataLibrary/institution/index.vue')
const dataInsRoutes = [
// 机构资源库路由
{
path: "/dataLibrary/dataInstitution",
name: "DataIns",
component: DataIns,
meta: {
title: '政府机构', // 显示在tag上的文字
affix: false, // 是否为固定tag(首页通常设置为true,不可关闭)
keepAlive: true // 是否需要缓存
}
},
]
export default dataInsRoutes
\ No newline at end of file
// 新闻资源库
const DataNews = () => import('@/views/dataLibrary/news/index.vue')
const dataNewsRoutes = [
// 新闻资源库路由
{
path: "/dataLibrary/dataNews",
name: "DataNews",
component: DataNews,
meta: {
title: '新闻', // 显示在tag上的文字
affix: false, // 是否为固定tag(首页通常设置为true,不可关闭)
keepAlive: true // 是否需要缓存
}
},
]
export default dataNewsRoutes
\ No newline at end of file
// 风险信号资源库
const DataRiskSignal = () => import('@/views/dataLibrary/riskSignal/index.vue')
const dataRiskSignalRoutes = [
// 风险信号资源库路由
{
path: "/dataLibrary/dataRiskSignal",
name: "DataRiskSignal",
component: DataRiskSignal,
meta: {
title: '风险信号', // 显示在tag上的文字
affix: false, // 是否为固定tag(首页通常设置为true,不可关闭)
keepAlive: true // 是否需要缓存
}
},
]
export default dataRiskSignalRoutes
\ No newline at end of file
// 法案资源库
const CongressMan = () => import('@/views/dataLibrary/technologyFigures/congressMan/index.vue')
const Minister = () => import('@/views/dataLibrary/technologyFigures/minister/index.vue')
const TechnologyLeader = () => import('@/views/dataLibrary/technologyFigures/technologyLeader/index.vue')
const ThinkTankResearcher = () => import('@/views/dataLibrary/technologyFigures/thinkTankResearcher/index.vue')
// 风险信号资源库
const DataTechnologyFigures = () => import('@/views/dataLibrary/technologyFigures/index.vue')
const dataDecreeRoutes = [
// 科技法案资源库路由
const dataTechnologyFiguresRoutes = [
// 风险信号资源库路由
{
path: "/dataLibrary/congressMan",
name: "CongressMan",
component: CongressMan,
path: "/dataLibrary/dataTechnologyFigures",
name: "DataTechnologyFigures",
component: DataTechnologyFigures,
meta: {
title: '国会议员', // 显示在tag上的文字
affix: false, // 是否为固定tag(首页通常设置为true,不可关闭)
keepAlive: true // 是否需要缓存
}
},
{
path: "/dataLibrary/minister",
name: "Minister",
component: Minister,
meta: {
title: '机构主官', // 显示在tag上的文字
affix: false, // 是否为固定tag(首页通常设置为true,不可关闭)
keepAlive: true // 是否需要缓存
}
},
{
path: "/dataLibrary/technologyLeader",
name: "TechnologyLeader",
component: TechnologyLeader,
meta: {
title: '科技企业领袖', // 显示在tag上的文字
affix: false, // 是否为固定tag(首页通常设置为true,不可关闭)
keepAlive: true // 是否需要缓存
}
},
{
path: "/dataLibrary/thinkTankResearcher",
name: "ThinkTankResearcher",
component: ThinkTankResearcher,
meta: {
title: '智库研究人员', // 显示在tag上的文字
title: '科技人物', // 显示在tag上的文字
affix: false, // 是否为固定tag(首页通常设置为true,不可关闭)
keepAlive: true // 是否需要缓存
}
......@@ -49,4 +16,4 @@ const dataDecreeRoutes = [
]
export default dataDecreeRoutes
\ No newline at end of file
export default dataTechnologyFiguresRoutes
\ No newline at end of file
import { createRouter, createWebHistory } from "vue-router";
import { getIsLoggedIn } from "@/utils/auth";
import { setToken, removeToken, getToken } from "@/api/request.js";
/** localStorage:跨标签页记录当前前端的 bootId(与 vite define 的 __APP_BOOT_ID__ 对齐) */
const VITE_BOOT_STORAGE_KEY = "app_vite_boot_id";
/** 退出后强制回登录页(跨标签页/刷新生效) */
const FORCE_LOGIN_KEY = "force_login";
// ===== SSO/重定向相关(切线上接口时先注释保留) =====
// /** 强制走统一登录(仅前端控制):退出登录后置 1,直到 SSO 回跳带 ?token= 再清除 */
// const FORCE_SSO_LOGIN_KEY = "force_sso_login";
function getCurrentViteBootId() {
try {
// eslint-disable-next-line no-undef
return typeof __APP_BOOT_ID__ !== "undefined" ? String(__APP_BOOT_ID__) : "";
} catch {
return "";
}
}
/** 仅同步 bootId(SSO 回跳带 ?token= 时会提前 return,须单独写入,否则下次进入 /ZMOverView 会误判 boot 变更而清 token) */
function persistViteBootIdOnly() {
if (typeof window === "undefined") return;
const current = getCurrentViteBootId();
if (!current) return;
try {
window.localStorage.setItem(VITE_BOOT_STORAGE_KEY, current);
} catch {
// ignore
}
}
/**
* 每次重启 Vite/重新构建后 __APP_BOOT_ID__ 会变;仅当「曾记录过旧 bootId 且与当前不一致」时清空 token。
* 用 localStorage 而非 sessionStorage,避免新开标签页时误把有效 token 清掉导致请求不带 token、全员 401。
* 注意:须在处理完 URL ?token= 之后再执行,避免先清 token 再写入的竞态。
*/
function clearTokenIfNewDevBoot() {
if (typeof window === "undefined") return;
try {
const current = getCurrentViteBootId();
if (!current) return;
const saved = window.localStorage.getItem(VITE_BOOT_STORAGE_KEY);
if (saved && saved !== current) {
removeToken();
}
window.localStorage.setItem(VITE_BOOT_STORAGE_KEY, current);
} catch {
// ignore
}
}
// /** 统一认证网关:未登录时整页跳转此处,由后端再跳到登录页;登录成功后回跳到前端并带 ?token= */
// const SSO_GATEWAY_ORIGIN =
// import.meta.env.VITE_SSO_GATEWAY_ORIGIN || "http://172.19.21.228:28080";
//
// /** 网关注册的回跳地址常为 /callback;保存 token 后应进入业务首页,避免停在无路由的 /callback */
// const SSO_POST_LOGIN_PATH =
// import.meta.env.VITE_SSO_POST_LOGIN_PATH || "/ZMOverView";
/**
* 是否与「请求里 getToken()」一致:避免出现仅空白/脏数据时仍认为已登录 → 不走 SSO 且请求头无 token
*/
function hasStoredAuthToken() {
return Boolean(getToken());
}
const Home = () => import('@/views/home/index.vue')
const DataLibrary = () => import('@/views/dataLibrary/index.vue')
......@@ -33,6 +97,8 @@ const routes = [
{
path: "/",
name: "Home",
// 访问根路径时默认进入概览页(否则 Home 的 router-view 为空,看起来像白屏)
redirect: "/ZMOverView",
component: Home,
children: [
...fileRoutes,
......@@ -73,18 +139,88 @@ const router = createRouter({
routes
});
// 路由守卫 - 设置页面标题
// 路由守卫:SSO + 设置页面标题
// 1)首次/无本地 token:整页跳到网关 /api/v2 + 当前路径,走后端登录
// 2)登录成功回跳带 ?token=:先 setToken 并同步 bootId,再去掉 URL 中的 token(须先于 clearTokenIfNewDevBoot,避免误清刚写入的登录态)
// 3)已有本地 token:正常走前端路由
router.beforeEach((to, from, next) => {
// 登录态:同一次前端服务 BOOT_ID 内跨刷新/跨新标签有效;服务重启后自动失效
const isAuthed = getIsLoggedIn();
// ===== SSO/重定向逻辑(切线上接口时停用,保留注释) =====
// const queryToken = to.query && to.query.token != null && String(to.query.token).trim() !== ""
// ? String(to.query.token).trim()
// : "";
//
// if (queryToken) {
// setToken(queryToken);
// // 成功回跳拿到 token,说明统一登录链路已完成,清除强制标记
// try {
// if (typeof window !== "undefined") {
// window.sessionStorage.removeItem(FORCE_SSO_LOGIN_KEY);
// }
// } catch {
// // ignore
// }
// persistViteBootIdOnly();
// const restQuery = { ...to.query };
// delete restQuery.token;
// const isGatewayCallback =
// to.path === "/callback" || to.path.replace(/\/$/, "") === "/callback";
// const targetPath = isGatewayCallback ? SSO_POST_LOGIN_PATH : to.path;
// next({
// path: targetPath,
// query: restQuery,
// hash: to.hash,
// replace: true,
// });
// return;
// }
//
// // 若用户点了“退出登录”,即使本地还有残留 token/或别处写回,也强制先走统一登录链路
// try {
// if (typeof window !== "undefined" && window.sessionStorage.getItem(FORCE_SSO_LOGIN_KEY) === "1") {
// removeToken();
// const targetUrl = `${SSO_GATEWAY_ORIGIN}/api/v2${to.fullPath || "/"}`;
// window.location.replace(targetUrl);
// next(false);
// return;
// }
// } catch {
// // ignore
// }
// 外网/线上版本:不因重启清登录态;仅开发环境需要此逻辑
// clearTokenIfNewDevBoot();
if (import.meta.env.DEV) {
clearTokenIfNewDevBoot();
}
// 启用站内登录页:无 token 时统一进入 /login(不做外部重定向)
// 退出登录后:无论当前 getToken() 返回什么,都强制先去 /login,直到再次登录清除此标记
let forceLoginActive = false;
try {
if (typeof window !== "undefined" && window.localStorage.getItem(FORCE_LOGIN_KEY) === "1") {
forceLoginActive = true;
if (to.path !== "/login") {
next({ path: "/login", replace: true });
return;
}
}
} catch {
// ignore
}
const isLoginRoute = to.name === "Login" || /^\/login\/?$/.test(String(to.path || ""));
const isAuthed = hasStoredAuthToken();
// 已登录:不应停留在 /login(首页应为 /ZMOverView)
// 说明:force_login=1 时表示用户主动退出,仍允许进入 /login;否则一律跳回业务首页
if (isAuthed && to.path === "/login" && !forceLoginActive) {
const redirect = (to.query && to.query.redirect) ? String(to.query.redirect) : "";
next({ path: redirect || "/ZMOverView", replace: true });
return;
}
if (!isLoginRoute && !isAuthed) {
next({
path: "/login",
query: { redirect: to.fullPath }
});
if (!isAuthed && to.path !== "/login") {
// 防止误入 /callback 空白页
next({ path: "/login", replace: true });
return;
}
......
......@@ -15,6 +15,7 @@ const BillProgressForecast = () => import('@/views/bill/influence/ProgressForeca
const BillInfluenceScientificResearch = () => import('@/views/bill/influence/scientificResearch/index.vue')
const BillRelevantCircumstance = () => import('@/views/bill/relevantCircumstance/index.vue')
const BillVersionCompare = () => import('@/views/bill/versionCompare/index.vue')
const BillAllCommittee = () => import('@/views/bill/allCommittee/index.vue')
const billRoutes = [
......@@ -28,6 +29,19 @@ const billRoutes = [
isShowHeader: true
}
},
{
path: "/bill/allCommittee",
name: "BillAllCommittee",
component: BillAllCommittee,
meta: {
title: "法案委员会列表",
isShowHeader: true
}
},
{
path: "/billAllCommittee",
redirect: "/bill/allCommittee"
},
{
path: "/billLayout",
name: "BillLayoutContainer",
......
// 规则限制
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
/** 从各概览页跳转风险信号管理页,并自动打开列表第一条详情的 el-dialog */
export const OPEN_FIRST_RISK_DETAIL_QUERY_KEY = "openFirstDetail";
/**
* 概览页 -> 风险信号管理页(新页面打开,不弹窗)
* @param {import('vue-router').Router} router
*/
export const navigateToViewRiskSignal = (router) => {
const route = router.resolve({
path: "/viewRiskSignal"
});
window.open(route.href, "_blank");
};
/**
* 概览页 -> 风险信号管理页(新页面打开,并自动打开第一条详情弹窗)
* @param {import('vue-router').Router} router
*/
export const navigateToViewRiskSignalOpenFirstDetail = (router) => {
const route = router.resolve({
path: "/viewRiskSignal",
query: { [OPEN_FIRST_RISK_DETAIL_QUERY_KEY]: "1" }
});
window.open(route.href, "_blank");
};
......@@ -47,8 +47,8 @@
<div
class="risk-signals-item"
v-for="(item, index) in warningList"
:key="index"
@click="handleClickToDetailO(item)"
:key="item.signalId || item.billId || index"
@click="handleRiskSignalItemToManage"
:class="{ highlighted: item.eventType === highlightedEventType }"
>
<div
......@@ -115,6 +115,7 @@
</div>
</div>
</div>
<RiskSignalOverviewDetailDialog v-model="isRiskOverviewDetailOpen" />
</div>
</template>
......@@ -123,6 +124,7 @@ import { color } from "echarts";
import { onMounted, ref, computed } from "vue";
import WaveBall from "./WaveBall.vue";
import { getBillRiskSignal } from "@/api/bill/billHome";
import RiskSignalOverviewDetailDialog from "@/components/base/RiskSignalOverviewDetailDialog/index.vue";
const sectionTab = [
{
textColor: "rgba(9, 88, 217, 1)",
......@@ -289,12 +291,10 @@ const handleSwithCurNews = name => {
}
};
// 查看详情 传递参数
const handleClickToDetailO = item => {
window.sessionStorage.setItem("billId", item.billId);
window.sessionStorage.setItem("curTabName", item.name || item.signalTitle);
const route = router.resolve("/billLayout?billId=" + item.billId);
window.open(route.href, "_blank");
const isRiskOverviewDetailOpen = ref(false);
const handleRiskSignalItemToManage = () => {
isRiskOverviewDetailOpen.value = true;
};
const highlightedEventType = ref("");
......
......@@ -355,6 +355,9 @@ import { ElMessage } from "element-plus";
import { ArrowLeft, ArrowRight } from "@element-plus/icons-vue";
import SimplePagination from "@/components/SimplePagination.vue";
import CustomContainer from "@/components/Container/index.vue";
import { useGotoNewsDetail } from "@/router/modules/news";
const gotoNewsDetail = useGotoNewsDetail();
const router = useRouter();
......@@ -515,13 +518,15 @@ const getUSGovernmentLatestDynamicData = async () => {
const handleClickTitle = item => {
if (!item || !item.id) return;
// 打开新标签页
const { href } = router.resolve({
path: "/newsAnalysis",
query: {
newsId: item.id
}
});
window.open(href, "_blank");
// const { href } = router.resolve({
// path: "/newsAnalysis",
// query: {
// newsId: item.id
// }
// });
// window.open(href, "_blank");
gotoNewsDetail(item.id);
};
const timelineList = ref([]);
......
......@@ -300,6 +300,8 @@ import {
getThreeMonthSanctionProcess
} from "@/api/allGovernment.js";
import CalendarHeatmap from "./components/CalendarHeatmap.vue";
import { useGotoNewsDetail } from "@/router/modules/news";
const gotoNewsDetail = useGotoNewsDetail();
const router = useRouter();
const activeDate = inject("activeDate");
......@@ -501,13 +503,15 @@ const updateRankListByPage = () => {
const handleNewsClick = item => {
if (!item || !item.id) return;
// 打开新标签页
const { href } = router.resolve({
path: "/newsAnalysis",
query: {
newsId: item.id
}
});
window.open(href, "_blank");
// const { href } = router.resolve({
// path: "/newsAnalysis",
// query: {
// newsId: item.id
// }
// });
// window.open(href, "_blank");
gotoNewsDetail(item.id);
};
// 点击联合制裁项-如果是政令则跳转详情
......
......@@ -47,12 +47,8 @@
<!-- <div class="right-num" :class="{ click: currentAntiType === 'num' }">排华联盟数量</div> -->
<!-- <div class="right-num" :class="{ click: currentAntiType === 'active' }">排华联盟动态</div> -->
<button-list :list="buttonList" :active-id="activeButtonId" @click="setActiveButtonId"></button-list>
<el-select
v-model="selectedFieldForLatest"
@change="handleFieldChange"
placeholder="全部领域"
class="field-select"
>
<el-select v-model="selectedFieldForLatest" @change="handleFieldChange" placeholder="全部领域"
class="field-select">
<el-option v-for="item in fieldOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
......@@ -69,22 +65,12 @@
<div class="right-num">参与排华联盟</div>
</div>
</div>
<div
class="item"
v-for="(item, index) in countList"
:key="index"
@click="handleCountryClick(item)"
:class="{ 'selected-country': currentSelectedCountry && currentSelectedCountry.name === item.name }"
>
<div class="item" v-for="(item, index) in countList" :key="index" @click="handleCountryClick(item)"
:class="{ 'selected-country': currentSelectedCountry && currentSelectedCountry.name === item.name }">
<div class="item-left">
<img :src="item.image" alt />
<el-tooltip
effect="dark"
:content="item.zhName"
popper-class="common-prompt-popper"
placement="top"
:show-after="500"
>
<el-tooltip effect="dark" :content="item.zhName" popper-class="common-prompt-popper" placement="top"
:show-after="500">
<span>{{ item.zhName }}</span>
</el-tooltip>
</div>
......@@ -100,22 +86,12 @@
<div class="right-num">{{ unionDynamicCount }}条动态</div>
</div>
</div>
<div
class="item carousel-item"
v-for="(item, index) in carouselPageList"
:key="index"
<div class="item carousel-item" v-for="(item, index) in carouselPageList" :key="index"
@click="handleUnionItemClick(item)"
:class="{ 'selected-item': currentSelectedUnion && currentSelectedUnion.id === item.id }"
>
:class="{ 'selected-item': currentSelectedUnion && currentSelectedUnion.id === item.id }">
<div class="item-top">
<div class="top-img">
<img
:src="ele"
:class="{ img1: idx !== 0 }"
alt
v-for="(ele, idx) in item.imageList"
:key="idx"
/>
<img :src="ele" :class="{ img1: idx !== 0 }" alt v-for="(ele, idx) in item.imageList" :key="idx" />
</div>
<div class="top-num">{{ item.count }}</div>
</div>
......@@ -159,13 +135,8 @@
<img src="./assets/news.png" alt />
<span>排华联盟最新动态</span>
</div>
<el-select
v-model="selectUnion"
placeholder="全部排华联盟"
class="custom-select"
:style="{ width: '160px' }"
@change="handleUnionChange"
>
<el-select v-model="selectUnion" placeholder="全部排华联盟" class="custom-select" :style="{ width: '160px' }"
@change="handleUnionChange">
<el-option v-for="item in unionListOption" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
......@@ -175,13 +146,8 @@
<img :src="item.image || defaultImg" alt />
<span @click="handleClick(item)">{{ item.title }}</span>
</div>
<el-tooltip
effect="dark"
:content="item.content"
popper-class="common-prompt-popper"
placement="top"
:show-after="500"
>
<el-tooltip effect="dark" :content="item.content" popper-class="common-prompt-popper" placement="top"
:show-after="500">
<template #content>
<div class="item-tooltip">
{{ item.content }}
......@@ -201,12 +167,8 @@
</div>
</div>
</div>
<simple-pagination
v-model:current-page="newsCurrentPage"
:page-size="newsPageSize"
:total="allNewsList.length"
@page-change="handleUnionPageChange"
/>
<simple-pagination v-model:current-page="newsCurrentPage" :page-size="newsPageSize" :total="allNewsList.length"
@page-change="handleUnionPageChange" />
</div>
<div class="warning">
<div class="warning-title">
......@@ -218,26 +180,16 @@
<div class="item-title">
<div class="title-left">
<div class="img-list">
<img
:src="ele.image || defaultImg"
:class="{ img1: index !== 0 }"
@error="e => (e.target.src = defaultImg)"
alt
v-for="(ele, index) in item.countryList"
:key="index"
/>
<img :src="ele.image || defaultImg" :class="{ img1: index !== 0 }"
@error="e => (e.target.src = defaultImg)" alt v-for="(ele, index) in item.countryList"
:key="index" />
</div>
<div class="left-content">{{ getContent(item.countryList) }}</div>
</div>
<div class="title-right">{{ item.statementList?.length }}次合作</div>
</div>
<div class="item-content">
<div
class="content-list"
v-for="(ele, idx) in item.statementList"
:key="idx"
@click="handleClick(ele)"
>
<div class="content-list" v-for="(ele, idx) in item.statementList" :key="idx" @click="handleClick(ele)">
<div class="list-left">
<!-- <span>{{ getName(ele.industryList) }}</span> -->
<AreaTag v-if="getName(ele.industryList)" :tagName="getName(ele.industryList)"></AreaTag>
......@@ -248,12 +200,8 @@
</div>
</div>
</div>
<simple-pagination
v-model:current-page="warningCurrentPage"
:page-size="warningPageSize"
:total="allWarningList.length"
@page-change="handleWarningPageChange"
/>
<simple-pagination v-model:current-page="warningCurrentPage" :page-size="warningPageSize"
:total="allWarningList.length" @page-change="handleWarningPageChange" />
</div>
</div>
......@@ -305,6 +253,9 @@ import { get, union, update } from "lodash";
import ButtonList from "@/components/buttonList/buttonList.vue";
import { fieldOptions, COLORS, countryNameMap, nameMap } from "@/views/ZMOverView/public.js";
import { countryCoordMap, convertAsiaCenterCoord } from "@/assets/json/countryCoordMap.js";
import { useGotoNewsDetail } from "@/router/modules/news";
const gotoNewsDetail = useGotoNewsDetail();
const buttonList = ref([
{
......@@ -1485,13 +1436,14 @@ const getDynamicList = async () => {
}
};
const handleClick = item => {
const { href } = router.resolve({
path: "/newsAnalysis",
query: {
newsId: item.id
}
});
window.open(href, "_blank");
// const { href } = router.resolve({
// path: "/newsAnalysis",
// query: {
// newsId: item.id
// }
// });
// window.open(href, "_blank");
gotoNewsDetail(item.id);
};
// 获取联盟预警
......@@ -1859,6 +1811,7 @@ watch(activeDate, async () => {
height: calc(100% - 48px);
display: flex;
justify-content: space-between;
.content-left {
width: 320px;
......@@ -1873,6 +1826,7 @@ watch(activeDate, async () => {
overflow: hidden;
border: 1px solid rgb(231, 243, 255);
border-radius: 10px;
// gap: 12px;
.count-box-header {
display: flex;
......@@ -1967,9 +1921,11 @@ watch(activeDate, async () => {
border-radius: 0px;
border-bottom: 1px solid rgb(231, 243, 255);
padding: 10px;
&.selected-item {
background-color: rgb(246, 250, 255) !important;
}
.item-top {
width: 100%;
height: 16px;
......@@ -2509,6 +2465,7 @@ watch(activeDate, async () => {
height: calc(100% - 80px);
padding: 33px 48px 40px 50px;
}
.anti-area-datasource {
text-align: center;
font-family: Source Han Sans CN;
......@@ -2518,6 +2475,7 @@ watch(activeDate, async () => {
align-items: center;
justify-content: center;
gap: 5px;
.tips-icon {
margin-top: 2px;
width: 16px;
......@@ -2567,6 +2525,7 @@ watch(activeDate, async () => {
height: 430px;
// padding: 33px 48px 59px 50px;
}
.anti-area-datasource {
text-align: center;
font-family: Source Han Sans CN;
......@@ -2576,6 +2535,7 @@ watch(activeDate, async () => {
align-items: center;
justify-content: center;
gap: 5px;
.tips-icon {
margin-top: 2px;
width: 16px;
......
......@@ -43,9 +43,15 @@
<div style="display: flex">
<!-- 风险信号列表 -->
<div class="risk-signals" ref="riskSignalsRef">
<div class="risk-signals-item" v-for="(item, index) in warningList" :key="index"
@mouseenter="onMouseEnter(item, index)" @mouseleave="onMouseLeave"
:class="['risk-signals-item', { 'risk-signals-item-hightLight': riskSignalActiveIndex === index }]">
<div
class="risk-signals-item"
v-for="(item, index) in warningList"
:key="item.signalId != null ? String(item.signalId) : 'risk-' + index"
@mouseenter="onMouseEnter(item, index)"
@mouseleave="onMouseLeave"
@click.stop="handleRiskSignalRowToManage"
:class="['risk-signals-item', { 'risk-signals-item-hightLight': riskSignalActiveIndex === index }]"
>
<div class="item-left" :class="{
'item-status-1': item.signalLevel === '特别重大',
'item-status-2': item.signalLevel === '重大风险',
......@@ -119,6 +125,7 @@
</div>
</div>
</div>
<RiskSignalOverviewDetailDialog v-model="isRiskOverviewDetailOpen" />
</div>
</template>
......@@ -128,6 +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 } 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";
......@@ -645,12 +654,14 @@ const filteredHotNewsList = computed(() => {
return hotNewsList.value.filter(newsItem => newsItem.signalId === currentHoveredSignalId.value);
});
const isRiskOverviewDetailOpen = ref(false);
const handleRiskSignalRowToManage = () => {
isRiskOverviewDetailOpen.value = true;
};
const handleToRiskManage = () => {
// 这里的路由路径请根据实际情况修改
// router.push('/riskSignalManage');
const route = router.resolve("/viewRiskSignal");
window.open(route.href, "_blank");
console.log("跳转到风险信号管理");
navigateToViewRiskSignal(router);
};
const highlightedEventType = ref("");
......
<template>
<div class="view-box">
<div class="container-box">
<div class="hard-box">
<div class="hard-name text-title-0-show">美国国会委员会</div>
<div class="hard-num text-title-2-show">{{ committeeInfo.total }}</div>
<div style="width: 0; flex: auto"></div>
<div class="hard-input">
<el-input
v-model="committeeInfo.keyWord"
@keyup.enter="onAllCommittee()"
style="width: 100%; height: 100%"
:suffix-icon="Search"
placeholder="搜索委员会"
/>
</div>
</div>
<div class="date-box">
<div class="date-text">近期美国国会各委员会涉华提案数量汇总</div>
<TimeTabPane @time-click="handleDateChange" activeTime="近一年" />
</div>
<div class="committee-list" ref="refCommittee" v-loading="committeeInfo.loading">
<div class="committee-item" v-for="item in committeeInfo.list" :key="item.id" @click="handleToDataLibrary(item)">
<div class="item-left">
<img :src="iconCommit" alt="" />
</div>
<div class="item-right">
<div class="item-name one-line-ellipsis">{{ item.name }}</div>
<div class="item-chamber one-line-ellipsis">{{ item.chamber }}</div>
</div>
<div class="item-total">{{ item.count }}</div>
<el-icon color="var(--color-primary-100)">
<ArrowRightBold />
</el-icon>
</div>
</div>
<div class="pagination-box">
<el-pagination
@current-change="onAllCommittee"
:pageSize="committeeInfo.pageSize"
:current-page="committeeInfo.pageNum"
background
layout="prev, pager, next"
:total="committeeInfo.total"
/>
</div>
</div>
</div>
</template>
<script setup name="BillAllCommittee">
import { onMounted, reactive, ref } from "vue";
import { Search } from "@element-plus/icons-vue";
import { ArrowRightBold } from "@element-plus/icons-vue";
import router from "@/router";
import TimeTabPane from "@/components/base/TimeTabPane/index.vue";
import { getStatisticsBillCountByCommittee } from "@/api/bill/billHome";
import iconCommit from "../billHome/assets/icons/icon-commit.png";
const committeeInfo = reactive({
loading: false,
pageNum: 1,
pageSize: 8,
total: 0,
keyWord: "",
dateDesc: "近一年",
list: []
});
const getChamberLabel = orgType => {
if (orgType === "Senate") return "参议院";
if (orgType === "House") return "众议院";
return orgType || "";
};
const onAllCommittee = async num => {
committeeInfo.pageNum = num || 1;
committeeInfo.loading = true;
try {
const res = await getStatisticsBillCountByCommittee({
dateDesc: committeeInfo.dateDesc
});
if (res.code === 200 && Array.isArray(res.data)) {
const source = res.data
.map(item => ({
id: `${item.orgType || ""}-${item.orgName || ""}`,
name: item.orgName,
chamber: getChamberLabel(item.orgType),
count: Number(item.count || 0)
}))
.filter(item => !committeeInfo.keyWord || item.name?.includes(committeeInfo.keyWord))
.sort((a, b) => (b.count || 0) - (a.count || 0));
committeeInfo.total = source.length;
const start = (committeeInfo.pageNum - 1) * committeeInfo.pageSize;
committeeInfo.list = source.slice(start, start + committeeInfo.pageSize);
} else {
committeeInfo.total = 0;
committeeInfo.list = [];
}
} catch (error) {
committeeInfo.total = 0;
committeeInfo.list = [];
}
committeeInfo.loading = false;
};
const handleDateChange = event => {
committeeInfo.dateDesc = event?.time || "近一年";
onAllCommittee();
};
const handleToDataLibrary = item => {
const route = router.resolve({
path: "/dataLibrary/countryBill",
query: {
selectedOrg: item.name,
selectedCongress: item.chamber
}
});
window.open(route.href, "_blank");
};
const refCommittee = ref();
onMounted(() => {
let height = 2;
if (refCommittee.value) {
height = Math.floor(refCommittee.value?.clientHeight / 120);
}
committeeInfo.pageSize = height * 4;
onAllCommittee();
});
</script>
<style scoped lang="scss">
.view-box {
width: 100%;
height: 100%;
background: url("../billHome/assets/images/background.png"), linear-gradient(180deg, rgba(229, 241, 254, 1) 0%, rgba(246, 251, 255, 0) 30%);
background-size: 100% 100%;
display: flex;
justify-content: center;
.container-box {
width: 1600px;
padding: 50px 0 20px;
display: flex;
flex-direction: column;
.hard-box {
display: flex;
align-items: center;
width: 100%;
.hard-name {
color: var(--text-primary-90-color);
height: 62px;
line-height: 62px !important;
}
.hard-num {
height: 36px;
background-color: var(--color-primary-100);
color: var(--bg-white-100);
border-radius: 18px;
line-height: 36px !important;
padding: 0 16px;
margin-left: 16px;
}
.hard-input {
background-color: var(--el-fill-color-blank);
border-radius: var(--el-border-radius-base);
box-shadow: 0 0 0 1px var(--el-border-color) inset;
box-sizing: border-box;
margin-left: 20px;
width: 180px;
height: 32px;
}
}
.date-box {
margin-top: 6px;
display: flex;
align-items: center;
width: 100%;
.date-text {
width: 20px;
flex: auto;
font-size: 18px;
line-height: 18px;
font-family: Source Han Sans CN;
color: var(--text-primary-80-color);
}
}
.committee-list {
width: 100%;
height: 20px;
padding: 16px 0;
margin-top: 10px;
flex: auto;
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 104px;
gap: 16px;
.committee-item {
padding: 0 16px;
display: flex;
background: rgba(255, 255, 255, 0.65);
border: 1px solid rgba(255, 255, 255, 1);
border-radius: 10px;
box-shadow: 0 0 20px 0 rgba(25, 69, 130, 0.1);
align-items: center;
cursor: pointer;
transition: transform 0.3s ease, box-shadow 0.3s ease;
&:hover {
transform: translateY(-3px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
.item-left {
width: 48px;
height: 48px;
margin-right: 12px;
img {
width: 100%;
height: 100%;
}
}
.item-right {
flex: auto;
min-width: 0;
.item-name {
font-size: 20px;
font-weight: 700;
line-height: 22px;
color: var(--text-primary-80-color);
}
.item-chamber {
margin-top: 6px;
font-size: 14px;
color: var(--text-primary-50-color);
}
}
.item-total {
font-size: 20px;
font-weight: 700;
line-height: 20px;
color: var(--color-primary-100);
margin-right: 6px;
}
}
}
.pagination-box {
display: flex;
justify-content: center;
}
}
}
</style>
......@@ -9,34 +9,22 @@
:containerRef="containerRef" areaName="法案" :enableBillTypeSwitch="true" defaultBillSearchType="federal" />
</div>
<div class="committee-cards-section">
<div class="committee-cards-filter">
<span class="committee-cards-desc">近期美国国会各委员会涉华提案数量汇总</span>
<el-radio-group v-model="committeeTimeRange" class="committee-time-switch" size="default">
<el-radio-button v-for="item in committeeTimeOptions" :key="item.value" :value="item.value">
<span class="committee-time-switch-inner">
<el-icon v-if="committeeTimeRange === item.value" class="committee-time-switch-icon">
<Calendar />
</el-icon>
{{ item.label }}
</span>
</el-radio-button>
</el-radio-group>
</div>
<div class="committee-cards-row">
<div v-for="item in committeeCardList" :key="item.id" class="committee-card"
@click="handleToDataLibrary(item)">
<div class="committee-card-icon">
<img :src="iconCommit" alt="委员会头像" />
</div>
<div class="committee-card-content">
<div class="committee-card-name">{{ item.name }}</div>
<div class="committee-card-chamber">{{ item.chamber }}</div>
</div>
<div class="committee-card-count">{{ item.count }}&gt;</div>
</div>
</div>
</div>
<SummaryCardsPanel
descriptionText="近期美国国会各委员会涉华提案数量汇总"
:cards="committeeCards"
:totalCount="committeeTotalCount"
:tipIcon="box7HeaderIcon"
:defaultAvatar="iconCommit"
:loading="committeeLoading"
activeTime="近一年"
emptyText="暂无数据"
moreText="查看全部委员会"
:moreCardMinCount="7"
@time-click="handleCommitteeTimeClick"
@name-click="handleToDataLibrary"
@count-click="handleToDataLibrary"
@more-click="handleToCommitteeMore"
/>
<DivideHeader id="position1" class="divide1" :titleText="'最新动态'"></DivideHeader>
<div class="home-content-center">
......@@ -118,7 +106,7 @@
</el-carousel>
</div>
</OverviewMainBox>
<RiskSignal :list="warningList" @more-click="handleToMoreRiskSignal" @item-click="handleClickToDetailO"
<RiskSignal :list="warningList" @more-click="handleToMoreRiskSignal" @item-click="handleRiskSignalItemToManage"
riskLevel="signalLevel" postDate="signalTime" name="signalTitle" />
</div>
......@@ -250,14 +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 } from "@/utils/riskSignalOverviewNavigate";
import setChart from "@/utils/setChart";
import {
getBillIndustry,
......@@ -275,7 +267,7 @@ import {
import { getPersonSummaryInfo } from "@/api/common/index";
import { getChartAnalysis } from "@/api/aiAnalysis/index";
import DivideHeader from "@/components/DivideHeader.vue";
import overviewMainBox from "@/components/base/boxBackground/overviewMainBox.vue";
import OverviewMainBox from "@/components/base/boxBackground/overviewMainBox.vue";
import OverviewCard from "./OverviewCard.vue";
import ResourceLibrarySection from "./ResourceLibrarySection.vue";
import { useContainerScroll } from "@/hooks/useScrollShow";
......@@ -294,7 +286,6 @@ import box7HeaderIcon from "./assets/images/box7-header-icon.png";
import iconCommit from "./assets/icons/icon-commit.png";
import { ElMessage } from "element-plus";
import { Calendar } from "@element-plus/icons-vue";
import { useGotoNewsDetail } from "@/router/modules/news";
// 跳转人物主页(MessageBubble 的 person-click 传入整条列表项,需取 personId)
......@@ -368,15 +359,11 @@ const handleClickToCharacter = async item => {
const containerRef = ref(null);
const { isShow } = useContainerScroll(containerRef);
// 委员会卡片占位数据
const committeeTimeRange = ref("近一月");
const committeeTimeOptions = [
{ label: "近一周", value: "近一周" },
{ label: "近一月", value: "近一月" },
{ label: "近一年", value: "近一年" },
{label:"全部时间", value: "全部时间"}
];
// 委员会卡片数据
const committeeTimeRange = ref("近一年");
const committeeCardList = ref([]);
const committeeTotalCount = ref(0);
const committeeLoading = ref(false);
const getChamberLabel = orgType => {
if (orgType === "Senate") return "参议院";
......@@ -384,27 +371,51 @@ const getChamberLabel = orgType => {
return orgType || "";
};
const committeeCards = computed(() => {
return committeeCardList.value.map(item => ({
id: item.id,
name: item.name,
subText: item.chamber,
count: item.count
}));
});
const handleGetCommitteeBillCount = async () => {
committeeLoading.value = true;
try {
const res = await getStatisticsBillCountByCommittee({
dateDesc: committeeTimeRange.value
});
if (res.code === 200 && Array.isArray(res.data)) {
committeeCardList.value = res.data
const mappedList = res.data
.map(item => ({
id: `${item.orgType || ""}-${item.orgName || ""}`,
name: item.orgName,
chamber: getChamberLabel(item.orgType),
count: Number(item.count || 0)
}))
.sort((a, b) => (b.count || 0) - (a.count || 0))
.slice(0, 3);
.sort((a, b) => (b.count || 0) - (a.count || 0));
committeeTotalCount.value = mappedList.length;
committeeCardList.value = mappedList.slice(0, 3);
} else {
committeeTotalCount.value = 0;
committeeCardList.value = [];
}
} catch (error) {
committeeTotalCount.value = 0;
committeeCardList.value = [];
}
committeeLoading.value = false;
};
const handleCommitteeTimeClick = event => {
committeeTimeRange.value = event?.time || "近一年";
handleGetCommitteeBillCount();
};
const handleToCommitteeMore = () => {
const route = router.resolve({ path: "/bill/allCommittee" });
window.open(route.href, "_blank");
};
const hotBillList = ref([]); // 热门法案列表
......@@ -457,11 +468,15 @@ const handleClickToDetailO = item => {
// router.push("/billLayout?billId=" + item.billId)
};
const isRiskOverviewDetailOpen = ref(false);
const handleRiskSignalItemToManage = () => {
isRiskOverviewDetailOpen.value = true;
};
// 查看更多风险信号
const handleToMoreRiskSignal = () => {
const route = router.resolve("/viewRiskSignal");
window.open(route.href, "_blank");
// router.push("/viewRiskSignal")
navigateToViewRiskSignal(router);
};
// 查看更多新闻资讯(新闻主页)
......@@ -1364,13 +1379,9 @@ watch(box8selectetedTime, () => {
handleBox8Data();
});
watch(
committeeTimeRange,
() => {
handleGetCommitteeBillCount();
},
{ immediate: true }
);
onMounted(() => {
handleGetCommitteeBillCount();
});
const handleToPosi = id => {
const element = document.getElementById(id);
......@@ -1406,16 +1417,25 @@ const handleResize = () => {
// 下钻至资源库
const handleToDataLibrary = (item) => {
// console.log('item', item);
const selectParam = {
selectedOrg: item.name,
selectedCongress: item.chamber
}
const route = router.resolve({
path: "/dataLibrary/countryBill",
query: selectParam
window.sessionStorage.setItem("curTabName", item.id);
const curRoute = router.resolve({
path: "/institution",
query: {
id: item.id
}
});
window.open(route.href, "_blank");
window.open(curRoute.href, "_blank");
// console.log('item', item);
// const selectParam = {
// selectedOrg: item.name,
// selectedCongress: item.chamber
// }
// const route = router.resolve({
// path: "/dataLibrary/countryBill",
// query: selectParam
// });
// window.open(route.href, "_blank");
}
......
......@@ -134,9 +134,10 @@ const handleAnalysisClick = analysisType => {
// 进展预测 -> 法案简介页(法案进展)
if (analysisType === "forsee") {
router.push({
const target = router.resolve({
path: `/billLayout/ProgressForecast/${billId}`,
});
window.open(target.href, "_blank");
return;
}
......
<template>
<div class="industry-wrap">
<div class="left">
<AnalysisBox title="涉及行业" :showAllBtn="false" width="100%" height="100%">
<div class="left-main">
<div class="left-center">
<el-select v-model="curHylyId" placeholder="请选择领域" class="left-center-select"
@change="handleIndustryChange">
<el-option v-for="item in industryList" :key="item.id" :label="item.name || item.hylymc"
:value="item.id" />
</el-select>
<el-input v-model="companySearchKeyword" placeholder="搜索实体" class="left-center-search"
:suffix-icon="Search" clearable />
</div>
<div class="left-list">
<div class="left-list-title">实体名称</div>
<div class="left-list-content">
<el-empty v-if="!curCompanyList?.length" style="padding: 60px 0;" description="暂无数据"
:image-size="100" />
<el-scrollbar v-else height="100%" always>
<div class="item-box">
<div class="item"
:class="{ itemActive: companyActiveIndex === ((currentPage - 1) * pageSize + idx) }"
@click="handleClickCompany(val, idx)" v-for="(val, idx) in curCompanyList"
:key="val.id">
<div class="item-icon">
<img :src="defaultIcon2" alt="" class="item-img" />
</div>
<div class="title"
:class="{ titleActive: companyActiveIndex === ((currentPage - 1) * pageSize + idx) }">
{{ val.name }}
</div>
<div class="icon">
<img v-if="val.status === 'up'" :src="upIcon" alt="" />
<img v-if="val.status === 'down'" :src="downIcon" alt="" />
<BillPageShell>
<div class="wrap">
<div class="left">
<AnalysisBox title="涉及行业" :showAllBtn="false" width="100%" height="100%">
<div class="left-main">
<div class="left-center">
<el-select
v-model="curHylyId"
placeholder="请选择领域"
class="left-center-select"
@change="handleIndustryChange"
>
<el-option
v-for="item in industryList"
:key="item.id"
:label="item.name || item.hylymc"
:value="item.id"
/>
</el-select>
<el-input
v-model="companySearchKeyword"
placeholder="搜索实体"
class="left-center-search"
:suffix-icon="Search"
clearable
/>
</div>
<div class="left-list">
<div class="left-list-title">实体名称</div>
<div class="left-list-content">
<el-empty
v-if="!curCompanyList?.length"
style="padding: 60px 0;"
description="暂无数据"
:image-size="100"
/>
<el-scrollbar v-else height="100%" always>
<div class="item-box">
<div
v-for="(val, idx) in curCompanyList"
:key="val.id"
class="item"
:class="{ itemActive: companyActiveIndex === ((currentPage - 1) * pageSize + idx) }"
@click="handleClickCompany(val, idx)"
>
<div class="item-icon">
<img :src="defaultIcon2" alt="" class="item-img" />
</div>
<div
class="title"
:class="{ titleActive: companyActiveIndex === ((currentPage - 1) * pageSize + idx) }"
>
{{ val.name }}
</div>
<div class="icon">
<img v-if="val.status === 'up'" :src="upIcon" alt="" />
<img v-if="val.status === 'down'" :src="downIcon" alt="" />
</div>
</div>
</div>
</div>
</el-scrollbar>
</el-scrollbar>
</div>
</div>
</div>
<div class="left-pagination">
<div class="left-pagination-left">{{ `共 ${filteredCompanyList.length} 项` }}</div>
<div class="left-pagination-right">
<el-pagination @current-change="handleCurrentChange" :pageSize="pageSize"
:current-page="currentPage" size="small" background layout="prev, pager, next"
:total="filteredCompanyList.length" />
<div class="left-pagination">
<div class="left-pagination-left">{{ `共 ${filteredCompanyList.length} 项` }}</div>
<div class="left-pagination-right">
<el-pagination
@current-change="handleCurrentChange"
:pageSize="pageSize"
:current-page="currentPage"
size="small"
background
layout="prev, pager, next"
:total="filteredCompanyList.length"
/>
</div>
</div>
</div>
</div>
</AnalysisBox>
</div>
<div class="box2">
<AnalysisBox :showAllBtn="false">
<template #custom-title>
<div class="custom-title">
<div class="title-left">
<div :class="['title-item', {'title-active': contentType==1}]" @click="headerContentType(1)">
<div class="title-icon">
<img :src="contentType==1 ? icon1620 : icon1621" alt="">
</AnalysisBox>
</div>
<div class="right">
<AnalysisBox :showAllBtn="false">
<template #custom-title>
<div class="custom-title">
<div class="title-left">
<div :class="['title-item', { 'title-active': contentType==1 }]" @click="headerContentType(1)">
<div class="title-icon">
<img :src="contentType==1 ? icon1620 : icon1621" alt="" />
</div>
<div>产业链</div>
</div>
<div>产业链</div>
</div>
<div :class="['title-item', {'title-active': contentType==2}]" @click="headerContentType(2)">
<div class="title-icon">
<img :src="contentType==2 ? icon422 : icon423" alt="">
<div :class="['title-item', { 'title-active': contentType==2 }]" @click="headerContentType(2)">
<div class="title-icon">
<img :src="contentType==2 ? icon422 : icon423" alt="" />
</div>
<div>实体关系</div>
</div>
<div>实体关系</div>
</div>
<div class="title-right" v-if="contentType==1">
<el-select v-model="industryChain.id" style="width: 100%" @change="onDecreeChainNodes">
<el-option v-for="item in industryChain.list" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</div>
</div>
<div class="title-right" v-if="contentType==1">
<el-select v-model="industryChain.id" style="width: 100%" @change="onDecreeChainNodes">
<el-option v-for="item in industryChain.list" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</template>
<div class="box2-main">
<AiTips :tips="tips" />
<div class="graph-box" v-if="contentType==1">
<ChartChain :listData="fishbone.list" :baseData="fishbone.base" />
</div>
<div class="graph-box" v-if="contentType==2 && graphInfo.nodes.length">
<GraphChart :nodes="graphInfo.nodes" :links="graphInfo.links" layoutType="force" />
</div>
</div>
</template>
<div class="box2-main">
<AiTips :tips="tips" />
<div class="graph-box" v-if="contentType==1">
<ChartChain :listData="fishbone.list" :baseData="fishbone.base" />
</div>
<div class="graph-box" v-if="contentType==2 && graphInfo.nodes.length">
<GraphChart :nodes="graphInfo.nodes" :links="graphInfo.links" layoutType="force" />
</div>
</div>
</AnalysisBox>
</AnalysisBox>
</div>
</div>
</div>
</BillPageShell>
</template>
<script setup>
......@@ -94,6 +124,8 @@ import { ref, onMounted, onBeforeUnmount, nextTick, computed, watch, reactive }
import { useRoute } from "vue-router";
import * as echarts from "echarts";
import BillPageShell from "@/views/bill/components/layout/BillPageShell.vue";
const route = useRoute();
import { getCompanyList, getHylyList, getCompanyDetail } from "@/api/influence";
import getLineChart from "./utils/lineChart";
......@@ -809,7 +841,7 @@ onMounted(async () => {
</script>
<style lang="scss" scoped>
.industry-wrap {
.wrap {
width: 100%;
height: 100%;
display: flex;
......@@ -1012,7 +1044,7 @@ onMounted(async () => {
.right {
margin-top: 16px;
margin-left: 16px;
width: 1247px;
width: 1104px;
height: 847px;
position: relative;
......@@ -1487,7 +1519,7 @@ onMounted(async () => {
}
.box-footer {
width: 1218px;
width: 100%;
height: 40px;
display: flex;
box-sizing: border-box;
......@@ -1510,13 +1542,17 @@ onMounted(async () => {
.box-footer-center {
margin-left: 13px;
margin-top: 8px;
width: 1119px;
flex: 1;
min-width: 0;
height: 24px;
color: var(--color-main-active);
font-family: Microsoft YaHei;
font-size: 14px;
font-weight: 400;
line-height: 24px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.box-footer-right {
......@@ -1539,10 +1575,10 @@ onMounted(async () => {
}
}
.box2 {
.right {
margin-top: 16px;
margin-left: 16px;
width: 1247px;
width: 1104px;
height: 847px;
position: relative;
......
......@@ -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);
......
......@@ -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"
}
};
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论