提交 f58ff710 authored 作者: 朱政's avatar 朱政

Merge branch 'pre' into zz-dev

流水线 #579 已通过 于阶段
in 1 分 36 秒
...@@ -225,31 +225,6 @@ const menuList = ref([ ...@@ -225,31 +225,6 @@ const menuList = ref([
} }
]); ]);
const handleToModule = item => {
const curRoute = router.resolve({
path: item.path
});
window.open(curRoute.href, "_blank");
};
const searchText = ref("");
const handleSearch = () => {
const curRoute = router.resolve({
path: "/searchResults",
query: {
searchText: searchText.value
}
});
window.open(curRoute.href, "_blank");
};
const handleClickTitle = item => {
if (item.name === "主要国家科技动向感知" || item.name === "主要国家竞争科技安全") {
ElMessage.warning("当前功能正在开发中,敬请期待!");
}
};
const handleOpenPage = page => { const handleOpenPage = page => {
const pageObj = { const pageObj = {
znwd: "/chat", znwd: "/chat",
......
...@@ -27,7 +27,6 @@ import Menu12 from "@/assets/icons/overview/menu12.png"; ...@@ -27,7 +27,6 @@ import Menu12 from "@/assets/icons/overview/menu12.png";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
const router = useRouter(); const router = useRouter();
const route = useRoute();
import useTagsViewStore from '@/stores/tagsView.js' import useTagsViewStore from '@/stores/tagsView.js'
...@@ -48,17 +47,6 @@ router.beforeEach((to, from, next) => { ...@@ -48,17 +47,6 @@ router.beforeEach((to, from, next) => {
next() next()
}) })
const isShowAiBox = ref(false);
const closeAiBox = () => {
isShowAiBox.value = false;
};
const openAiBox = () => {
isShowAiBox.value = true;
};
const personTypeList = ref([]); const personTypeList = ref([]);
// 获取人物类别 // 获取人物类别
...@@ -75,146 +63,6 @@ const handleGetPersonType = async () => { ...@@ -75,146 +63,6 @@ const handleGetPersonType = async () => {
} catch (error) {} } catch (error) {}
}; };
const isCurrentOverview = computed(() => {
if (route.path === "/ZMOverView") {
return true;
} else {
return false;
}
});
// 概览页标题列表
const homeTitleList = ref([
{
name: "中美科技博弈",
path: "/ZMOverView",
disabled: false
},
{
name: "主要国家科技动向感知",
path: "",
disabled: true
},
{
name: "主要国家竞争科技安全",
path: "",
disabled: true
}
]);
const homeActiveTitleIndex = ref(0);
const isShowMenu = ref(false);
const handleShowMenu = (index, isShow) => {
if (index === 0) {
isShowMenu.value = isShow;
}
};
const handleHoverMenu = isShow => {
isShowMenu.value = isShow;
};
const menuList = ref([
{
title: "中美科技博弈概览",
icon: Menu1,
path: "/ZMOverView"
},
{
title: "科技法案",
icon: Menu2,
path: "/billHome"
},
{
title: "科技政令",
icon: Menu3,
path: "/decree"
},
{
title: "美国科技智库",
icon: Menu4,
path: "/thinkTank"
},
{
title: "出口管制",
icon: Menu5,
path: "/exportControl"
},
{
title: "科研合作限制",
icon: Menu6,
path: "/cooperationRestrictions"
},
{
title: "投融资限制",
icon: Menu7,
path: "/finance"
},
{
title: "市场准入限制",
icon: Menu8,
path: "/marketAccessRestrictions"
},
{
title: "规则限制",
icon: Menu9,
path: "/ruleRestrictions"
},
{
title: "美国科技人物观点",
icon: Menu10,
path: "/technologyFigures"
},
{
title: "美国主要创新主体动向",
icon: Menu11,
path: "/innovationSubject"
},
{
title: "美国科研资助体系",
icon: Menu12,
path: "/scientificFunding"
}
]);
const handleToModule = item => {
const curRoute = router.resolve({
path: item.path
});
window.open(curRoute.href, "_blank");
};
const searchText = ref("");
const handleSearch = () => {
const curRoute = router.resolve({
path: "/searchResults",
query: {
searchText: searchText.value
}
});
window.open(curRoute.href, "_blank");
};
const handleClickTitle = item => {
if (item.name === "主要国家科技动向感知" || item.name === "主要国家竞争科技安全") {
ElMessage.warning("当前功能正在开发中,敬请期待!");
}
};
const handleOpenPage = page => {
const pageObj = {
znwd: "/chat",
znxb: "/writtingAsstaint"
};
window.open(pageObj[page], "_blank");
};
const handleClickToolBox = () => {
ElMessage.warning("当前功能正在开发中,敬请期待!");
};
onMounted(() => { onMounted(() => {
handleGetPersonType(); handleGetPersonType();
}); });
......
// 引入 axios 请求
import axios from 'axios'
// 引入 element-plus 里面的消息提示
import { ElMessage } from 'element-plus'
import { getToken, setToken, removeToken, formatBearerAuthorization } from '@/api/request.js'
export { getToken, setToken, removeToken }
// 【新增】全局 AbortController,用于管理所有通过此 service 发出的请求
let globalAbortController = new AbortController()
// 【新增】暴露一个方法,供外部(如路由守卫)调用以取消所有正在进行的请求
export const cancelAllRequests = () => {
if (globalAbortController) {
globalAbortController.abort()
// 创建一个新的 Controller 以备下次使用
globalAbortController = new AbortController()
}
}
// 创建 axios 实例
const service = axios.create({
timeout: 30 * 1000 // 请求超时时间
})
// request 拦截器:与主 request 一致,就地改 config,避免 mergeConfig 破坏 url
service.interceptors.request.use(config => {
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.set('token', token, true)
} else {
config.headers.delete('token')
config.headers.delete('Token')
}
const reqUrl = String(config.url ?? '')
if (reqUrl.includes('aiAnalysis')) {
const aiApiKey = import.meta.env.VITE_AI_ANALYSIS_API_KEY
if (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)
}
}
// 【新增】将全局控制器的 signal 注入到当前请求中
// 注意:如果 config 中已经手动传入了 signal(例如组件内单独控制),则优先使用组件内的
if (!config.signal) {
config.signal = globalAbortController.signal
}
return config
}, error => {
console.log(error)
return Promise.reject(error)
})
// response 拦截器
service.interceptors.response.use(
response => {
const res = response?.data
if (!res) {
return Promise.reject(new Error('响应数据为空'))
}
// 根据需求:接口返回 code 不等于 200 的时候报错
if (res.code !== 200) {
ElMessage({
message: res.message || '请求失败',
type: 'error',
duration: 3 * 1000
})
return Promise.reject(res)
}
return res.data
},
error => {
console.log('err' + error)
const isCanceledError =
error?.code === 'ERR_CANCELED' ||
error?.name === 'CanceledError' ||
error?.name === 'AbortError' ||
(typeof error?.message === 'string' && /canceled/i.test(error.message))
if (isCanceledError) return Promise.reject(error)
// 处理 token 过期或无效的情况
const errUrl = String(error.config?.url || '')
const isAiAnalysisRequest = errUrl.includes('aiAnalysis')
if (
error.response &&
(error.response.status === 401 || error.response.status === 403) &&
!isAiAnalysisRequest
) {
ElMessage({
message: 'Token 已过期,请重新登录',
type: 'error',
duration: 3 * 1000
})
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: typeof error?.message === 'string' ? error.message : '请求失败',
type: 'error',
duration: 3 * 1000
})
}
return Promise.reject(error)
}
)
// 封装通用请求函数(支持 http(config) 和 http.get/post 等调用方式)
function http(config) {
return service(config)
}
// 为 http 函数添加快捷方法
http.get = function(url, params) {
return service({ url, method: 'get', params })
}
http.post = function(url, data) {
return service({ url, method: 'post', data })
}
http.put = function(url, data) {
return service({ url, method: 'put', data })
}
http.delete = function(url, params) {
return service({ url, method: 'delete', params })
}
export { http }
export default service
\ No newline at end of file
// 引入 axios 请求 // src/api/finance/service.js
import axios from 'axios' import axios from 'axios'
// 引入 element-plus 里面的消息提示
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { getToken, setToken, removeToken, formatBearerAuthorization } from '@/api/request.js' import { getToken, setToken, removeToken } from '@/api/request.js'
export { getToken, setToken, removeToken } export { getToken, setToken, removeToken }
// 定义全局控制器,以便在取消后重新赋值
let currentAbortController = new AbortController()
/**
* 获取当前有效的 AbortSignal
* 供 axios 拦截器和 fetch 请求共同使用
*/
export const getAbortSignal = () => {
return currentAbortController.signal
}
/**
* 取消所有正在进行的请求
* 路由守卫中调用此方法
*/
export const cancelAllRequests = () => {
// 1. 终止当前控制器的所有请求
currentAbortController.abort()
// 2. 创建一个新的控制器,供后续新请求使用
currentAbortController = new AbortController()
}
// 创建 axios 实例 // 创建 axios 实例
const service = axios.create({ const service = axios.create({
timeout: 30 * 1000 // 请求超时时间 30s timeout: 30 * 1000 // 请求超时时间 30s
}) })
// request 拦截器:与主 request 一致,就地改 config,避免 mergeConfig 破坏 url // request 拦截器
service.interceptors.request.use(config => { service.interceptors.request.use(config => {
const raw = getToken() const raw = getToken()
const token = raw ? String(raw).trim() : "" const token = raw ? String(raw).trim() : ""
// 处理 Headers
if (!config.headers) { if (!config.headers) {
config.headers = new axios.AxiosHeaders() config.headers = new axios.AxiosHeaders()
} else if (!(config.headers instanceof axios.AxiosHeaders)) { } else if (!(config.headers instanceof axios.AxiosHeaders)) {
config.headers = axios.AxiosHeaders.from(config.headers) config.headers = axios.AxiosHeaders.from(config.headers)
} }
// 设置 Token
if (token) { if (token) {
config.headers.set('token', token, true) config.headers.set('token', token, true)
} else { } else {
config.headers.delete('token') config.headers.delete('token')
config.headers.delete('Token') config.headers.delete('Token')
// ===== 旧逻辑保留(勿删):Authorization: Bearer <token> =====
// config.headers.delete('Authorization')
// config.headers.delete('authorization')
} }
// 处理 AI 分析接口的特殊 Header
const reqUrl = String(config.url ?? '') const reqUrl = String(config.url ?? '')
if (reqUrl.includes('aiAnalysis')) { if (reqUrl.includes('aiAnalysis')) {
const aiApiKey = import.meta.env.VITE_AI_ANALYSIS_API_KEY const aiApiKey = import.meta.env.VITE_AI_ANALYSIS_API_KEY
if (aiApiKey) { if (aiApiKey) {
// 确保 headers 存在
if (!config.headers) { if (!config.headers) {
config.headers = new axios.AxiosHeaders() config.headers = new axios.AxiosHeaders()
} else if (!(config.headers instanceof axios.AxiosHeaders)) { } else if (!(config.headers instanceof axios.AxiosHeaders)) {
...@@ -42,6 +67,13 @@ service.interceptors.request.use(config => { ...@@ -42,6 +67,13 @@ service.interceptors.request.use(config => {
config.headers.set('X-API-Key', aiApiKey) config.headers.set('X-API-Key', aiApiKey)
} }
} }
// 将全局控制器的 signal 注入到 axios 请求中
// 如果用户手动传入了 signal,则优先使用用户的(虽然少见)
if (!config.signal) {
config.signal = getAbortSignal()
}
return config return config
}, error => { }, error => {
console.log(error) console.log(error)
...@@ -55,7 +87,6 @@ service.interceptors.response.use( ...@@ -55,7 +87,6 @@ service.interceptors.response.use(
if (!res) { if (!res) {
return Promise.reject(new Error('响应数据为空')) return Promise.reject(new Error('响应数据为空'))
} }
// 根据需求:接口返回 code 不等于 200 的时候报错
if (res.code !== 200) { if (res.code !== 200) {
ElMessage({ ElMessage({
message: res.message || '请求失败', message: res.message || '请求失败',
...@@ -67,19 +98,25 @@ service.interceptors.response.use( ...@@ -67,19 +98,25 @@ service.interceptors.response.use(
return res.data return res.data
}, },
error => { error => {
console.log('err' + error) // 精准识别取消错误,避免弹窗骚扰用户
const isCanceledError = const isCanceledError =
axios.isCancel(error) ||
error?.code === 'ERR_CANCELED' || error?.code === 'ERR_CANCELED' ||
error?.name === 'CanceledError' || error?.name === 'CanceledError' ||
error?.name === 'AbortError' || error?.name === 'AbortError' ||
(typeof error?.message === 'string' && /canceled/i.test(error.message)) (typeof error?.message === 'string' && /canceled/i.test(error.message))
if (isCanceledError) return Promise.reject(error) if (isCanceledError) {
// 静默处理取消错误,不弹窗,不打印.error
return Promise.reject(error)
}
console.log('err' + error)
// 处理 token 过期或无效的情况 // 处理 Token 过期
const errUrl = String(error.config?.url || '') const errUrl = String(error.config?.url || '')
const isAiAnalysisRequest = errUrl.includes('aiAnalysis') const isAiAnalysisRequest = errUrl.includes('aiAnalysis')
if ( if (
error.response && error.response &&
(error.response.status === 401 || error.response.status === 403) && (error.response.status === 401 || error.response.status === 403) &&
...@@ -94,14 +131,11 @@ service.interceptors.response.use( ...@@ -94,14 +131,11 @@ service.interceptors.response.use(
const hadToken = const hadToken =
h && h &&
(typeof h.get === 'function' (typeof h.get === 'function'
? Boolean( ? Boolean(h.get('token') || h.get('Token'))
h.get('token') || h.get('Token') : Boolean(h.token || h.Token))
)
: Boolean(
h.token || h.Token
))
if (hadToken) removeToken() if (hadToken) removeToken()
} else { } else {
// 只有非取消、非 Token 过期的错误才弹出通用提示
ElMessage({ ElMessage({
message: typeof error?.message === 'string' ? error.message : '请求失败', message: typeof error?.message === 'string' ? error.message : '请求失败',
type: 'error', type: 'error',
...@@ -112,12 +146,11 @@ service.interceptors.response.use( ...@@ -112,12 +146,11 @@ service.interceptors.response.use(
} }
) )
// 封装通用请求函数(支持 http(config) 和 http.get/post 等调用方式) // 封装通用请求函数
function http(config) { function http(config) {
return service(config) return service(config)
} }
// 为 http 函数添加快捷方法
http.get = function(url, params) { http.get = function(url, params) {
return service({ url, method: 'get', params }) return service({ url, method: 'get', params })
} }
......
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16.000000" height="16.000000" fill="none">
<rect id="降序 12" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
<path id="路径" d="M12.6473 7.70235L14.0825 6.30313C14.1606 6.22657 14.2121 6.12657 14.2278 6.01719C14.27 5.74376 14.0793 5.49063 13.8059 5.45001L9.83871 4.87344L8.06527 1.27813C8.01683 1.17969 7.93715 1.10001 7.83871 1.05157C7.59183 0.929693 7.29183 1.03126 7.1684 1.27813L5.39496 4.87344L1.42777 5.45001C1.3184 5.46563 1.2184 5.51719 1.14183 5.59532C0.949647 5.79376 0.952772 6.10938 1.15121 6.30313L4.02152 9.10157L3.3434 13.0531C3.32465 13.1609 3.34183 13.2734 3.3934 13.3703C3.52152 13.6141 3.82465 13.7094 4.0684 13.5797L7.61684 11.7141L8.50394 12.1805" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" />
<path id="矢量 1983" d="M0 0L4 0" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" transform="matrix(0,1,-1,0,12,9)" />
<path id="矢量 1984" d="M10 12L12 14L14 12" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" />
</svg>
<template>
<div class="grade-sort-select-box">
<el-select v-model="isSort" placeholder="评分排序" style="width: 130px" @change="handlePxChange">
<template #prefix>
<div style="display: flex; align-items: center; height: 100%">
<img v-if="isSort" src="./down.svg" style="width: 16px; height: 16px" />
<img v-else src="./up.svg" style="width: 16px; height: 16px" />
</div>
</template>
<el-option v-for="item in gradeList" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
</template>
<script setup>
import { ref } from 'vue'
const isSort = ref(true)
const gradeList = ref([
{ label: "评分倒序", value: true },
{ label: "评分正序", value: false }
])
const emits = defineEmits(['handlePxChange'])
const handlePxChange = () => {
emits('handlePxChange', isSort.value)
}
</script>
<style scoped>
.grade-sort-select-box {
height: 42px;
box-sizing: border-box;
padding: 5px 0;
}
</style>
\ No newline at end of file
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16.000000" height="16.000000" fill="none">
<rect id="降序 11" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
<path id="矢量 1983" d="M0 0L4 0" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" transform="matrix(0,1,-1,0,12,10)" />
<path id="矢量 1984" d="M10 10.9969L12.0003 9L14 10.9969" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" />
<path id="路径" d="M12.6473 7.70235L14.0825 6.30313C14.1606 6.22657 14.2121 6.12657 14.2278 6.01719C14.27 5.74376 14.0793 5.49063 13.8059 5.45001L9.83871 4.87344L8.06527 1.27813C8.01683 1.17969 7.93715 1.10001 7.83871 1.05157C7.59183 0.929693 7.29183 1.03126 7.1684 1.27813L5.39496 4.87344L1.42777 5.45001C1.3184 5.46563 1.2184 5.51719 1.14183 5.59532C0.949647 5.79376 0.952772 6.10938 1.15121 6.30313L4.02152 9.10157L3.3434 13.0531C3.32465 13.1609 3.34183 13.2734 3.3934 13.3703C3.52152 13.6141 3.82465 13.7094 4.0684 13.5797L7.61684 11.7141L9.39105 12.6469" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" />
</svg>
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16.000000" height="16.000000" fill="none">
<rect id="降序 8" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
<rect id="矩形 6349" width="12.000000" height="1.000000" x="2.000000" y="3.000000" rx="0.500000" fill="rgb(95, 101, 108)" />
<rect id="矩形 6350" width="12.000000" height="1.000000" x="2.000000" y="6.000000" rx="0.500000" fill="rgb(95, 101, 108)" />
<rect id="矩形 6351" width="6.000000" height="1.000000" x="2.000000" y="9.000000" rx="0.500000" fill="rgb(95, 101, 108)" />
<rect id="矩形 6352" width="6.000000" height="1.000000" x="2.000000" y="12.000000" rx="0.500000" fill="rgb(95, 101, 108)" />
<path id="矢量 1983" d="M0 0L4 0" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" transform="matrix(0,1,-1,0,12,9)" />
<path id="矢量 1984" d="M10 12L12 14L14 12" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" />
</svg>
<template>
<div class="heat-sort-select-box">
<el-select v-model="isSort" placeholder="热度排序" style="width: 130px" @change="handlePxChange">
<template #prefix>
<div style="display: flex; align-items: center; height: 100%">
<img v-if="isSort" src="./down.svg" style="width: 16px; height: 16px" />
<img v-else src="./up.svg" style="width: 16px; height: 16px" />
</div>
</template>
<el-option v-for="item in heatList" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
</template>
<script setup>
import { ref } from 'vue'
const isSort = ref(true)
const heatList = ref([
{ label: "热度倒序", value: true },
{ label: "热度正序", value: false }
])
const emits = defineEmits(['handlePxChange'])
const handlePxChange = () => {
emits('handlePxChange', isSort.value)
}
</script>
<style scoped>
.heat-sort-select-box {
height: 42px;
box-sizing: border-box;
padding: 5px 0;
}
</style>
\ No newline at end of file
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16.000000" height="16.000000" fill="none">
<rect id="降序 10" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
<path id="矢量 1983" d="M0 0L4 0" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" transform="matrix(0,1,-1,0,12,10)" />
<path id="矢量 1984" d="M10 10.9969L12.0003 9L14 10.9969" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" />
<rect id="矩形 6349" width="12.000000" height="1.000000" x="2.000000" y="3.000000" rx="0.500000" fill="rgb(95, 101, 108)" />
<rect id="矩形 6350" width="12.000000" height="1.000000" x="2.000000" y="6.000000" rx="0.500000" fill="rgb(95, 101, 108)" />
<rect id="矩形 6351" width="6.000000" height="1.000000" x="2.000000" y="9.000000" rx="0.500000" fill="rgb(95, 101, 108)" />
<rect id="矩形 6352" width="6.000000" height="1.000000" x="2.000000" y="12.000000" rx="0.500000" fill="rgb(95, 101, 108)" />
</svg>
<template> <template>
<RelationGraph style="width: 100%; height: 100%;" ref="graphRef" :options="graphOptions" @node-click="onNodeClick" <RelationGraph class="graph-box" style="width: 100%; height: 100%;" ref="graphRef" :options="graphOptions" @node-click="onNodeClick"
@line-click="onLineClick" /> @line-click="onLineClick" />
</template> </template>
...@@ -18,6 +18,13 @@ const graphOptions = { ...@@ -18,6 +18,13 @@ const graphOptions = {
allowSwitchJunctionPoint: true, allowSwitchJunctionPoint: true,
defaultLineShape: 1, defaultLineShape: 1,
distance_coefficient: 1, distance_coefficient: 1,
// 背景图配置
backgroundImage: new URL('@/assets/images/bg/companyGraph-bg.png', import.meta.url).href, // Vite 引入本地图片
// backgroundImage: 'https://example.com/your-bg-image.png', // 或者直接用网络图片
backgroundImageNoRepeat: true, // 图片不重复
backgroundImageOffsetX: 0, // X轴偏移
backgroundImageOffsetY: 0, // Y轴偏移
// backgroundColor: '#f5f5f5', // 背景底色
layouts: [ layouts: [
{ {
layoutName: 'center', layoutName: 'center',
...@@ -123,4 +130,9 @@ onMounted(() => { ...@@ -123,4 +130,9 @@ onMounted(() => {
}); });
</script> </script>
<style scoped lang="scss"></style> <style scoped lang="scss">
// .graph-box{
// background: url('@/assets/images/bg/chart-bg.png');
// background: orange;
// }
</style>
<template> <template>
<RelationGraph <RelationGraph style="width: 100%; height: 100%" ref="graphRef" :options="currentGraphOptions"
style="width: 100%; height: 100%" :on-node-click="onNodeClick" :on-line-click="onLineClick">
ref="graphRef"
:options="currentGraphOptions"
:on-node-click="onNodeClick"
:on-line-click="onLineClick"
>
<template #node="{ node }"> <template #node="{ node }">
<div <div class="custom-node" :style="{
class="custom-node" backgroundColor: node.color || 'var(--color-primary-50)'
:style="{ }">
backgroundColor: node.color || 'var(--color-primary-50)' <span :style="{
}" color: node.fontColor || '#ffffff',
> fontSize: node.customFontSize || '24px',
<span fontWeight: 'normal',
:style="{ textAlign: 'center',
color: node.fontColor || '#ffffff', wordBreak: 'break-word',
fontSize: node.customFontSize || '24px', padding: '0 8px'
fontWeight: 'normal', }">
textAlign: 'center',
wordBreak: 'break-word',
padding: '0 8px'
}"
>
{{ node.text }} {{ node.text }}
</span> </span>
</div> </div>
...@@ -76,7 +66,14 @@ const baseGraphOptionsH = { ...@@ -76,7 +66,14 @@ const baseGraphOptionsH = {
defaultNodeBorderColor: "var(--color-primary-100)", defaultNodeBorderColor: "var(--color-primary-100)",
defaultLineColor: "var(--color-primary-50)", defaultLineColor: "var(--color-primary-50)",
defaultNodeColor: "var(--color-primary-50)", defaultNodeColor: "var(--color-primary-50)",
defaultNodeFontColor: "var(--text-primary-90-color)" defaultNodeFontColor: "var(--text-primary-90-color)",
// 背景图配置
backgroundImage: new URL('@/assets/images/bg/companyGraph-bg.png', import.meta.url).href, // Vite 引入本地图片
// backgroundImage: 'https://example.com/your-bg-image.png', // 或者直接用网络图片
backgroundImageNoRepeat: true, // 图片不重复
backgroundImageOffsetX: 0, // X轴偏移
backgroundImageOffsetY: 0, // Y轴偏移
// backgroundColor: '#f5f5f5', // 背景底色
}; };
// 基础垂直配置 // 基础垂直配置
...@@ -102,7 +99,14 @@ const baseGraphOptionsV = { ...@@ -102,7 +99,14 @@ const baseGraphOptionsV = {
defaultNodeBorderWidth: 2, defaultNodeBorderWidth: 2,
defaultLineColor: "var(--color-primary-50)", defaultLineColor: "var(--color-primary-50)",
defaultNodeColor: "var(--color-primary-50)", defaultNodeColor: "var(--color-primary-50)",
defaultNodeFontColor: "var(--bg-white-100)" defaultNodeFontColor: "var(--bg-white-100)",
// 背景图配置
backgroundImage: new URL('@/assets/images/bg/companyGraph-bg.png', import.meta.url).href, // Vite 引入本地图片
// backgroundImage: 'https://example.com/your-bg-image.png', // 或者直接用网络图片
backgroundImageNoRepeat: true, // 图片不重复
backgroundImageOffsetX: 0, // X轴偏移
backgroundImageOffsetY: 0, // Y轴偏移
// backgroundColor: '#f5f5f5', // 背景底色
}; };
// 【核心修改】根据 isReversed 动态计算配置 // 【核心修改】根据 isReversed 动态计算配置
...@@ -184,6 +188,7 @@ onMounted(() => { ...@@ -184,6 +188,7 @@ onMounted(() => {
justify-content: center; justify-content: center;
} }
} }
.custom-node { .custom-node {
border-radius: 50%; border-radius: 50%;
display: flex; display: flex;
......
<template> <template>
<RelationGraph <RelationGraph style="width: 100%; height: 100%" ref="graphRef" :options="graphOptions" @node-click="onNodeClick"
style="width: 100%; height: 100%" @line-click="onLineClick">
ref="graphRef"
:options="graphOptions"
@node-click="onNodeClick"
@line-click="onLineClick"
>
<!-- 自定义节点插槽 - 这是官方推荐的方式 --> <!-- 自定义节点插槽 - 这是官方推荐的方式 -->
<template #node="{ node }"> <template #node="{ node }">
<div <div class="custom-node" :style="{
class="custom-node" backgroundColor: node.color || 'var(--color-primary-50)'
:style="{ }">
backgroundColor: node.color || 'var(--color-primary-50)'
}"
>
{{ console.log(node) }} {{ console.log(node) }}
<!-- 在这里自由控制文字样式 --> <!-- 在这里自由控制文字样式 -->
<div class="img" v-if="node.data.pic"> <div class="img" v-if="node.data.pic">
<img :src="node.data.pic" alt="" /> <img :src="node.data.pic" alt="" />
</div> </div>
<span <span class="text" :style="{
class="text" color: node.fontColor || 'var(--text-primary-80-color)',
:style="{ fontSize: node.customFontSize || '24px' // 可以从数据中读取
color: node.fontColor || 'var(--text-primary-80-color)', }">
fontSize: node.customFontSize || '24px' // 可以从数据中读取
}"
>
{{ node.text }} {{ node.text }}
</span> </span>
</div> </div>
...@@ -57,7 +46,14 @@ const graphOptions = ref({ ...@@ -57,7 +46,14 @@ const graphOptions = ref({
layout: { layout: {
layoutName: "force" layoutName: "force"
}, },
defaultJunctionPoint: "border" defaultJunctionPoint: "border",
// 背景图配置
backgroundImage: new URL('@/assets/images/bg/companyGraph-bg.png', import.meta.url).href, // Vite 引入本地图片
// backgroundImage: 'https://example.com/your-bg-image.png', // 或者直接用网络图片
backgroundImageNoRepeat: true, // 图片不重复
backgroundImageOffsetX: 0, // X轴偏移
backgroundImageOffsetY: 0, // Y轴偏移
// backgroundColor: '#f5f5f5', // 背景底色
// defaultNodeWidth: 150, // 全局默认节点宽度 // defaultNodeWidth: 150, // 全局默认节点宽度
// defaultNodeHeight: 150, // 全局默认节点高度 // defaultNodeHeight: 150, // 全局默认节点高度
}); });
......
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
<el-icon color="var(--color-primary-100)"> <el-icon color="var(--color-primary-100)">
<ArrowRightBold /> <ArrowRightBold />
</el-icon> </el-icon>
<div class="item-dot" v-if="item.delta">+{{ item.delta }}</div> <div class="item-dot" v-if="item.delta">{{ dotPrefix }}{{ item.delta }}</div>
</div> </div>
<div v-if="shouldShowMoreCard" class="summary-item" @click="emit('more-click')"> <div v-if="shouldShowMoreCard" class="summary-item" @click="emit('more-click')">
...@@ -100,6 +100,10 @@ const props = defineProps({ ...@@ -100,6 +100,10 @@ const props = defineProps({
loading: { loading: {
type: Boolean, type: Boolean,
default: false default: false
},
dotPrefix: {
type: String,
default: "+"
} }
}); });
......
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16.000000" height="16.000000" fill="none">
<rect id="降序 6" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
<path id="椭圆 465" d="M8 14.5C6.20507 14.5 4.67301 13.8654 3.40381 12.5962C2.1346 11.327 1.5 9.79492 1.5 8C1.5 6.20507 2.1346 4.67301 3.40381 3.40381C4.67301 2.1346 6.20507 1.5 8 1.5C9.79492 1.5 11.327 2.1346 12.5962 3.40381C13.8654 4.67301 14.5 6.20507 14.5 8L13.5 8C13.5 6.48122 12.963 5.18485 11.8891 4.11091C10.8151 3.03697 9.51878 2.5 8 2.5C6.48122 2.5 5.18485 3.03697 4.11091 4.11091C3.03697 5.18486 2.5 6.48122 2.5 8C2.5 9.51878 3.03697 10.8151 4.11091 11.8891C5.18485 12.963 6.48122 13.5 8 13.5L8 14.5ZM14.48 7.98L14.5 8C14.5 8.28 14.28 8.5 14 8.5C13.72 8.5 13.5 8.28 13.5 8L13.52 7.98L14.48 7.98ZM7.98 13.52L8 13.5C8.28 13.5 8.5 13.72 8.5 14C8.5 14.28 8.28 14.5 8 14.5L7.98 14.48L7.98 13.52Z" fill="rgb(95, 101, 108)" fill-rule="nonzero" />
<path id="矢量 1981" d="M4 8L8 8" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" />
<path id="矢量 1982" d="M0 0L4 0" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" transform="matrix(0,1,-1,0,8,4)" />
<path id="矢量 1983" d="M0 0L4 0" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" transform="matrix(0,1,-1,0,12,9)" />
<path id="矢量 1984" d="M10 12L12 14L14 12" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" />
</svg>
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16.000000" height="16.000000" fill="none">
<rect id="降序 8" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
<rect id="矩形 6349" width="12.000000" height="1.000000" x="2.000000" y="3.000000" rx="0.500000" fill="rgb(95, 101, 108)" />
<rect id="矩形 6350" width="12.000000" height="1.000000" x="2.000000" y="6.000000" rx="0.500000" fill="rgb(95, 101, 108)" />
<rect id="矩形 6351" width="6.000000" height="1.000000" x="2.000000" y="9.000000" rx="0.500000" fill="rgb(95, 101, 108)" />
<rect id="矩形 6352" width="6.000000" height="1.000000" x="2.000000" y="12.000000" rx="0.500000" fill="rgb(95, 101, 108)" />
<path id="矢量 1983" d="M0 0L4 0" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" transform="matrix(0,1,-1,0,12,9)" />
<path id="矢量 1984" d="M10 12L12 14L14 12" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" />
</svg>
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16.000000" height="16.000000" fill="none">
<rect id="降序 12" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
<path id="路径" d="M12.6473 7.70235L14.0825 6.30313C14.1606 6.22657 14.2121 6.12657 14.2278 6.01719C14.27 5.74376 14.0793 5.49063 13.8059 5.45001L9.83871 4.87344L8.06527 1.27813C8.01683 1.17969 7.93715 1.10001 7.83871 1.05157C7.59183 0.929693 7.29183 1.03126 7.1684 1.27813L5.39496 4.87344L1.42777 5.45001C1.3184 5.46563 1.2184 5.51719 1.14183 5.59532C0.949647 5.79376 0.952772 6.10938 1.15121 6.30313L4.02152 9.10157L3.3434 13.0531C3.32465 13.1609 3.34183 13.2734 3.3934 13.3703C3.52152 13.6141 3.82465 13.7094 4.0684 13.5797L7.61684 11.7141L8.50394 12.1805" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" />
<path id="矢量 1983" d="M0 0L4 0" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" transform="matrix(0,1,-1,0,12,9)" />
<path id="矢量 1984" d="M10 12L12 14L14 12" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" />
</svg>
<template>
<div class="time-sort-select-box">
<el-select v-model="sortValue" placeholder="排序方式" style="width: 130px" @change="handlePxChange">
<template #prefix>
<div style="display: flex; align-items: center; height: 100%">
<img v-if="sortValue === 1" src="./down.svg" style="width: 16px; height: 16px" />
<img v-else-if="sortValue === 2" src="./up.svg" style="width: 16px; height: 16px" />
<img v-else-if="sortValue === 3" src="./down1.svg" style="width: 16px; height: 16px" />
<img v-else-if="sortValue === 4" src="./down2.svg" style="width: 16px; height: 16px" />
</div>
</template>
<el-option v-for="item in sortList" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
const props = defineProps({
sortDemension: {
type: Number,
default: 1
}
})
const sortValue = ref(1)
const sortList = computed(() => {
if (props.sortDemension === 1) {
return [
{ label: "时间倒序", value: 1 },
{ label: "时间正序", value: 2 },
]
} else if (props.sortDemension === 2) {
return [
{ label: "时间倒序", value: 1 },
{ label: "时间正序", value: 2 },
{ label: "评分倒序", value: 3 },
]
} else {
return [
{ label: "时间倒序", value: 1 },
{ label: "时间正序", value: 2 },
{ label: "评分倒序", value: 3 },
{ label: "热度倒序", value: 4 },
]
}
})
const emits = defineEmits(['handlePxChange'])
const handlePxChange = () => {
emits('handlePxChange', sortValue.value)
}
</script>
<style scoped>
.time-sort-select-box {
height: 42px;
box-sizing: border-box;
padding: 5px 0;
}
</style>
\ No newline at end of file
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16.000000" height="16.000000" fill="none">
<rect id="降序 7" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
<path id="椭圆 465" d="M8 14.5C6.20507 14.5 4.67301 13.8654 3.40381 12.5962C2.1346 11.327 1.5 9.79492 1.5 8C1.5 6.20507 2.1346 4.67301 3.40381 3.40381C4.67301 2.1346 6.20507 1.5 8 1.5C9.79492 1.5 11.327 2.1346 12.5962 3.40381C13.8654 4.67301 14.5 6.20507 14.5 8L13.5 8C13.5 6.48122 12.963 5.18485 11.8891 4.11091C10.8151 3.03697 9.51878 2.5 8 2.5C6.48122 2.5 5.18485 3.03697 4.11091 4.11091C3.03697 5.18486 2.5 6.48122 2.5 8C2.5 9.51878 3.03697 10.8151 4.11091 11.8891C5.18485 12.963 6.48122 13.5 8 13.5L8 14.5ZM14.48 7.98L14.5 8C14.5 8.28 14.28 8.5 14 8.5C13.72 8.5 13.5 8.28 13.5 8L13.52 7.98L14.48 7.98ZM7.98 13.52L8 13.5C8.28 13.5 8.5 13.72 8.5 14C8.5 14.28 8.28 14.5 8 14.5L7.98 14.48L7.98 13.52Z" fill="rgb(95, 101, 108)" fill-rule="nonzero" />
<path id="矢量 1981" d="M4 8L8 8" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" />
<path id="矢量 1982" d="M0 0L4 0" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" transform="matrix(0,1,-1,0,8,4)" />
<path id="矢量 1983" d="M0 0L4 0" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" transform="matrix(0,1,-1,0,12,10)" />
<path id="矢量 1984" d="M10 10.9969L12.0003 9L14 10.9969" stroke="rgb(95, 101, 108)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.000000" />
</svg>
...@@ -2,6 +2,8 @@ import { createRouter, createWebHistory } from "vue-router"; ...@@ -2,6 +2,8 @@ import { createRouter, createWebHistory } from "vue-router";
import { setToken, removeToken, getToken } from "@/api/request.js"; import { setToken, removeToken, getToken } from "@/api/request.js";
import { AUTH_LOGOUT_CHANNEL } from "@/utils/authCrossTabLogout.js"; import { AUTH_LOGOUT_CHANNEL } from "@/utils/authCrossTabLogout.js";
import { cancelAllRequests } from "@/api/finance/service.js"
/** localStorage:跨标签页记录当前前端的 bootId(与 vite define 的 __APP_BOOT_ID__ 对齐) */ /** localStorage:跨标签页记录当前前端的 bootId(与 vite define 的 __APP_BOOT_ID__ 对齐) */
const VITE_BOOT_STORAGE_KEY = "app_vite_boot_id"; const VITE_BOOT_STORAGE_KEY = "app_vite_boot_id";
/** 退出后强制回登录页(跨标签页/刷新生效) */ /** 退出后强制回登录页(跨标签页/刷新生效) */
...@@ -145,51 +147,9 @@ const router = createRouter({ ...@@ -145,51 +147,9 @@ const router = createRouter({
// 2)登录成功回跳带 ?token=:先 setToken 并同步 bootId,再去掉 URL 中的 token(须先于 clearTokenIfNewDevBoot,避免误清刚写入的登录态) // 2)登录成功回跳带 ?token=:先 setToken 并同步 bootId,再去掉 URL 中的 token(须先于 clearTokenIfNewDevBoot,避免误清刚写入的登录态)
// 3)已有本地 token:正常走前端路由 // 3)已有本地 token:正常走前端路由
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
// ===== SSO/重定向逻辑(切线上接口时停用,保留注释) ===== // 【新增】在每次路由跳转开始前,取消上一个页面所有未完成的请求
// const queryToken = to.query && to.query.token != null && String(to.query.token).trim() !== "" // 这能防止旧页面的数据回来覆盖新页面,也能减少服务器压力
// ? String(to.query.token).trim() cancelAllRequests();
// : "";
//
// 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) { if (import.meta.env.DEV) {
clearTokenIfNewDevBoot(); clearTokenIfNewDevBoot();
} }
......
...@@ -35,7 +35,7 @@ const billRoutes = [ ...@@ -35,7 +35,7 @@ const billRoutes = [
component: BillAllCommittee, component: BillAllCommittee,
meta: { meta: {
title: "法案委员会列表", title: "法案委员会列表",
isShowHeader: true isShowHeader: false
} }
}, },
{ {
......
<template>
<el-row class="wrapper layout-grid-line">
<el-col :span="span">
<pre>
{{
`
import TimeSortSelectBox from '@/components/base/TimeSortSelectBox/index.vue'
<div class="time-box">
<TimeSortSelectBox @handle-px-change="handleTimePx" />
<TimeSortSelectBox :sort-demension="2" @handle-px-change="handleTimePx" />
<TimeSortSelectBox :sort-demension="3" @handle-px-change="handleTimePx" />
</div>
`
}}
</pre>
<div class="time-box">
<TimeSortSelectBox @handle-px-change="handleTimePx" />
<TimeSortSelectBox :sort-demension="2" @handle-px-change="handleTimePx" />
<TimeSortSelectBox :sort-demension="3" @handle-px-change="handleTimePx" />
</div>
</el-col>
</el-row>
</template>
<script setup>
import { ref } from 'vue'
import '@/styles/common.scss'
import TimeSortSelectBox from '@/components/base/TimeSortSelectBox/index.vue'
import HeatSortSelectBox from '@/components/base/HeatSortSelectBox/index.vue'
import GradeSortSelectBox from '@/components/base/GradeSortSelectBox/index.vue'
const span = 12
const handleTimePx = (val) => {
alert(val)
}
const handleHeatPx = (val) => {
alert(val)
}
const handleGradePx = (val) => {
alert(val)
}
</script>
<style lang="scss" scoped>
.time-box {
width: 700px;
height: 400px;
padding: 100px;
background: #F2F8FF;
border: 1px solid var(--bg-black-5);
display: flex;
gap: 8px;
}
</style>
\ No newline at end of file
...@@ -8,8 +8,8 @@ ...@@ -8,8 +8,8 @@
<div class="text-title-1-show">文字样式</div> <div class="text-title-1-show">文字样式</div>
<TextStyle /> <TextStyle />
<div class="text-title-1-show">通用样式/组件</div> <div class="text-title-1-show">通用样式/组件</div>
<div style="position: relative; min-height: 700px;"> <div style="position: relative; height: 800px;">
<el-tabs tabPosition="left" class="tabs-nav-no-wrap left-float-nav-tabs dev-style-tabs"> <el-tabs tabPosition="left" style="position: relative; height: 700px;" class="tabs-nav-no-wrap left-float-nav-tabs dev-style-tabs">
<el-tab-pane label="通用" lazy> <el-tab-pane label="通用" lazy>
<common-page /> <common-page />
</el-tab-pane> </el-tab-pane>
...@@ -73,6 +73,9 @@ ...@@ -73,6 +73,9 @@
<el-tab-pane label="激活工作框" lazy> <el-tab-pane label="激活工作框" lazy>
<WorkingBox /> <WorkingBox />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="自定义排序" lazy>
<TimeSortSelectBox />
</el-tab-pane>
</el-tabs> </el-tabs>
</div> </div>
</el-space> </el-space>
...@@ -107,6 +110,7 @@ import RelationChart from './RelationChart/index.vue' ...@@ -107,6 +110,7 @@ import RelationChart from './RelationChart/index.vue'
import RelationCenterChart from './RelationCenterChart/index.vue' import RelationCenterChart from './RelationCenterChart/index.vue'
import RelationForceChart from './RelationForceChart/index.vue' import RelationForceChart from './RelationForceChart/index.vue'
import WorkingBox from './WorkingBox/index.vue' import WorkingBox from './WorkingBox/index.vue'
import TimeSortSelectBox from './TimeSortSelectBox/index.vue'
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
......
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { getPersonSummaryInfo } from "@/api/common/index";
const router = useRouter() const router = useRouter()
// 跳转法案详情 // 跳转法案详情
...@@ -27,8 +28,20 @@ export const goToDecree = (id, tabName) => { ...@@ -27,8 +28,20 @@ export const goToDecree = (id, tabName) => {
window.open(route.href, "_blank"); window.open(route.href, "_blank");
}; };
// 跳转智库 // 跳转智库详情
export const goToThinkTank = (id, tabName) => { export const goToThinkTank = (id, tabName) => {
window.sessionStorage.setItem("curTabName", tabName);
const curRoute = router.resolve({
name: "ThinkTankDetail",
params: {
id: id
}
});
window.open(curRoute.href, "_blank");
}
// 跳转智库报告详情
export const goToThinkTankReport = (id, tabName) => {
window.sessionStorage.setItem("curTabName", tabName); window.sessionStorage.setItem("curTabName", tabName);
const route = router.resolve({ const route = router.resolve({
name: "ReportDetail", name: "ReportDetail",
...@@ -49,4 +62,129 @@ export const goToInstitution = (id, tabName) => { ...@@ -49,4 +62,129 @@ export const goToInstitution = (id, tabName) => {
} }
}); });
window.open(curRoute.href, "_blank"); window.open(curRoute.href, "_blank");
} }
\ No newline at end of file
// 跳转人物详情
export const goToCharacterPage = async (id, tabName) => {
window.sessionStorage.setItem('curTabName', tabName)
const personTypeList = JSON.parse(window.sessionStorage.getItem("personTypeList"));
let type
let personTypeName
const params = {
personId: id
}
// 先获取人物全局信息
try {
const res = await getPersonSummaryInfo(params)
if (res.code === 200 && res.data) {
const arr = personTypeList.filter(item => {
return item.typeId === res.data.personType;
})
if (arr && arr.length) {
personTypeName = arr[0].typeName;
if (personTypeName === "科技企业领袖") {
type = 1;
} else if (personTypeName === "国会议员") {
type = 2;
} else if (personTypeName === "智库研究人员") {
type = 3;
} else {
personTypeName = "其他类型";
const route = router.resolve({
path: "/characterPage",
query: {
personId: item.id
}
});
window.open(route.href, "_blank");
return;
}
const route = router.resolve({
path: "/characterPage",
query: {
type: type, // type=1为科技企业领袖,2为国会议员,3为智库研究人员
personId: item.id
}
});
window.open(route.href, "_blank");
} else {
personTypeName = "";
const route = router.resolve({
path: "/characterPage",
query: {
personId: item.id
}
});
window.open(route.href, "_blank");
return;
}
} else {
const route = router.resolve({
path: "/characterPage",
query: {
personId: item.id
}
});
window.open(route.href, "_blank");
return;
}
} catch (error) {
const route = router.resolve({
path: "/characterPage",
query: {
personId: item.id
}
});
window.open(route.href, "_blank");
return;
}
}
// 跳转企业
export const goToCompanyPage = (id, tabName) => {
window.sessionStorage.setItem('curTabName', tabName)
const route = router.resolve({
name: "companyPages",
params: {
id: id
}
});
window.open(route.href, "_blank");
}
// 跳转新闻详情
export const goToNewsPage = (id, tabName) => {
window.sessionStorage.setItem("curTabName", tabName);
const route = router.resolve({
path: "/newsAnalysis",
query: {
newsId: id
}
});
window.open(route.href, "_blank");
}
// 跳转搜索详情页
export const goToSearch = (tabName, areaName, billSearchType) => {
window.sessionStorage.setItem("curTabName", tabName);
// if (!areaName) return;
const query = {
searchText: tabName,
areaName: areaName
};
if (enableBillTypeSwitch) {
query.billSearchType = billSearchType
}
const curRoute = router.resolve({
path: "/searchResults",
query
});
window.open(curRoute.href, "_blank");
}
// 跳转数据资源库
...@@ -14,6 +14,12 @@ ...@@ -14,6 +14,12 @@
placeholder="搜索委员会" placeholder="搜索委员会"
/> />
</div> </div>
<div class="hard-select">
<el-select v-model="committeeInfo.metricType" @change="onAllCommittee()" placeholder="统计口径" style="width: 160px; margin-left: 8px">
<el-option label="政令数据总量" :value="1" />
<el-option label="政令新增数量" :value="2" />
</el-select>
</div>
</div> </div>
<div class="date-box"> <div class="date-box">
...@@ -30,7 +36,7 @@ ...@@ -30,7 +36,7 @@
<div class="item-name one-line-ellipsis">{{ item.name }}</div> <div class="item-name one-line-ellipsis">{{ item.name }}</div>
<div class="item-chamber one-line-ellipsis">{{ item.chamber }}</div> <div class="item-chamber one-line-ellipsis">{{ item.chamber }}</div>
</div> </div>
<div class="item-total">{{ item.count }}</div> <div class="item-total">{{ getDisplayCount(item) }}</div>
<el-icon color="var(--color-primary-100)"> <el-icon color="var(--color-primary-100)">
<ArrowRightBold /> <ArrowRightBold />
</el-icon> </el-icon>
...@@ -48,6 +54,12 @@ ...@@ -48,6 +54,12 @@
/> />
</div> </div>
</div> </div>
<div class="back-bnt" @click="handleBack">
<el-icon>
<Back />
</el-icon>
<div class="back-text">返回</div>
</div>
</div> </div>
</template> </template>
...@@ -55,6 +67,7 @@ ...@@ -55,6 +67,7 @@
import { onMounted, reactive, ref } from "vue"; import { onMounted, reactive, ref } from "vue";
import { Search } from "@element-plus/icons-vue"; import { Search } from "@element-plus/icons-vue";
import { ArrowRightBold } from "@element-plus/icons-vue"; import { ArrowRightBold } from "@element-plus/icons-vue";
import { Back } from "@element-plus/icons-vue";
import router from "@/router"; import router from "@/router";
import TimeTabPane from "@/components/base/TimeTabPane/index.vue"; import TimeTabPane from "@/components/base/TimeTabPane/index.vue";
import { getStatisticsBillCountByCommittee } from "@/api/bill/billHome"; import { getStatisticsBillCountByCommittee } from "@/api/bill/billHome";
...@@ -66,6 +79,7 @@ const committeeInfo = reactive({ ...@@ -66,6 +79,7 @@ const committeeInfo = reactive({
pageSize: 8, pageSize: 8,
total: 0, total: 0,
keyWord: "", keyWord: "",
metricType: 1,
dateDesc: "近一年", dateDesc: "近一年",
list: [] list: []
}); });
...@@ -89,10 +103,11 @@ const onAllCommittee = async num => { ...@@ -89,10 +103,11 @@ const onAllCommittee = async num => {
id: `${item.orgType || ""}-${item.orgName || ""}`, id: `${item.orgType || ""}-${item.orgName || ""}`,
name: item.orgName, name: item.orgName,
chamber: getChamberLabel(item.orgType), chamber: getChamberLabel(item.orgType),
count: Number(item.count || 0) totalCount: Number(item.count || 0),
recentCount: Number(item.countRecent || item.totalRecent || item.recentCount || item.recent || item.newCount || 0)
})) }))
.filter(item => !committeeInfo.keyWord || item.name?.includes(committeeInfo.keyWord)) .filter(item => !committeeInfo.keyWord || item.name?.includes(committeeInfo.keyWord))
.sort((a, b) => (b.count || 0) - (a.count || 0)); .sort((a, b) => getSortValue(b) - getSortValue(a));
committeeInfo.total = source.length; committeeInfo.total = source.length;
const start = (committeeInfo.pageNum - 1) * committeeInfo.pageSize; const start = (committeeInfo.pageNum - 1) * committeeInfo.pageSize;
...@@ -108,20 +123,36 @@ const onAllCommittee = async num => { ...@@ -108,20 +123,36 @@ const onAllCommittee = async num => {
committeeInfo.loading = false; committeeInfo.loading = false;
}; };
const getSortValue = item => {
if (committeeInfo.metricType === 2) return Number(item?.recentCount || 0);
return Number(item?.totalCount || 0);
};
const getDisplayCount = item => {
return getSortValue(item);
};
const handleDateChange = event => { const handleDateChange = event => {
committeeInfo.dateDesc = event?.time || "近一年"; committeeInfo.dateDesc = event?.time || "近一年";
onAllCommittee(); onAllCommittee();
}; };
const handleToDataLibrary = item => { const handleToDataLibrary = item => {
const route = router.resolve({ router.push({
path: "/dataLibrary/countryBill", path: "/dataLibrary/countryBill",
query: { query: {
selectedOrg: item.name, selectedOrg: item.name,
selectedCongress: item.chamber selectedCongress: item.chamber
} }
}); });
window.open(route.href, "_blank"); };
const handleBack = () => {
if (window.history.length > 1) {
router.back();
return;
}
router.push("/billHome");
}; };
const refCommittee = ref(); const refCommittee = ref();
...@@ -143,6 +174,28 @@ onMounted(() => { ...@@ -143,6 +174,28 @@ onMounted(() => {
background-size: 100% 100%; background-size: 100% 100%;
display: flex; display: flex;
justify-content: center; justify-content: center;
position: relative;
.back-bnt {
position: absolute;
top: 16px;
left: 30px;
width: 86px;
height: 38px;
background-color: white;
border-radius: 19px;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-primary-65-color);
font-family: Source Han Sans CN;
font-size: 16px;
cursor: pointer;
}
.back-text {
margin-left: 6px;
}
.container-box { .container-box {
width: 1600px; width: 1600px;
...@@ -180,6 +233,11 @@ onMounted(() => { ...@@ -180,6 +233,11 @@ onMounted(() => {
width: 180px; width: 180px;
height: 32px; height: 32px;
} }
.hard-select {
height: 42px;
padding: 5px 0;
}
} }
.date-box { .date-box {
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
moreText="查看全部委员会" moreText="查看全部委员会"
:moreCardMinCount="1" :moreCardMinCount="1"
@time-click="handleCommitteeTimeClick" @time-click="handleCommitteeTimeClick"
@name-click="handleToDataLibrary" @name-click="handleToInstitution"
@count-click="handleToBillDataLibrary" @count-click="handleToBillDataLibrary"
@more-click="handleToCommitteeMore" @more-click="handleToCommitteeMore"
/> />
...@@ -56,7 +56,31 @@ ...@@ -56,7 +56,31 @@
<AreaTag v-for="(item, index) in bill.hylyList" :key="index" :tagName="item.industryName"> <AreaTag v-for="(item, index) in bill.hylyList" :key="index" :tagName="item.industryName">
</AreaTag> </AreaTag>
</div> </div>
<div class="box1-main-divider"></div>
<div class="box1-main-left-info1"> <div class="box1-main-left-info1">
<div class="info1-box">
<div class="icon"></div>
<div class="info1-box-left">{{ "提出部门:" }}</div>
<div class="info1-box-right info1-box-right--committee">
<template v-if="getLimitedCommitteeList(bill.committeeList).length">
<div
v-for="committee in getLimitedCommitteeList(bill.committeeList)"
:key="committee.committeeId || committee.committeeName"
class="committee-item"
>
<img
class="committee-logo"
:src="committee.logoUrl || iconCommit"
:alt="committee.committeeName || '提案部门'"
/>
<div class="committee-name">
{{ committee.committeeName || "--" }}
</div>
</div>
</template>
<div v-else>--</div>
</div>
</div>
<div class="info1-box"> <div class="info1-box">
<div class="icon"></div> <div class="icon"></div>
<div class="info1-box-left">{{ "提案人:" }}</div> <div class="info1-box-left">{{ "提案人:" }}</div>
...@@ -70,9 +94,11 @@ ...@@ -70,9 +94,11 @@
</div> </div>
</div> </div>
</div> </div>
<div class="box1-main-divider box1-main-divider--before-list"></div>
<div class="box1-main-left-info2"> <div class="box1-main-left-info2">
<div class="info2-item" v-for="(item, index) in bill.dyqkList" :key="index"> <div class="info2-item" v-for="(item, index) in getLimitedDyqkList(bill.dyqkList)"
<div class="time-line" v-if="index !== bill.dyqkList.length - 1"></div> :key="index">
<div class="time-line" v-if="!isLastDyqkItem(bill.dyqkList, index)"></div>
<div class="item-icon"> <div class="item-icon">
<img src="./assets/images/info2-icon.png" alt="" /> <img src="./assets/images/info2-icon.png" alt="" />
</div> </div>
...@@ -147,7 +173,7 @@ ...@@ -147,7 +173,7 @@
</div> </div>
</div> </div>
</OverviewCard> </OverviewCard>
<OverviewCard class="overview-card--single box6" title="领域分布情况" :icon="box6HeaderIcon"> <OverviewCard class="overview-card--single box6" title="领域分布情况" :icon="box7HeaderIcon">
<template #right> <template #right>
<el-select v-model="box9selectetedTime" placeholder="选择时间" style="width: 90px"> <el-select v-model="box9selectetedTime" placeholder="选择时间" style="width: 90px">
<el-option v-for="item in box9YearList" :key="item.value" :label="item.label" :value="item.value" /> <el-option v-for="item in box9YearList" :key="item.value" :label="item.label" :value="item.value" />
...@@ -186,7 +212,7 @@ ...@@ -186,7 +212,7 @@
<div v-else id="box7Chart" class="overview-chart"></div> <div v-else id="box7Chart" class="overview-chart"></div>
</div> </div>
<div class="overview-tip-row"> <div class="overview-tip-row">
<TipTab class="overview-tip" :text="'提出涉华科技法案委员会分布情况,数据来源:美国国会官网'" /> <TipTab class="overview-tip" :text="'涉华科技法案提案委员会分布情况,数据来源:美国国会官网'" />
<AiButton class="overview-tip-action" @mouseenter="handleShowAiPane('box7')" /> <AiButton class="overview-tip-action" @mouseenter="handleShowAiPane('box7')" />
</div> </div>
<div v-if="aiPaneVisible.box7" class="overview-ai-pane" @mouseleave="handleHideAiPane('box7')"> <div v-if="aiPaneVisible.box7" class="overview-ai-pane" @mouseleave="handleHideAiPane('box7')">
...@@ -194,7 +220,7 @@ ...@@ -194,7 +220,7 @@
</div> </div>
</div> </div>
</OverviewCard> </OverviewCard>
<OverviewCard class="overview-card--single box8" title="进展分布情况" :icon="box7HeaderIcon"> <OverviewCard class="overview-card--single box8" title="进展分布情况" :icon="box8HeaderIcon">
<template #right> <template #right>
<el-select v-model="box8selectetedTime" placeholder="选择时间" style="width: 90px"> <el-select v-model="box8selectetedTime" placeholder="选择时间" style="width: 90px">
<el-option v-for="item in box8YearList" :key="item.value" :label="item.label" :value="item.value" /> <el-option v-for="item in box8YearList" :key="item.value" :label="item.label" :value="item.value" />
...@@ -217,7 +243,7 @@ ...@@ -217,7 +243,7 @@
</div> </div>
</div> </div>
</OverviewCard> </OverviewCard>
<OverviewCard class="overview-card--single box9" title="关键条款词云" :icon="box7HeaderIcon"> <OverviewCard class="overview-card--single box9" title="关键条款词云" :icon="box6HeaderIcon">
<div class="overview-card-body box9-main"> <div class="overview-card-body box9-main">
<div class="overview-chart-wrap" v-loading="chartLoading.box9"> <div class="overview-chart-wrap" v-loading="chartLoading.box9">
<el-empty v-if="!wordCloudHasData" description="暂无数据" :image-size="100" /> <el-empty v-if="!wordCloudHasData" description="暂无数据" :image-size="100" />
...@@ -290,6 +316,7 @@ import getDoublePieChart from "./utils/doublePieChart"; ...@@ -290,6 +316,7 @@ import getDoublePieChart from "./utils/doublePieChart";
import box5HeaderIcon from "./assets/images/box5-header-icon.png"; import box5HeaderIcon from "./assets/images/box5-header-icon.png";
import box6HeaderIcon from "./assets/images/box6-header-icon.png"; import box6HeaderIcon from "./assets/images/box6-header-icon.png";
import box7HeaderIcon from "./assets/images/box7-header-icon.png"; import box7HeaderIcon from "./assets/images/box7-header-icon.png";
import box8HeaderIcon from "./assets/images/box8-header-icon.png"
import iconCommit from "./assets/icons/icon-commit.png"; import iconCommit from "./assets/icons/icon-commit.png";
import iconILetter from "./assets/icons/icon-iLetter.png"; import iconILetter from "./assets/icons/icon-iLetter.png";
...@@ -386,7 +413,9 @@ const committeeCards = computed(() => { ...@@ -386,7 +413,9 @@ const committeeCards = computed(() => {
orgId: item.orgId, orgId: item.orgId,
name: item.name, name: item.name,
subText: item.chamber, subText: item.chamber,
count: item.count count: item.total,
delta: item.count,
avatar: item.logoUrl
})); }));
}); });
...@@ -403,9 +432,11 @@ const handleGetCommitteeBillCount = async () => { ...@@ -403,9 +432,11 @@ const handleGetCommitteeBillCount = async () => {
orgId: item.orgId, orgId: item.orgId,
name: item.orgName, name: item.orgName,
chamber: getChamberLabel(item.orgType), chamber: getChamberLabel(item.orgType),
count: Number(item.count || 0) total: Number(item.total || 0),
count: Number(item.count || 0),
logoUrl: item.logoUrl || ""
})) }))
.sort((a, b) => (b.count || 0) - (a.count || 0)); .sort((a, b) => (b.total || 0) - (a.total || 0));
committeeTotalCount.value = mappedList.length; committeeTotalCount.value = mappedList.length;
committeeCardList.value = mappedList.slice(0, 3); committeeCardList.value = mappedList.slice(0, 3);
} else { } else {
...@@ -431,6 +462,7 @@ const handleToCommitteeMore = () => { ...@@ -431,6 +462,7 @@ const handleToCommitteeMore = () => {
const hotBillList = ref([]); // 热门法案列表 const hotBillList = ref([]); // 热门法案列表
const carouselRef = ref(null); const carouselRef = ref(null);
const MAX_DYQK_DISPLAY_COUNT = 3;
const handleCarouselChange = index => { const handleCarouselChange = index => {
if (hotBillList.value && hotBillList.value.length > 0) { if (hotBillList.value && hotBillList.value.length > 0) {
...@@ -438,6 +470,20 @@ const handleCarouselChange = index => { ...@@ -438,6 +470,20 @@ const handleCarouselChange = index => {
} }
}; };
const getLimitedDyqkList = dyqkList => {
if (!Array.isArray(dyqkList)) return [];
return dyqkList.slice(0, MAX_DYQK_DISPLAY_COUNT);
};
const getLimitedCommitteeList = committeeList => {
if (!Array.isArray(committeeList)) return [];
return committeeList.slice(0, 2);
};
const isLastDyqkItem = (dyqkList, index) => {
return index === getLimitedDyqkList(dyqkList).length - 1;
};
// 切换热门法案 // 切换热门法案
const handleSwithCurBill = name => { const handleSwithCurBill = name => {
if (name === "left") { if (name === "left") {
...@@ -500,25 +546,17 @@ const handleToMoreNews = () => { ...@@ -500,25 +546,17 @@ const handleToMoreNews = () => {
// 风险信号 // 风险信号
const warningList = ref([]); const warningList = ref([]);
const box7selectetedTime = ref("2025"); const currentYear = new Date().getFullYear();
const box7YearList = ref([ const recentFiveYearOptions = Array.from({ length: 5 }, (_, index) => {
{ const year = String(currentYear - index);
label: "2025", return {
value: "2025" label: year,
}, value: year
{ };
label: "2024", });
value: "2024"
}, const box7selectetedTime = ref(String(currentYear));
{ const box7YearList = ref(recentFiveYearOptions);
label: "2023",
value: "2023"
},
{
label: "2022",
value: "2022"
}
]);
const aiPaneVisible = ref({ const aiPaneVisible = ref({
box5: false, box5: false,
...@@ -684,25 +722,8 @@ const handleHideAiPane = key => { ...@@ -684,25 +722,8 @@ const handleHideAiPane = key => {
}; };
}; };
const box8selectetedTime = ref("2025"); const box8selectetedTime = ref(String(currentYear));
const box8YearList = ref([ const box8YearList = ref(recentFiveYearOptions);
{
label: "2025",
value: "2025"
},
{
label: "2024",
value: "2024"
},
{
label: "2023",
value: "2023"
},
{
label: "2022",
value: "2022"
}
]);
// 涉华法案数量使用的领域分类列表 // 涉华法案数量使用的领域分类列表
const categoryList = ref([]); const categoryList = ref([]);
...@@ -757,7 +778,7 @@ const handleGetNews = async () => { ...@@ -757,7 +778,7 @@ const handleGetNews = async () => {
newsList.value = res.data.map(item => { newsList.value = res.data.map(item => {
return { return {
...item, ...item,
from: `${item.newsOrg} · ${item.newsDate ? item.newsDate.slice(5) : ""}` from: `${item.newsDate ? item.newsDate : ""} · ${item.newsOrg || ""}`
}; };
}); });
} else { } else {
...@@ -947,50 +968,70 @@ const box7HasData = ref(true); ...@@ -947,50 +968,70 @@ const box7HasData = ref(true);
const box7AiData = ref({ inner: [], outer: [] }); const box7AiData = ref({ inner: [], outer: [] });
const handleBox7Data = async () => { const handleBox7Data = async () => {
chartLoading.value = { ...chartLoading.value, box7: true }; chartLoading.value = { ...chartLoading.value, box7: true };
const selectParam = {
moduleType: '国会法案',
key: 3,
selectedDate: box7selectetedTime.value ? JSON.stringify([box7selectetedTime.value + '-01-01', box7selectetedTime.value + '-12-31']) : '',
isInvolveCn: true
}
try { try {
const res = await getBillPostOrg({ year: box7selectetedTime.value }); const res = await getBillPostOrg({ year: box7selectetedTime.value });
console.log("法案提出部门", res); console.log("法案提出部门", res);
const orgBillNumList = res?.data?.orgBillNumList || []; let innerData = [];
const orgBillNumMap = res?.data?.orgBillNumMap || {}; let outerData = [];
if (res.code === 200 && Array.isArray(orgBillNumList) && orgBillNumList.length > 0) { if (Array.isArray(res?.data)) {
box7HasData.value = true; innerData = res.data
// 必须等待DOM更新,因为v-if切换可能导致元素刚被创建 .map(item => ({
await nextTick(); name: item?.orgName || item?.orgType || "",
value: Number(item?.count || 0)
}))
.filter(item => item.name && item.value > 0);
outerData = res.data.flatMap(item => {
const typeName = item?.orgName || item?.orgType || "";
const list = Array.isArray(item?.orgBillNumList) ? item.orgBillNumList : [];
return list
.map(child => ({
name: child?.orgName || "",
value: Number(child?.count || 0),
percent: typeof child?.percent === "number" ? child.percent : Number(child?.percent || 0),
type: child?.orgType || typeName
}))
.filter(child => child.name && child.value > 0);
});
} else {
const orgBillNumList = res?.data?.orgBillNumList || [];
const orgBillNumMap = res?.data?.orgBillNumMap || {};
const data1 = []; const data1 = [];
const houseTotal = Number(orgBillNumMap?.House || 0); const houseTotal = Number(orgBillNumMap?.House || 0);
const senateTotal = Number(orgBillNumMap?.Senate || 0); const senateTotal = Number(orgBillNumMap?.Senate || 0);
if (houseTotal > 0) data1.push({ name: "众议院", value: houseTotal }); if (houseTotal > 0) data1.push({ name: "众议院", value: houseTotal });
if (senateTotal > 0) data1.push({ name: "参议院", value: senateTotal }); if (senateTotal > 0) data1.push({ name: "参议院", value: senateTotal });
innerData = data1;
const getOrgTypeLabel = orgType => (orgType === "Senate" ? "参议院" : "众议院"); outerData = orgBillNumList
const typeOrderMap = { 众议院: 0, 参议院: 1 };
const data2 = orgBillNumList
.map(item => ({ .map(item => ({
name: item.orgName, name: item.orgName,
value: Number(item.count || 0), value: Number(item.count || 0),
percent: typeof item.percent === "number" ? item.percent : Number(item.percent || 0), percent: typeof item.percent === "number" ? item.percent : Number(item.percent || 0),
type: getOrgTypeLabel(item.orgType) type: item.orgType === "Senate" ? "参议院" : "众议院"
})) }))
// 关键:外环顺序必须按内环(众→参)分组,否则扇区角度会交错导致“不对应” .filter(item => item.name && item.value > 0);
.sort((a, b) => { }
const t1 = typeOrderMap[a.type] ?? 99; if (res.code === 200 && innerData.length > 0 && outerData.length > 0) {
const t2 = typeOrderMap[b.type] ?? 99; box7HasData.value = true;
if (t1 !== t2) return t1 - t2; // 必须等待DOM更新,因为v-if切换可能导致元素刚被创建
return (b.value ?? 0) - (a.value ?? 0); await nextTick();
});
const selectParam = { const typeOrderMap = new Map(innerData.map((item, index) => [item.name, index]));
moduleType: '国会法案', const data2 = outerData.sort((a, b) => {
key: 3, const t1 = typeOrderMap.get(a.type) ?? 99;
selectedDate: box7selectetedTime.value ? JSON.stringify([box7selectetedTime.value + '-01-01', box7selectetedTime.value + '-12-31']) : '', const t2 = typeOrderMap.get(b.type) ?? 99;
isInvolveCn: true if (t1 !== t2) return t1 - t2;
} return (b.value ?? 0) - (a.value ?? 0);
});
const box7Chart = getDoublePieChart(data1, data2); const box7Chart = getDoublePieChart(innerData, data2);
setChart(box7Chart, "box7Chart", true, selectParam); setChart(box7Chart, "box7Chart", true, selectParam);
box7AiData.value = { inner: data1, outer: data2 }; box7AiData.value = { inner: innerData, outer: data2 };
} else { } else {
// 接口异常(如500)时,清空图表数据以避免报错或显示错误信息 // 接口异常(如500)时,清空图表数据以避免报错或显示错误信息
box7HasData.value = false; box7HasData.value = false;
...@@ -1009,7 +1050,7 @@ const handleBox7Data = async () => { ...@@ -1009,7 +1050,7 @@ const handleBox7Data = async () => {
watch(box7selectetedTime, () => { watch(box7selectetedTime, () => {
handleBox7Data(); handleBox7Data();
}); });
// 查看社交媒体详情 // 查看人物详情
const handleToSocialDetail = item => { const handleToSocialDetail = item => {
const route = router.resolve({ const route = router.resolve({
path: "/characterPage", path: "/characterPage",
...@@ -1057,7 +1098,7 @@ const handleBox6 = async () => { ...@@ -1057,7 +1098,7 @@ const handleBox6 = async () => {
// 涉华领域分布 // 涉华领域分布
const box9ChartData = ref([]); const box9ChartData = ref([]);
const box9selectetedTime = ref("2025"); const box9selectetedTime = ref(String(currentYear));
// 立法状态下拉:提出法案、众议院通过、参议院通过、解决分歧、呈交总统、完成立法 // 立法状态下拉:提出法案、众议院通过、参议院通过、解决分歧、呈交总统、完成立法
// v-model 存储的是接口需要的 status 值(直接作为接口参数) // v-model 存储的是接口需要的 status 值(直接作为接口参数)
const box9LegislativeStatus = ref("提出法案"); const box9LegislativeStatus = ref("提出法案");
...@@ -1069,28 +1110,7 @@ const box9LegislativeStatusList = ref([ ...@@ -1069,28 +1110,7 @@ const box9LegislativeStatusList = ref([
{ label: "呈交总统", value: "呈交总统" }, { label: "呈交总统", value: "呈交总统" },
{ label: "完成立法", value: "完成立法" } { label: "完成立法", value: "完成立法" }
]); ]);
const box9YearList = ref([ const box9YearList = ref(recentFiveYearOptions);
{
label: "2026",
value: "2026"
},
{
label: "2025",
value: "2025"
},
{
label: "2024",
value: "2024"
},
{
label: "2023",
value: "2023"
},
{
label: "2022",
value: "2022"
}
]);
const box9HasData = ref(true); const box9HasData = ref(true);
let box9ChartInstance = null; let box9ChartInstance = null;
const BOX9_MAX_DOMAIN_COUNT = 7; const BOX9_MAX_DOMAIN_COUNT = 7;
...@@ -1149,7 +1169,7 @@ const handleBox9Data = async () => { ...@@ -1149,7 +1169,7 @@ const handleBox9Data = async () => {
}; };
}), }),
null, null,
{ showCount: false } { showCount: true, countUnit: "项" }
); );
// 记录埋点时,将当前选中的立法状态映射为序号(0-4) // 记录埋点时,将当前选中的立法状态映射为序号(0-4)
const selectedIndex = box9LegislativeStatusList.value.findIndex( const selectedIndex = box9LegislativeStatusList.value.findIndex(
...@@ -1428,7 +1448,7 @@ const handleResize = () => { ...@@ -1428,7 +1448,7 @@ const handleResize = () => {
}; };
// 下钻至资源库 // 下钻至资源库
const handleToDataLibrary = (item) => { const handleToInstitution = (item) => {
const orgId = item?.orgId || item?.id; const orgId = item?.orgId || item?.id;
if (!orgId) return; if (!orgId) return;
...@@ -2076,12 +2096,22 @@ onUnmounted(() => { ...@@ -2076,12 +2096,22 @@ onUnmounted(() => {
gap: 8px; gap: 8px;
} }
.box1-main-divider {
margin-top: 18px;
margin-bottom: 18px;
width: 468px;
height: 1px;
background: var(--bg-black-5);
}
.box1-main-left-info1 { .box1-main-left-info1 {
margin-top: 25px; margin-top: 0;
margin-left: 4px; margin-left: 4px;
.info1-box { .info1-box {
display: flex; display: flex;
min-height: 30px;
align-items: flex-start;
.icon { .icon {
margin-top: 15px; margin-top: 15px;
...@@ -2104,18 +2134,60 @@ onUnmounted(() => { ...@@ -2104,18 +2134,60 @@ onUnmounted(() => {
.info1-box-right { .info1-box-right {
margin-left: 40px; margin-left: 40px;
height: 30px; min-height: 30px;
color: var(--text-primary-65-color); color: var(--text-primary-65-color);
font-family: Microsoft YaHei; font-family: Microsoft YaHei;
font-size: var(--font-size-base); font-size: var(--font-size-base);
font-weight: 400; font-weight: 400;
line-height: 30px; line-height: 30px;
} }
.info1-box-right--committee {
display: flex;
flex-direction: column;
justify-content: flex-start;
gap: 4px;
height: auto;
min-height: 30px;
line-height: 22px;
padding-top: 4px;
.committee-item {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.committee-logo {
width: 18px;
height: 18px;
min-width: 18px;
border-radius: 50%;
object-fit: cover;
background: var(--bg-black-5);
}
.committee-name {
color: var(--text-primary-65-color);
font-family: Microsoft YaHei;
font-size: var(--font-size-base);
font-weight: 400;
line-height: 22px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
} }
} }
.box1-main-divider--before-list {
margin-top: 18px;
}
.box1-main-left-info2 { .box1-main-left-info2 {
margin-top: 21px; margin-top: 0;
height: 200px; height: 200px;
width: 440px; width: 440px;
position: relative; position: relative;
...@@ -2216,7 +2288,7 @@ onUnmounted(() => { ...@@ -2216,7 +2288,7 @@ onUnmounted(() => {
left: 0; left: 0;
bottom: 0; bottom: 0;
box-sizing: border-box; box-sizing: border-box;
padding: 9px 10px 12px 10px; padding: 9px 20px 12px 20px;
.inner-box-header { .inner-box-header {
height: 30px; height: 30px;
......
...@@ -69,7 +69,7 @@ const getDoublePieChart = (data1, data2) => { ...@@ -69,7 +69,7 @@ const getDoublePieChart = (data1, data2) => {
const name = truncateLabel(params?.name, 6) const name = truncateLabel(params?.name, 6)
const value = params?.value ?? 0 const value = params?.value ?? 0
const percent = typeof params?.percent === 'number' ? params.percent : 0 const percent = typeof params?.percent === 'number' ? params.percent : 0
return `{name|${name}}\n{time|${value} ${percent}%}` return `{name|${name}}\n{time|${value} ${percent}%}`
}, },
minMargin: 5, minMargin: 5,
edgeDistance: 10, edgeDistance: 10,
......
...@@ -10,6 +10,7 @@ const truncateLabel = (value, maxLen = 6) => { ...@@ -10,6 +10,7 @@ const truncateLabel = (value, maxLen = 6) => {
const getPieChart = (data, colorList, options = {}) => { const getPieChart = (data, colorList, options = {}) => {
const showCount = options.showCount !== false const showCount = options.showCount !== false
const countUnit = options.countUnit || '条'
const chartColors = Array.isArray(colorList) && colorList.length ? colorList : MUTICHARTCOLORS const chartColors = Array.isArray(colorList) && colorList.length ? colorList : MUTICHARTCOLORS
let option = { let option = {
color: chartColors, color: chartColors,
...@@ -38,7 +39,7 @@ const getPieChart = (data, colorList, options = {}) => { ...@@ -38,7 +39,7 @@ const getPieChart = (data, colorList, options = {}) => {
const name = truncateLabel(params?.name, 6) const name = truncateLabel(params?.name, 6)
const value = params?.value ?? 0 const value = params?.value ?? 0
const percent = typeof params?.percent === 'number' ? params.percent : 0 const percent = typeof params?.percent === 'number' ? params.percent : 0
const labelText = showCount ? `${value} ${percent}%` : `${percent}%` const labelText = showCount ? `${value}${countUnit} ${percent}%` : `${percent}%`
return `{name|${name}}\n{time|${labelText}}` return `{name|${name}}\n{time|${labelText}}`
}, },
minMargin: 5, minMargin: 5,
......
...@@ -75,7 +75,7 @@ ...@@ -75,7 +75,7 @@
</div> </div>
</div> --> </div> -->
</div> </div>
<div class="main" v-if="curArea !== '实体清单'" > <div class="main" v-if="curArea !== '实体清单'">
<div class="item" v-for="(item, index) in searchResults" :key="index" @click="handleToPage(item)"> <div class="item" v-for="(item, index) in searchResults" :key="index" @click="handleToPage(item)">
<div class="item-left" v-if="item.img"> <div class="item-left" v-if="item.img">
<img :src="item?.img" alt="" /> <img :src="item?.img" alt="" />
...@@ -93,7 +93,7 @@ ...@@ -93,7 +93,7 @@
</div> </div>
<el-empty v-if="!searchResults.length"></el-empty> <el-empty v-if="!searchResults.length"></el-empty>
</div> </div>
<div class="main1" v-if="curArea === '实体清单'" > <div class="main1" v-if="curArea === '实体清单'">
<div class="item" v-for="(item, index) in searchResults" :key="index" @click="handleToPage(item)"> <div class="item" v-for="(item, index) in searchResults" :key="index" @click="handleToPage(item)">
<div class="main-header"> <div class="main-header">
<div class="title" v-html="item?.originalTitle"></div> <div class="title" v-html="item?.originalTitle"></div>
...@@ -358,9 +358,9 @@ const handleSearch = async (isShowResultTip) => { ...@@ -358,9 +358,9 @@ const handleSearch = async (isShowResultTip) => {
} }
} }
} catch (error) { } catch (error) {
console.error('error', error); console.error('error', error);
} finally { } finally {
isLoading.value = false isLoading.value = false
} }
...@@ -397,25 +397,44 @@ const handleToPage = async item => { ...@@ -397,25 +397,44 @@ const handleToPage = async item => {
} else if (personTypeName === "智库研究人员") { } else if (personTypeName === "智库研究人员") {
type = 3; type = 3;
} else { } else {
personTypeName = ""; personTypeName = "其他类型";
ElMessage.warning("找不到当前人员的类型值!"); const route = router.resolve({
path: "/characterPage",
query: {
personId: item.id
}
});
window.open(route.href, "_blank");
return; return;
} }
const route = router.resolve({ const route = router.resolve({
path: "/characterPage", path: "/characterPage",
query: { query: {
type: type, // type=1为科技企业领袖,2为国会议员,3为智库研究人员 type: type, // type=1为科技企业领袖,2为国会议员,3为智库研究人员
personId: id personId: item.id
} }
}); });
window.open(route.href, "_blank"); window.open(route.href, "_blank");
} else { } else {
personTypeName = ""; personTypeName = "";
ElMessage.warning("找不到当前人员的类型值!"); const route = router.resolve({
path: "/characterPage",
query: {
personId: item.id
}
});
window.open(route.href, "_blank");
return; return;
} }
} else { } else {
ElMessage.warning("获取人物全局信息错误"); // ElMessage.warning("获取人物全局信息错误");
const route = router.resolve({
path: "/characterPage",
query: {
personId: item.id
}
});
window.open(route.href, "_blank");
return; return;
} }
} catch (error) { } } catch (error) { }
...@@ -445,7 +464,7 @@ const handleToPage = async item => { ...@@ -445,7 +464,7 @@ const handleToPage = async item => {
break; break;
case "智库": case "智库":
curRoute = router.resolve({ curRoute = router.resolve({
name: "ReportDetail", name: "ThinkTankDetail",
params: { params: {
id: item.id id: item.id
} }
...@@ -517,36 +536,36 @@ const handleToPage = async item => { ...@@ -517,36 +536,36 @@ const handleToPage = async item => {
} }
}); });
break break
case "商业管制清单事件": // case "商业管制清单事件":
curRoute = router.resolve({ // curRoute = router.resolve({
path: "/exportControl/singleSanction", // path: "/exportControl/singleSanction",
query: { // query: {
id: item.id, // id: item.id,
sanTypeId: 2, // sanTypeId: 2,
date: item.date // date: item.date
} // }
}); // });
break // break
case "SDN清单事件": // case "SDN清单事件":
curRoute = router.resolve({ // curRoute = router.resolve({
path: "/exportControl/singleSanction", // path: "/exportControl/singleSanction",
query: { // query: {
id: item.id, // id: item.id,
sanTypeId: 2, // sanTypeId: 2,
date: item.date // date: item.date
} // }
}); // });
break // break
case "涉军企业清单事件": // case "涉军企业清单事件":
curRoute = router.resolve({ // curRoute = router.resolve({
path: "/exportControl/singleSanction", // path: "/exportControl/singleSanction",
query: { // query: {
id: item.id, // id: item.id,
sanTypeId: 2, // sanTypeId: 2,
date: item.date // date: item.date
} // }
}); // });
break // break
case "机构": case "机构":
curRoute = router.resolve({ curRoute = router.resolve({
path: "/institution", path: "/institution",
...@@ -998,6 +1017,18 @@ const handleCompClick = item => { ...@@ -998,6 +1017,18 @@ const handleCompClick = item => {
letter-spacing: 0px; letter-spacing: 0px;
text-align: left; text-align: left;
overflow: hidden; overflow: hidden;
/* 2. 关键属性:将元素转为弹性盒模型 */
display: -webkit-box;
/* 3. 限制显示的行数(修改数字即可改变行数) */
-webkit-line-clamp: 2;
/* 4. 设置文字垂直排列方向 */
-webkit-box-orient: vertical;
/* 5. 隐藏超出部分 */
overflow: hidden;
/* 6. 显示省略号 */
text-overflow: ellipsis;
/* 可选:修复文字间距/换行问题 */
word-break: break-all;
} }
.item-right-footer { .item-right-footer {
...@@ -1164,7 +1195,7 @@ const handleCompClick = item => { ...@@ -1164,7 +1195,7 @@ const handleCompClick = item => {
.content { .content {
margin-top: 10px; margin-top: 10px;
min-height: 0; min-height: 0;
// max-height: 48px; max-height: 48px;
font-family: Microsoft YaHei; font-family: Microsoft YaHei;
font-size: 16px; font-size: 16px;
color: rgba(59, 65, 75, 1); color: rgba(59, 65, 75, 1);
...@@ -1173,6 +1204,18 @@ const handleCompClick = item => { ...@@ -1173,6 +1204,18 @@ const handleCompClick = item => {
letter-spacing: 0px; letter-spacing: 0px;
text-align: left; text-align: left;
overflow: hidden; overflow: hidden;
/* 2. 关键属性:将元素转为弹性盒模型 */
display: -webkit-box;
/* 3. 限制显示的行数(修改数字即可改变行数) */
-webkit-line-clamp: 2;
/* 4. 设置文字垂直排列方向 */
-webkit-box-orient: vertical;
/* 5. 隐藏超出部分 */
overflow: hidden;
/* 6. 显示省略号 */
text-overflow: ellipsis;
/* 可选:修复文字间距/换行问题 */
word-break: break-all;
} }
.time { .time {
......
...@@ -12,6 +12,10 @@ const getBarChart = (data) => { ...@@ -12,6 +12,10 @@ const getBarChart = (data) => {
}, },
yAxis: { yAxis: {
type: 'value', type: 'value',
name: '数量',
nameTextStyle: {
padding: [-10, 0, 0, -40] // [上, 右, 下, 左],向左移动10px
},
splitLine: { splitLine: {
show: true, show: true,
lineStyle: { lineStyle: {
......
...@@ -24,6 +24,9 @@ const getLineChart = (dataX, dataY) => { ...@@ -24,6 +24,9 @@ const getLineChart = (dataX, dataY) => {
yAxis: { yAxis: {
type: 'value', type: 'value',
name: '数量', name: '数量',
nameTextStyle: {
padding: [-10, 0, 0, -40] // [上, 右, 下, 左],向左移动10px
},
splitLine: { splitLine: {
show: true, show: true,
lineStyle: { lineStyle: {
......
...@@ -199,7 +199,7 @@ const staticsDemensionList = ref([ ...@@ -199,7 +199,7 @@ const staticsDemensionList = ref([
name: '制裁时间', name: '制裁时间',
active: true, active: true,
chartTypeList: ['折线图', '柱状图'], chartTypeList: ['折线图', '柱状图'],
chartTitle: '商业管制清单制裁时间变化趋势', chartTitle: '涉军企业清单制裁时间变化趋势',
data: { data: {
dataX: [], dataX: [],
dataY: [] dataY: []
...@@ -217,14 +217,14 @@ const staticsDemensionList = ref([ ...@@ -217,14 +217,14 @@ const staticsDemensionList = ref([
name: '科技领域', name: '科技领域',
active: false, active: false,
chartTypeList: ['饼状图'], chartTypeList: ['饼状图'],
chartTitle: '商业管制清单科技领域分布', chartTitle: '涉军企业清单科技领域分布',
data: [] data: []
}, },
{ {
name: '物项类别', name: '物项类别',
active: false, active: false,
chartTypeList: ['饼状图'], chartTypeList: ['饼状图'],
chartTitle: '商业管制清单物项类别分布', chartTitle: '涉军企业清单物项类别分布',
data: [] data: []
} }
...@@ -908,13 +908,13 @@ const initParam = () => { ...@@ -908,13 +908,13 @@ const initParam = () => {
} }
// 跳转政令详情 // 跳转企业详情
const handleClickToDetail = (curEntity) => { const handleClickToDetail = (curEntity) => {
window.sessionStorage.setItem("curTabName", curEntity.title); window.sessionStorage.setItem("curTabName", curEntity.title);
const route = router.resolve({ const route = router.resolve({
name: "companyPages", name: "companyPages",
params: { params: {
id: curEntity.id id: curEntity.organizationId
} }
}); });
window.open(route.href, "_blank"); window.open(route.href, "_blank");
......
...@@ -234,7 +234,7 @@ const staticsDemensionList = ref([ ...@@ -234,7 +234,7 @@ const staticsDemensionList = ref([
name: '制裁时间', name: '制裁时间',
active: true, active: true,
chartTypeList: ['折线图', '柱状图'], chartTypeList: ['折线图', '柱状图'],
chartTitle: '实体清单制裁时间变化趋势', chartTitle: 'SDN清单制裁时间变化趋势',
data: { data: {
dataX: [], dataX: [],
dataY: [] dataY: []
...@@ -252,28 +252,28 @@ const staticsDemensionList = ref([ ...@@ -252,28 +252,28 @@ const staticsDemensionList = ref([
name: '科技领域', name: '科技领域',
active: false, active: false,
chartTypeList: ['饼状图'], chartTypeList: ['饼状图'],
chartTitle: '实体清单科技领域分布', chartTitle: 'SDN清单科技领域分布',
data: [] data: []
}, },
{ {
name: '实体类型', name: 'SDN类型',
active: false, active: false,
chartTypeList: ['饼状图'], chartTypeList: ['饼状图'],
chartTitle: '实体类型分布', chartTitle: 'SDN类型分布',
data: [] data: []
}, },
{ {
name: '实体国家地区', name: 'SDN国家地区',
active: false, active: false,
chartTypeList: ['饼状图'], chartTypeList: ['饼状图'],
chartTitle: '实体国家地区分布', chartTitle: 'SDN国家地区分布',
data: [] data: []
}, },
{ {
name: '实体省份', name: 'SDN省份',
active: false, active: false,
chartTypeList: ['饼状图'], chartTypeList: ['饼状图'],
chartTitle: '实体省份分布', chartTitle: 'SDN省份分布',
data: [] data: []
} }
]) ])
......
...@@ -157,6 +157,11 @@ const desMap = { ...@@ -157,6 +157,11 @@ const desMap = {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-end; align-items: flex-end;
&:hover{
text-decoration: underline;
background: var(--color-primary-2);
}
.quantity-title-des { .quantity-title-des {
display: flex; display: flex;
......
...@@ -3,25 +3,12 @@ ...@@ -3,25 +3,12 @@
<div class="home-main" ref="homeMainRef"> <div class="home-main" ref="homeMainRef">
<div class="home-top-bg"></div> <div class="home-top-bg"></div>
<div class="home-main-header"> <div class="home-main-header">
<SearchContainer <SearchContainer style="margin-bottom: 0; margin-top: 48px; height: fit-content" v-if="homeMainRef"
style="margin-bottom: 0; margin-top: 48px; height: fit-content" placeholder="搜索出口管制" :containerRef="homeMainRef" areaName="实体清单" />
v-if="homeMainRef"
placeholder="搜索出口管制"
:containerRef="homeMainRef"
areaName="实体清单"
/>
<div class="home-main-header-footer-info"> <div class="home-main-header-footer-info">
<InfoCard <InfoCard v-for="(item, index) in infoList" :key="item.id" :title="item.nameZh" :subtitle="item.nameAbbr"
v-for="(item, index) in infoList" :description="item.description" :quantity="item.postCount" :unit="item.unit" :color="infoListColor[index]"
:key="item.id" @click="handleToEntityListNoId(item)" />
:title="item.nameZh"
:subtitle="item.nameAbbr"
:description="item.description"
:quantity="item.postCount"
:unit="item.unit"
:color="infoListColor[index]"
@click="handleToEntityListNoId(item)"
/>
</div> </div>
</div> </div>
...@@ -50,7 +37,7 @@ ...@@ -50,7 +37,7 @@
<el-carousel <el-carousel
ref="carouselRef" ref="carouselRef"
height="370px" height="370px"
:autoplay="true" :autoplay="false"
:interval="3000" :interval="3000"
arrow="never" arrow="never"
indicator-position="none" indicator-position="none"
...@@ -59,9 +46,7 @@ ...@@ -59,9 +46,7 @@
<el-carousel-item v-for="(item, index) in entitiesDataInfoList" :key="item.id + index"> <el-carousel-item v-for="(item, index) in entitiesDataInfoList" :key="item.id + index">
<div> <div>
<div class="box1-top"> <div class="box1-top">
<div class="box1-top-title"> <div class="box1-top-title">{{ item.postDate }}——{{ item.name }}</div>
{{ item.postDate }}——BIS《实体清单增列与修订条目》
</div>
<div class="box1-top-content"> <div class="box1-top-content">
<div class="box1-top-content-item"> <div class="box1-top-content-item">
<span class="box1-top-content-item-title">· 发布机构:</span> <span class="box1-top-content-item-title">· 发布机构:</span>
...@@ -73,13 +58,7 @@ ...@@ -73,13 +58,7 @@
</div> </div>
<div class="box1-top-content-item"> <div class="box1-top-content-item">
<span class="box1-top-content-item-title">· 涉及领域:</span> <span class="box1-top-content-item-title">· 涉及领域:</span>
<!-- <div
class="box1-top-content-item-tags"
v-for="(domainItem, index) in item.domains"
:key="index"
>
<el-tag :type="getTagType(domainItem)">{{ domainItem }}</el-tag>
</div> -->
<AreaTag <AreaTag
v-for="(domainItem, index) in item.domains" v-for="(domainItem, index) in item.domains"
:key="index" :key="index"
...@@ -92,18 +71,10 @@ ...@@ -92,18 +71,10 @@
<div class="box1-bottom-sanTypeId" v-if="item.sanEntities?.length"> <div class="box1-bottom-sanTypeId" v-if="item.sanEntities?.length">
<div class="box1-bottom-title">· 涉及主要实体:</div> <div class="box1-bottom-title">· 涉及主要实体:</div>
<div class="box1-bottom-content"> <div class="box1-bottom-content">
<div <div class="box1-bottom-content-item" v-for="(ett, index) in item.sanEntities" :key="index"
class="box1-bottom-content-item" @click="handleEntityClick(ett)">
v-for="(ett, index) in item.sanEntities" <el-image v-if="ett.img" class="box1-bottom-content-item-img" :src="ett.img"
:key="index" alt=""></el-image>
@click="handleEntityClick(ett)"
>
<el-image
v-if="ett.img"
class="box1-bottom-content-item-img"
:src="ett.img"
alt=""
></el-image>
<div v-else class="box1-bottom-content-item-imgUndefined"> <div v-else class="box1-bottom-content-item-imgUndefined">
{{ {{
(ett.orgName || ett.orgNameZh)?.match( (ett.orgName || ett.orgNameZh)?.match(
...@@ -120,12 +91,8 @@ ...@@ -120,12 +91,8 @@
<div class="box1-bottom-sanTypeId" v-if="item.sanItems?.length > 0"> <div class="box1-bottom-sanTypeId" v-if="item.sanItems?.length > 0">
<div class="box1-bottom-title">· 涉及管制物项:</div> <div class="box1-bottom-title">· 涉及管制物项:</div>
<div class="box1-bottom-content__wx"> <div class="box1-bottom-content__wx">
<div <div class="box1-bottom-content__wx-item" v-for="(ett, index) in item.sanItems" :key="index"
class="box1-bottom-content__wx-item" @click="handleWxClick(item)">
v-for="(ett, index) in item.sanItems"
:key="index"
@click="handleWxClick(item)"
>
<div class="box1-bottom-content__wx-item-id"> <div class="box1-bottom-content__wx-item-id">
{{ ett.id }} {{ ett.id }}
</div> </div>
...@@ -146,7 +113,7 @@ ...@@ -146,7 +113,7 @@
}}</span> }}</span>
</div> </div>
<div class="box1-absolute-num"> <div class="box1-absolute-num">
{{ item.cnEntityCount }}{{ item.sanTypeId == allSanTypeIds[0] ? "家" : "" }} {{ item.cnEntityCount }}{{ item.sanTypeId == allSanTypeIds[0] ? "家" : "" }}
</div> </div>
</div> </div>
</div> </div>
...@@ -199,14 +166,8 @@ ...@@ -199,14 +166,8 @@
</div> </div>
</template> </template>
</custom-container> --> </custom-container> -->
<RiskSignal <RiskSignal :list="warningList" @item-click="handleToRiskSignalDetail" @more-click="handleToMoreRiskSignal"
:list="warningList" riskLevel="signalLevel" postDate="signalTime" name="signalTitle" />
@item-click="handleToRiskSignalDetail"
@more-click="handleToMoreRiskSignal"
riskLevel="signalLevel"
postDate="signalTime"
name="signalTitle"
/>
</el-col> </el-col>
</el-row> </el-row>
...@@ -229,19 +190,11 @@ ...@@ -229,19 +190,11 @@
</custom-container> </custom-container>
</el-col> --> </el-col> -->
<div class="center-center"> <div class="center-center">
<NewsList <NewsList :newsList="newsList" @item-click="handleNewsInfoClick" @more-click="handleToMoreNews"
:newsList="newsList" content="newsContent" />
@item-click="handleNewsInfoClick"
@more-click="handleToMoreNews" <MessageBubble :messageList="socialMediaList" @person-click="handlePerClick" imageUrl="avatar"
content="newsContent" @more-click="handleToSocialDetail" />
/>
<MessageBubble
:messageList="socialMediaList"
@person-click="handlePerClick"
imageUrl="avatar"
@more-click="handleToSocialDetail"
/>
<!-- <custom-container title="社交媒体" :titleIcon="dialogIcon" height="450px"> <!-- <custom-container title="社交媒体" :titleIcon="dialogIcon" height="450px">
<template #default> <template #default>
<div class="dialog-list"> <div class="dialog-list">
...@@ -261,30 +214,20 @@ ...@@ -261,30 +214,20 @@
<div class="box3"> <div class="box3">
<div class="box3-content"> <div class="box3-content">
<div class="box3-content-title">实体清单发布频次统计</div> <div class="box3-content-title">实体清单发布频次统计</div>
<el-table <el-table :data="entityListReleaseFreq" stripe style="width: 100%" @row-click="handleEntityRowClick">
:data="entityListReleaseFreq"
stripe
style="width: 100%"
@row-click="handleEntityRowClick"
>
<el-table-column prop="year" label="年份" width="200" /> <el-table-column prop="year" label="年份" width="200" />
<el-table-column label="发布次数" width="300"> <el-table-column label="发布次数" width="300">
<template #default="scope"> <template #default="scope">
<div style="display: flex; align-items: center"> <div style="display: flex; align-items: center">
<span style="margin-right: 10px; width: 40px">{{ scope.row.num }}</span> <span style="margin-right: 10px; width: 40px">{{ scope.row.num }}</span>
<el-progress <el-progress :percentage="scope.row.percent * 100" :show-text="false"
:percentage="scope.row.percent * 100" :status="getStatus(scope.row.percent)" />
:show-text="false"
:status="getStatus(scope.row.percent)"
/>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="重点领域" width="220" align="center"> <el-table-column label="重点领域" width="220" align="center">
<template #default="scope"> <template #default="scope">
<div <div style="display: flex; justify-content: center; align-items: center; gap: 5px">
style="display: flex; justify-content: center; align-items: center; gap: 5px"
>
<AreaTag v-for="tag in scope.row.tags" :key="tag" :tagName="tag" /> <AreaTag v-for="tag in scope.row.tags" :key="tag" :tagName="tag" />
<!-- <el-tag v-for="tag in scope.row.tags" :key="tag" :type="getTagType(tag)">{{ <!-- <el-tag v-for="tag in scope.row.tags" :key="tag" :type="getTagType(tag)">{{
tag tag
...@@ -306,30 +249,21 @@ ...@@ -306,30 +249,21 @@
</div> </div>
<div class="box3-content"> <div class="box3-content">
<div class="box3-content-title">商业管制清单发布频次统计</div> <div class="box3-content-title">商业管制清单发布频次统计</div>
<el-table <el-table :data="commerceControlListReleaseFreq" stripe style="width: 100%"
:data="commerceControlListReleaseFreq" @row-click="handleCommercialRowClick">
stripe
style="width: 100%"
@row-click="handleCommercialRowClick"
>
<el-table-column prop="year" label="年份" width="200" /> <el-table-column prop="year" label="年份" width="200" />
<el-table-column label="发布次数" width="300"> <el-table-column label="发布次数" width="300">
<template #default="scope"> <template #default="scope">
<div style="display: flex; align-items: center"> <div style="display: flex; align-items: center">
<span style="margin-right: 10px; width: 40px">{{ scope.row.num }}</span> <span style="margin-right: 10px; width: 40px">{{ scope.row.num }}</span>
<el-progress <el-progress :percentage="scope.row.percent * 100" :show-text="false"
:percentage="scope.row.percent * 100" :status="getStatus(scope.row.percent)" />
:show-text="false"
:status="getStatus(scope.row.percent)"
/>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="重点领域" width="220" align="center"> <el-table-column label="重点领域" width="220" align="center">
<template #default="scope"> <template #default="scope">
<div <div style="display: flex; justify-content: center; align-items: center; gap: 5px">
style="display: flex; justify-content: center; align-items: center; gap: 5px"
>
<AreaTag v-for="tag in scope.row.tags" :key="tag" :tagName="tag" /> <AreaTag v-for="tag in scope.row.tags" :key="tag" :tagName="tag" />
<!-- <el-tag v-for="tag in scope.row.tags" :key="tag" :type="getTagType(tag)">{{ <!-- <el-tag v-for="tag in scope.row.tags" :key="tag" :type="getTagType(tag)">{{
tag tag
...@@ -359,11 +293,8 @@ ...@@ -359,11 +293,8 @@
<template #default="scope"> <template #default="scope">
<div style="display: flex; align-items: center"> <div style="display: flex; align-items: center">
<span style="margin-right: 10px; width: 40px">{{ scope.row.num }}</span> <span style="margin-right: 10px; width: 40px">{{ scope.row.num }}</span>
<el-progress <el-progress :percentage="scope.row.percent * 100" :show-text="false"
:percentage="scope.row.percent * 100" :status="getStatus(scope.row.percent)" />
:show-text="false"
:status="getStatus(scope.row.percent)"
/>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
...@@ -391,12 +322,8 @@ ...@@ -391,12 +322,8 @@
<el-checkbox v-model="domainChecked" label="50%规则" size="large" /> <el-checkbox v-model="domainChecked" label="50%规则" size="large" />
</template> </template>
<template #default> <template #default>
<EChart <EChart :option="radarOption" autoresize :style="{ height: '420px' }"
:option="radarOption" @chart-click="handleRadarChartClick" />
autoresize
:style="{ height: '420px' }"
@chart-click="handleRadarChartClick"
/>
<div class="data-origin-box"> <div class="data-origin-box">
<div class="data-origin-icon"> <div class="data-origin-icon">
<img :src="tipsIcon" alt="" /> <img :src="tipsIcon" alt="" />
...@@ -414,24 +341,15 @@ ...@@ -414,24 +341,15 @@
<custom-container title="制裁清单数量增长趋势" :titleIcon="qushiIcon" height="540px"> <custom-container title="制裁清单数量增长趋势" :titleIcon="qushiIcon" height="540px">
<template #header-right> <template #header-right>
<div style="display: flex; align-items: center; gap: 16px"> <div style="display: flex; align-items: center; gap: 16px">
<el-checkbox <el-checkbox v-if="selectedEntityId != '13'" v-model="trendChecked" label="50%规则" size="large" />
v-if="selectedEntityId != '13'"
v-model="trendChecked"
label="50%规则"
size="large"
/>
<el-select v-model="selectedEntityId" placeholder="请选择清单类型" style="width: 160px"> <el-select v-model="selectedEntityId" placeholder="请选择清单类型" style="width: 160px">
<el-option v-for="item in infoList" :key="item.id" :label="item.nameZh" :value="item.id" /> <el-option v-for="item in infoList" :key="item.id" :label="item.nameZh" :value="item.id" />
</el-select> </el-select>
</div> </div>
</template> </template>
<template #default> <template #default>
<EChart <EChart :option="trendOption" autoresize :style="{ height: '420px' }"
:option="trendOption" @chart-click="handleMultiBarChartClick" />
autoresize
:style="{ height: '420px' }"
@chart-click="handleMultiBarChartClick"
/>
<div class="data-origin-box"> <div class="data-origin-box">
<div class="data-origin-icon"> <div class="data-origin-icon">
<img :src="tipsIcon" alt="" /> <img :src="tipsIcon" alt="" />
...@@ -450,13 +368,9 @@ ...@@ -450,13 +368,9 @@
<el-row :gutter="16" style="width: 1600px; margin: 0 auto; margin-top: 39px; padding-bottom: 60px"> <el-row :gutter="16" style="width: 1600px; margin: 0 auto; margin-top: 39px; padding-bottom: 60px">
<CustomTitle id="position4" title="资源库" style="margin-top: 0px" /> <CustomTitle id="position4" title="资源库" style="margin-top: 0px" />
<div class="resource-tabs"> <div class="resource-tabs">
<div <div v-for="tab in resourceTabs" :key="tab.value" class="resource-tab-item"
v-for="tab in resourceTabs"
:key="tab.value"
class="resource-tab-item"
:class="{ active: activeResourceTab == tab.value, disabled: tab.disabled }" :class="{ active: activeResourceTab == tab.value, disabled: tab.disabled }"
@click="handleResourceTabClick(tab)" @click="handleResourceTabClick(tab)">
>
{{ tab.label }} {{ tab.label }}
</div> </div>
</div> </div>
...@@ -469,25 +383,15 @@ ...@@ -469,25 +383,15 @@
<div class="box4-item" v-for="(item, idx) in sanctionProcessList" :key="item.title"> <div class="box4-item" v-for="(item, idx) in sanctionProcessList" :key="item.title">
<div class="box4-item-left"> <div class="box4-item-left">
<el-image :src="dotIcon" alt="图片" class="box4-item-left-icon" /> <el-image :src="dotIcon" alt="图片" class="box4-item-left-icon" />
<div <div class="box4-item-left-line" v-if="idx + 1 != sanctionProcessList.length"></div>
class="box4-item-left-line"
v-if="idx + 1 != sanctionProcessList.length"
></div>
</div> </div>
<div class="box4-item-right"> <div class="box4-item-right">
<div class="box4-item-right-header" @click="handleSanc(item)"> <div class="box4-item-right-header" @click="handleSanc(item)">
<span class="box4-item-right-header-title" <span class="box4-item-right-header-title">{{ item.postDate }}{{ item.title }}</span>
>{{ item.postDate }}{{ item.title }}</span
>
<span class="box4-item-right-header-desc">{{ item.desc }}</span> <span class="box4-item-right-header-desc">{{ item.desc }}</span>
</div> </div>
<el-tooltip <el-tooltip effect="dark" :content="item.content" popper-class="common-prompt-popper"
effect="dark" placement="top" :show-after="500">
:content="item.content"
popper-class="common-prompt-popper"
placement="top"
:show-after="500"
>
<div class="box4-item-right-content"> <div class="box4-item-right-content">
{{ item.content }} {{ item.content }}
</div> </div>
...@@ -495,12 +399,8 @@ ...@@ -495,12 +399,8 @@
</div> </div>
</div> </div>
</div> </div>
<div <div class="box4-footer" :style="{ marginTop: sanctionProcessList.length > 0 ? '0px' : 'auto' }">
class="box4-footer" <el-button type="primary" link @click="handleGetMore">查看更多
:style="{ marginTop: sanctionProcessList.length > 0 ? '0px' : 'auto' }"
>
<el-button type="primary" link @click="handleGetMore"
>查看更多
<el-icon> <el-icon>
<DArrowRight /> <DArrowRight />
</el-icon> </el-icon>
...@@ -517,24 +417,13 @@ ...@@ -517,24 +417,13 @@
</template> </template>
<template #default> <template #default>
<div class="box5"> <div class="box5">
<el-table <el-table :data="entitiesList" class="sanction-table" stripe empty-text="暂无数据" height="700px"
:data="entitiesList" header-row-class-name="table-header" row-class-name="table-row">
class="sanction-table"
stripe
empty-text="暂无数据"
height="700px"
header-row-class-name="table-header"
row-class-name="table-row"
>
<el-table-column prop="name" label="实体名称" min-width="200"> <el-table-column prop="name" label="实体名称" min-width="200">
<template #default="scope"> <template #default="scope">
<div class="tableName" @click="handleCompClick(scope.row)"> <div class="tableName" @click="handleCompClick(scope.row)">
<el-image <el-image v-if="scope.row.img" class="box1-bottom-content-item-img" :src="scope.row.img"
v-if="scope.row.img" alt=""></el-image>
class="box1-bottom-content-item-img"
:src="scope.row.img"
alt=""
></el-image>
<div v-else class="box1-bottom-content-item-imgUndefined"> <div v-else class="box1-bottom-content-item-imgUndefined">
{{ {{
(scope.row.name || scope.row.enName)?.match( (scope.row.name || scope.row.enName)?.match(
...@@ -588,19 +477,13 @@ ...@@ -588,19 +477,13 @@
<el-table-column prop="revenue" label="50%规则子企业" width="280" align="right"> <el-table-column prop="revenue" label="50%规则子企业" width="280" align="right">
<template #default="scope"> <template #default="scope">
<div class="num-item" v-if="scope.row.ruleOrgCount > 0"> <div class="num-item" v-if="scope.row.ruleOrgCount > 0">
<div <div class="name-item" :class="[
class="name-item" 'revenue-cell',
:class="[ scope.row.revenue === '无营收数据' ? 'no-revenue' : ''
'revenue-cell', ]">
scope.row.revenue === '无营收数据' ? 'no-revenue' : ''
]"
>
{{ scope.row.ruleOrgList[0].orgName }}...等 {{ scope.row.ruleOrgList[0].orgName }}...等
</div> </div>
<div <div style="width: 50px; color: #409eff; cursor: pointer" @click="handleOrgClick(scope.row)">
style="width: 50px; color: #409eff; cursor: pointer"
@click="handleOrgClick(scope.row)"
>
{{ scope.row.ruleOrgCount }}家> {{ scope.row.ruleOrgCount }}家>
</div> </div>
</div> </div>
...@@ -612,15 +495,8 @@ ...@@ -612,15 +495,8 @@
<!-- <div class="pagination-info"> <!-- <div class="pagination-info">
第{{ currentPage }}页,共{{ totalPages }}页 第{{ currentPage }}页,共{{ totalPages }}页
</div> --> </div> -->
<el-pagination <el-pagination v-model:current-page="currentPage" :page-size="pageSize" :total="total"
v-model:current-page="currentPage" :pager-count="5" layout="prev, pager, next" background @current-change="handlePageChange" />
:page-size="pageSize"
:total="total"
:pager-count="5"
layout="prev, pager, next"
background
@current-change="handlePageChange"
/>
</div> </div>
</div> </div>
</template> </template>
...@@ -686,14 +562,8 @@ ...@@ -686,14 +562,8 @@
</div> </div>
<div class="right-footer"> <div class="right-footer">
<div class="total-count">{{ totalAll }}</div> <div class="total-count">{{ totalAll }}</div>
<el-pagination <el-pagination v-model:current-page="currentPageAll" :page-size="pageSizeAll" :total="totalAll"
v-model:current-page="currentPageAll" layout="prev, pager, next" background @current-change="handlePageChangeAll" />
:page-size="pageSizeAll"
:total="totalAll"
layout="prev, pager, next"
background
@current-change="handlePageChangeAll"
/>
</div> </div>
</div> </div>
</div> </div>
...@@ -706,12 +576,8 @@ ...@@ -706,12 +576,8 @@
</template> </template>
</el-row> </el-row>
</div> </div>
<RuleSubsidiaryDialog <RuleSubsidiaryDialog v-model="dialogVisible" :company-name="currentRuleCompany" :total-count="currentRuleCount"
v-model="dialogVisible" :data-list="currentOrgList" />
:company-name="currentRuleCompany"
:total-count="currentRuleCount"
:data-list="currentOrgList"
/>
</div> </div>
<el-dialog v-model="mediaVisible" title="社交媒体信息" width="500" :before-close="handleMediaClose"> <el-dialog v-model="mediaVisible" title="社交媒体信息" width="500" :before-close="handleMediaClose">
<div class="dialog-content"> <div class="dialog-content">
...@@ -724,13 +590,8 @@ ...@@ -724,13 +590,8 @@
</div> </div>
</template> </template>
</el-dialog> </el-dialog>
<RiskSignalOverviewDetailDialog <RiskSignalOverviewDetailDialog v-model="isRiskOverviewDetailOpen" :row="riskOverviewDetailRow"
v-model="isRiskOverviewDetailOpen" name-field="signalTitle" post-date-field="signalTime" risk-level-field="signalLevel" />
:row="riskOverviewDetailRow"
name-field="signalTitle"
post-date-field="signalTime"
risk-level-field="signalLevel"
/>
</template> </template>
<script setup> <script setup>
...@@ -1457,7 +1318,7 @@ const fetchSanctionList = async () => { ...@@ -1457,7 +1318,7 @@ const fetchSanctionList = async () => {
}); });
totalAll.value = res.totalElements; totalAll.value = res.totalElements;
} }
} catch (error) {} } catch (error) { }
}; };
const handlePageChangeAll = val => { const handlePageChangeAll = val => {
...@@ -1751,7 +1612,7 @@ const handleGetHylyList = async () => { ...@@ -1751,7 +1612,7 @@ const handleGetHylyList = async () => {
hylymc: "全部分类" hylymc: "全部分类"
}; };
categoryList.value = [obj, ...categoryList.value]; categoryList.value = [obj, ...categoryList.value];
} catch (error) {} } catch (error) { }
}; };
const chart1Data = ref({ const chart1Data = ref({
...@@ -1961,11 +1822,12 @@ const handleMediaClick = item => { ...@@ -1961,11 +1822,12 @@ const handleMediaClick = item => {
flex-direction: column; flex-direction: column;
gap: 20px; gap: 20px;
position: relative; position: relative;
width: 1036px;
.box1-left-arrow { .box1-left-arrow {
position: absolute; position: absolute;
z-index: 9999; z-index: 9999;
left: -20px; left: -24px;
top: 135px; top: 135px;
width: 24px !important; width: 24px !important;
height: 48px; height: 48px;
...@@ -1989,7 +1851,7 @@ const handleMediaClick = item => { ...@@ -1989,7 +1851,7 @@ const handleMediaClick = item => {
.box1-right-arrow { .box1-right-arrow {
position: absolute; position: absolute;
z-index: 9999; z-index: 9999;
right: -20px; right: 0px;
top: 135px; top: 135px;
width: 24px; width: 24px;
height: 48px; height: 48px;
...@@ -2024,6 +1886,7 @@ const handleMediaClick = item => { ...@@ -2024,6 +1886,7 @@ const handleMediaClick = item => {
box-sizing: border-box; box-sizing: border-box;
background: linear-gradient(to right, rgba(206, 79, 81, 0), rgba(206, 79, 81, 0.3)); background: linear-gradient(to right, rgba(206, 79, 81, 0), rgba(206, 79, 81, 0.3));
cursor: pointer; cursor: pointer;
&-des { &-des {
display: flex; display: flex;
gap: 5px; gap: 5px;
...@@ -2053,6 +1916,10 @@ const handleMediaClick = item => { ...@@ -2053,6 +1916,10 @@ const handleMediaClick = item => {
color: $base-color; color: $base-color;
margin-top: 10px; margin-top: 10px;
margin-bottom: 15px; margin-bottom: 15px;
max-width: 80%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
&-content { &-content {
...@@ -2079,7 +1946,7 @@ const handleMediaClick = item => { ...@@ -2079,7 +1946,7 @@ const handleMediaClick = item => {
height: 172px; height: 172px;
padding-top: 16px; padding-top: 16px;
box-sizing: border-box; box-sizing: border-box;
padding-right: 24px;
&-title { &-title {
font-size: 16px; font-size: 16px;
font-weight: 700; font-weight: 700;
...@@ -2095,18 +1962,21 @@ const handleMediaClick = item => { ...@@ -2095,18 +1962,21 @@ const handleMediaClick = item => {
padding-left: 10px; padding-left: 10px;
height: 156px; height: 156px;
overflow: auto; overflow: auto;
&-item { &-item {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
gap: 10px; gap: 10px;
cursor: pointer; cursor: pointer;
&-id { &-id {
font-family: "Source Han Sans CN"; font-family: "Source Han Sans CN";
font-size: 16px; font-size: 16px;
font-weight: 700; font-weight: 700;
color: rgb(95, 101, 108); color: rgb(95, 101, 108);
} }
&-txt { &-txt {
font-size: 16px; font-size: 16px;
font-weight: 400; font-weight: 400;
...@@ -2115,6 +1985,7 @@ const handleMediaClick = item => { ...@@ -2115,6 +1985,7 @@ const handleMediaClick = item => {
} }
} }
} }
&-content { &-content {
display: flex; display: flex;
gap: 15px; gap: 15px;
...@@ -2347,6 +2218,7 @@ const handleMediaClick = item => { ...@@ -2347,6 +2218,7 @@ const handleMediaClick = item => {
} }
.box3-content { .box3-content {
// flex: 1; // flex: 1;
.el-progress--line { .el-progress--line {
width: 82px; width: 82px;
......
...@@ -60,9 +60,10 @@ ...@@ -60,9 +60,10 @@
<div> <div>
<div class="box1-top"> <div class="box1-top">
<div class="box1-top-title"> <div class="box1-top-title">
{{ item.postDate }}——{{ <!-- {{ item.postDate }}——{{
item.sanTypeId == allSanTypeIds[0] ? "OFAC" : "DoD" item.sanTypeId == allSanTypeIds[0] ? "OFAC" : "DoD"
}}《实体清单增列与修订条目》 }}《实体清单增列与修订条目》 -->
{{ item.postDate }}——{{ item.name }}
</div> </div>
<div class="box1-top-content"> <div class="box1-top-content">
<div class="box1-top-content-item"> <div class="box1-top-content-item">
...@@ -1073,7 +1074,7 @@ const handleToEntityList = item => { ...@@ -1073,7 +1074,7 @@ const handleToEntityList = item => {
console.log("这是什么数据1 =>", item); console.log("这是什么数据1 =>", item);
let id = item?.id; let id = item?.id;
let sanTypeId = item?.sanTypeId || 1; let sanTypeId = item?.sanTypeId || 1;
let date = entitiesDataInfoList.value[currentCarouselIndex.value].postDate let date = entitiesDataInfoList.value[currentCarouselIndex.value].postDate;
if (!id) { if (!id) {
const currentItem = entitiesDataInfoList.value[currentCarouselIndex.value]; const currentItem = entitiesDataInfoList.value[currentCarouselIndex.value];
id = currentItem?.id; id = currentItem?.id;
...@@ -1826,14 +1827,20 @@ const handleToDataLibrary = item => { ...@@ -1826,14 +1827,20 @@ const handleToDataLibrary = item => {
onMounted(async () => { onMounted(async () => {
console.log("finance 页面 mounted"); console.log("finance 页面 mounted");
try { try {
// 获取趋势图数据
fetchTrendData();
fetchRiskSignals("0109");
// 获取社交媒体信息
fetchSocialMediaInfo();
// 获取新闻资讯
fetchNewsInfo();
const [dataCount, entitiesDataInfo, industryCountByYear, cclList] = await Promise.all([ const [dataCount, entitiesDataInfo, industryCountByYear, cclList] = await Promise.all([
getDataCount(), getDataCount(),
getLatestEntityListInfo(), getLatestEntityListInfo(),
getReleaseCount(2) getReleaseCount(2)
// getReleaseCount(3) // getReleaseCount(3)
]); ]);
// 交换第二个和第三个元素
// [dataCount[1], dataCount[2]] = [dataCount[2], dataCount[1]];
console.log("dataCount", dataCount); console.log("dataCount", dataCount);
infoList.value = dataCount.slice(0, 2).map((item, idx) => { infoList.value = dataCount.slice(0, 2).map((item, idx) => {
return { return {
...@@ -1874,6 +1881,11 @@ onMounted(async () => { ...@@ -1874,6 +1881,11 @@ onMounted(async () => {
tags: item.domain tags: item.domain
}; };
}); });
await fetchSanctionProcess(sanctionPage.value, 10);
// 获取雷达图数据
await fetchRadarData(domainChecked.value);
// 获取出口管制制裁措施
await fetchSanctionList();
entityListReleaseFreqChart.interpret({ entityListReleaseFreqChart.interpret({
type: "柱状图", type: "柱状图",
name: "美国商务部发布实体清单的频次", name: "美国商务部发布实体清单的频次",
...@@ -1892,21 +1904,6 @@ onMounted(async () => { ...@@ -1892,21 +1904,6 @@ onMounted(async () => {
name: "美国商务部发布商业管制清单的频次", name: "美国商务部发布商业管制清单的频次",
data: commerceControlListReleaseFreq.value data: commerceControlListReleaseFreq.value
}); });
// 获取趋势图数据
fetchTrendData();
fetchRiskSignals("0109");
// 获取社交媒体信息
fetchSocialMediaInfo();
// 获取新闻资讯
fetchNewsInfo();
// fetchEntitiesList(currentPage.value, pageSize.value);
await fetchSanctionProcess(sanctionPage.value, 10);
// 获取雷达图数据
await fetchRadarData(domainChecked.value);
// 获取出口管制制裁措施
await fetchSanctionList();
} catch (err) { } catch (err) {
console.log("此处报错?"); console.log("此处报错?");
console.log(err); console.log(err);
...@@ -1957,11 +1954,12 @@ const handleMediaClick = item => { ...@@ -1957,11 +1954,12 @@ const handleMediaClick = item => {
flex-direction: column; flex-direction: column;
gap: 20px; gap: 20px;
position: relative; position: relative;
width: 1036px;
.box1-left-arrow { .box1-left-arrow {
position: absolute; position: absolute;
z-index: 9999; z-index: 9999;
left: -20px; left: -24px;
top: 135px; top: 135px;
width: 24px !important; width: 24px !important;
height: 48px; height: 48px;
...@@ -1985,7 +1983,7 @@ const handleMediaClick = item => { ...@@ -1985,7 +1983,7 @@ const handleMediaClick = item => {
.box1-right-arrow { .box1-right-arrow {
position: absolute; position: absolute;
z-index: 9999; z-index: 9999;
right: -20px; right: 0px;
top: 135px; top: 135px;
width: 24px; width: 24px;
height: 48px; height: 48px;
...@@ -2050,6 +2048,10 @@ const handleMediaClick = item => { ...@@ -2050,6 +2048,10 @@ const handleMediaClick = item => {
color: $base-color; color: $base-color;
margin-top: 10px; margin-top: 10px;
margin-bottom: 15px; margin-bottom: 15px;
max-width: 80%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
&-content { &-content {
......
import { ref } from "vue";
export const useChartInterpretation = () => {
const loading = ref(false);
const interpretation = ref("");
const error = ref(null);
const interpret = async text => {
loading.value = true;
error.value = null;
interpretation.value = "";
try {
const response = await fetch("/aiAnalysis/chart_interpretation", {
method: "POST",
headers: {
"X-API-Key": "aircasKEY19491001",
"Content-Type": "application/json"
},
body: JSON.stringify({ text })
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (line.startsWith("data: ")) {
const content = line.substring(6);
const textMatch = content.match(/"解读":\s*"([^"]*)"/);
if (textMatch && textMatch[1]) {
interpretation.value = textMatch[1];
}
}
}
}
} catch (err) {
error.value = err.message || "AI 解读失败";
console.error("AI Chart Interpretation Error:", err);
} finally {
loading.value = false;
}
};
return {
loading,
interpretation,
error,
interpret
};
};
// src/views/finance/utils/common.js
import { ref } from "vue"; import { ref } from "vue";
// 【新增】引入获取全局 Signal 的方法
import { getAbortSignal } from "@/api/finance/service.js";
export const useChartInterpretation = () => { export const useChartInterpretation = () => {
const loading = ref(false); const loading = ref(false);
...@@ -10,14 +14,20 @@ export const useChartInterpretation = () => { ...@@ -10,14 +14,20 @@ export const useChartInterpretation = () => {
error.value = null; error.value = null;
interpretation.value = ""; interpretation.value = "";
// 【新增】在请求发起前获取当前的 Signal
// 注意:必须在每次调用 interpret 时重新获取,以确保拿到最新的 controller 的 signal
const signal = getAbortSignal();
try { try {
// 【修改】在 fetch 中传入 signal
const response = await fetch("/aiAnalysis/chart_interpretation", { const response = await fetch("/aiAnalysis/chart_interpretation", {
method: "POST", method: "POST",
headers: { headers: {
"X-API-Key": "aircasKEY19491001", "X-API-Key": "aircasKEY19491001",
"Content-Type": "application/json" "Content-Type": "application/json"
}, },
body: JSON.stringify({ text }) body: JSON.stringify({ text }),
signal: signal // 【关键】绑定取消信号
}); });
if (!response.ok) { if (!response.ok) {
...@@ -29,6 +39,7 @@ export const useChartInterpretation = () => { ...@@ -29,6 +39,7 @@ export const useChartInterpretation = () => {
let buffer = ""; let buffer = "";
while (true) { while (true) {
// reader.read() 会在 signal abort 时抛出 AbortError
const { done, value } = await reader.read(); const { done, value } = await reader.read();
if (done) break; if (done) break;
...@@ -39,18 +50,43 @@ export const useChartInterpretation = () => { ...@@ -39,18 +50,43 @@ export const useChartInterpretation = () => {
for (const line of lines) { for (const line of lines) {
if (line.startsWith("data: ")) { if (line.startsWith("data: ")) {
const content = line.substring(6); const content = line.substring(6);
const textMatch = content.match(/"解读":\s*"([^"]*)"/); // 尝试解析 JSON
if (textMatch && textMatch[1]) { try {
interpretation.value = textMatch[1]; const jsonMatch = content.match(/\{.*\}/);
} if (jsonMatch) {
const parsed = JSON.parse(jsonMatch[0]);
if (parsed["解读"]) {
interpretation.value = parsed["解读"];
}
} else {
// 兼容旧的正则匹配
const textMatch = content.match(/"解读":\s*"([^"]*)"/);
if (textMatch && textMatch[1]) {
interpretation.value = textMatch[1];
}
}
} catch (e) {
// 忽略解析错误
}
} }
} }
} }
} catch (err) { } catch (err) {
// 【关键】判断是否是因路由切换导致的取消
if (err.name === 'AbortError') {
console.log('AI 解读请求已取消');
loading.value = false; // 关闭 loading
return; // 直接返回,不设置 error,不弹窗
}
error.value = err.message || "AI 解读失败"; error.value = err.message || "AI 解读失败";
console.error("AI Chart Interpretation Error:", err); console.error("AI Chart Interpretation Error:", err);
} finally { } finally {
loading.value = false; // 只有在非 AbortError 的情况下,才由 finally 统一关闭 loading
// 如果上面 catch 中已经 return 了,这里不会执行
if (err?.name !== 'AbortError') {
loading.value = false;
}
} }
}; };
...@@ -60,4 +96,4 @@ export const useChartInterpretation = () => { ...@@ -60,4 +96,4 @@ export const useChartInterpretation = () => {
error, error,
interpret interpret
}; };
}; };
\ No newline at end of file
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论