提交 002c3693 authored 作者: 张烨's avatar 张烨

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

流水线 #374 已通过 于阶段
in 1 分 39 秒
......@@ -199,7 +199,6 @@
"resolved": "https://registry.npmmirror.com/@antv/g6/-/g6-4.8.25.tgz",
"integrity": "sha512-8mdTnN9QMVNQZtlXmftL8fvRsa4L+GajK58Zp51wyrGLFyjeop8R0QSkCALW45DWP2TaQeZAPtjhQUU/wf5hIg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@antv/g6-pc": "0.8.25"
}
......@@ -2172,7 +2171,6 @@
"resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz",
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/lodash": "*"
}
......@@ -2289,7 +2287,6 @@
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.21.tgz",
"integrity": "sha512-SXlyk6I5eUGBd2v8Ie7tF6ADHE9kCR6mBEuPyH1nUZ0h6Xx6nZI29i12sJKQmzbDyr2tUHMhhTt51Z6blbkTTQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/parser": "^7.28.3",
"@vue/compiler-core": "3.5.21",
......@@ -2896,7 +2893,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
......@@ -3255,7 +3251,6 @@
"resolved": "https://registry.npmmirror.com/cytoscape/-/cytoscape-3.33.1.tgz",
"integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10"
}
......@@ -3653,7 +3648,6 @@
"resolved": "https://registry.npmmirror.com/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
"peer": true,
"engines": {
"node": ">=12"
}
......@@ -3742,7 +3736,6 @@
"version": "0.8.5",
"resolved": "https://registry.npmmirror.com/dagre/-/dagre-0.8.5.tgz",
"integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==",
"peer": true,
"dependencies": {
"graphlib": "^2.1.8",
"lodash": "^4.17.15"
......@@ -3950,7 +3943,6 @@
"resolved": "https://registry.npmmirror.com/echarts/-/echarts-5.6.0.tgz",
"integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"tslib": "2.3.0",
"zrender": "5.6.1"
......@@ -5342,15 +5334,13 @@
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/lodash-es": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
"integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/lodash-unified": {
"version": "1.0.3",
......@@ -5398,7 +5388,6 @@
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz",
"integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==",
"license": "MIT",
"peer": true,
"dependencies": {
"argparse": "^2.0.1",
"entities": "^4.4.0",
......@@ -6478,7 +6467,6 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
......@@ -6575,7 +6563,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
......@@ -6977,7 +6964,6 @@
"integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
......@@ -8148,7 +8134,6 @@
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
......@@ -8208,7 +8193,6 @@
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.21.tgz",
"integrity": "sha512-xxf9rum9KtOdwdRkiApWL+9hZEMWE90FHh8yS1+KJAiWYh+iGWV1FquPjoO9VUHQ+VIhsCXNNyZ5Sf4++RVZBA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.21",
"@vue/compiler-sfc": "3.5.21",
......
......@@ -259,26 +259,7 @@ export async function getHistoryBillListWithStage(personId, params = {}) {
}
}
// /**
// * 获取潜在提案举措分析
// * GET /api/personHomepage/historyBill/clause/{personId}
// * @param {string} personId - 人物ID
// * @param {Object} params - 查询参数
// */
// export async function getPotentialClauseAnalysis(personId, params = {}) {
// const queryString = Object.entries(params)
// .filter(([, value]) => value !== undefined && value !== null && value !== '')
// .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
// .join('&')
// const url = queryString
// ? `/api/personHomepage/historyBill/clause/${personId}?${queryString}`
// : `/api/personHomepage/historyBill/clause/${personId}`
// return request(url, {
// method: 'GET',
// })
// }
/**
* 获取排序选项
......@@ -331,31 +312,32 @@ export function getProgressPrediction(billId) {
})
}
/**
* 获取相似法案列表
* @param {Object} params - 查询参数
* @param {string} params.billIds - 当前法案的ID
* @param {string[]} params.domains - 领域名称列表
* @param {string} params.patternType - 政府结构类型,如 "统一政府"/"分裂政府"/"微弱多数"
* @param {string} params.proposalType - 提案类型,如 "两党共同提案"/"单政党提案(共和党提案)"/"单政党提案(民主党提案)"
* @param {string} params.domains - 领域名称,多个用逗号拼接
* @param {string} params.patternType - 政府结构类型
* @param {string} params.proposalType - 提案类型
* @param {string} params.timeFrame - 时间范围(单个字符串)
* @param {number} params.maxNum - 返回最大数量
* @returns {Promise<Object>} 相似法案列表
*/
export function getSimiBills(params = {}) {
// domains 如果是数组则用逗号拼接
const domains = Array.isArray(params.domains)
? params.domains.join(',')
: params.domains
return request('/api/BillProgressPrediction/simiBills', {
method: 'GET',
params: {
billIds: params.billIds,
domains: domains,
domains: params.domains,
patternType: params.patternType,
proposalType: params.proposalType
proposalType: params.proposalType,
timeFrame: params.timeFrame,
maxNum: params.maxNum || 50
}
})
}
/**
* 格式化日期 YYYY-MM-DD -> YYYY年M月D日
* @param {string} dateStr - 日期字符串
......
......@@ -201,3 +201,16 @@ export async function getSubjectList(params) {
params
})
}
/**
* 获取调研项目列表
* @param {string} personId
* @param {Object} params
*/
export function getInvestigationProject(personId, params) {
return request({
method: 'GET',
url: `/api/personHomepage/investigationProject/${personId}`,
params,
})
}
\ No newline at end of file
......@@ -393,13 +393,13 @@ export function getDomainDistribution(sanctionDate = "2025-11-11") {
* startTime: string
* }[]>}
*/
export function getEntitiesList(sanTypeIds = ["1"], pageNum = 1, pageSize = 10, sanctionDate = "", isCn = false) {
export function getEntitiesList(sanTypeId="1", pageNum = 1, pageSize = 10, sanctionDate = "", isCn = false) {
return request200(
request({
method: "POST",
url: "/api/sanctionList/pageQuery",
data: {
sanTypeIds,
sanTypeId,
pageNum,
pageSize,
sanctionDate,
......
import { createRouter, createWebHistory } from "vue-router";
import { getIsLoggedIn } from "@/utils/auth";
const Home = () => import('@/views/home/index.vue')
const DataLibrary = () => import('@/views/dataLibrary/index.vue')
......@@ -21,6 +22,14 @@ const dataRoutes = Object.keys(datas).reduce((acc, path) => {
}, [])
const routes = [
{
path: "/login",
name: "Login",
component: () => import("@/views/login/index.vue"),
meta: {
title: "登录"
}
},
{
path: "/",
name: "Home",
......@@ -66,6 +75,19 @@ const router = createRouter({
// 路由守卫 - 设置页面标题
router.beforeEach((to, from, next) => {
// 登录态:同一次前端服务 BOOT_ID 内跨刷新/跨新标签有效;服务重启后自动失效
const isAuthed = getIsLoggedIn();
const isLoginRoute = to.name === "Login" || /^\/login\/?$/.test(String(to.path || ""));
if (!isLoginRoute && !isAuthed) {
next({
path: "/login",
query: { redirect: to.fullPath }
});
return;
}
if (to.meta.title) {
if (to.meta.dynamicTitle) {
console.log('to', to);
......
......@@ -4,7 +4,7 @@ const ZMOverview = () => import('@/views/ZMOverView/index.vue')
const ZMOverviewRoutes = [
{
path: "/",
redirect: "/ZMOverView"
redirect: "/login"
},
{
path: "/ZMOverView",
......
......@@ -123,18 +123,8 @@ const billRoutes = [
component: BillInfluenceScientificResearch,
// meta: { title: "对华科研影响" }
},
// {
// path: "ProgressForecast",
// name: "BillProgressForecast",
// component: BillProgressForecast,
// // meta: { title: "对华科研影响" }
// }
]
}, {
path: "ProgressForecast/:id",
name: "BillProgressForecast",
component: BillProgressForecast,
// meta: { title: "对华科研影响" }
},
{
......@@ -155,6 +145,14 @@ const billRoutes = [
}
]
},
{
path: "ProgressForecast/:id",
name: "BillProgressForecast",
component: BillProgressForecast,
meta: {
title: "法案预测"
}
},
]
export default billRoutes
\ No newline at end of file
......@@ -16,7 +16,7 @@ const financeRoutes = [
{
path: "/finance/sdnlistoverview",
name: "sdnlistOverview",
component: () => import("@/views/finance/v2.0EntityList/index.vue"),
component: () => import("@/views/finance/entityList/index.vue"),
meta: {
title: "SDN制裁清单概览"
}
......@@ -24,8 +24,8 @@ const financeRoutes = [
// V2.0单条制裁详情
{
path: "/finance/singleSanction",
name: "singleSanction",
component: () => import("@/views/finance/v2.0SingleSanction/index.vue"),
name: "sdnSingleSanction",
component: () => import("@/views/finance/singleSanction/index.vue"),
meta: {
title: "单条制裁详情",
dynamicTitle: true
......@@ -34,8 +34,8 @@ const financeRoutes = [
// V2.0单条制裁详情-实体清单原文
{
path: "/exportControl/origin",
name: "entityListOrigin",
component: () => import("@/views/exportControl/v2.0SingleSanction/originPage/index.vue")
name: "financeEntityListOrigin",
component: () => import("@/views/finance/singleSanction/originPage/index.vue")
// meta: {
// title: "实体清单原文"
// }
......@@ -44,7 +44,7 @@ const financeRoutes = [
{
path: "/finance/cmccontrolList",
name: "cmccontrolList",
component: () => import("@/views/finance/v2.0CommercialControlList/index.vue"),
component: () => import("@/views/finance/commercialControlList/index.vue"),
meta: {
title: "涉军企业清单概览"
}
......
// 规则限制
const RuleRestriction = () => import('@/views/ruleRestriction/index.vue')
const RuleRestrictionDetail = () => import('@/views/ruleRestriction/detail/index.vue')
const RuleRestrictionsAlliance = () => import('@/views/ruleRestriction/alliance/index.vue')
const ruleRestrictionsRoutes = [
// 规则限制
......@@ -22,6 +23,14 @@ const ruleRestrictionsRoutes = [
title: "规则限制详情",
dynamicTitle: true
}
}, {
path: "/ruleRestrictions/alliance",
name: "RuleRestrictionsAlliance",
component: RuleRestrictionsAlliance,
meta: {
title: "规则限制联盟详情",
dynamicTitle: true
}
},
]
......
const STORAGE_LOGIN_FLAG = "isLoggedIn";
const STORAGE_LOGIN_BOOT_ID = "loginBootId";
const getBootId = () => {
// 由 vite.config.js 的 define 注入:重启 dev server 会变化
try {
// eslint-disable-next-line no-undef
return String(__APP_BOOT_ID__ || "");
} catch (_) {
return "";
}
};
export const getIsLoggedIn = () => {
try {
const flag = window.localStorage.getItem(STORAGE_LOGIN_FLAG) === "1";
const savedBootId = window.localStorage.getItem(STORAGE_LOGIN_BOOT_ID) || "";
return flag && savedBootId && savedBootId === getBootId();
} catch (_) {
return false;
}
};
export const setIsLoggedIn = (val) => {
try {
if (val) {
window.localStorage.setItem(STORAGE_LOGIN_FLAG, "1");
window.localStorage.setItem(STORAGE_LOGIN_BOOT_ID, getBootId());
} else {
window.localStorage.removeItem(STORAGE_LOGIN_FLAG);
window.localStorage.removeItem(STORAGE_LOGIN_BOOT_ID);
}
} catch (_) {
// ignore
}
};
......@@ -125,7 +125,7 @@
<DivideHeader id="position2" class="divide2" :titleText="'资讯要闻'"></DivideHeader>
<div class="center-center">
<NewsList :newsList="newsList" img="newsImage" title="newsTitle" from="from" content="newsContent"
@item-click="handleClickNewsDetail" />
@item-click="handleClickNewsDetail" @more-click="handleToMoreNews" />
<MessageBubble :messageList="messageList" imageUrl="personImage" @more-click="handleToSocialDetail"
@person-click="handleClickToCharacter" name="personName" content="remarks" source="orgName" />
</div>
......@@ -297,22 +297,26 @@ import { ElMessage } from "element-plus";
import { Calendar } from "@element-plus/icons-vue";
import { useGotoNewsDetail } from "@/router/modules/news";
// 跳转人物主页
const handleClickToCharacter = async (id, name) => {
// 跳转人物主页(MessageBubble 的 person-click 传入整条列表项,需取 personId)
const handleClickToCharacter = async item => {
if (!item?.personId) {
ElMessage.warning("缺少人员信息,无法跳转");
return;
}
const personTypeList = JSON.parse(window.sessionStorage.getItem("personTypeList"));
let type = 0;
let personTypeName = "";
const params = {
personId: id
personId: item.personId
};
try {
const res = await getPersonSummaryInfo(params);
console.log("人物全局信息2", res);
if (res.code === 200 && res.data) {
const arr = personTypeList.filter(item => {
return item.typeId === res.data.personType;
const arr = personTypeList.filter(pt => {
return pt.typeId === res.data.personType;
});
console.log("arr", arr);
......@@ -331,12 +335,12 @@ const handleClickToCharacter = async (id, name) => {
ElMessage.warning("找不到当前人员的类型值!");
return;
}
window.sessionStorage.setItem("curTabName", name);
window.sessionStorage.setItem("curTabName", item.personName);
const route = router.resolve({
path: "/characterPage",
query: {
type: type, // type=1为科技企业领袖,2为国会议员,3为智库研究人员
personId: id
personId: item.personId
}
});
window.open(route.href, "_blank");
......@@ -458,6 +462,12 @@ const handleToMoreRiskSignal = () => {
window.open(route.href, "_blank");
// router.push("/viewRiskSignal")
};
// 查看更多新闻资讯(新闻主页)
const handleToMoreNews = () => {
const route = router.resolve("/newsBrief");
window.open(route.href, "_blank");
};
// 风险信号
const warningList = ref([]);
......
......@@ -36,7 +36,7 @@ export default {
</script>
<style scoped lang="scss">
@import '@/styles/bill-tokens.scss';
@use '@/styles/bill-tokens.scss' as *;
.bill-page-shell {
width: 100%;
......
......@@ -87,7 +87,7 @@ export default {
</script>
<style scoped lang="scss">
@import '@/styles/bill-tokens.scss';
@use '@/styles/bill-tokens.scss' as *;
.bill-panel {
width: 100%;
......
......@@ -93,7 +93,7 @@ export default {
</script>
<style scoped lang="scss">
@import '@/styles/bill-tokens.scss';
@use '@/styles/bill-tokens.scss' as *;
.bill-two-column {
width: 100%;
......
<template>
<div class="action-buttons">
<div class="action-left">
<button class="btn-secondary" @click="emit('reset')">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
......@@ -7,6 +8,10 @@
</svg>
<span>重置筛选条件</span>
</button>
<button v-if="showSetAsCurrent" class="btn-secondary" @click="emit('setAsCurrent')">
<span>设置为当前提案</span>
</button>
</div>
<div class="action-right">
<button
v-if="showPrevious"
......@@ -36,12 +41,14 @@ defineProps<{
previousText?: string
nextText?: string
nextDisabled?: boolean
showSetAsCurrent?: boolean
}>()
const emit = defineEmits<{
reset: []
previous: []
next: []
setAsCurrent: []
}>()
</script>
......@@ -53,6 +60,12 @@ const emit = defineEmits<{
padding-top: 24px;
}
.action-left {
display: flex;
align-items: center;
gap: 12px;
}
.action-right {
display: flex;
align-items: center;
......
......@@ -5,7 +5,7 @@
<img src="../assets/fitller.svg" />
<h2 class="section-title text-title-3-bold">核心相似度维度筛选</h2>
</div>
<button class="btn-outline" @click="setAsCurrent">设置为当前提案</button>
</div>
<div class="divider" />
<div class="fields-grid">
......@@ -62,6 +62,7 @@
<div class="field-content">
<el-select
v-model="localValues.oppositionProposer"
multiple
placeholder="请选择"
style="width: 420px"
@change="handleChange"
......@@ -109,21 +110,21 @@ const props = defineProps<{
defaultFilters?: Record<string, string[]>
}>()
// 本地 v-model 值
// 本地 v-model 值(proposalTime 为单选,用 string)
const localValues = ref({
policyArea: [] as string[],
governmentType: [] as string[],
oppositionProposer: '' as string,
oppositionProposer: [] as string[],
proposalTime: '' as string,
})
// 根据 proposalInfo 计算初始筛选值(即"设置为当前提案"的目标状态)
function buildInitialValues(info?: ProposalInfo | null): Record<string, string[] | string> {
if (!info) return { policyArea: [], governmentType: [], oppositionProposer: '', proposalTime: '' }
function buildInitialValues(info?: ProposalInfo | null) {
if (!info) return { policyArea: [], governmentType: [], oppositionProposer: [], proposalTime: '' }
return {
policyArea: info.defaultDomains?.length ? [...info.defaultDomains] : [...(info.areas || [])],
governmentType: info.patternType ? [info.patternType] : [],
oppositionProposer: '',
oppositionProposer: [],
proposalTime: '',
}
}
......@@ -142,7 +143,7 @@ function reset() {
localValues.value = {
policyArea: [],
governmentType: [],
oppositionProposer: '',
oppositionProposer: [],
proposalTime: '',
}
}
......@@ -152,14 +153,39 @@ function setAsCurrent() {
Object.assign(localValues.value, buildInitialValues(props.proposalInfo))
}
defineExpose({ reset, setAsCurrent, getValues: () => localValues.value })
defineExpose({
reset,
setAsCurrent,
getValues: () => ({
...localValues.value,
// proposalTime 单选,包装为数组保持传值一致
proposalTime: localValues.value.proposalTime ? [localValues.value.proposalTime] : []
})
})
// 固定的政策领域选项(15个)
const policyAreaOptions = [
{ value: '人工智能', label: '人工智能' },
{ value: '生物科技', label: '生物科技' },
{ value: '新一代通信网络', label: '新一代通信网络' },
{ value: '量子科技', label: '量子科技' },
{ value: '新能源', label: '新能源' },
{ value: '集成电路', label: '集成电路' },
{ value: '海洋', label: '海洋' },
{ value: '先进制造', label: '先进制造' },
{ value: '新材料', label: '新材料' },
{ value: '航空航天', label: '航空航天' },
{ value: '太空', label: '太空' },
{ value: '深海', label: '深海' },
{ value: '极地', label: '极地' },
{ value: '核', label: '核' },
{ value: '其他', label: '其他' }
]
// 固定的字段配置,options 由 proposalInfo 动态注入
// 固定的字段配置
const fields = computed(() => {
const billDomain = props.proposalInfo?.billDomain
const domainOptions = Array.isArray(billDomain) ? billDomain.map(d => ({ value: d, label: d })) : []
return {
policyArea: { options: domainOptions },
policyArea: { options: policyAreaOptions },
governmentType: {
options: [
{ value: '统一政府', label: '统一政府' },
......@@ -177,9 +203,9 @@ const fields = computed(() => {
},
proposalTime: {
options: [
{ value: '近三年', label: '近三年' },
{ value: '近五年', label: '近五年' },
{ value: '近十年', label: '近十年' },
{ value: '全部', label: '全部' }
{ value: '近十年', label: '近十年' }
]
}
}
......
......@@ -3,7 +3,7 @@
<div class="header-content">
<div class="header-left">
<div class="text-title-2-bold">科技法案立法进展预测</div>
<div class="text-tip-2 text-primary-65-clor">H.R.1-大而美法案(2025年5月20日)</div>
<div class="text-tip-2 text-primary-65-clor" v-if="props.proposalInfo">{{ props.proposalInfo.title+"("+ props.proposalInfo.date+")" }}</div>
</div>
<div class="header-actions">
<button class="action-btn">
......@@ -30,6 +30,11 @@
</template>
<script setup lang="ts">
import type { ProposalInfo } from '../api'
const props = defineProps<{
proposalInfo?: ProposalInfo | null
defaultFilters?: Record<string, string[]>
}>()
</script>
<style scoped>
......@@ -45,6 +50,7 @@
background: #ffffff;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
margin-bottom: 16px;
margin-left: 0px;
}
.header-content {
......
......@@ -13,7 +13,9 @@
/>
</div>
<ActionButtons
show-set-as-current
@reset="handleReset"
@set-as-current="handleSetAsCurrent"
@next="handleNext"
/>
</template>
......@@ -65,6 +67,11 @@ function handleReset() {
filterSectionRef.value?.reset()
}
// 设置为当前提案:恢复初始状态
function handleSetAsCurrent() {
filterSectionRef.value?.setAsCurrent()
}
// 下一步:获取已选的筛选条件并传给父组件
function handleNext() {
const selectedFilters = filterSectionRef.value?.getValues() || {}
......
......@@ -122,26 +122,27 @@ const selectedCount = computed(() => {
async function loadData() {
loading.value = true
try {
// 使用真实 API,传入 billIds、domains、patternType、proposalType
// 标准 API 参数格式
const params = {
billIds: filterParams?.value.billIds,
domains: filterParams?.value.domains || [],
domains: (filterParams?.value.domains || []).join(','),
patternType: filterParams?.value.patternType || '统一政府',
proposalType: filterParams?.value.proposalType || '两党共同提案'
proposalType: filterParams?.value.proposalType || '两党共同提案',
timeFrame: filterParams?.value.timeFrame?.[0] || '',
maxNum: 50
}
const response = await getSimiBills(params)
if (response && response.data) {
// 保存原始数据(新 API 返回结构中 simi_bills 是法案数组)
rawBillsData.value = response.data.simi_bills || []
// 保存原始数据
rawBillsData.value = response.data
const { stats: apiStats, bills: apiBills } = transformSimiBillsData(response)
stats.value = apiStats
bills.value = apiBills
} else {
stats.value = null
bills.value = []
rawBillsData.value = []
}
} catch (error) {
console.error('获取相似法案失败:', error)
......@@ -187,8 +188,9 @@ function handleBack() {
function handleStartAnalysis() {
// 获取用户勾选的法案ID列表
const selectedIds = bills.value.filter(b => b.selected).map(b => b.id)
// 从原始数据中筛选出用户勾选的法案
const selectedBills = rawBillsData.value.filter(b => selectedIds.includes(b.bill_id))
// 从原始数据中筛选出用户勾选的法案(确保 rawBillsData 是数组)
const rawData = Array.isArray(rawBillsData.value) ? rawBillsData.value : []
const selectedBills = rawData.filter(b => selectedIds.includes(b.bill_id))
emit('next', selectedBills)
}
</script>
......@@ -210,12 +212,16 @@ function handleStartAnalysis() {
.content-wrapper {
flex: 1;
overflow: auto;
overflow-y: auto;
display: flex;
flex-direction: column;
min-height: 0;
}
.stats-section {
display: flex;
flex-direction: column;
height: 100%;
}
.stats-header {
......@@ -333,6 +339,10 @@ function handleStartAnalysis() {
display: flex;
flex-direction: column;
gap: 16px;
flex: 1;
overflow-y: auto;
min-height: 0;
padding-bottom: 16px;
}
.action-footer {
......
......@@ -31,6 +31,8 @@
<div class="highlight-wave text-regular"></div>
<div class="highlight-content text-regular">
<p class="highlight-title text-regular">该法案的通过概率为 {{ predictionData?.overallProbability || '—' }}</p>
<!-- {{ predictionData.factorAnalysis }} -->
<p class="highlight-text text-regular" v-for="value in predictionData.factorAnalysis" :key="value">{{ '*'+value }}</p>
<p class="highlight-text text-regular">{{ predictionData?.highlightText }}</p>
</div>
</div>
......
......@@ -78,12 +78,19 @@
<div class="introduction-wrap-right-main">
<div class="right-main-box1">
<div class="name-box">
<div class="person-box">
<div class="person-item" :class="{ nameItemActive: box3BtnActive === item.name }"
@click="handleClcikBox3Btn(item.name, index)" v-for="(item, index) in personList" :key="index">
{{ item.name }}
</div>
</div>
<el-select
v-model="box3BtnActive"
class="person-select"
placeholder="请选择提出人"
@change="handleClcikBox3Btn"
>
<el-option
v-for="item in personList"
:key="item.id || item.name"
:label="item.name"
:value="item.name"
/>
</el-select>
</div>
<div class="info-box">
<div class="info-left">
......@@ -302,9 +309,10 @@ const personList = ref([]);
const curPerson = ref({});
const box3BtnActive = ref("");
const handleClcikBox3Btn = (name, index) => {
const handleClcikBox3Btn = name => {
box3BtnActive.value = name;
curPerson.value = personList.value[index];
const targetPerson = personList.value.find(item => item.name === name);
curPerson.value = targetPerson || {};
};
// 法案提出人
......@@ -800,59 +808,8 @@ onMounted(() => {
margin-left: 22px;
margin-top: 10px;
.person-box {
width: 500px;
overflow-x: hidden;
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
padding-bottom: 5px;
&::-webkit-scrollbar {
height: 4px;
}
&::-webkit-scrollbar-thumb {
border-radius: 4px;
background: #e1e1e1;
}
&::-webkit-scrollbar-track {
background: transparent;
}
.person-item {
min-height: 28px;
height: auto;
box-sizing: border-box;
border: 1px solid var(--btn-plain-border-color);
border-radius: 4px;
background: var(--btn-plain-bg-color);
color: var(--btn-plain-text-color);
font-family: Microsoft YaHei;
font-size: 14px;
font-weight: 400;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
margin-right: 8px;
padding: 1px 12px;
cursor: pointer;
white-space: normal;
word-break: break-all;
line-height: 18px;
flex-shrink: 1;
max-width: 170px;
text-align: center;
margin-bottom: 10px;
}
.nameItemActive {
border: 1px solid var(--btn-active-border-color);
background: var(--btn-active-bg-color);
color: var(--btn-active-text-color);
}
.person-select {
width: 100%;
}
}
......
......@@ -22,6 +22,12 @@
<span class="meta-value sponsor-name" :title="allSponsorNames">
{{ firstSponsorName }}
</span>
</div>
<div class="meta-row">
<span class="meta-label">委员会:</span>
<span class="meta-value sponsor-name" :title="allSponsorNames">
{{ firstSponsorName }}
</span>
</div>
<div class="meta-row">
<span class="meta-label">相关领域:</span>
......@@ -114,6 +120,7 @@ const currentStageIndex = computed(() => {
gap: 13px;
align-items: center;
height: 100%;
margin:15px 1px
}
.bill-card-preview {
......
......@@ -23,9 +23,10 @@
<template v-else>
<BillCard
v-for="bill in bills"
:key="bill.id"
:key="bill.billId"
:bill="bill"
:progress-stages="progressStages"
@click="handleBillMoreClick(bill.billId)"
/>
<div v-if="total > 0" class="bill-pagination">
......@@ -46,7 +47,10 @@
<script setup>
import BillCard from './BillCard.vue'
import { useRouter } from "vue-router";
const router = useRouter();
import { useRoute } from "vue-router";
const route = useRoute();
defineProps({
bills: { type: Array, required: true },
loading: { type: Boolean, default: false },
......@@ -56,27 +60,45 @@ defineProps({
progressStages: { type: Array, default: () => ['提出', '众议院通过', '参议院通过', '分歧协调', '提交总统', '法案通过'] },
})
defineEmits(['page-change'])
/** 政策建议关联法案:新标签页打开法案介绍页,billId 随接口 id 变化 */
const handleBillMoreClick = (bill) => {
const billId = bill;
// debugger
if (!billId) {
return;
}
const route = router.resolve({
path: "/billLayout/bill/introduction",
query: { billId: String(billId) }
});
window.open(route.href, "_blank");
};
</script>
<style scoped>
/* Bill List */
.bill-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.bill-list-loading {
display: flex;
flex-direction: column;
gap: 16px;
}
.bill-list-skeleton {
display: flex;
gap: 24px;
background: #fff;
border: 1px solid #ebedf0;
border-radius: 8px;
background: var(--bg-white-100);
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
padding: 24px;
}
.skeleton-left {
width: 120px;
height: 190px;
......@@ -84,30 +106,35 @@ defineEmits(['page-change'])
border-radius: 4px;
animation: pulse 1.5s ease-in-out infinite;
}
.skeleton-right {
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
}
.skeleton-line {
height: 16px;
background: #f0f0f0;
border-radius: 4px;
animation: pulse 1.5s ease-in-out infinite;
}
.skeleton-line.w30 { width: 30%; }
.skeleton-line.w40 { width: 40%; }
.skeleton-line.w50 { width: 50%; }
.skeleton-line.w60 { width: 60%; }
.skeleton-divider {
height: 1px;
background: #ebedf0;
background: #eaeced;
}
.bill-list-empty {
background: #fff;
border: 1px solid #ebedf0;
border-radius: 8px;
background: var(--bg-white-100);
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
padding: 48px;
text-align: center;
color: #bfbfbf;
......@@ -117,6 +144,7 @@ defineEmits(['page-change'])
align-items: center;
gap: 12px;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
......@@ -129,6 +157,7 @@ defineEmits(['page-change'])
justify-content: space-between;
padding: 16px 0 0;
}
.bill-pagination-total {
font-size: 14px;
font-family: 'Microsoft YaHei', sans-serif;
......@@ -139,10 +168,11 @@ defineEmits(['page-change'])
/* Override el-pagination to match design */
.bill-pagination :deep(.el-pagination) {
--el-pagination-bg-color: #fff;
--el-pagination-bg-color: var(--bg-white-100);
--el-pagination-hover-color: rgba(5, 95, 194, 1);
padding: 0;
}
.bill-pagination :deep(.el-pagination.is-background .btn-prev),
.bill-pagination :deep(.el-pagination.is-background .btn-next),
.bill-pagination :deep(.el-pagination.is-background .el-pager li) {
......@@ -151,20 +181,23 @@ defineEmits(['page-change'])
line-height: 32px;
border-radius: 4px;
border: 1px solid rgba(234, 236, 238, 1);
background: #fff;
background: var(--bg-white-100);
color: rgba(59, 65, 75, 1);
font-size: 14px;
font-family: 'Microsoft YaHei', sans-serif;
margin: 0 3px;
}
.bill-pagination :deep(.el-pagination.is-background .el-pager li:not(.is-disabled).is-active) {
background: #fff;
background: var(--bg-white-100);
color: rgba(5, 95, 194, 1);
border-color: rgba(5, 95, 194, 1);
}
.bill-pagination :deep(.el-pagination.is-background .el-pager li:not(.is-disabled):hover) {
color: rgba(5, 95, 194, 1);
}
.bill-pagination :deep(.el-pagination.is-background .btn-prev:hover),
.bill-pagination :deep(.el-pagination.is-background .btn-next:hover) {
color: rgba(5, 95, 194, 1);
......
......@@ -29,7 +29,7 @@
</div>
<div class="doc-info">
<div class="doc-number">{{ billNumber }}</div>
<div class="doc-serial">{{ billSerial }}</div>
<div class="doc-serial" :title="billSerial">{{ billSerial }}</div>
</div>
</div>
</template>
......@@ -147,5 +147,13 @@ defineProps({
color: rgba(95, 101, 108, 1);
line-height: 24px;
text-align: center;
width:15em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 6px 10px;
font-size: 14px;
cursor: pointer;
}
</style>
......@@ -46,7 +46,7 @@
</div>
</AnalysisBox>
<AnalysisBox title=" 金钱来源" width="1064px" height="512px" :show-all-btn="false" class="left-center">
<AnalysisBox title=" 资金来源" width="1064px" height="512px" :show-all-btn="false" class="left-center">
<template #header-btn>
<div class="input">
<el-select v-model="selectedOption" placeholder="请选择" class="select" :teleported="true">
......@@ -80,12 +80,12 @@
background @current-change="handleFundPageChange" />
</div>
</div>
<div class="bottom">
<!-- <div class="bottom">
<img src="./assets/ai.png" alt="" class="icon">
<div class="text1">
</div>
<img src="./assets/right.png" alt="" class="icon1">
</div>
</div> -->
</AnalysisBox>
<AnalysisBox title="最新动态" width="1064px" height="1330px" :show-all-btn="false" class="left-bottom">
<template #header-btn>
......@@ -375,6 +375,19 @@ import img5 from "./assets/img5.png";
import DefaultIcon1 from '@/assets/icons/default-icon1.png'
import DefaultIcon2 from '@/assets/icons/default-icon2.png'
/** 政策建议关联法案:新标签页打开法案介绍页,billId 随接口 id 变化 */
const handleBillMoreClick = (bill) => {
const billId = bill?.id;
if (!billId) {
return;
}
const route = router.resolve({
path: "/billLayout/bill/introduction",
query: { billId: String(billId) }
});
window.open(route.href, "_blank");
};
import { useRoute } from 'vue-router';
const route = useRoute();
const personId = ref(route.query.personId || "Y000064");
......@@ -1120,22 +1133,27 @@ const handleClickTag = async (tag) => {
.left-bottom {
width: 100%;
height: 1330px;
height: 100%;
background-color: rgba(255, 255, 255, 1);
border-radius: 10px;
box-shadow: 0px 0px 20px rgba(25, 69, 130, 0.1);
display: flex;
flex-direction: column;
overflow: hidden;
:deep(.wrapper-header) {
position: relative;
display: flex;
align-items: center;
height: 56px;
/* 根据设计图调整 */
padding: 0 24px;
flex-shrink: 0;
}
:deep(.header-btn){
:deep(.header-btn) {
top: 16px;
}
.title {
width: 100%;
height: 85px;
......@@ -1143,6 +1161,7 @@ const handleClickTag = async (tag) => {
align-items: center;
padding: 14px 12px 45px 0;
position: relative;
flex-shrink: 0;
.box {
width: 8px;
......@@ -1182,7 +1201,6 @@ const handleClickTag = async (tag) => {
top: 50%;
transform: translateY(-50%);
right: 120px;
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
......@@ -1196,7 +1214,6 @@ const handleClickTag = async (tag) => {
width: 16px;
height: 16px;
accent-color: #1890ff;
cursor: pointer;
margin: 0;
}
......@@ -1209,21 +1226,26 @@ const handleClickTag = async (tag) => {
.main {
width: 1064px;
height: 1133px;
box-sizing: border-box;
padding-right: 50px;
padding: 0 50px 0 0;
position: relative;
z-index: 110;
z-index: 1;
height: calc(100% - 5px - 76px);
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
flex: 0 0 auto;
.main-item {
width: 1014px;
margin-bottom: 40px;
display: flex;
flex-direction: row;
align-items: flex-start;
margin-bottom: 20px;
padding-bottom: 20px;
position: relative;
.time {
width: 77px;
box-sizing: border-box;
......@@ -1231,6 +1253,7 @@ const handleClickTag = async (tag) => {
display: flex;
flex-direction: column;
align-items: flex-end;
flex-shrink: 0;
.year {
font-size: 16px;
......@@ -1251,6 +1274,7 @@ const handleClickTag = async (tag) => {
.image {
margin-right: 20px;
flex-shrink: 0;
img {
width: 24px;
......@@ -1260,6 +1284,7 @@ const handleClickTag = async (tag) => {
.content {
width: 873px;
min-width: 0;
.content-type1 {
background-color: rgba(246, 250, 255, 1);
......@@ -1320,6 +1345,7 @@ const handleClickTag = async (tag) => {
width: 873px;
display: flex;
justify-content: space-between;
gap: 12px;
.tag {
font-size: 14px;
......@@ -1364,49 +1390,45 @@ const handleClickTag = async (tag) => {
line-height: 22px;
color: rgb(132, 136, 142);
cursor: pointer;
white-space: nowrap;
}
}
}
}
.main-item::after {
content: '';
.main-item::after {
content: "";
position: absolute;
left: 109px; /* 与圆点对齐 */
top: 24px; /* 从圆点下方开始 */
bottom: -20px; /* 延伸到下一个 item */
left: 109px;
top: 24px;
height: calc(100% - 24px);
width: 1px;
background-color: rgb(230, 231, 232);
z-index: -1;
}
.line-test {
position: absolute;
top: 10px;
left: 109px;
border: 1px solid rgb(230, 231, 232);
z-index: -1;
position: absolute;
bottom: 10px;
z-index: 0;
}
.main-item:last-child::after {
display: none;
}
.line {
width: 100%;
height: 1px;
background-color: rgb(234, 236, 238);
margin-top: 30px;
border: none;
.line-test {
display: none;
}
}
.pagination {
width: 100%;
height: 76px;
margin: 20px auto;
margin: 0;
display: flex;
padding: 0 31px 0 36px;
justify-content: space-between;
align-items: center;
border-top: 1px solid rgb(234, 236, 238);
position: relative;
z-index: 3;
background: #fff;
flex-shrink: 0;
.total {
font-size: 16px;
......@@ -1468,7 +1490,7 @@ const handleClickTag = async (tag) => {
background-color: #fff;
}
}
}
}
}
.right {
......@@ -1734,7 +1756,7 @@ const handleClickTag = async (tag) => {
.content-item {
width: 454px;
margin-bottom: 60px;
// margin-bottom: 60px;
margin-left: 26px;
position: relative;
......@@ -2080,4 +2102,32 @@ const handleClickTag = async (tag) => {
color: rgba(170, 173, 177, 1);
font-size: 14px;
}
.baseInfo-item {
display: flex;
align-items: flex-start;
}
.baseInfo-item-title {
flex: 0 0 88px; /* 固定左侧宽度 */
font-size: 16px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(59, 65, 75);
white-space: nowrap; /* 左侧不换行 */
}
.baseInfo-item-content {
flex: 1;
min-width: 0;
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(59, 65, 75);
display: flex;
flex-wrap: wrap; /* 右侧满了再换行 */
gap: 8px 12px; /* 行列间距可调 */
}
.education-item {
white-space: nowrap; /* 每个学校+专业作为一个整体不拆行 */
}
</style>
......@@ -109,10 +109,17 @@
<div class="baseInfo-item-content">{{ characterBasicInfo.country }}</div>
</div>
<div class="baseInfo-item">
<div class="baseInfo-item">
<div class="baseInfo-item-title">教育背景:</div>
<div class="baseInfo-item-content" v-for="value in characterBasicInfo.educationList"
:key="value.school">
<div class="baseInfo-item-content">
<span
class="education-item"
v-for="value in characterBasicInfo.educationList"
:key="value.school"
>
{{ value.school + "(" + value.major + ")" }}
</span>
</div>
</div>
</div>
<div class="baseInfo-item">
......@@ -832,33 +839,120 @@ onMounted(() => {
}
.left-bottom {
.check-input {
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 1);
border-radius: 10px;
box-shadow: 0px 0px 20px rgba(25, 69, 130, 0.1);
display: flex;
flex-direction: column;
overflow: hidden;
:deep(.wrapper-header) {
position: relative;
display: flex;
align-items: center;
height: 56px;
padding: 0 24px;
flex-shrink: 0;
}
:deep(.header-btn) {
top: 16px;
}
.title {
width: 100%;
height: 85px;
display: flex;
align-items: center;
padding: 14px 12px 45px 0;
position: relative;
flex-shrink: 0;
.box {
width: 8px;
height: 20px;
background-color: rgb(5, 95, 194);
border-bottom-right-radius: 4px;
border-top-right-radius: 4px;
margin-right: 14px;
}
.text {
font-size: 20px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 26px;
color: rgb(5, 95, 194);
}
.btn {
width: 60px;
height: 28px;
margin-left: auto;
img {
width: 28px;
height: 28px;
cursor: pointer;
}
img:first-child {
margin-right: 4px;
}
}
.input {
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 120px;
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(132, 136, 142);
display: flex;
align-items: center;
gap: 8px;
input {
margin-right: 8px;
input[type="checkbox"] {
width: 16px;
height: 16px;
accent-color: #1890ff;
cursor: pointer;
margin: 0;
}
span {
margin-left: 0 !important;
}
}
}
.main {
width: 1064px;
height: 1357px;
box-sizing: border-box;
padding-right: 50px;
padding: 0 50px 0 0;
position: relative;
z-index: 110;
margin-top: 10px;
z-index: 1;
height: calc(100% - 5px - 76px);
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
flex: 0 0 auto;
.main-item {
width: 1014px;
display: flex;
flex-direction: row;
align-items: flex-start;
margin-bottom: 20px;
padding-bottom: 20px;
position: relative;
.time {
width: 77px;
box-sizing: border-box;
......@@ -866,8 +960,16 @@ onMounted(() => {
display: flex;
flex-direction: column;
align-items: flex-end;
flex-shrink: 0;
.year {
font-size: 16px;
font-weight: 700;
font-family: "Microsoft YaHei";
color: rgb(5, 95, 194);
line-height: 24px;
}
.year,
.date {
font-size: 16px;
font-weight: 700;
......@@ -879,6 +981,7 @@ onMounted(() => {
.image {
margin-right: 20px;
flex-shrink: 0;
img {
width: 24px;
......@@ -888,6 +991,7 @@ onMounted(() => {
.content {
width: 873px;
min-width: 0;
.content-type1 {
background-color: rgba(246, 250, 255, 1);
......@@ -948,7 +1052,7 @@ onMounted(() => {
width: 873px;
display: flex;
justify-content: space-between;
margin-top: 10px;
gap: 12px;
.tag {
font-size: 14px;
......@@ -993,39 +1097,45 @@ onMounted(() => {
line-height: 22px;
color: rgb(132, 136, 142);
cursor: pointer;
white-space: nowrap;
}
}
}
}
.main-item::after {
content: '';
content: "";
position: absolute;
left: 109px;
top: 24px;
bottom: -20px;
height: calc(100% - 24px);
width: 1px;
background-color: rgb(230, 231, 232);
z-index: -1;
z-index: 0;
}
.main-item:last-child::after {
display: none;
}
.line-test {
position: absolute;
top: 10px;
left: 109px;
height: 1300px;
border: 1px solid rgb(230, 231, 232);
z-index: -1;
display: none;
}
}
.pagination {
width: 100%;
height: 76px;
margin: 20px auto;
margin: 0;
display: flex;
padding: 0 31px 0 36px;
justify-content: space-between;
align-items: center;
border-top: 1px solid rgb(234, 236, 238);
position: relative;
z-index: 3;
background: #fff;
flex-shrink: 0;
.total {
font-size: 16px;
......@@ -1087,7 +1197,7 @@ onMounted(() => {
background-color: #fff;
}
}
}
}
}
.right {
......@@ -1468,4 +1578,32 @@ onMounted(() => {
.tab-select {
width: 120px;
}
.baseInfo-item {
display: flex;
align-items: flex-start;
}
.baseInfo-item-title {
flex: 0 0 88px; /* 固定左侧宽度 */
font-size: 16px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(59, 65, 75);
white-space: nowrap; /* 左侧不换行 */
}
.baseInfo-item-content {
flex: 1;
min-width: 0;
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(59, 65, 75);
display: flex;
flex-wrap: wrap; /* 右侧满了再换行 */
gap: 8px 12px; /* 行列间距可调 */
}
.education-item {
white-space: nowrap; /* 每个学校+专业作为一个整体不拆行 */
}
</style>
<template>
<div class="news-pagination-bar">
<div class="news-pagination-bar" v-if="total>1">
<span class="news-pagination-total">{{ total }}篇新闻报告</span>
<div class="news-pagination">
<button class="pg-btn" :disabled="currentPage <= 1" @click="$emit('update:currentPage', currentPage - 1)">
......
......@@ -20,11 +20,11 @@
<button
:class="['news-tab', { active: activeTab === 'local' }]"
@click="$emit('update:activeTab', 'local')"
>智库报告</button>
> 报告</button>
<button
:class="['news-tab', { active: activeTab === 'capital' }]"
@click="$emit('update:activeTab', 'capital')"
>调查项目</button>
> 项目</button>
</div>
<div class="news-sort" ref="sortDropdownRef">
......@@ -172,9 +172,10 @@ onUnmounted(() => document.removeEventListener('click', handleClickOutside))
margin-left: auto;
position: relative;
flex-shrink: 0;
margin-right: 10px;
}
.news-sort-btn {
width: 120px;
width: 150px;
height: 32px;
display: flex;
align-items: center;
......@@ -185,6 +186,7 @@ onUnmounted(() => document.removeEventListener('click', handleClickOutside))
background: rgba(255, 255, 255, 1);
cursor: pointer;
box-sizing: border-box;
margin-top: 15px;
}
.news-sort-btn:hover {
border-color: rgba(5, 95, 194, 0.4);
......
......@@ -18,13 +18,13 @@
<div class="news-main">
<div class="news-grid">
<NewsCard
v-for="item in newsList"
v-for="item in (activeTab === 'local' ? newsList : projectList)"
:key="item.reportId"
:item="item"
/>
</div>
<NewsPagination
:total="totalNews"
:total="activeTab === 'local' ? totalNews : totalProjects"
v-model:current-page="currentPage"
:page-size="pageSize"
/>
......@@ -41,7 +41,7 @@ import NewsSidebar from './NewsSidebar.vue'
import NewsCard from './NewsCard.vue'
import NewsPagination from './NewsPagination.vue'
import { getIndustryKeyList } from '@/api/bill/billHome.js'
import { getFindingsReport } from '@/api/characterPage/characterPage.js'
import { getFindingsReport, getInvestigationProject } from '@/api/characterPage/characterPage.js'
const route = useRoute()
const personId = computed(() => route.params.personId || route.query.personId || '')
......@@ -51,7 +51,9 @@ const activeTab = ref('local')
const currentPage = ref(1)
const pageSize = 12
const totalNews = ref(0)
const totalProjects = ref(0)
const newsList = ref([])
const projectList = ref([])
const loading = ref(false)
const sortBy = ref('publishTimeDesc')
......@@ -79,17 +81,16 @@ async function loadFilterOptions() {
]
}
function buildParams() {
function buildParams({ includeSearchText = false } = {}) {
const params = {
currentPage: currentPage.value - 1,
pageSize,
sortFun: sortBy.value === 'publishTimeAsc',
industryIds: selectedDomains.value.includes('all') ? '' : JSON.stringify(selectedDomains.value),
years: selectedTimes.value.includes('all') ? '' : JSON.stringify(selectedTimes.value),
}
if (!selectedDomains.value.includes('all')) {
params.industryIds = selectedDomains.value
}
if (!selectedTimes.value.includes('all')) {
params.years = selectedTimes.value
if (includeSearchText) {
params.searchText = searchText.value
}
return params
}
......@@ -97,14 +98,18 @@ function buildParams() {
function updateSort(value) {
sortBy.value = value
currentPage.value = 1
if (activeTab.value === 'local') {
loadNews()
} else {
loadProjects()
}
}
async function loadNews() {
if (!personId.value) return
loading.value = true
try {
const params = buildParams()
const params = buildParams({ includeSearchText: true })
const res = await getFindingsReport(personId.value, params)
if (res.code === 200 && res.data) {
newsList.value = res.data.content || []
......@@ -118,6 +123,32 @@ async function loadNews() {
}
}
async function loadProjects() {
if (!personId.value) return
loading.value = true
try {
const params = buildParams()
const res = await getInvestigationProject(personId.value, params)
if (res.code === 200 && res.data) {
projectList.value = (res.data.content || []).map(item => ({
reportId: item.projectId,
title: item.name,
content: item.description,
imageUrl: item.imageUrl,
time: item.time,
sourceName: item.sourceName,
industryList: item.industryList,
}))
totalProjects.value = res.data.totalElements || 0
} else {
projectList.value = []
totalProjects.value = 0
}
} finally {
loading.value = false
}
}
function toggleDomain(id) {
if (id === 'all') {
selectedDomains.value = ['all']
......@@ -142,17 +173,43 @@ function toggleTime(id) {
}
}
watch(searchText, () => {
currentPage.value = 1
if (activeTab.value === 'local') {
loadNews()
} else {
loadProjects()
}
})
watch(activeTab, (val) => {
currentPage.value = 1
if (val === 'local') {
loadNews()
} else {
loadProjects()
}
})
watch(
() => [selectedDomains.value, selectedTimes.value],
() => {
currentPage.value = 1
if (activeTab.value === 'local') {
loadNews()
} else {
loadProjects()
}
},
{ deep: true }
)
watch(currentPage, () => {
if (activeTab.value === 'local') {
loadNews()
} else {
loadProjects()
}
})
onMounted(async () => {
......
......@@ -103,11 +103,18 @@
<div class="baseInfo-item-title">国籍:</div>
<div class="baseInfo-item-content">{{ characterBasicInfo.country }}</div>
</div>
<div class="baseInfo-item">
<div class="baseInfo-item">
<div class="baseInfo-item-title">教育背景:</div>
<div class="baseInfo-item-content long"
v-for="item in characterBasicInfo.educationList">
{{ item.school + '--' + item.major }}<br>
<div class="baseInfo-item-content">
<span
class="education-item"
v-for="value in characterBasicInfo.educationList"
:key="value.school"
>
{{ value.school + "(" + value.major + ")" }}
</span>
</div>
</div>
</div>
<div class="baseInfo-item">
......@@ -1220,4 +1227,32 @@ const companyList = ref([
color: rgba(170, 173, 177, 1);
font-size: 14px;
}
.baseInfo-item {
display: flex;
align-items: flex-start;
}
.baseInfo-item-title {
flex: 0 0 88px; /* 固定左侧宽度 */
font-size: 16px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(59, 65, 75);
white-space: nowrap; /* 左侧不换行 */
}
.baseInfo-item-content {
flex: 1;
min-width: 0;
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(59, 65, 75);
display: flex;
flex-wrap: wrap; /* 右侧满了再换行 */
gap: 8px 12px; /* 行列间距可调 */
}
.education-item {
white-space: nowrap; /* 每个学校+专业作为一个整体不拆行 */
}
</style>
\ No newline at end of file
......@@ -39,6 +39,7 @@ const getCharacterGlobalInfoFn = async () => {
const res = await getCharacterGlobalInfo(params);
if (res.code === 200) {
console.log("人物全局信息111", res);
document.title=res.data.name
if (res.data) {
characterInfo.value = res.data;
const personType = characterInfo.value.personType;
......
......@@ -15,8 +15,8 @@
<div class="main-content" ref="homeMainRef" :class="{ 'scroll-main': isShow }">
<div class="home-top-bg"></div>
<!-- 搜索栏部分 -->
<SearchContainer v-if="homeMainRef" :countInfo="cooperationCountInfo" placeholder="搜索合作限制"
:containerRef="homeMainRef" areaName="" />
<SearchContainer v-if="homeMainRef" placeholder="搜索合作限制" :containerRef="homeMainRef" areaName=""
style="height: fit-content" />
<!-- 最新动态 -->
<div class="newdata" id="position1">
......@@ -164,7 +164,7 @@ const handleToPosi = id => {
background-size: 100% 100%;
position: absolute;
width: 100%;
height: 100%;
z-index: -100;
top: -64px;
}
......@@ -177,7 +177,7 @@ const handleToPosi = id => {
.search {
width: 960px;
height: 168px;
margin: 0 auto 68px auto;
.search-main {
......@@ -368,12 +368,12 @@ const handleToPosi = id => {
.reslib {
width: 1600px;
height: 1633px;
margin: 0 auto 0px auto;
.reslib-main {
width: 1600px;
height: 1565px;
margin-top: 26px;
}
}
......
......@@ -105,13 +105,13 @@
></el-image>
<div v-else class="box1-bottom-content-item-imgUndefined">
{{
(ett.entityNameZh || ett.enName)?.match(
(ett.orgName || ett.orgNameZh)?.match(
/[\u4e00-\u9fa5a-zA-Z0-9]/
)?.[0]
}}
</div>
<div class="box1-bottom-content-item-txt">
{{ ett.name || ett.entityNameZh }}
{{ ett.orgName || ett.orgNameZh }}
</div>
</div>
</div>
......@@ -1494,7 +1494,7 @@ watch(
const fetchEntitiesList = async (page = 1, size = 10) => {
try {
console.log("activeResourceTabItem.value.id", activeResourceTabItem.value.id);
const res = await getEntitiesList(activeResourceTabItem.value.id.join(","), page, size);
const res = await getEntitiesList(activeResourceTabItem.value.id, page, size);
if (res) {
entitiesList.value = res.content.map(item => ({
...item,
......
......@@ -25,8 +25,12 @@
<div class="left-center">
<AnalysisBox title="出口管制分类编码(ECCN)" :showAllBtn="false">
<div class="button-list">
<div :class="['button', { click: item.isClick }]" @click="changeECCN(item)" v-for="(item, i) in ECCNList"
:key="i">
<div
:class="['button', { click: item.isClick }]"
@click="changeECCN(item)"
v-for="(item, i) in ECCNList"
:key="i"
>
<span>{{ item.ranking }}{{ item.name }}</span>
</div>
</div>
......@@ -44,9 +48,17 @@
<AnalysisBox title="商业管制清单更新历史" :showAllBtn="false">
<template #header-btn>
<div class="filters">
<el-select v-model="selectedDomain" placeholder="Select"
style="width: 150px; height: 32px; margin-right: 16px">
<el-option v-for="item in domainOptions" :key="item.value" :label="item.label" :value="item.value" />
<el-select
v-model="selectedDomain"
placeholder="Select"
style="width: 150px; height: 32px; margin-right: 16px"
>
<el-option
v-for="item in domainOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<!-- <el-checkbox v-model="onlyChina">只看涉华动态</el-checkbox> -->
</div>
......@@ -59,15 +71,21 @@
</div>
<div class="img-zone">
<img :src="item.icon || title" alt />
<div v-if="i < sanctionList.length - 1"
:class="['img-line', { 'img-line-last': i === sanctionList.length - 1 }]">
</div>
<div
v-if="i < sanctionList.length - 1"
:class="['img-line', { 'img-line-last': i === sanctionList.length - 1 }]"
></div>
</div>
<div class="main">
<div class="main-title">{{ item.name }}</div>
<!-- <div class="main-title" @click="handleClick(item)">{{ item.name }}</div> -->
<el-tooltip effect="dark" :content="item.summary" popper-class="common-prompt-popper" placement="top"
:show-after="500">
<el-tooltip
effect="dark"
:content="item.summary"
popper-class="common-prompt-popper"
placement="top"
:show-after="500"
>
<div class="main-desc">{{ item.summary }}</div>
</el-tooltip>
<div class="tag-box">
......@@ -82,8 +100,14 @@
</div>
<div class="left-footer">
<div class="total-count">共 {{ totalAll }} 项</div>
<el-pagination v-model:current-page="currentPageAll" :page-size="pageSizeAll" :total="totalAll"
layout="prev, pager, next" background @current-change="handlePageChangeAll" />
<el-pagination
v-model:current-page="currentPageAll"
:page-size="pageSizeAll"
:total="totalAll"
layout="prev, pager, next"
background
@current-change="handlePageChangeAll"
/>
</div>
</AnalysisBox>
</div>
......@@ -111,17 +135,31 @@
<span>关键人物</span>
</div>
<div class="key-person-list">
<div class="person-item" v-for="(item, index) in publishInfo.personList" :key="index"
@click="handlePerClick(item)">
<div
class="person-item"
v-for="(item, index) in publishInfo.personList"
:key="index"
@click="handlePerClick(item)"
>
<img :src="item.imageUrl" alt />
<div class="person-info">
<el-tooltip effect="dark" :content="item.name" popper-class="common-prompt-popper" placement="top"
:show-after="500">
<el-tooltip
effect="dark"
:content="item.name"
popper-class="common-prompt-popper"
placement="top"
:show-after="500"
>
<div class="name">{{ item.name }}</div>
</el-tooltip>
<el-tooltip effect="dark" :content="item.position" popper-class="common-prompt-popper" placement="top"
:show-after="500">
<el-tooltip
effect="dark"
:content="item.position"
popper-class="common-prompt-popper"
placement="top"
:show-after="500"
>
<div class="title1">{{ item.position }}</div>
</el-tooltip>
</div>
......@@ -189,7 +227,6 @@ const handleClickOrg = item => {
}
});
window.open(route.href, "_blank");
};
// 处理点击关键人物的方法
......@@ -209,13 +246,12 @@ const handlePerClick = item => {
const handleClick = item => {
console.log("点击了实体名称:", item);
const route = router.resolve({
path: "/exportControl/singleSanction",
path: "/finance/singleSanction",
query: {
id: item.id
}
});
window.open(route.href, "_blank");
};
const selectedDomain = ref(0);
......
......@@ -376,29 +376,156 @@ const getRegionCountData = async () => {
};
// 实体清单-数据统计- 制裁实体领域数量变化趋势
const domainNumChartOption = ref({});
const domainNumChartOption = ref({
color: [],
tooltip: {
trigger: "axis",
backgroundColor: "rgba(255, 255, 255, 0.9)",
textStyle: {
color: "#666"
},
extraCssText: "box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); border-radius: 4px;"
},
grid: {
top: "15%",
right: "2%",
bottom: "5%",
left: "2%",
containLabel: true
},
legend: {
type: "scroll",
show: true,
top: 0,
left: "center",
icon: "circle",
itemWidth: 12,
itemHeight: 12,
data: [],
textStyle: {
fontFamily: "Microsoft YaHei",
fontSize: 16,
fontWeight: 400,
lineHeight: 24,
color: "rgb(95, 101, 108)"
}
},
xAxis: [
{
type: "category",
boundaryGap: false,
data: [],
axisLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
color: "#999",
fontSize: 12,
margin: 15
}
}
],
yAxis: [
{
type: "value",
min: 0,
// max: 100,
// interval: 20,
axisLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
color: "#999",
fontSize: 12
},
splitLine: {
lineStyle: {
type: "dashed",
color: "#E0E6F1"
}
}
}
],
series: []
});
const activeDomainTab = ref("year");
const handleDomainTabChange = tab => {
activeDomainTab.value = tab;
getDomainNumData();
};
// 处理领域趋势数据的方法
const processDomainTrendData = rawData => {
// 提取所有的月份作为标题
const titles = rawData.map(item => item.year).reverse();
// 收集所有不重复的领域名称
const domainNamesSet = new Set();
rawData.forEach(item => {
item.domainCountInfo.forEach(domain => {
domainNamesSet.add(domain.name);
});
});
const domainNames = Array.from(domainNamesSet);
// 定义颜色映射
const colorMap = {
人工智能: "#E34D59",
新一代通信网络: "#FF9F1C",
: "#FFB3B3",
生物科技: "#00A79D",
量子科技: "#7B61FF",
先进制造: "#363B42",
新能源: "#2BA471",
太空: "#3762F0",
集成电路: "#0052D9",
新材料: "#FFD900",
航空航天: "#3762F0",
海洋: "#76D1FF",
深海: "#002060",
其他: "#A6A6A6"
};
// 生成数据系列
const dataSeries = domainNames.map(domainName => {
const values = rawData
.map(yearData => {
const domainItem = yearData.domainCountInfo.find(d => d.name === domainName);
return domainItem ? domainItem.count : 0;
})
.reverse(); // 数据值也需要跟随标题反转顺序
return {
name: domainName,
color: colorMap[domainName] || `#${Math.floor(Math.random() * 16777215).toString(16)}`, // 如果没有预定义颜色,则随机生成
value: values
};
});
return {
title: titles,
data: dataSeries
};
};
const getDomainNumData = async () => {
// 参数
const param = {
IDsanTypeId: activeDomainTab.value === "year" ? "year" : "record",
type: sanTypeId.value
countType: activeDomainTab.value === "year" ? "year" : "record",
sanTypeId: sanTypeId.value
};
try {
const res = await getDomainNum(param);
if (res && res.code === 200) {
domainNumChartOption.value = getMultiLineChart({
data: res.data || [],
xAxis: res.xAxis || [],
yAxis: res.yAxis || [],
title: "制裁实体领域数量变化趋势",
xAxisName: "时间",
yAxisName: "数量"
});
if (res && res.length > 0) {
const processedData = processDomainTrendData(res);
domainNumChartOption.value = getMultiLineChart(processedData);
console.log("获取实体清单-数据统计-processedData:", processedData);
console.log("获取实体清单-数据统计-domainNumChartOption:", domainNumChartOption.value);
}
} catch (error) {
console.error("获取实体清单-数据统计-制裁实体领域数量变化趋势失败:", error);
......
......@@ -13,6 +13,7 @@
</div>
</div>
<div class="main">
<div v-if="activeIndex == 0">
<div class="left">
<AnalysisBox title="选择制裁">
<div class="left-main">
......@@ -88,7 +89,10 @@
<div class="fishbone-wrapper">
<div class="fishbone-scroll-container" ref="scrollContainerRef">
<div class="fishbone" ref="fishboneRef" v-if="fishboneDataList.length > 0">
<div class="main-line" :style="{ width: fishboneDataList.length * 200 + 300 + 'px' }">
<div
class="main-line"
:style="{ width: fishboneDataList.length * 200 + 300 + 'px' }"
>
<!-- 主轴上的标签 -->
<div
class="main-line-text"
......@@ -117,7 +121,11 @@
v-for="(item, index) in getLeftItems(causeGroup.causes)"
:key="'left-' + index"
>
<img :src="defaultTitle || item.picture" alt="" class="company-icon" />
<img
:src="defaultTitle || item.picture"
alt=""
class="company-icon"
/>
<div class="text" :title="item.name">{{ item.name }}</div>
<div class="line"></div>
</div>
......@@ -129,7 +137,11 @@
:key="'right-' + index"
>
<div class="line"></div>
<img :src="defaultTitle || item.picture" alt="" class="company-icon" />
<img
:src="defaultTitle || item.picture"
alt=""
class="company-icon"
/>
<div class="text" :title="item.name">{{ item.name }}</div>
</div>
</div>
......@@ -148,7 +160,11 @@
v-for="(item, index) in getLeftItems(causeGroup.causes)"
:key="'left-bottom-' + index"
>
<img :src="defaultTitle || item.picture" alt="" class="company-icon" />
<img
:src="defaultTitle || item.picture"
alt=""
class="company-icon"
/>
<div class="text" :title="item.name">{{ item.name }}</div>
<div class="line"></div>
</div>
......@@ -160,7 +176,11 @@
:key="'right-bottom-' + index"
>
<div class="line"></div>
<img :src="defaultTitle || item.picture" alt="" class="company-icon" />
<img
:src="defaultTitle || item.picture"
alt=""
class="company-icon"
/>
<div class="text" :title="item.name">{{ item.name }}</div>
</div>
</div>
......@@ -239,6 +259,10 @@
</AnalysisBox>
</div>
</div>
<div v-if="activeIndex == 1">
<constrained-association />
</div>
</div>
</div>
</template>
......@@ -252,6 +276,7 @@ import {
getDeepMiningIndustryFishbone,
getDeepMiningIndustryEntity
} from "@/api/exportControlV2.0";
import constrainedAssociation from "./components/constrainedAssociation.vue";
import { useRoute } from "vue-router";
const route = useRoute();
......@@ -461,7 +486,7 @@ const handleSanctionSelect = id => {
startAutoPlay();
};
const activeTab = ref(["制裁时序分析"]);
const activeTab = ref(["制裁时序分析", "限制关联分析"]);
const activeIndex = ref(0);
const dateRange = ref(["2025-01-01", "2025-12-31"]);
......@@ -532,10 +557,7 @@ onUnmounted(() => {
</script>
<style scoped lang="scss">
* {
margin: 0;
padding: 0;
}
.deep-mining {
width: 1601px;
......
......@@ -239,7 +239,7 @@ const handleClick = item => {
// console.log("点击了实体名称:", item);
window.sessionStorage.setItem("curTabName", `${item.year}-${item.date}《${item.name}》`);
const route = router.resolve({
path: "/exportControl/singleSanction",
path: "/finance/singleSanction",
query: {
id: item.id,
sanTypeId: item.sanTypeId || 1
......
......@@ -105,13 +105,13 @@
></el-image>
<div v-else class="box1-bottom-content-item-imgUndefined">
{{
(ett.entityNameZh || ett.enName)?.match(
(ett.orgName || ett.orgNameZh)?.match(
/[\u4e00-\u9fa5a-zA-Z0-9]/
)?.[0]
}}
</div>
<div class="box1-bottom-content-item-txt">
{{ ett.name || ett.entityNameZh }}
{{ ett.orgName || ett.orgNameZh }}
</div>
</div>
</div>
......@@ -779,7 +779,7 @@ const handleToPosi = id => {
const handleToRiskSignalDetail = item => {
window.sessionStorage.setItem("curTabName", item.title);
const routeData = router.resolve({
path: "/exportControl/singleSanction",
path: "/finance/singleSanction",
query: {
id: item.sanId
}
......@@ -842,7 +842,7 @@ const checkedTime = ref(["全部时间"]);
const handleTitleClick = item => {
window.sessionStorage.setItem("curTabName", `${item.year}-${item.dateStr}${item.title}》`);
const route = router.resolve({
path: "/exportControl/singleSanction",
path: "/finance/singleSanction",
query: {
id: item.id,
sanTypeId: item.sanTypeId
......@@ -1377,7 +1377,7 @@ watch(
const fetchEntitiesList = async (page = 1, size = 10) => {
try {
console.log("activeResourceTabItem.value.id", activeResourceTabItem.value.id);
const res = await getEntitiesList(activeResourceTabItem.value.id, page, size);
const res = await getEntitiesList(activeResourceTabItem.value.id.join(","), page, size);
if (res) {
entitiesList.value = res.content.map(item => ({
...item,
......@@ -1638,7 +1638,7 @@ const handleSanc = item => {
console.log("activeResourceTabItem.value.id", activeResourceTabItem.value.id);
window.sessionStorage.setItem("curTabName", `${item.postDate}${item.title}》`);
const route = router.resolve({
path: "/exportControl/singleSanction",
path: "/finance/singleSanction",
query: {
id: item.id,
sanTypeId: activeResourceTabItem.value.id.join(",")
......@@ -1773,7 +1773,7 @@ onMounted(async () => {
fetchSocialMediaInfo();
// 获取新闻资讯
fetchNewsInfo();
fetchEntitiesList(currentPage.value, pageSize.value);
// fetchEntitiesList(currentPage.value, pageSize.value);
await fetchSanctionProcess(sanctionPage.value, 10);
// 获取雷达图数据
await fetchRadarData(domainChecked.value);
......@@ -1970,7 +1970,7 @@ const handleMediaClick = item => {
flex-wrap: wrap;
justify-content: space-between;
padding-left: 10px;
height: 156px;
max-height: 160px;
overflow: auto;
&-item {
......
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论