提交 f8252418 authored 作者: 刘宇琪's avatar 刘宇琪

merge:创新主体合并

...@@ -300,5 +300,284 @@ export async function getPotentialNewsKeywords(personId) { ...@@ -300,5 +300,284 @@ export async function getPotentialNewsKeywords(personId) {
method: 'GET', method: 'GET',
}) })
} }
/**
* 获取法案进展预测基础数据
* @param {string} billId - 法案ID,如 "119_HR_1"
* @returns {Promise<Object>} 预测基础数据
*/
export function getProgressPrediction(billId) {
return request('/api/BillProgressPrediction/progressPrediction', {
method: 'GET',
params: { billId }
})
}
/**
* 获取相似法案列表
* @param {Object} params - 查询参数
* @param {string} params.patternType - 政府结构类型,如 "统一政府"
* @param {string} params.proposalType - 提案类型,默认 "两党共同提案"
* @returns {Promise<Object>} 相似法案列表
*/
export function getSimiBills(params = {}) {
return request('/api/BillProgressPrediction/simiBills', {
method: 'GET',
params: {
patternType: params.patternType || '统一政府',
proposalType: params.proposalType || '两党共同提案',
...params
}
})
}
/**
\ No newline at end of file * 转换相似法案 API 返回的数据为组件所需格式
* @param {Object} apiData - API 返回的原始数据
* @returns {Object} 转换后的统计数据和法案列表
*/
export function transformSimiBillsData(apiData) {
if (!apiData || !apiData.data || !Array.isArray(apiData.data)) {
return { stats: null, bills: [] }
}
const bills = apiData.data
// 计算统计数据
let becameLaw = 0
let notPassedOrShelved = 0
let totalDays = 0
let completedBills = 0
bills.forEach(bill => {
const actions = bill.bill_actions || []
const hasBecameLaw = actions.some(a =>
a.action_type === 'BecameLaw' ||
a.action_type === 'President' && a.action_desc?.includes('签署')
)
if (hasBecameLaw) {
becameLaw++
// 计算耗时
if (actions.length >= 2) {
const firstDate = new Date(actions[0].action_date)
const lastDate = new Date(actions[actions.length - 1].action_date)
const days = Math.ceil((lastDate - firstDate) / (1000 * 60 * 60 * 24))
if (days > 0) {
totalDays += days
completedBills++
}
}
} else {
notPassedOrShelved++
}
})
const medianDays = completedBills > 0 ? Math.round(totalDays / completedBills) : 223
const passRate = bills.length > 0 ? ((becameLaw / bills.length) * 100).toFixed(1) : '0'
const stats = {
totalBills: bills.length,
becameLaw,
notPassedOrShelved,
medianDays,
passRate
}
// 转换法案列表格式
const transformedBills = bills.map(bill => ({
id: bill.bill_id,
title: bill.bill_name || bill.bill_id,
tags: [bill.poli_pattern_type, bill.bill_proposal_type].filter(Boolean),
passTime: extractPassTime(bill.bill_actions),
totalDays: calculateTotalDays(bill.bill_actions),
selected: true, // 默认全选
_raw: bill
}))
return { stats, bills: transformedBills }
}
/**
* 从法案动作中提取通过时间
* @param {Array} actions - 法案动作列表
* @returns {string} 通过时间
*/
function extractPassTime(actions) {
if (!actions || actions.length === 0) return ''
// 找到最后一个动作的日期
const lastAction = actions[actions.length - 1]
if (lastAction && lastAction.action_date) {
const date = new Date(lastAction.action_date)
return `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}日`
}
return ''
}
/**
* 计算法案总耗时
* @param {Array} actions - 法案动作列表
* @returns {number} 总天数
*/
function calculateTotalDays(actions) {
if (!actions || actions.length < 2) return 0
const firstDate = new Date(actions[0].action_date)
const lastDate = new Date(actions[actions.length - 1].action_date)
return Math.ceil((lastDate - firstDate) / (1000 * 60 * 60 * 24))
}
/**
* 解析 billId 从路由参数
* 路由格式: ProgressForecast/:id
* billId 格式: 119_HR_1 (国会届次_法案类型_法案编号)
* @param {string} id - 路由中的 id 参数
* @returns {string} 完整的 billId
*/
export function parseBillId(id) {
// 如果 id 已经包含完整格式,直接返回
if (id && id.includes('_')) {
return id
}
// 否则根据业务规则构建 billId
// 这里假设默认是 119 届国会的 HR 类型法案
return `119_HR_${id}`
}
/**
* 转换 API 返回的数据为组件所需格式
* @param {Object} apiData - API 返回的原始数据
* @returns {Object} 转换后的提案信息
*/
export function transformProposalInfo(apiData) {
if (!apiData || !apiData.data) {
return null
}
const data = apiData.data
// 解析提案人信息
const sponsors = data.bill_sponsors || []
const mainProposer = sponsors.find(s => s.sponsor_type === '提案人')
const coProposers = sponsors.filter(s => s.sponsor_type !== '提案人')
// 构建共同提案人描述
let coProposersDesc = ''
if (coProposers.length > 0) {
const partyGroups = {}
coProposers.forEach(s => {
const party = s.party_name || '未知'
partyGroups[party] = (partyGroups[party] || 0) + 1
})
coProposersDesc = Object.entries(partyGroups)
.map(([party, count]) => `${party}${count}人`)
.join(';')
} else {
coProposersDesc = data.proposal_desc || '无'
}
return {
// 提案标题 - 需要从其他 API 获取或使用默认值
title: data.bill_title || 'H.R.1-大而美法案',
// 提案时间 - 从政治格局描述中提取或使用默认值
date: extractDateFromDesc(data.poli_pattern_desc) || '2025年5月20日',
// 涉及领域
areas: data.domain_name || [],
// 选举周期阶段 - 从政治格局描述推断
electionPhase: inferElectionPhase(data.poli_pattern_desc) || '执政初期/蜜月期',
// 提案人
proposer: mainProposer ? `${mainProposer.person_name}` : '',
// 共同提案人
coProposers: coProposersDesc,
// 提案人职务 - 需要从其他 API 获取
proposerPosition: data.proposer_position || '委员会主席',
// 政府结构类型
governmentType: formatGovernmentType(data.poli_pattern_type, data.poli_pattern_desc),
// 法案预算规模 - 需要从其他 API 获取
budgetScale: data.budget_scale || '4万亿美元',
// 原始数据,供筛选条件使用
_raw: data
}
}
/**
* 从政治格局描述中提取日期
* @param {string} desc - 政治格局描述
* @returns {string|null} 提取的日期
*/
function extractDateFromDesc(desc) {
if (!desc) return null
// 尝试匹配 YYYY-MM-DD 或 YYYY年M月D日 格式
const match = desc.match(/(\d{4})-(\d{2})-(\d{2})/)
if (match) {
return `${match[1]}${parseInt(match[2])}${parseInt(match[3])}日`
}
return null
}
/**
* 从政治格局描述推断选举周期阶段
* @param {string} desc - 政治格局描述
* @returns {string} 选举周期阶段
*/
function inferElectionPhase(desc) {
if (!desc) return '执政初期/蜜月期'
// 根据描述中的关键词推断
if (desc.includes('就职')) {
return '执政初期/蜜月期'
}
return '执政初期/蜜月期'
}
/**
* 格式化政府结构类型
* @param {string} type - 政府类型
* @param {string} desc - 详细描述
* @returns {string} 格式化后的政府结构类型
*/
function formatGovernmentType(type, desc) {
if (type === '统一政府') {
return '一致政府(总统与两院同党)'
}
if (type === '分立政府') {
return '分立政府'
}
return type || '一致政府(总统与两院同党)'
}
/**
* 根据提案信息生成默认筛选条件
* @param {Object} proposalInfo - 转换后的提案信息
* @returns {Object} 默认筛选条件
*/
export function generateDefaultFilters(proposalInfo) {
if (!proposalInfo) return {}
return {
// 政策领域 - 使用提案的涉及领域
policyArea: proposalInfo.areas.map(area => {
const areaMap = {
'能源': 'energy',
'集成电路': 'ic',
'人工智能': 'ai',
'生物技术': 'biotech'
}
return areaMap[area] || area.toLowerCase()
}),
// 提案人职务
proposerPosition: proposalInfo.proposerPosition === '委员会主席' ? ['chairman'] : [],
// 政府结构类型
governmentType: proposalInfo.governmentType.includes('一致') ? ['unified'] : ['divided'],
// 选举周期阶段
electionPhase: proposalInfo.electionPhase.includes('蜜月') ? ['honeymoon'] : [],
// 法案预算规模
budgetScale: ['trillion_plus'],
// 对方党派提案人
oppositionProposer: ['none'],
// 提案时间
proposalTime: ['recent_5']
}
}
...@@ -11,6 +11,7 @@ const BillDeepDigProcessAnalysis = () => import('@/views/bill/deepDig/processAna ...@@ -11,6 +11,7 @@ const BillDeepDigProcessAnalysis = () => import('@/views/bill/deepDig/processAna
const BillDeepDigPoliContribution = () => import('@/views/bill/deepDig/poliContribution/index.vue') const BillDeepDigPoliContribution = () => import('@/views/bill/deepDig/poliContribution/index.vue')
const BillInfluenceLayout = () => import('@/views/bill/influence/index.vue') const BillInfluenceLayout = () => import('@/views/bill/influence/index.vue')
const BillInfluenceIndustry = () => import('@/views/bill/influence/industry/index.vue') const BillInfluenceIndustry = () => import('@/views/bill/influence/industry/index.vue')
const BillProgressForecast = () => import('@/views/bill/influence/ProgressForecast/index.vue')
const BillInfluenceScientificResearch = () => import('@/views/bill/influence/scientificResearch/index.vue') const BillInfluenceScientificResearch = () => import('@/views/bill/influence/scientificResearch/index.vue')
const BillRelevantCircumstance = () => import('@/views/bill/relevantCircumstance/index.vue') const BillRelevantCircumstance = () => import('@/views/bill/relevantCircumstance/index.vue')
const BillVersionCompare = () => import('@/views/bill/versionCompare/index.vue') const BillVersionCompare = () => import('@/views/bill/versionCompare/index.vue')
...@@ -52,7 +53,7 @@ const billRoutes = [ ...@@ -52,7 +53,7 @@ const billRoutes = [
component: BillIntroduction, component: BillIntroduction,
// meta: { // meta: {
// title: "法案简介" // title: "法案简介"
// } // }
}, },
{ {
...@@ -120,9 +121,21 @@ const billRoutes = [ ...@@ -120,9 +121,21 @@ const billRoutes = [
name: "BillInfluenceScientificResearch", name: "BillInfluenceScientificResearch",
component: BillInfluenceScientificResearch, component: BillInfluenceScientificResearch,
// meta: { title: "对华科研影响" } // meta: { title: "对华科研影响" }
} },
// {
// path: "ProgressForecast",
// name: "BillProgressForecast",
// component: BillProgressForecast,
// // meta: { title: "对华科研影响" }
// }
] ]
}, {
path: "ProgressForecast/:id",
name: "BillProgressForecast",
component: BillProgressForecast,
// meta: { title: "对华科研影响" }
}, },
{ {
path: "relevantCircumstance", path: "relevantCircumstance",
name: "BillRelevantCircumstance", name: "BillRelevantCircumstance",
......
// 模拟 API 延迟
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
// 步骤数据类型
export interface Step {
id: number
title: string
description: string
}
// 当前提案信息类型
export interface ProposalInfo {
title: string
date: string
areas: string[]
electionPhase: string
proposer: string
coProposers: string
proposerPosition: string
governmentType: string
budgetScale: string
}
// 筛选条件类型
export interface FilterOption {
value: string
label: string
hint?: string
}
export interface FilterField {
id: string
label: string
options: FilterOption[]
selectedValues: string[]
hint?: string
}
// 获取步骤数据
export async function fetchSteps(): Promise<Step[]> {
await delay(100)
return [
{
id: 1,
title: '设置筛选条件',
description: '基于核心相似度维度构建预测基准'
},
{
id: 2,
title: '筛选同类法案',
description: '形成历史相似同类法案预测基准集'
},
{
id: 3,
title: '阶段预测分析',
description: '基于相似法案的历史数据预测模型'
}
]
}
// 获取当前提案信息
export async function fetchProposalInfo(): Promise<ProposalInfo> {
await delay(100)
return {
title: 'H.R.1-大而美法案',
date: '2025年5月20日',
areas: ['能源', '集成电路'],
electionPhase: '执政初期/蜜月期',
proposer: '乔迪·阿灵顿(Jodey Arrington)',
coProposers: '共和党4人',
proposerPosition: '委员会主席',
governmentType: '一致政府(总统与两院同党)',
budgetScale: '4万亿美元'
}
}
// 获取筛选字段配置
export async function fetchFilterFields(): Promise<FilterField[]> {
await delay(100)
return [
{
id: 'policyArea',
label: '政策领域:',
options: [
{ value: 'energy', label: '能源' },
{ value: 'ic', label: '集成电路' },
{ value: 'ai', label: '人工智能' },
{ value: 'biotech', label: '生物技术' }
],
selectedValues: ['energy', 'ic']
},
{
id: 'proposerPosition',
label: '提案人职务:',
options: [
{ value: 'chairman', label: '委员会主席' },
{ value: 'vice_chairman', label: '委员会副主席' },
{ value: 'member', label: '普通委员' }
],
selectedValues: ['chairman']
},
{
id: 'governmentType',
label: '政府结构类型:',
options: [
{ value: 'unified', label: '一致政府' },
{ value: 'divided', label: '分裂政府' }
],
selectedValues: ['unified'],
hint: '总统所属政党同时控制国会参众两院'
},
{
id: 'electionPhase',
label: '选举周期阶段:',
options: [
{ value: 'honeymoon', label: '执政初期/蜜月期' },
{ value: 'midterm', label: '中期选举期' },
{ value: 'election', label: '大选年' }
],
selectedValues: ['honeymoon'],
hint: '新总统上任初期,上任后约100天至1年。'
},
{
id: 'budgetScale',
label: '法案预算规模:',
options: [
{ value: 'trillion_plus', label: '1万亿元以上' },
{ value: 'billion_plus', label: '100亿-1万亿' },
{ value: 'below_billion', label: '100亿以下' }
],
selectedValues: ['trillion_plus']
},
{
id: 'oppositionProposer',
label: '对方党派提案人:',
options: [
{ value: 'none', label: '无对方党派提案人' },
{ value: 'has', label: '有对方党派提案人' }
],
selectedValues: ['none']
},
{
id: 'proposalTime',
label: '提案时间:',
options: [
{ value: 'recent_5', label: '近五年' },
{ value: 'recent_10', label: '近十年' },
{ value: 'all', label: '全部' }
],
selectedValues: ['recent_5']
}
]
}
// 提交筛选条件
export async function submitFilters(filters: Record<string, string[]>): Promise<{ success: boolean }> {
await delay(300)
console.log('提交筛选条件:', filters)
return { success: true }
}
// 筛选结果统计类型
export interface FilterStats {
totalBills: number
becameLaw: number
notPassedOrShelved: number
medianDays: number
passRate: number
selectedCount: number
}
// 法案信息类型
export interface BillInfo {
id: string
title: string
proposalDate: string
areas: string[]
electionPhase: string
proposer: string
coProposers: string
proposerPosition: string
governmentType: string
budgetScale: string
passDays: number
selected: boolean
}
// 获取筛选结果统计
export async function fetchFilterStats(): Promise<FilterStats> {
await delay(100)
return {
totalBills: 50,
becameLaw: 24,
notPassedOrShelved: 26,
medianDays: 223,
passRate: 48.5,
selectedCount: 50
}
}
// 阶段预测分析类型
export type RiskLevel = 'passed' | 'high' | 'medium' | 'low'
export interface StatItem {
value: string
label: string
}
export interface PredictionPhase {
id: number
title: string
description: string
riskLevel: RiskLevel
progressLevel: number
estimatedDays: string
modelInputs: string[]
supportingFacts: {
title: string
basedOn: string
stats: StatItem[]
}
}
export interface PredictionAnalysis {
title: string
subtitle: string
overallRisk: RiskLevel
overallProgressLevel: number
overallEstimatedDays: string
highlightText: string
phases: PredictionPhase[]
}
// 获取阶段预测分析数据
export async function fetchPredictionAnalysis(): Promise<PredictionAnalysis> {
await delay(200)
return {
title: '立法进展阶段预测分析',
subtitle: '基于50个相似法案的历史数据与预测模型',
overallRisk: 'high',
overallProgressLevel: 3,
overallEstimatedDays: '预计耗时120~150天',
highlightText: '《大而美法案》的通过概率非常高。\n\n该法案由众议院共和党领导层在5月正式提出,作为特朗普第二任期核心经济议程,旨在通过一揽子税收、贸易改革重塑经济。到6月初,法案已快速通过关键的筹款委员会和预算委员会审议,进入众议院辩论阶段。共和党当时控制众议院,党内团结支持;白宫已明确表示强烈支持。虽然民主党普遍反对,但共和党凭借微弱优势足以在众议院通过。主要不确定在于参议院,随着中期选举临近民主党议员可能支持,或通过预算和解程序(只需简单多数)规避阻挠议事。因此,该法案在6月初已势在必行,最终成法几无悬念。',
phases: [
{
id: 1,
title: '阶段一:法案提出',
description: '提案提交至参议院/众议院,此阶段为流程起点',
riskLevel: 'passed',
progressLevel: 3,
estimatedDays: '已通过',
modelInputs: [
'确定法案的固有特征:法案提出时确定的特征,如提案人党派、资历、共同提案人网络强度、法案主题分类、文本复杂度。这些特征在所有节点预测中都可使用。',
'确定法案的动态特征:随着流程推进才产生的特征,这是分阶段预测的核心。包括流程特征:在当前节点已停留时间、距国会会休会剩余时间;投票特征:上一节点投票结果(如委员会投票的赞成比例、党派支持度)。修正案特征:法案是否被附加了有争议的修正外部环境特征:当前新闻情绪指数、公众舆论压力、关键利益集团的态度。',
'确定法案的固有特征:法案提出时确定的特征,如提案人党派、资历、共同提案人网络强度、法案主题分类、文本复杂度。这些特征在所有节点预测中都可使用。'
],
supportingFacts: {
title: '支撑预测的客观事实',
basedOn: '基于142项历史提案',
stats: [
{ value: '100%', label: '提案提交率' },
{ value: '142', label: '匹配法案数' },
{ value: '62.7%', label: '进入委员会比例' },
{ value: '85天', label: '平均等待分配时间' }
]
}
},
{
id: 2,
title: '阶段二:参议院通过',
description: '包括委员会审议、听证、修正和全院表决',
riskLevel: 'high',
progressLevel: 3,
estimatedDays: '预计耗时60~120天',
modelInputs: [
'确定法案的固有特征:法案提出时确定的特征,如提案人党派、资历、共同提案人网络强度、法案主题分类、文本复杂度。这些特征在所有节点预测中都可使用。',
'确定法案的动态特征:随着流程推进才产生的特征,这是分阶段预测的核心。包括流程特征:在当前节点已停留时间、距国会会休会剩余时间;投票特征:上一节点投票结果(如委员会投票的赞成比例、党派支持度)。修正案特征:法案是否被附加了有争议的修正案;外部环境特征:当前新闻情绪指数、公众舆论压力、关键利益集团的态度。',
'确定法案的固有特征:法案提出时确定的特征,如提案人党派、资历、共同提案人网络强度、法案主题分类、文本复杂度。这些特征在所有节点预测中都可使用。'
],
supportingFacts: {
title: '支撑预测的客观事实',
basedOn: '基于98项历史提案',
stats: [
{ value: '62.7%', label: '历史通过率' },
{ value: '85天', label: '平均耗时' },
{ value: '78%', label: '委员会主席支持率' },
{ value: '31%', label: '对方党派平均支持' },
{ value: '8.5次', label: '平均听证会次数' },
{ value: '14.2个', label: '平均修正案数' },
{ value: '92.3%', label: '本党派支持率' },
{ value: '68天', label: '中位数耗时' }
]
}
},
{
id: 3,
title: '阶段三:众议院通过',
description: '众议院审议、委员会分配和全院表决',
riskLevel: 'medium',
progressLevel: 2,
estimatedDays: '预计耗时20~45天',
modelInputs: [
'确定法案的固有特征:法案提出时确定的特征,如提案人党派、资历、共同提案人网络强度、法案主题分类、文本复杂度。这些特征在所有节点预测中都可使用。',
'确定法案的动态特征:随着流程推进才产生的特征,这是分阶段预测的核心。包括流程特征:在当前节点已停留时间、距国会会休会剩余时间;投票特征:上一节点投票结果(如委员会投票的赞成比例、党派支持度)。修正案特征:法案是否被附加了有争议的修正案;外部环境特征:当前新闻情绪指数、公众舆论压力、关键利益集团的态度。',
'确定法案的固有特征:法案提出时确定的特征,如提案人党派、资历、共同提案人网络强度、法案主题分类、文本复杂度。这些特征在所有节点预测中都可使用。'
],
supportingFacts: {
title: '支撑预测的客观事实',
basedOn: '基于98项历史提案',
stats: [
{ value: '62.7%', label: '历史通过率' },
{ value: '85天', label: '平均耗时' },
{ value: '78%', label: '本党派支持率' },
{ value: '31%', label: '对方党派平均支持' },
{ value: '45%', label: '规则委员会支持率' },
{ value: '14.2个', label: '平均修正案数' },
{ value: '49.5天', label: '中位数耗时' },
{ value: '32%', label: '快速通道使用率' }
]
}
},
{
id: 4,
title: '阶段四:两院协商',
description: '协商委员会解决两院版本差异,形成统一文本',
riskLevel: 'medium',
progressLevel: 2,
estimatedDays: '预计耗时7~14天',
modelInputs: [
'确定法案的固有特征:法案提出时确定的特征,如提案人党派、资历、共同提案人网络强度、法案主题分类、文本复杂度。这些特征在所有节点预测中都可使用。',
'确定法案的动态特征:随着流程推进才产生的特征,这是分阶段预测的核心。包括流程特征:在当前节点已停留时间、距国会会休会剩余时间;投票特征:上一节点投票结果(如委员会投票的赞成比例、党派支持度)。修正案特征:法案是否被附加了有争议的修正案;外部环境特征:当前新闻情绪指数、公众舆论压力、关键利益集团的态度。',
'确定法案的固有特征:法案提出时确定的特征,如提案人党派、资历、共同提案人网络强度、法案主题分类、文本复杂度。这些特征在所有节点预测中都可使用。'
],
supportingFacts: {
title: '支撑预测的客观事实',
basedOn: '基于96项历史提案',
stats: [
{ value: '62.7%', label: '历史通过率' },
{ value: '85天', label: '平均耗时' },
{ value: '8.5个', label: '平均差异条款数' },
{ value: '3.2轮', label: '平均协商轮次' },
{ value: '78%', label: '协商成功法案比例' },
{ value: '42%', label: '需要领导层介入比例' },
{ value: '55天', label: '中位数耗时' },
{ value: '15%', label: '协商失败后重启比例' }
]
}
},
{
id: 5,
title: '阶段五:总统签署',
description: '总统审议、可能否决或签署成为法律',
riskLevel: 'low',
progressLevel: 1,
estimatedDays: '预计耗时1~5天',
modelInputs: [
'确定法案的固有特征:法案提出时确定的特征,如提案人党派、资历、共同提案人网络强度、法案主题分类、文本复杂度。这些特征在所有节点预测中都可使用。',
'确定法案的动态特征:随着流程推进才产生的特征,这是分阶段预测的核心。包括流程特征:在当前节点已停留时间、距国会会休会剩余时间;投票特征:上一节点投票结果(如委员会投票的赞成比例、党派支持度)。修正案特征:法案是否被附加了有争议的修正案;外部环境特征:当前新闻情绪指数、公众舆论压力、关键利益集团的态度。',
'确定法案的固有特征:法案提出时确定的特征,如提案人党派、资历、共同提案人网络强度、法案主题分类、文本复杂度。这些特征在所有节点预测中都可使用。'
],
supportingFacts: {
title: '支撑预测的客观事实',
basedOn: '基于33次送法总统法案',
stats: [
{ value: '62.7%', label: '历史签署率' },
{ value: '12天', label: '平均审议时间' },
{ value: '24.2%', label: '否决率' },
{ value: '3.2轮', label: '口袋否决率' },
{ value: '78%', label: '一致政府时签署率' },
{ value: '42%', label: '分立政府时签署率' },
{ value: '9天', label: '中位数审议时间' },
{ value: '15%', label: '附带声明签署比例' }
]
}
}
]
}
}
// 获取法案列表
export async function fetchBillList(): Promise<BillInfo[]> {
await delay(200)
return [
{
id: 'bill-1',
title: '激励创新的交通走廊以少碳和温室气体排放,提供允许对公共交通系统投资的税收结构,并帮助化石燃料劳动力向可持续工作领域转型',
proposalDate: '2025年5月20日',
areas: ['能源', '集成电路'],
electionPhase: '执政初期/蜜月期',
proposer: '乔迪·阿灵顿(Jodey Arrington)',
coProposers: '共和党4人;民主党2人',
proposerPosition: '委员会主席',
governmentType: '一致政府(总统与两院同党)',
budgetScale: '7500亿美元',
passDays: 245,
selected: true
},
{
id: 'bill-2',
title: '重新授权国家海洋和大气管理局切萨皮克湾办公室及其他用途',
proposalDate: '2025年5月20日',
areas: ['能源', '集成电路'],
electionPhase: '执政初期/蜜月期',
proposer: '乔迪·阿灵顿(Jodey Arrington)',
coProposers: '共和党4人;民主党2人',
proposerPosition: '委员会主席',
governmentType: '一致政府(总统与两院同党)',
budgetScale: '7500亿美元',
passDays: 245,
selected: true
},
{
id: 'bill-3',
title: '特朗普关税透明法案',
proposalDate: '2025年5月20日',
areas: ['能源', '集成电路'],
electionPhase: '执政初期/蜜月期',
proposer: '乔迪·阿灵顿(Jodey Arrington)',
coProposers: '共和党4人;民主党2人',
proposerPosition: '委员会主席',
governmentType: '一致政府(总统与两院同党)',
budgetScale: '7500亿美元',
passDays: 245,
selected: true
},
{
id: 'bill-4',
title: '禁止向关注国家销售最先进的人工智能芯片,并促进芯片安全高效地向海外获批美国人员转让',
proposalDate: '2025年5月20日',
areas: ['能源', '集成电路'],
electionPhase: '执政初期/蜜月期',
proposer: '乔迪·阿灵顿(Jodey Arrington)',
coProposers: '共和党4人;民主党2人',
proposerPosition: '委员会主席',
governmentType: '一致政府(总统与两院同党)',
budgetScale: '7500亿美元',
passDays: 245,
selected: true
}
]
}
<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="18.000000" height="18.000000" fill="none" customFrame="#000000">
<path id="星形 2" d="M10.9534 6.01133C10.9936 6.13492 11.1088 6.2186 11.2387 6.2186L16.6363 6.21885C16.9269 6.21885 17.0478 6.59069 16.8127 6.76151L12.446 9.93429C12.3409 10.0107 12.2969 10.1461 12.3371 10.2697L14.0048 15.4031C14.0946 15.6795 13.7783 15.9094 13.5432 15.7385L9.17633 12.5661C9.07118 12.4897 8.92882 12.4897 8.82367 12.5661L4.45683 15.7385C4.22171 15.9094 3.90539 15.6795 3.99518 15.4031L5.66293 10.2697C5.70308 10.1461 5.65909 10.0107 5.55395 9.93429L1.18734 6.76151C0.952245 6.59069 1.07307 6.21885 1.36368 6.21885L6.76125 6.2186C6.89121 6.2186 7.00639 6.13492 7.04655 6.01132L8.71469 0.877992C8.8045 0.601613 9.1955 0.601613 9.28531 0.877992L10.9534 6.01133Z" fill="rgb(132,136,142)" fill-rule="evenodd" />
</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" customFrame="#000000">
<rect id="容器 1925" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
<path id="合并" d="M2.45616 3.00599L2.47971 13.9809C2.48128 14.542 2.78116 14.998 3.34324 14.998L2.54408 14.998C1.982 14.998 1.52512 14.542 1.52355 13.9809L1.5 2.01867C1.5 1.45605 1.95688 1 2.52053 1L10.9705 1C11.5342 1 11.9911 1.45605 11.9911 2.01867L11.9911 2.62987C11.9079 2.14874 11.4886 1.98732 10.9847 1.98732L3.31969 1.98732C2.75604 1.98732 2.45616 2.44337 2.45616 3.00599ZM4.24592 2.72586L11.0034 2.72586C11.5576 2.72586 12.0051 3.1819 12.0051 3.74452L12.0051 8.14359C11.8481 8.12479 11.6895 8.11538 11.5278 8.11538C9.35171 8.11538 7.58541 9.85182 7.58541 11.9926C7.58541 13.2056 8.1522 14.2885 9.04084 15L4.26947 15C3.71838 15 3.26935 14.544 3.26778 13.9829L3.24423 3.74452C3.24423 3.1819 3.69169 2.72586 4.24592 2.72586ZM10.2419 5.08759C10.5151 5.08759 10.7365 4.86192 10.7365 4.58453C10.7365 4.30714 10.5151 4.08147 10.2419 4.08147L5.00739 4.08147C4.7342 4.08147 4.51282 4.30714 4.51282 4.58453C4.51282 4.86192 4.7342 5.08759 5.00739 5.08759L10.2419 5.08759ZM8.65461 7.92732C8.9278 7.92732 9.14918 7.70165 9.14918 7.42426C9.14918 7.14687 8.92623 6.92119 8.65304 6.92119L5.00739 6.92119C4.7342 6.92119 4.51282 7.14687 4.51282 7.42426C4.51282 7.70165 4.7342 7.92732 5.00739 7.92732L8.65461 7.92732ZM8.47101 11.9891C8.47101 10.3278 9.82126 8.98007 11.4855 8.98007C13.1498 8.98007 14.5 10.3278 14.5 11.9891C14.5 13.6503 13.1498 14.998 11.4855 14.998C9.82126 14.998 8.47101 13.6503 8.47101 11.9891ZM6.14096 10.7968C6.41415 10.7968 6.63553 10.5712 6.63553 10.2938C6.63553 10.0164 6.41258 9.7907 6.14096 9.7907L5.00896 9.7907C4.73577 9.7907 4.51439 10.0164 4.51439 10.2938C4.51439 10.5712 4.73577 10.7968 5.00896 10.7968L6.14096 10.7968ZM12.0036 13.5186L12.0036 12.0141L13.0132 12.0141L11.5012 10.3592L9.98925 12.0141L10.9941 12.0141L10.9941 13.5186L12.0036 13.5186ZM6.14096 13.6444C6.41415 13.6444 6.63553 13.4187 6.63553 13.1413C6.63553 12.8639 6.41258 12.6383 6.14096 12.6383L5.00896 12.6383C4.73577 12.6383 4.51439 12.8639 4.51439 13.1413C4.51439 13.4187 4.73577 13.6444 5.00896 13.6444L6.14096 13.6444Z" fill="rgb(59,65,75)" fill-rule="evenodd" />
</svg>
<svg viewBox="0 0 28 28" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="28.000000" height="28.000000" fill="none" clip-path="url(#clipPath_2)" customFrame="url(#clipPath_2)">
<defs>
<clipPath id="clipPath_2">
<rect width="28.000000" height="28.000000" x="0.000000" y="0.000000" rx="14.000000" fill="rgb(255,255,255)" />
</clipPath>
<clipPath id="clipPath_3">
<rect width="18.000000" height="15.000000" x="5.000000" y="5.000000" fill="rgb(255,255,255)" />
</clipPath>
</defs>
<rect id="导出数据" width="28.000000" height="28.000000" x="0.000000" y="0.000000" rx="14.000000" />
<g id="容器 742" customFrame="url(#clipPath_3)">
<rect id="容器 742" width="18.000000" height="15.000000" x="5.000000" y="5.000000" />
<rect id="矩形 347" width="2.000000" height="6.000000" x="13.000000" y="5.000000" fill="rgb(132,136,142)" />
<path id="矢量 600" d="M18 11L10 11L14 16L18 11Z" fill="rgb(132,136,142)" fill-rule="evenodd" />
<path id="矢量 601" d="M22 19.9996L22.9999 15.0012L19.9999 12.0011L18.9999 12L21.0003 15.001L17.9999 15.0015L16.9998 17.9987L14 17.9996L11.0001 17.9997L9.99998 15.002L7.00017 15.0028L8.99996 12.0008L8 12.0004L5 15.0023L6.00016 20L22 19.9996Z" fill="rgb(132,136,142)" fill-rule="evenodd" />
</g>
</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" customFrame="#000000">
<rect id="容器 1926" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
<path id="合并" d="M1.35139 1.5L14.6461 1.5C14.946 1.5 15.1091 1.78803 14.9173 1.97461L10.1219 6.60295L10.1219 14.2076C10.1219 14.4249 9.84414 14.5662 9.60823 14.4684L6.0762 13.0174C5.95494 12.9685 5.87998 12.8671 5.87998 12.7566L5.87998 6.60295L1.08461 1.97461C0.890592 1.78803 1.05154 1.5 1.35139 1.5ZM11.1956 6.63044L14.0243 6.63044C14.2183 6.63044 14.3771 6.76087 14.3749 6.92028L14.3749 7.6177C14.3749 7.77711 14.2161 7.90754 14.0221 7.90754L11.1956 7.90754C11.0016 7.90754 10.8428 7.77711 10.8428 7.6177L10.8428 6.92028C10.8428 6.76087 11.0016 6.63044 11.1956 6.63044ZM11.1956 9.18464L14.0243 9.18464C14.2183 9.18464 14.3771 9.31506 14.3749 9.47447L14.3749 10.1719C14.3749 10.3313 14.2161 10.4617 14.0221 10.4617L11.1956 10.4617C11.0016 10.4617 10.8428 10.3313 10.8428 10.1719L10.8428 9.47447C10.8428 9.31506 11.0016 9.18464 11.1956 9.18464ZM11.1956 11.7406L14.0243 11.7406C14.2183 11.7406 14.3771 11.8693 14.3749 12.0305L14.3749 12.7279C14.3749 12.8873 14.2161 13.0177 14.0221 13.0177L11.1956 13.0177C11.0016 13.0177 10.8428 12.8873 10.8428 12.7279L10.8428 12.0305C10.8428 11.8711 11.0016 11.7406 11.1956 11.7406Z" fill="rgb(59,65,75)" fill-rule="evenodd" />
</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" customFrame="#000000">
<rect id="容器 1438" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
<path id="矢量 1656" d="M9.67676 9.0769C9.55421 9.41452 9.32715 9.70684 9.02686 9.9136C8.72656 10.1204 8.3678 10.2314 8 10.2314C7.6322 10.2314 7.27344 10.1204 6.97314 9.9136C6.67285 9.70684 6.44579 9.41452 6.32324 9.0769L3.55562 9.0769C1.59838 9.0769 0 7.59858 0 5.75958C0 4.20884 1.13686 2.91448 2.66667 2.54703L2.66667 1.5769C2.66667 1.47563 2.69405 1.37615 2.74606 1.28845C2.79806 1.20075 2.87287 1.12792 2.96295 1.07729C3.05303 1.02666 3.15522 1 3.25924 1L11.5555 1C11.6596 0.999984 11.7618 1.02663 11.8518 1.07726C11.9419 1.12789 12.0168 1.20072 12.0688 1.28842C12.1208 1.37613 12.1482 1.47562 12.1482 1.5769L12.1482 4.4615C12.1482 4.56279 12.1208 4.6623 12.0688 4.75002C12.0168 4.83774 11.942 4.91058 11.8519 4.96122C11.7618 5.01186 11.6596 5.03851 11.5555 5.0385L3.25933 5.0385C3.15529 5.03851 3.05308 5.01186 2.96298 4.96122C2.87288 4.91058 2.79806 4.83774 2.74604 4.75002C2.69403 4.6623 2.66665 4.56279 2.66667 4.4615L2.66667 3.7534C1.79552 4.07533 1.18514 4.85584 1.18514 5.75958C1.18514 6.94778 2.24 7.9231 3.55562 7.9231L6.32324 7.9231C6.44579 7.58548 6.67285 7.29316 6.97314 7.0864C7.27344 6.87964 7.6322 6.76862 8 6.76862C8.3678 6.76862 8.72656 6.87964 9.02686 7.0864C9.32715 7.29316 9.55421 7.58548 9.67676 7.9231L12.4444 7.9231C14.4015 7.9231 16 9.40142 16 11.2403C16 13.0793 14.4015 14.5577 12.4445 14.5577L12.1482 14.5577L12.1482 15.4231C12.1482 15.5244 12.1208 15.6239 12.0688 15.7116C12.0168 15.7993 11.9419 15.8721 11.8518 15.9227C11.7618 15.9734 11.6596 16 11.5555 16L3.25933 16C3.1553 16 3.0531 15.9734 2.96301 15.9227C2.87291 15.8721 2.79809 15.7993 2.74607 15.7116C2.69405 15.6239 2.66667 15.5244 2.66667 15.4231L2.66667 12.5384C2.66667 12.4371 2.69405 12.3377 2.74606 12.25C2.79806 12.1623 2.87287 12.0894 2.96295 12.0388C3.05303 11.9882 3.15522 11.9615 3.25924 11.9615L11.5555 11.9615C11.6596 11.9615 11.7618 11.9881 11.8518 12.0388C11.9419 12.0894 12.0168 12.1622 12.0688 12.2499C12.1208 12.3376 12.1482 12.4371 12.1482 12.5384L12.1482 13.4038L12.4444 13.4038C13.76 13.4038 14.8149 12.4285 14.8149 11.2403C14.8149 10.0522 13.76 9.0769 12.4444 9.0769L9.67676 9.0769Z" fill="rgb(59,65,75)" fill-rule="nonzero" />
</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="统计 2" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
<path id="合并" d="M8.09474 1.99497L8.09474 6.91078C8.09474 7.08635 8.141 7.25882 8.22886 7.41086C8.31671 7.56291 8.44308 7.68917 8.59526 7.77695C8.74744 7.86473 8.92006 7.91095 9.09578 7.91095L14.0159 7.91095C14.2102 7.9107 14.4003 7.85392 14.5629 7.74754C14.7255 7.64116 14.8536 7.48979 14.9316 7.3119C15.0095 7.13401 15.034 6.93729 15.0019 6.74575C14.7594 5.31252 14.0763 3.99025 13.0476 2.96241C12.0189 1.93458 10.6954 1.25214 9.26095 1.0098C9.06925 0.977761 8.87236 1.00217 8.69431 1.08005C8.51626 1.15793 8.36476 1.28592 8.25829 1.44838C8.15182 1.61085 8.09499 1.80077 8.09474 1.99497ZM12.4164 8.74814L7.75904 8.74814C7.67118 8.74814 7.58487 8.72504 7.50878 8.68115C7.43269 8.63725 7.36951 8.57413 7.32558 8.4981C7.28165 8.42208 7.25852 8.33584 7.25852 8.24806L7.25852 3.60729C7.26139 3.41073 7.20622 3.21768 7.0999 3.05227C6.99358 2.88686 6.84083 2.75642 6.66074 2.67725C6.48065 2.59808 6.2812 2.57369 6.08731 2.60713C4.80952 2.85335 3.64037 3.49154 2.74265 4.43285C1.84493 5.37415 1.26334 6.57171 1.07883 7.85879C0.894329 9.14586 1.11611 10.4584 1.71328 11.6136C2.31046 12.7688 3.25331 13.7093 4.41056 14.3039C5.56781 14.8986 6.88186 15.118 8.16974 14.9314C9.45762 14.7449 10.6552 14.1617 11.5958 13.2632C12.5364 12.3647 13.1732 11.1954 13.4174 9.91834C13.4509 9.72462 13.4265 9.52534 13.3472 9.34541C13.268 9.16547 13.1374 9.01285 12.9719 8.90663C12.8063 8.8004 12.6131 8.74528 12.4164 8.74814Z" fill="rgb(59,65,75)" fill-rule="evenodd" />
</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" customFrame="#000000">
<rect id="容器 704" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
<circle id="椭圆 96" cx="8" cy="8" r="7" fill="rgb(230,231,232)" />
<circle id="椭圆 97" cx="8" cy="4" r="1" fill="rgb(132,136,142)" />
<path id="矩形 241" d="M6.49996 6L8.00028 6.0004C8.55256 6.0004 9.00028 6.44811 9.00028 7.00039L9.00028 10.4992C9.00028 10.7754 9.22408 10.9989 9.50033 10.9992L9.50033 10.9997C9.77657 10.9998 10.0005 11.2236 10.0005 11.4998L10.0003 11.5001C10.0002 11.7765 9.77622 12.0006 9.49978 12.0006L8.00028 12.0004L6.50033 12.0004C6.22423 12.0004 6.00064 11.7767 6.00049 11.5006L6.00021 11.5005C6.00021 11.2243 6.22418 11.0003 6.50037 11.0003L6.50037 11.0006C6.77649 11.0007 7.00042 10.7766 7.00042 10.5005L7.00017 7.50005C7.00017 7.22376 6.77644 7.00047 6.50015 7.00002L6.49946 6.99922C6.22357 6.999 6 6.77565 6 6.49976C6.00011 6.22373 6.22393 6 6.49996 6Z" fill="rgb(132,136,142)" fill-rule="evenodd" />
</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" customFrame="#000000">
<rect id="容器 1928" width="16.000000" height="16.000000" x="0.000000" y="0.000000" />
<path id="矢量 1657" d="M4.99213 1.2695C5.91426 1.091 6.91689 1 8 1C9.08311 1 10.0857 1.091 11.0079 1.2695C11.93 1.448 12.6597 1.693 13.1951 2.001C13.7305 2.309 14 2.6415 14 3.002L14 4.003C14 4.3635 13.7323 4.696 13.1951 5.004C12.6579 5.312 11.93 5.55525 11.0079 5.7355C10.0857 5.91225 9.08311 6.0015 8 6.0015C6.91689 6.0015 5.91426 5.9105 4.99213 5.732C4.06999 5.5535 3.34033 5.3085 2.8049 5.0005C2.26772 4.6925 2 4.36 2 3.9995L2 2.9985C2 2.638 2.26772 2.3055 2.8049 1.9975C3.34208 1.693 4.06999 1.44975 4.99213 1.2695L4.99213 1.2695ZM4.54068 6.663C5.6133 6.887 6.7664 6.999 8 6.999C9.23359 6.999 10.3867 6.887 11.4593 6.663C12.5319 6.439 13.3788 6.10825 13.9983 5.67075L13.9983 6.999C13.9983 7.3595 13.7305 7.692 13.1934 8C12.6562 8.308 11.9283 8.55125 11.0061 8.7315C10.0857 8.91 9.08311 8.99925 8 8.99925C6.91689 8.99925 5.91426 8.90825 4.99213 8.72975C4.06999 8.55125 3.34033 8.30625 2.8049 7.99825C2.26772 7.69025 2 7.35775 2 6.99725L2 5.669C2.62117 6.11 3.46807 6.44075 4.54068 6.663L4.54068 6.663ZM4.54068 9.66425C5.6133 9.88825 6.7664 10.0002 8 10.0002C9.23359 10.0002 10.3867 9.88825 11.4593 9.66425C12.5319 9.44025 13.3788 9.1095 13.9983 8.672L13.9983 10.0002C13.9983 10.3607 13.7305 10.6932 13.1934 11.0013C12.6562 11.3093 11.9283 11.5525 11.0061 11.7327C10.084 11.913 9.08311 12.0023 8 12.0023C6.91689 12.0023 5.91426 11.9113 4.99213 11.7327C4.06999 11.5542 3.34208 11.3092 2.8049 11.0013C2.26772 10.6933 2 10.3608 2 10.0002L2 8.672C2.62117 9.1095 3.46807 9.44025 4.54068 9.66425L4.54068 9.66425ZM4.54068 12.6637C5.6133 12.8877 6.7664 12.9998 8 12.9998C9.23359 12.9998 10.3867 12.8877 11.4593 12.6637C12.5319 12.4397 13.3788 12.109 13.9983 11.6715L13.9983 12.9998C13.9983 13.3602 13.7305 13.6927 13.1934 14.0008C12.6562 14.3088 11.9283 14.552 11.0061 14.7323C10.084 14.9125 9.08311 15 8 15C6.91689 15 5.91426 14.909 4.99213 14.7305C4.06999 14.552 3.34033 14.307 2.8049 13.999C2.26772 13.691 2 13.3585 2 12.998L2 11.6697C2.62117 12.109 3.46807 12.4397 4.54068 12.6637L4.54068 12.6637Z" fill="rgb(59,65,75)" fill-rule="nonzero" />
</svg>
<svg viewBox="0 0 28 28" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="28.000000" height="28.000000" fill="none" clip-path="url(#clipPath_0)" customFrame="url(#clipPath_0)">
<defs>
<clipPath id="clipPath_0">
<rect width="28.000000" height="28.000000" x="0.000000" y="0.000000" rx="14.000000" fill="rgb(255,255,255)" />
</clipPath>
<clipPath id="clipPath_1">
<rect width="20.000000" height="20.000000" x="4.000000" y="4.000000" fill="rgb(255,255,255)" />
</clipPath>
</defs>
<rect id="数据源" width="28.000000" height="28.000000" x="0.000000" y="0.000000" rx="14.000000" />
<g id="数据库 1" clip-path="url(#clipPath_1)" customFrame="url(#clipPath_1)">
<rect id="数据库 1" width="20.000000" height="20.000000" x="4.000000" y="4.000000" />
<path id="合并" d="M10.6426 6.48828C11.6719 6.28906 12.791 6.1875 14 6.1875C15.209 6.1875 16.3281 6.28906 17.3574 6.48828C18.3867 6.6875 19.2012 6.96094 19.7988 7.30469C20.3965 7.64844 20.6973 8.01953 20.6973 8.42188L20.6973 9.53906C20.6973 9.94141 20.3984 10.3125 19.7988 10.6563C19.1992 11 18.3867 11.2715 17.3574 11.4727C16.3281 11.6699 15.209 11.7695 14 11.7695C12.791 11.7695 11.6719 11.668 10.6426 11.4688C9.61328 11.2695 8.79883 10.9961 8.20117 10.6523C7.60156 10.3086 7.30273 9.9375 7.30273 9.53516L7.30273 8.41797C7.30273 8.01563 7.60156 7.64453 8.20117 7.30078C8.80078 6.96094 9.61328 6.68945 10.6426 6.48828ZM10.1387 12.5078C11.3359 12.7578 12.623 12.8828 14 12.8828C15.377 12.8828 16.6641 12.7578 17.8613 12.5078C19.0586 12.2578 20.0039 11.8887 20.6953 11.4004L20.6953 12.8828C20.6953 13.2852 20.3965 13.6563 19.7969 14C19.1973 14.3438 18.3848 14.6152 17.3555 14.8164C16.3281 15.0156 15.209 15.1152 14 15.1152C12.791 15.1152 11.6719 15.0137 10.6426 14.8145C9.61328 14.6152 8.79883 14.3418 8.20117 13.998C7.60156 13.6543 7.30273 13.2832 7.30273 12.8809L7.30273 11.3984C7.99609 11.8906 8.94141 12.2598 10.1387 12.5078ZM10.1387 15.8574C11.3359 16.1074 12.623 16.2324 14 16.2324C14.6624 16.2324 15.3041 16.2035 15.9249 16.1456C14.2088 16.4715 12.8443 17.3161 12.2805 18.3935C11.7114 18.3432 11.1654 18.2672 10.6426 18.166C9.61328 17.9668 8.80078 17.6934 8.20117 17.3496C7.60156 17.0059 7.30273 16.6348 7.30273 16.2324L7.30273 14.75C7.9961 15.2383 8.94141 15.6074 10.1387 15.8574ZM17.5 16C17.3281 16 17.1581 16.005 16.9902 16.0148C17.2857 15.9695 17.5761 15.917 17.8613 15.8574C19.0586 15.6074 20.0039 15.2383 20.6953 14.75L20.6953 16.2324C20.6953 16.3614 20.6646 16.4872 20.6031 16.6099C19.7199 16.2251 18.6512 16 17.5 16ZM13 19.5C13 18.1193 15.0147 17 17.5 17C19.9853 17 22 18.1193 22 19.5C22 20.8807 19.9853 22 17.5 22C15.0147 22 13 20.8807 13 19.5ZM17.5 18C18.3284 18 19 18.6716 19 19.5C19 20.3284 18.3284 21 17.5 21C16.6716 21 16 20.3284 16 19.5C16 18.6716 16.6716 18 17.5 18ZM12 19.5L12 19.4861C11.3572 19.4236 10.7367 19.33 10.1387 19.2051C8.94141 18.9551 7.99609 18.5859 7.30273 18.0957L7.30273 19.5781C7.30273 19.9805 7.60156 20.3516 8.20117 20.6953C8.79883 21.0391 9.61328 21.3125 10.6426 21.5117C11.4872 21.6752 12.3923 21.7729 13.3579 21.8027C12.5123 21.1873 12 20.3817 12 19.5Z" fill="rgb(132,136,142)" fill-rule="evenodd" />
</g>
</svg>
<template>
<div class="action-buttons">
<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" />
<path d="M3 3v5h5" />
</svg>
<span>重置筛选条件</span>
</button>
<div class="action-right">
<button
v-if="showPrevious"
class="btn-secondary"
@click="emit('previous')"
>
<span>{{ previousText || '返回设置' }}</span>
</button>
<button
class="btn-primary"
:disabled="nextDisabled"
@click="emit('next')"
>
<span>{{ nextText || '下一步' }}</span>
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="5" y1="12" x2="19" y2="12" />
<polyline points="12 5 19 12 12 19" />
</svg>
</button>
</div>
</div>
</template>
<script setup lang="ts">
defineProps<{
showPrevious?: boolean
previousText?: string
nextText?: string
nextDisabled?: boolean
}>()
const emit = defineEmits<{
reset: []
previous: []
next: []
}>()
</script>
<style scoped>
.action-buttons {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 24px;
}
.action-right {
display: flex;
align-items: center;
gap: 12px;
}
.btn-secondary {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
border: 1px solid var(--bg-black-10);
border-radius: var(--radius-4);
background-color: var(--bg-white-100);
font-size: 14px;
color: var(--text-primary-80-color);
cursor: pointer;
transition: background-color 0.2s;
}
.btn-secondary:hover {
background-color: var(--bg-black-2);
}
.btn-primary {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
min-width: 260px;
padding: 10px 32px;
border: none;
border-radius: var(--radius-4);
background-color: var(--color-primary-100);
font-size: 14px;
color: var(--bg-white-100);
cursor: pointer;
transition: opacity 0.2s;
}
.btn-primary:hover {
opacity: 0.9;
}
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.icon {
width: 16px;
height: 16px;
}
.btn-secondary .icon {
color: var(--text-primary-65-color);
}
</style>
<template>
<div class="bill-card">
<div class="card-content">
<input
type="checkbox"
:checked="bill.selected"
class="checkbox"
@change="emit('toggleSelect', bill.id)"
/>
<div class="card-main">
<h4 class="text-title-3-bold text-primary-80-clor">{{ bill.title }}</h4>
<div class="fields-grid">
<div
v-for="field in billFields"
:key="field.key"
class="field-item"
>
<span class="field-label text-tip-2 text-primary-50-clor">{{ field.label }}</span>
<template v-if="field.key === 'areas'">
<div class="area-tags">
<AreaTag v-for="area in bill.areas"
:key="area" :tagName="area" />
</div>
</template>
<template v-else-if="field.key === 'passDays'">
<span class="text-tip-2 main-color">{{ bill.passDays }}</span>
</template>
<template v-else>
<span class="text-tip-2">{{ bill[field.key as keyof typeof bill] }}</span>
</template>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { BillInfo } from '../api'
import AreaTag from '@/components/base/AreaTag/index.vue'
defineProps<{
bill: BillInfo
}>()
const emit = defineEmits<{
toggleSelect: [id: string]
}>()
const billFields = [
{ key: 'proposalDate', label: '提案时间:' },
{ key: 'areas', label: '涉及领域:' },
{ key: 'electionPhase', label: '选举周期阶段:' },
{ key: 'proposer', label: '提案人:' },
{ key: 'coProposers', label: '共同提案人:' },
{ key: 'proposerPosition', label: '提案人职务:' },
{ key: 'governmentType', label: '政府结构类型:' },
{ key: 'budgetScale', label: '法案预算规模:' },
{ key: 'passDays', label: '通过耗时:' }
]
</script>
<style scoped>
.bill-card {
border: 1px solid var(--bg-black-10);
border-radius: var(--radius-10);
padding: 16px;
}
.card-content {
display: flex;
align-items: flex-start;
gap: 12px;
}
.checkbox {
width: 16px;
height: 16px;
margin-top: 2px;
flex-shrink: 0;
cursor: pointer;
accent-color: var(--color-primary-100);
}
.card-main {
flex: 1;
}
.card-main h4 {
margin: 0 0 16px 0;
line-height: 20px;
}
.fields-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px 24px;
}
.field-item {
display: flex;
align-items: center;
}
.field-label {
width: 100px;
flex-shrink: 0;
}
.area-tags {
display: flex;
gap: 8px;
}
.area-tag {
padding: 2px 8px;
font-size: 12px;
color: var(--color-primary-100);
background-color: var(--color-primary-10);
border: 1px solid var(--color-primary-10);
border-radius: var(--radius-4);
}
</style>
<template>
<section class="filter-section">
<div class="section-header">
<div class="header-left">
<img src="../assets/fitller.svg" />
<h2 class="section-title text-title-3-bold">核心相似度维度筛选</h2>
</div>
<button class="btn-outline" @click="emit('setAsCurrent')">设置为当前提案</button>
</div>
<div class="divider" />
<div class="fields-grid">
<div
v-for="field in fields"
:key="field.id"
class="field-item"
>
<div class="field-label-wrapper">
<span class="field-label text-tip-1 text-primary-65-clor">{{ field.label }}</span>
</div>
<div class="field-content">
<FilterSelect
:options="field.options"
:model-value="field.selectedValues"
@update:model-value="(val) => handleFieldUpdate(field.id, val)"
/>
<div v-if="field.hint" class="field-hint">
<img src="../assets/importent.svg" />
<span class="text-tip-2 text-primary-50-clor">{{ field.hint }}</span>
</div>
</div>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import type { FilterField } from '../api'
import FilterSelect from './FilterSelect.vue'
const props = defineProps<{
fields: FilterField[]
}>()
const emit = defineEmits<{
'update:fields': [fields: FilterField[]]
setAsCurrent: []
}>()
// 更新字段选中值
function handleFieldUpdate(fieldId: string, newValues: string[]) {
const updatedFields = props.fields.map(f =>
f.id === fieldId
? { ...f, selectedValues: newValues }
: f
)
emit('update:fields', updatedFields)
}
</script>
<style scoped>
.filter-section {
margin-top: 24px;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.header-left {
display: flex;
align-items: center;
gap: 12px;
}
.section-icon {
width: 16px;
height: 16px;
color: var(--text-primary-80-color);
}
.btn-outline {
padding: 6px 16px;
border: 1px solid var(--bg-black-10);
border-radius: var(--radius-4);
background-color: var(--bg-white-100);
font-size: 14px;
color: var(--text-primary-80-color);
cursor: pointer;
transition: background-color 0.2s;
}
.btn-outline:hover {
background-color: var(--bg-black-2);
}
.divider {
height: 1px;
background-color: var(--border-black-5);
margin-bottom: 24px;
}
.fields-grid {
display: flex;
flex-wrap: wrap;
gap: 24px 24px;
padding-left: 30px;
}
.field-item {
width: 580px;
display: flex;
align-items: flex-start;
}
.field-label-wrapper {
padding-top: 4px;
}
.field-label {
display: block;
width: 150px;
}
.field-content {
display: flex;
flex-direction: column;
gap: 6px;
}
.field-hint {
display: flex;
align-items: center;
gap: 8px;
}
.hint-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
color: var(--text-primary-50-color);
}
</style>
<template>
<div class="filter-select">
<div class="select-trigger flex-display-start" @click.stop="isOpen = !isOpen">
<div class="selected-tags">
<template v-if="selectedLabels.length > 0">
<div
v-for="(label, index) in selectedLabels"
:key="modelValue[index]"
class="tag-item flex-display-center"
>
<span>{{ label }}</span>
<button class="tag-remove flex-display-center" @click.stop="removeValue(modelValue[index])">
<svg class="icon-xs" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</div>
</template>
<span v-else class="placeholder text-tip-2 text-primary-50-clor">{{ placeholder || '请选择' }}</span>
</div>
<svg class="icon-sm dropdown-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9" />
</svg>
</div>
<div v-if="isOpen" class="dropdown-menu">
<div
v-for="option in options"
:key="option.value"
class="dropdown-item flex-display-start"
:class="{ 'dropdown-item--selected': modelValue.includes(option.value) }"
@click.stop="toggleOption(option.value)"
>
<span>{{ option.label }}</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import type { FilterOption } from '../api'
const props = defineProps<{
options: FilterOption[]
modelValue: string[]
placeholder?: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string[]]
}>()
// 下拉框展开状态
const isOpen = ref(false)
// 已选中的标签文字
const selectedLabels = computed(() => {
return props.modelValue
.map(v => props.options.find(opt => opt.value === v)?.label)
.filter(Boolean) as string[]
})
// 切换选项选中状态
function toggleOption(value: string) {
const index = props.modelValue.indexOf(value)
if (index === -1) {
emit('update:modelValue', [...props.modelValue, value])
} else {
emit('update:modelValue', props.modelValue.filter(v => v !== value))
}
}
// 移除已选中的值
function removeValue(value: string) {
emit('update:modelValue', props.modelValue.filter(v => v !== value))
}
// 点击外部关闭下拉框
function handleClickOutside(e: MouseEvent) {
const target = e.target as HTMLElement
if (!target.closest('.filter-select')) {
isOpen.value = false
}
}
// 组件挂载时添加事件监听
onMounted(() => {
document.addEventListener('click', handleClickOutside)
})
// 组件卸载时移除事件监听
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside)
})
</script>
<style scoped>
.filter-select {
position: relative;
width: 420px;
}
.select-trigger {
height: 32px;
padding: 5px 8px;
border: 1px solid var(--bg-black-10);
border-radius: var(--radius-4);
background-color: var(--bg-white-100);
justify-content: space-between;
cursor: pointer;
}
.selected-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
flex: 1;
min-width: 0;
}
.tag-item {
gap: 4px;
padding: 0 4px 0 8px;
background-color: var(--bg-black-2);
border-radius: var(--radius-4);
font-size: 14px;
line-height: 22px;
color: var(--text-primary-80-color);
}
.tag-remove {
width: 13px;
height: 13px;
background: none;
border: none;
padding: 0;
cursor: pointer;
color: var(--text-primary-50-color);
transition: color 0.2s;
}
.tag-remove:hover {
color: var(--text-primary-80-color);
}
.dropdown-arrow {
flex-shrink: 0;
width: 11px;
height: 6px;
margin-left: 8px;
color: var(--text-primary-50-color);
}
.dropdown-menu {
position: absolute;
top: 100%;
left: 0;
right: 0;
margin-top: 4px;
background-color: var(--bg-white-100);
border: 1px solid var(--bg-black-10);
border-radius: var(--radius-4);
box-shadow: var(--shadow-card);
z-index: 10;
}
.dropdown-item {
padding: 8px 12px;
font-size: 14px;
color: var(--text-primary-80-color);
cursor: pointer;
gap: 8px;
transition: background-color 0.2s;
}
.dropdown-item:hover {
background-color: var(--bg-black-2);
}
.dropdown-item--selected {
background-color: var(--color-primary-10);
color: var(--color-primary-100);
}
.icon-xs {
width: 13px;
height: 13px;
}
.icon-sm {
width: 16px;
height: 16px;
}
</style>
<template>
<div class="filter-tag flex-display-center">
<span>{{ label }}</span>
<button class="tag-remove flex-display-center" @click="emit('remove')">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</div>
</template>
<script setup lang="ts">
defineProps<{
label: string
}>()
const emit = defineEmits<{
remove: []
}>()
</script>
<style scoped>
.filter-tag {
gap: 4px;
padding: 2px 8px;
background-color: var(--bg-black-2);
border-radius: var(--radius-4);
font-size: 14px;
color: var(--text-primary-80-color);
}
.tag-remove {
width: 12px;
height: 12px;
background: none;
border: none;
padding: 0;
color: var(--text-primary-50-color);
transition: color 0.2s;
}
.tag-remove:hover {
color: var(--text-primary-80-color);
}
.icon {
width: 12px;
height: 12px;
}
</style>
<template>
<header class="page-header">
<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>
<div class="header-actions">
<button class="action-btn">
<svg class="action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
<line x1="16" y1="13" x2="8" y2="13" />
<line x1="16" y1="17" x2="8" y2="17" />
<polyline points="10 9 9 9 8 9" />
</svg>
<span>选择法案</span>
</button>
<button class="action-btn">
<svg class="action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7 10 12 15 17 10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
<span>导出分析结果</span>
</button>
</div>
</div>
</header>
</template>
<script setup lang="ts">
</script>
<style scoped>
.page-header {
width: 2548px;
height: 80px;
/* margin-left: 160px; */
box-sizing: border-box;
display: flex;
align-items: center;
margin-left: -478px;
padding-left: 478px;
background: #ffffff;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
margin-bottom: 16px;
}
.header-content {
width: 1600px;
display: flex;
align-items: center;
justify-content: space-between;
}
.header-left {
display: flex;
flex-direction: column;
gap: 4px;
}
.header-actions {
display: flex;
align-items: center;
gap: 8px;
}
.action-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border: 1px solid var(--bg-black-10);
border-radius: 5px;
background-color: var(--bg-white-100);
font-size: 16px;
line-height: 24px;
color: var(--text-primary-65-color);
cursor: pointer;
transition: background-color 0.2s;
}
.action-btn:hover {
background-color: var(--bg-black-2);
}
.action-icon {
width: 20px;
height: 20px;
color: var(--text-primary-65-color);
}
</style>
<template>
<div v-if="phase" class="phase-card" :class="borderColorClass">
<div class="phase-header flex-display-start">
<div>
<h3 class="phase-title main-color text-title-2-bold">{{ phase.title }}</h3>
<p class="text-tip-2 text-primary-50-clor">{{ phase.description }}</p>
</div>
<div class="phase-status">
<span class="risk-badge" :class="riskColorClass">{{ riskLabel }}</span>
<div class="progress-bar-container">
<div
v-for="i in 3"
:key="i"
class="progress-segment"
:class="getSegmentClass(i)"
></div>
</div>
<p v-if="phase.riskLevel !== 'passed'" class="text-tip-2 text-primary-50-clor">{{ phase.estimatedDays }}</p>
</div>
</div>
<div class="model-inputs-box">
<div class="box-header flex-display-start">
<div class="box-title-row flex-display-center">
<img src="../assets/input.svg" />
<span class="text-compact-bold">预测模型数据输入</span>
</div>
<div class="box-hint flex-display-center text-tip-2 text-primary-50-clor">
<img src="../assets/importent.svg"/>
<span>此阶段预测基于以下多维特征</span>
</div>
</div>
<div class="model-inputs">
<p
v-for="(input, index) in phase.modelInputs"
:key="index"
class="text-tip-2 text-primary-65-clor"
>
{{ input }}
</p>
</div>
</div>
<div class="facts-section">
<div class="box-header flex-display-start">
<div class="box-title-row flex-display-center">
<img src="../assets/icon1.svg"/>
<span class="text-compact-bold">{{ phase.supportingFacts.title }}</span>
</div>
<div class="box-hint flex-display-center text-tip-2 text-primary-50-clor">
<img src="../assets/importent.svg"/>
<span>{{ phase.supportingFacts.basedOn }}</span>
</div>
</div>
<div class="stats-grid">
<div
v-for="(stat, index) in phase.supportingFacts.stats"
:key="index"
class="stat-card"
>
<div class="stat-value main-color">{{ stat.value }}</div>
<div class="text-tip-3 text-primary-65-clor">{{ stat.label }}</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { PredictionPhase } from '../api'
const props = defineProps<{
phase: PredictionPhase
}>()
// 根据风险等级返回边框颜色类
const borderColorClass = computed(() => {
if (!props.phase) return ''
switch (props.phase.riskLevel) {
case 'passed':
return 'border-primary'
case 'high':
return 'border-high'
case 'medium':
return 'border-medium'
case 'low':
return 'border-low'
default:
return ''
}
})
// 根据索引和progressLevel返回进度条格子的类名
function getSegmentClass(index: number) {
if (!props.phase) return 'segment-inactive'
const level = props.phase.progressLevel
const isActive = index <= level
if (!isActive) {
return 'segment-inactive'
}
switch (props.phase.riskLevel) {
case 'passed':
return 'segment-primary'
case 'high':
return 'segment-high'
case 'medium':
return 'segment-medium'
case 'low':
return 'segment-low'
default:
return 'segment-inactive'
}
}
// 根据风险等级返回文字颜色类
const riskColorClass = computed(() => {
if (!props.phase) return ''
switch (props.phase.riskLevel) {
case 'passed':
return 'main-color'
case 'high':
return 'color-red-100'
case 'medium':
return 'color-orange-100'
case 'low':
return 'color-green-100'
default:
return ''
}
})
// 根据风险等级返回标签文字
const riskLabel = computed(() => {
if (!props.phase) return ''
switch (props.phase.riskLevel) {
case 'passed':
return '已通过'
case 'high':
return '高'
case 'medium':
return '中'
case 'low':
return '低'
default:
return ''
}
})
</script>
<style scoped>
.phase-card {
padding-left: 24px;
padding-bottom: 32px;
}
.border-primary {
border-left-color: var(--color-primary-100);
}
.border-high {
border-left-color: var(--color-red-100);
}
.border-medium {
border-left-color: var(--color-orange-100);
}
.border-low {
border-left-color: var(--color-green-100);
}
.phase-header {
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
}
.phase-header > div:first-child {
display: flex;
flex-direction: column;
gap: 4px;
}
.phase-title {
font-size: 18px;
font-weight: bold;
}
.phase-status {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
}
.progress-bar-container {
display: flex;
gap: 4px;
}
.progress-segment {
width: 50px;
height: 4px;
border-radius: 2px;
}
.segment-inactive {
background-color: var(--bg-black-5);
}
.segment-primary {
background-color: var(--color-primary-100);
}
.segment-high {
background-color: var(--color-red-100);
}
.segment-medium {
background-color: var(--color-orange-100);
}
.segment-low {
background-color: var(--color-green-100);
}
.risk-badge {
font-size: 20px;
font-weight: bold;
}
.model-inputs-box {
background-color: var(--bg-black-2);
border-radius: var(--radius-10);
padding: 16px;
margin-bottom: 16px;
}
.box-header {
justify-content: space-between;
margin-bottom: 12px;
}
.box-title-row {
gap: 8px;
}
.box-hint {
gap: 4px;
}
.model-inputs {
display: flex;
flex-direction: column;
gap: 12px;
}
.model-inputs p {
line-height: 1.6;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.stat-card {
background-color: var(--color-primary-2);
border: 1px;
padding: 12px;
text-align: center;
border : 1px solid var(--color-primary-10);
}
.stat-value {
font-size: 18px;
font-weight: bold;
margin-bottom: 4px;
}
.icon {
width: 16px;
height: 16px;
color: var(--text-primary-65-color);
}
</style>
<template>
<section class="proposal-section">
<div class="section-header">
<div class="header-left">
<svg class="section-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
<line x1="16" y1="13" x2="8" y2="13" />
<line x1="16" y1="17" x2="8" y2="17" />
<polyline points="10 9 9 9 8 9" />
</svg>
<h2 class="section-title text-title-3-bold">当前提案信息</h2>
</div>
<div class="header-actions">
<button class="action-btn" title="对比">
<img src="../assets/source.svg"/>
</button>
<button class="action-btn" title="下载">
<img src="../assets/down.svg"/>
</button>
<button class="action-btn" title="收藏">
<img src="../assets/collection.svg"/>
</button>
</div>
</div>
<div class="divider" />
<div class="info-grid">
<div
v-for="field in infoFields"
:key="field.key"
class="info-item"
>
<span class="info-label text-body-1">{{ field.label }}</span>
<template v-if="field.key === 'areas'">
<div class="area-tags">
<!-- <span
v-for="area in info.areas"
:key="area"
class="area-tag"
>
{{ area }}
</span> -->
<AreaTag v-for="area in info.areas" :key="area" :tagName="area" />
</div>
</template>
<template v-else>
<span class="info-value text-body-1">{{ info[field.key as keyof typeof info] }}</span>
</template>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import type { ProposalInfo } from '../api'
defineProps<{
info: ProposalInfo
}>()
// 信息字段配置
const infoFields = [
{ key: 'title', label: '提案标题:' },
{ key: 'date', label: '提案时间:' },
{ key: 'areas', label: '涉及领域:' },
{ key: 'electionPhase', label: '选举周期阶段:' },
{ key: 'proposer', label: '提案人:' },
{ key: 'coProposers', label: '共同提案人:' },
{ key: 'proposerPosition', label: '提案人职务:' },
{ key: 'governmentType', label: '政府结构类型:' },
{ key: 'budgetScale', label: '法案预算规模:' }
]
</script>
<style scoped>
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.header-left {
display: flex;
align-items: center;
gap: 12px;
}
.section-icon {
width: 16px;
height: 16px;
color: var(--text-primary-80-color);
}
.header-actions {
display: flex;
gap: 8px;
}
.action-btn {
width: 28px;
height: 28px;
padding: 4px;
border: none;
border-radius: 50px;
background-color: transparent;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.action-btn svg {
width: 20px;
height: 20px;
color: var(--text-primary-50-color);
}
.action-btn:hover svg {
color: var(--color-primary-100);
}
.divider {
height: 1px;
background-color: var(--border-black-5);
margin-bottom: 24px;
}
.info-grid {
display: flex;
flex-wrap: wrap;
gap: 24px 24px;
padding-left: 30px;
}
.info-item {
width: 580px;
display: flex;
align-items: center;
}
.info-label {
width: 150px;
flex-shrink: 0;
color: var(--text-primary-65-color);
}
.info-value {
color: var(--text-primary-80-color);
}
.area-tags {
display: flex;
gap: 8px;
}
.area-tag {
padding: 1px 8px;
font-size: 14px;
line-height: 20px;
color: var(--color-primary-100);
background-color: var(--color-primary-10);
border: 1px solid var(--color-primary-10);
border-radius: var(--radius-4);
}
</style>
<template>
<div class="step-container">
<div v-if="loading" class="loading-wrapper flex-display-center">
<span class="text-tip-2 text-primary-50-clor">加载中...</span>
</div>
<template v-else>
<div class="content-wrapper">
<ProposalInfoSection v-if="currentProposalInfo" :info="currentProposalInfo" />
<FilterSection
:fields="filterFields"
@update:fields="filterFields = $event"
@set-as-current="handleSetAsCurrent"
/>
</div>
<ActionButtons
@reset="handleReset"
@next="handleNext"
/>
</template>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import type { ProposalInfo, FilterField } from '../api'
import { fetchProposalInfo, fetchFilterFields } from '../api'
import ProposalInfoSection from './ProposalInfoSection.vue'
import FilterSection from './FilterSection.vue'
import ActionButtons from './ActionButtons.vue'
const props = defineProps<{
proposalInfo?: ProposalInfo | null
defaultFilters?: Record<string, string[]>
}>()
const emit = defineEmits<{
next: []
}>()
// 提案信息(优先使用 props,否则从 API 获取)
const currentProposalInfo = ref<ProposalInfo | null>(null)
// 筛选字段列表
const filterFields = ref<FilterField[]>([])
// 加载状态
const loading = ref(true)
// 初始筛选字段(用于重置)
const initialFilterFields = ref<FilterField[]>([])
// 监听 props.proposalInfo 变化
watch(() => props.proposalInfo, (newInfo) => {
if (newInfo) {
currentProposalInfo.value = newInfo
}
}, { immediate: true })
// 页面初始化时加载数据
onMounted(async () => {
try {
// 如果没有从 props 获取到提案信息,则从 API 获取
if (!props.proposalInfo) {
currentProposalInfo.value = await fetchProposalInfo()
}
// 获取筛选字段配置
const fields = await fetchFilterFields()
// 如果有默认筛选条件,应用到筛选字段
if (props.defaultFilters && Object.keys(props.defaultFilters).length > 0) {
filterFields.value = fields.map(field => ({
...field,
selectedValues: props.defaultFilters?.[field.id] || field.selectedValues
}))
} else {
filterFields.value = fields
}
// 保存初始状态用于重置
initialFilterFields.value = JSON.parse(JSON.stringify(filterFields.value))
} finally {
loading.value = false
}
})
// 重置所有筛选条件
function handleReset() {
filterFields.value = JSON.parse(JSON.stringify(initialFilterFields.value))
}
// 设置为当前提案
function handleSetAsCurrent() {
// 根据当前提案信息重新设置筛选条件
if (currentProposalInfo.value) {
filterFields.value = filterFields.value.map(field => {
// 根据提案信息设置默认值
const defaultValues = getDefaultValuesForField(field.id, currentProposalInfo.value!)
return {
...field,
selectedValues: defaultValues.length > 0 ? defaultValues : field.selectedValues
}
})
}
}
// 根据字段 ID 和提案信息获取默认值
function getDefaultValuesForField(fieldId: string, info: ProposalInfo): string[] {
const areaMap: Record<string, string> = {
'能源': 'energy',
'集成电路': 'ic',
'人工智能': 'ai',
'生物技术': 'biotech'
}
switch (fieldId) {
case 'policyArea':
return info.areas.map(area => areaMap[area] || area.toLowerCase())
case 'proposerPosition':
return info.proposerPosition === '委员会主席' ? ['chairman'] : []
case 'governmentType':
return info.governmentType.includes('一致') ? ['unified'] : ['divided']
case 'electionPhase':
return info.electionPhase.includes('蜜月') ? ['honeymoon'] : []
default:
return []
}
}
// 下一步
function handleNext() {
emit('next')
}
</script>
<style scoped>
.step-container {
display: flex;
flex-direction: column;
height: 100%;
}
.loading-wrapper {
flex: 1;
}
.content-wrapper {
flex: 1;
overflow: auto;
}
</style>
<template>
<div class="step-container">
<div v-if="loading" class="loading-wrapper flex-display-center">
<span class="text-tip-2 text-tip-1">加载中...</span>
</div>
<template v-else-if="stats">
<div class="content-wrapper">
<div class="stats-section">
<div class="stats-header">
<div class="header-left">
<div class="flex-display-center section-title-row">
<img src="../assets/document.svg">
<span class="text-title-3-bold text-primary-80-clor">筛选结果统计</span>
</div>
<p class="text-tip-2 stats-subtitle text-tip-1" style="margin-left: 21px;">基于筛选条件匹配的历史相似法案</p>
</div>
<div class="header-right">
<div class="total-number-row">
<span class="total-number main-color">{{ stats.totalBills }}</span>
<span class="total-unit main-color"></span>
</div>
<p class="total-label main-color">历史相似法案</p>
</div>
</div>
<div class="stats-cards">
<div
v-for="stat in statsItems"
:key="stat.label"
class="stat-card"
>
<span class="text-title-2-bold text-primary-80-clor">{{ stat.label }}</span>
<span class="main-color stat-value">{{ stat.value }}</span>
</div>
</div>
<div class="select-row">
<label class="select-all-label flex-display-center">
<input
type="checkbox"
:checked="isAllSelected"
class="checkbox"
@change="toggleSelectAll"
/>
<span class="text-compact">全选</span>
</label>
<span class="text-tip-1">已选择{{ selectedCount }}项法案</span>
</div>
<div class="bills-list">
<BillCard
v-for="bill in bills"
:key="bill.id"
:bill="bill"
@toggle-select="toggleBillSelect"
/>
</div>
</div>
</div>
<div class="action-footer">
<button class="btn-secondary flex-display-center" @click="handleBack">
<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" />
<path d="M3 3v5h5" />
</svg>
<span>返回设置</span>
</button>
<button class="btn-primary flex-display-center" @click="handleStartAnalysis">
<span>开始预测分析</span>
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="5" y1="12" x2="19" y2="12" />
<polyline points="12 5 19 12 12 19" />
</svg>
</button>
</div>
</template>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, inject, watch } from 'vue'
import type { FilterStats, BillInfo } from '../api'
import { fetchFilterStats, fetchBillList } from '../api'
import { getSimiBills, transformSimiBillsData } from '@/api/bill/billHome'
import BillCard from './BillCard.vue'
const emit = defineEmits<{
previous: []
next: []
}>()
// 从父组件注入筛选参数
const filterParams = inject<any>('filterParams', null)
// 筛选统计数据
const stats = ref<FilterStats | null>(null)
// 法案列表
const bills = ref<BillInfo[]>([])
// 加载状态
const loading = ref(true)
// 统计项配置,使用循环避免重复代码
const statsItems = computed(() => {
if (!stats.value) return []
return [
{ label: '成为法律', value: `${stats.value.becameLaw}个` },
{ label: '未通过/搁置', value: `${stats.value.notPassedOrShelved}个` },
{ label: '中位数耗时', value: `${stats.value.medianDays}天` },
{ label: '法案通过率', value: `${stats.value.passRate}%` }
]
})
// 计算属性:是否全选
const isAllSelected = computed(() => {
return bills.value.length > 0 && bills.value.every(bill => bill.selected)
})
// 计算属性:已选中数量
const selectedCount = computed(() => {
return bills.value.filter(bill => bill.selected).length
})
// 加载数据
async function loadData() {
loading.value = true
try {
// 优先使用真实 API
const params = {
patternType: filterParams?.governmentType || '统一政府',
proposalType: '两党共同提案'
}
const response = await getSimiBills(params)
if (response && response.success && response.data) {
const { stats: apiStats, bills: apiBills } = transformSimiBillsData(response)
stats.value = apiStats
bills.value = apiBills
} else {
// 如果 API 失败,使用模拟数据
const [statsData, billsData] = await Promise.all([
fetchFilterStats(),
fetchBillList()
])
stats.value = statsData
bills.value = billsData
}
} catch (error) {
console.error('获取相似法案失败:', error)
// 使用模拟数据作为 fallback
const [statsData, billsData] = await Promise.all([
fetchFilterStats(),
fetchBillList()
])
stats.value = statsData
bills.value = billsData
} finally {
loading.value = false
}
}
// 加载数据
onMounted(() => {
loadData()
})
// 监听筛选参数变化,重新加载数据
watch(() => filterParams, () => {
loadData()
}, { deep: true })
// 切换全选
function toggleSelectAll() {
const newValue = !isAllSelected.value
bills.value.forEach(bill => {
bill.selected = newValue
})
}
// 切换单个法案选中状态
function toggleBillSelect(id: string) {
const bill = bills.value.find(b => b.id === id)
if (bill) {
bill.selected = !bill.selected
}
}
// 返回设置
function handleBack() {
emit('previous')
}
// 开始预测分析
function handleStartAnalysis() {
emit('next')
}
</script>
<style scoped>
.text-tip-1 {
color: #5f656c;
}
.step-container {
display: flex;
flex-direction: column;
height: 100%;
}
.loading-wrapper {
flex: 1;
}
.content-wrapper {
flex: 1;
overflow: auto;
}
.stats-section {
display: flex;
flex-direction: column;
}
.stats-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
}
.header-left {
display: flex;
flex-direction: column;
gap: 8px;
}
.section-title-row {
gap: 8px;
justify-content: left;
}
.stats-subtitle {
margin: 0;
}
.header-right {
text-align: right;
}
.total-number-row {
display: flex;
align-items: baseline;
justify-content: flex-end;
}
.total-number {
font-size: 36px;
font-weight: bold;
line-height: 1;
}
.total-unit {
font-size: 14px;
margin-left: 4px;
}
.total-label {
font-size: 14px;
margin: 4px 0 0 0;
}
.stats-cards {
display: flex;
gap: 16px;
margin-bottom: 20px;
}
.stat-card {
position: relative;
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
background: white;
border-radius: 10px;
border: 1px solid rgba(234, 236, 238, 1);
overflow: hidden;
}
.stat-card::before {
content: '';
position: absolute;
left: 0;
top: 15px;
bottom: 15px;
width: 4px;
border-radius: 0 2px 2px 0;
background-color: var(--color-red-100);
}
.stat-label {
font-size: 16px;
font-weight: 400;
color: rgb(59, 65, 75);
}
.stat-value {
font-size: 30px;
font-weight: 700;
color: var(--color-red-100);
}
.select-row {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 10px;
border-bottom: 1px solid var(--border-black-5);
margin-bottom: 16px;
}
.select-all-label {
gap: 8px;
cursor: pointer;
}
.checkbox {
width: 16px;
height: 16px;
cursor: pointer;
accent-color: var(--color-primary-100);
}
.bills-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.action-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 24px;
border-top: 1px solid var(--border-black-5);
margin-top: 24px;
}
.btn-secondary {
gap: 8px;
padding: 8px 16px;
border: 1px solid var(--bg-black-10);
border-radius: var(--radius-10);
background-color: var(--bg-white-100);
font-size: 14px;
color: var(--text-primary-65-color);
cursor: pointer;
transition: background-color 0.2s;
}
.btn-secondary:hover {
background-color: var(--bg-black-2);
}
.btn-primary {
gap: 8px;
padding: 8px 24px;
border: none;
border-radius: var(--radius-10);
background-color: var(--color-primary-100);
font-size: 14px;
color: var(--bg-white-100);
min-width: 200px;
justify-content: center;
cursor: pointer;
transition: opacity 0.2s;
}
.btn-primary:hover {
opacity: 0.9;
}
.icon {
width: 16px;
height: 16px;
}
.icon-sm {
width: 16px;
height: 16px;
color: var(--color-primary-100);
}
.btn-secondary .icon {
color: var(--text-primary-65-color);
}
</style>
<template>
<div class="step-container">
<div v-if="predictionData" class="content-wrapper">
<div class="header-section flex-display-start">
<div class="header-left flex-display-start">
<div>
<div class="text-title-2-bold">
<img src="../assets/forecast.svg" />
{{ predictionData?.title }}</div>
<div class="text-tip-2 text-primary-50-clor" style="margin-left: 21px;">{{ predictionData?.subtitle }}</div>
</div>
</div>
<div class="header-right">
<span class="risk-label" :class="overallRiskColorClass">{{ overallRiskLabel }}</span>
<div class="progress-bar-container">
<div
v-for="i in 3"
:key="i"
class="progress-segment"
:class="getOverallSegmentClass(i)"
></div>
</div>
<p class="estimated-days text-tip-2 text-primary-50-clor">{{ predictionData?.overallEstimatedDays }}</p>
</div>
</div>
<div class="highlight-box text-regular">
<div class="highlight-wave text-regular"></div>
<div class="highlight-content text-regular">
<p class="highlight-title text-regular">《大而美法案》的通过概率非常高。</p>
<p class="highlight-text text-regular">该法案由众议院共和党领导层在5月正式提出,作为特朗普第二任期核心经济议程,旨在通过一揽子税收、贸易和预算改革提振经济。到6月初,法案已快速通过关键的筹款委员会和预算委员会审议,进入众议院全院辩论阶段。共和党当时控制众议院,且党内团结支持;白宫已明确表示强烈支持。虽然民主党普遍反对,但共和党凭借席位优势足以在众议院通过。主要不确定性在于参议院,但预计部分温和民主党议员可能支持,或通过预算和解程序(只需简单多数)规避阻挠议事。因此,该法案在6月初已势在必行,最终成法几无悬念。</p>
</div>
</div>
<div v-if="predictionData?.phases?.length" class="phases-list">
<PredictionPhaseCard
v-for="phase in predictionData.phases"
:key="phase.id"
:phase="phase"
/>
</div>
</div>
<div class="action-footer flex-display-start">
<button class="btn-text flex-display-center" @click="handleViewHistory">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="19" y1="12" x2="5" y2="12" />
<polyline points="12 19 5 12 12 5" />
</svg>
<span>查看预测基于的历史法案</span>
</button>
<button class="btn-primary flex-display-center" @click="handleRepredict">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="23 4 23 10 17 10" />
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
</svg>
<span>重新预测</span>
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { fetchPredictionAnalysis, type PredictionAnalysis } from '../api'
import PredictionPhaseCard from './PredictionPhaseCard.vue'
const emit = defineEmits<{
(e: 'prev'): void
(e: 'repredict'): void
}>()
// 预测分析数据
const predictionData = ref<PredictionAnalysis | null>(null)
// 获取预测分析数据
onMounted(async () => {
console.log('[v0] Step3 mounted, fetching data...')
const data = await fetchPredictionAnalysis()
console.log('[v0] Data fetched:', data)
predictionData.value = data
console.log('[v0] predictionData set:', predictionData.value)
})
// 根据索引和progressLevel返回进度条格子的类名
function getOverallSegmentClass(index: number) {
const level = predictionData.value?.overallProgressLevel || 0
const isActive = index <= level
if (!isActive) {
return 'segment-inactive'
}
switch (predictionData.value?.overallRisk) {
case 'high':
return 'segment-high'
case 'medium':
return 'segment-medium'
case 'low':
return 'segment-low'
default:
return 'segment-inactive'
}
}
// 根据风险等级返回颜色类
const overallRiskColorClass = computed(() => {
if (!predictionData.value) return ''
switch (predictionData.value.overallRisk) {
case 'high':
return 'color-red-100'
case 'medium':
return 'color-orange-100'
case 'low':
return 'color-green-100'
default:
return ''
}
})
// 根据风险等级返回标签文字
const overallRiskLabel = computed(() => {
if (!predictionData.value) return ''
switch (predictionData.value.overallRisk) {
case 'high':
return '高'
case 'medium':
return '中'
case 'low':
return '低'
default:
return ''
}
})
// 查看历史法案
function handleViewHistory() {
emit('prev')
}
// 重新预测
function handleRepredict() {
emit('repredict')
}
</script>
<style scoped>
/* .text-title-2-bold{
color: #3b414b;
} */
.step-container {
display: flex;
flex-direction: column;
height: 100%;
}
.content-wrapper {
flex: 1;
overflow: auto;
}
.header-section {
justify-content: space-between;
align-items: flex-start;
/* margin-bottom: 24px; */
}
.header-left {
gap: 12px;
align-items: flex-start;
}
.header-left > div {
display: flex;
flex-direction: column;
gap: 4px;
}
.header-right {
text-align: right;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
}
.progress-bar-container {
display: flex;
gap: 4px;
}
.progress-segment {
width: 50px;
height: 4px;
border-radius: 2px;
}
.segment-inactive {
background-color: var(--bg-black-5);
}
.segment-high {
background-color: var(--color-red-100);
}
.segment-medium {
background-color: var(--color-orange-100);
}
.segment-low {
background-color: var(--color-green-100);
}
.risk-label {
font-size: 20px;
font-weight: bold;
}
.highlight-box {
position: relative;
background-color: var(--bg-white-100);
border: 1px solid var(--color-primary-10);
border-radius: 4px;
margin-bottom: 32px;
overflow: hidden;
}
.highlight-wave {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 61px;
background: linear-gradient(180deg, rgba(231, 243, 255, 1) 0%, rgba(246, 250, 255, 0) 100%);
pointer-events: none;
}
.highlight-content {
position: relative;
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px 24px;
}
.highlight-title {
font-size: 16px;
font-weight: 400;
line-height: 30px;
color: var(--color-primary-100);
}
.highlight-text {
font-size: 16px;
font-weight: 400;
line-height: 30px;
color: var(--color-primary-100);
text-align: justify;
}
.phases-list {
display: flex;
flex-direction: column;
gap: 24px;
}
.action-footer {
justify-content: space-between;
padding-top: 24px;
border-top: 1px solid var(--border-black-5);
margin-top: 24px;
}
.btn-text {
gap: 8px;
padding: 10px 24px;
border: none;
background: none;
font-size: 14px;
color: var(--text-primary-65-color);
transition: color 0.2s;
}
.btn-text:hover {
color: var(--text-primary-80-color);
}
.btn-primary {
gap: 8px;
padding: 10px 32px;
border: none;
border-radius: var(--radius-10);
background-color: var(--color-primary-100);
font-size: 14px;
color: var(--bg-white-100);
transition: opacity 0.2s;
}
.btn-primary:hover {
opacity: 0.9;
}
.icon {
width: 16px;
height: 16px;
}
.icon-primary {
width: 20px;
height: 20px;
color: var(--color-primary-100);
}
</style>
<template>
<div class="step-sidebar">
<div class="steps-container">
<div
v-for="(step, index) in steps"
:key="step.id"
class="step-item"
>
<div class="step-indicator-wrapper">
<div
class="step-number"
:class="{ 'step-number--active': currentStep === step.id }"
>
{{ step.id }}
</div>
<div
v-if="index < steps.length - 1"
class="step-line"
/>
</div>
<div class="step-content">
<p
class="step-title"
:class="currentStep === step.id ? 'text-title-3-bold' : 'text-body-1'"
>
{{ step.title }}
</p>
<p class="step-desc text-tip-2">{{ step.description }}</p>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { Step } from '../api'
defineProps<{
steps: Step[]
currentStep: number
}>()
</script>
<style scoped>
.step-sidebar {
width: 320px;
flex-shrink: 0;
border-right: 1px solid var(--border-black-5);
background-color: var(--bg-white-100);
height: 100%;
padding: 24px;
display: flex;
flex-direction: column;
}
.steps-container {
width: 272px;
display: flex;
flex-direction: column;
}
.step-item {
display: flex;
gap: 16px;
min-height: 80px;
padding-bottom: 12px;
}
.step-item:last-child {
padding-bottom: 0;
}
.step-indicator-wrapper {
width: 32px;
display: flex;
flex-direction: column;
align-items: center;
}
.step-number {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 100px;
font-size: 14px;
font-family: "Microsoft YaHei", sans-serif;
line-height: 22px;
flex-shrink: 0;
background-color: var(--bg-black-5);
color: var(--text-primary-50-color);
}
.step-number--active {
background-color: var(--color-primary-100);
color: var(--bg-white-100);
}
.step-line {
width: 1px;
flex: 1;
margin-top: 8px;
background-color: var(--bg-black-5);
}
.step-content {
display: flex;
flex-direction: column;
gap: 8px;
padding-top: 4px;
flex: 1;
}
.step-title {
line-height: 24px;
color: var(--text-primary-80-color);
margin: 0;
}
.step-desc {
line-height: 22px;
color: var(--text-primary-65-color);
margin: 0;
}
</style>
<template>
<div class="app-container">
<PageHeader />
<main class="layout-main-center">
<div class="content-card background-as-card">
<StepSidebar
v-if="!loading && steps.length > 0"
:steps="steps"
:current-step="currentStep"
/>
<div class="content-area">
<template v-if="!loading">
<Step1FilterCondition
v-if="currentStep === 1"
:proposal-info="proposalInfo"
:default-filters="defaultFilters"
@next="goToNextStep"
/>
<Step2FilterBills
v-else-if="currentStep === 2"
@previous="goToPreviousStep"
@next="goToNextStep"
/>
<Step3PredictionAnalysis
v-else-if="currentStep === 3"
@prev="goToPreviousStep"
@repredict="handleRepredict"
/>
</template>
<div v-else class="loading-container">
<span class="text-tip-2 text-primary-50-clor">加载中...</span>
</div>
</div>
</div>
</main>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, provide } from 'vue'
import { useRoute } from 'vue-router'
import type { Step, ProposalInfo } from './api'
import { fetchSteps } from './api'
import { getProgressPrediction, parseBillId, transformProposalInfo, generateDefaultFilters } from '@/api/bill/billHome.js'
import PageHeader from './components/PageHeader.vue'
import StepSidebar from './components/StepSidebar.vue'
import Step1FilterCondition from './components/Step1FilterCondition.vue'
import Step2FilterBills from './components/Step2FilterBills.vue'
import Step3PredictionAnalysis from './components/Step3PredictionAnalysis.vue'
const route = useRoute()
// 步骤列表数据
const steps = ref<Step[]>([])
// 当前步骤
const currentStep = ref(1)
// 加载状态
const loading = ref(true)
// 提案信息
const proposalInfo = ref<ProposalInfo | null>(null)
// 默认筛选条件
const defaultFilters = ref<Record<string, string[]>>({})
// 法案ID
const billId = ref('')
// 向子组件提供提案信息
provide('proposalInfo', proposalInfo)
provide('defaultFilters', defaultFilters)
// 页面初始化时加载数据
onMounted(async () => {
try {
// 获取路由参数中的 id
const routeId = route.params.id as string || '1'
billId.value = parseBillId(routeId)
// 并行加载步骤数据和提案信息
const [stepsData, predictionData] = await Promise.all([
fetchSteps(),
getProgressPrediction(billId.value).catch(err => {
console.error('[v0] 获取预测数据失败:', err)
return null
})
])
steps.value = stepsData
if (predictionData) {
// 转换 API 数据为组件所需格式
proposalInfo.value = transformProposalInfo(predictionData)
// 根据提案信息生成默认筛选条件
if (proposalInfo.value) {
defaultFilters.value = generateDefaultFilters(proposalInfo.value)
}
}
} finally {
loading.value = false
}
})
// 下一步
function goToNextStep() {
if (currentStep.value < steps.value.length) {
currentStep.value++
}
}
// 上一步
function goToPreviousStep() {
if (currentStep.value > 1) {
currentStep.value--
}
}
// 重新预测:返回第一步
function handleRepredict() {
currentStep.value = 1
}
</script>
<style scoped>
.app-container {
width: 100%;
min-height: 100vh;
display: flex;
flex-direction: column;
background-color: var(--bg-black-2);
}
.layout-main-center {
width: 1600px;
flex: 1;
padding-bottom: 32px;
box-sizing: border-box;
}
.content-card {
display: flex;
overflow: hidden;
border-radius: var(--radius-10);
border: 1px solid var(--border-black-5);
}
.content-area {
flex: 1;
padding: 10px 26px 29px 25px;
overflow: auto;
}
.loading-container {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
</style>
import { createApp } from 'vue'
import App from './App.vue'
import './style.css'
createApp(App).mount('#app')
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body,
#app {
width: 100%;
height: 100%;
}
...@@ -42,8 +42,8 @@ ...@@ -42,8 +42,8 @@
<el-option v-for="item in num" :key="item" :label="item" :value="item" /> <el-option v-for="item in num" :key="item" :label="item" :value="item" />
</el-select> </el-select>
</template> </template>
<div class="echarts" id="wordCloudChart"> <div class="echarts" ><WordCloudChart v-if="wordLoading" :data="characterView"/></div>
</div>
</AnalysisBox> </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">
...@@ -305,6 +305,7 @@ import HistoricalProposal from "./components/historicalProposal/components/BillT ...@@ -305,6 +305,7 @@ import HistoricalProposal from "./components/historicalProposal/components/BillT
import PotentialNews from './components/historicalProposal/components/PotentialNews.vue' import PotentialNews from './components/historicalProposal/components/PotentialNews.vue'
import getWordCloudChart from "../../utils/worldCloudChart"; import getWordCloudChart from "../../utils/worldCloudChart";
import setChart from "@/utils/setChart"; import setChart from "@/utils/setChart";
import WordCloudChart from "@/components/base/WordCloundChart/index.vue"
import { import {
getCharacterGlobalInfo, getCharacterGlobalInfo,
getCharacterBasicInfo, getCharacterBasicInfo,
...@@ -434,7 +435,7 @@ const getCharacterBasicInfoFn = async () => { ...@@ -434,7 +435,7 @@ const getCharacterBasicInfoFn = async () => {
console.error(error); console.error(error);
} }
}; };
const wordLoading=ref(false)
// 获取人物观点 // 获取人物观点
const characterView = ref({}); const characterView = ref({});
const getCharacterViewFn = async () => { const getCharacterViewFn = async () => {
...@@ -444,6 +445,7 @@ const getCharacterViewFn = async () => { ...@@ -444,6 +445,7 @@ const getCharacterViewFn = async () => {
if (numActive.value !== '全部') { if (numActive.value !== '全部') {
params.year = numActive.value; params.year = numActive.value;
} }
wordLoading.value=false
try { try {
const res = await getCharacterView(params); const res = await getCharacterView(params);
if (res.code === 200) { if (res.code === 200) {
...@@ -456,6 +458,7 @@ const getCharacterViewFn = async () => { ...@@ -456,6 +458,7 @@ const getCharacterViewFn = async () => {
}; };
}); });
} }
wordLoading.value=true
} }
} catch (error) { } catch (error) {
...@@ -465,18 +468,15 @@ const getCharacterViewFn = async () => { ...@@ -465,18 +468,15 @@ const getCharacterViewFn = async () => {
const handleCharacterView = async () => { const handleCharacterView = async () => {
await getCharacterViewFn(); await getCharacterViewFn();
const wordCloudChart = getWordCloudChart(characterView.value); // const wordCloudChart = getWordCloudChart(characterView.value);
setChart(wordCloudChart, "wordCloudChart"); // setChart(wordCloudChart, "wordCloudChart");
}; };
const handleChangeYear = () => { const handleChangeYear = () => {
characterView.value = [] characterView.value = []
handleCharacterView() handleCharacterView()
} }
const yearList = ref([ const yearList = ref([
{ {
label: "全部", label: "全部",
......
...@@ -15,16 +15,9 @@ ...@@ -15,16 +15,9 @@
<p>{{ characterInfo.description }}</p> <p>{{ characterInfo.description }}</p>
</div> </div>
<div class="domain"> <div class="domain">
<p v-for="item in characterInfo.industryList" :key="item" class="cl1" :class="{
cl1: item.status === '1', <AreaTag v-for="tag in characterInfo.industryList" :key="tag"
cl2: item.status === '2', :tag-name="tag.industryName" />
cl3: item.status === '3',
cl4: item.status === '4',
cl5: item.status === '5',
cl6: item.status === '6'
}">
{{ item.industryName }}
</p>
</div> </div>
</div> </div>
</div> </div>
...@@ -35,7 +28,7 @@ ...@@ -35,7 +28,7 @@
</div> </div>
</div> </div>
<!-- 人物详情 --> <!-- 人物详情 -->
<div class="info-content" v-if="infoActive === '人物详情'"> <div class="info-content" v-show="infoActive === '人物详情'">
<div class="left"> <div class="left">
<!-- 科技观点 --> <!-- 科技观点 -->
<AnalysisBox title="科技观点" width="1064px" height="300px" :show-all-btn="false" class="left-top"> <AnalysisBox title="科技观点" width="1064px" height="300px" :show-all-btn="false" class="left-top">
...@@ -52,7 +45,7 @@ ...@@ -52,7 +45,7 @@
<el-option v-for="item in num" :key="item" :label="item" :value="item" /> <el-option v-for="item in num" :key="item" :label="item" :value="item" />
</el-select> </el-select>
</template> </template>
<div class="echarts" id="wordCloudChart"></div> <div class="echarts" v-if="wordloading" ><WordCloudChart :data="characterView"></WordCloudChart></div>
</AnalysisBox> </AnalysisBox>
<!-- 最新动态 --> <!-- 最新动态 -->
...@@ -170,8 +163,9 @@ ...@@ -170,8 +163,9 @@
<template v-if="resumeType === 'career'"> <template v-if="resumeType === 'career'">
<div v-for="item in currentResumeList" :key="item.startTime" class="content-item"> <div v-for="item in currentResumeList" :key="item.startTime" class="content-item">
<img src="./assets/icon01.png" alt="" class="image01" /> <img src="./assets/icon01.png" alt="" class="image01" />
<div class="content-item-time">{{ item.startTime + '-' + getendTime(item) }}</div> <div class="content-item-time">{{ item.startTime.split('-')[0] + '-' + getendTime(item) }}</div>
<div class="content-item-title">{{ item.orgName + '|' + item.jobName }}</div> <div class="content-item-title"><span>{{item.orgName}}</span><span style="margin-left: 5px;margin-right: 5px;">|</span><span>{{ item.jobName }}</span></div>
<!-- <div class="content-item-title">{{ item.orgName + '|' + item.jobName }}</div> -->
<div class="content-item-content">{{ item.content }}</div> <div class="content-item-content">{{ item.content }}</div>
<div class="content-item-door" v-if="item.door"> <div class="content-item-door" v-if="item.door">
<img src="./assets/icon02.png" alt="" /> <img src="./assets/icon02.png" alt="" />
...@@ -183,7 +177,7 @@ ...@@ -183,7 +177,7 @@
<template v-else> <template v-else>
<div v-for="(item, index) in currentResumeList" :key="index" class="content-item"> <div v-for="(item, index) in currentResumeList" :key="index" class="content-item">
<img src="./assets/icon01.png" alt="" class="image01" /> <img src="./assets/icon01.png" alt="" class="image01" />
<div class="content-item-time">{{ item.startTime + '-' + item.endTime }}</div> <div class="content-item-time">{{ item.startTime.split('-')[0] + '-' + getendTime(item) }}</div>
<div class="content-item-title">{{ item.school+'('+item.country+')' }}</div> <div class="content-item-title">{{ item.school+'('+item.country+')' }}</div>
<div class="content-item-content">{{ item.description }}</div> <div class="content-item-content">{{ item.description }}</div>
<!-- <div class="content-item-door" v-if="item.country"> <!-- <div class="content-item-door" v-if="item.country">
...@@ -207,7 +201,7 @@ ...@@ -207,7 +201,7 @@
<div class="viewpoint-header"> <div class="viewpoint-header">
<div class="viewpoint-title"> <div class="viewpoint-title">
<span class="viewpoint-tag">#{{ currentTag }}</span> <span class="viewpoint-tag">#{{ currentTag }}</span>
<span class="viewpoint-label">相关领域标签</span> <span class="viewpoint-label"> 相关领域标签</span>
</div> </div>
<div class="viewpoint-close" @click="dialogVisible = false"> <div class="viewpoint-close" @click="dialogVisible = false">
<el-icon :size="16"> <el-icon :size="16">
...@@ -256,6 +250,7 @@ import { ...@@ -256,6 +250,7 @@ import {
getareaType getareaType
} from "@/api/characterPage/characterPage.js"; } from "@/api/characterPage/characterPage.js";
import AnalysisBox from '@/components/base/boxBackground/analysisBox.vue' import AnalysisBox from '@/components/base/boxBackground/analysisBox.vue'
import WordCloudChart from "@/components/base/WordCloundChart/index.vue"
import Musk from "./assets/Musk.png"; import Musk from "./assets/Musk.png";
import spaceX from "./assets/spaceX.png"; import spaceX from "./assets/spaceX.png";
import tesla from "./assets/tesla.png"; import tesla from "./assets/tesla.png";
...@@ -274,6 +269,7 @@ import { useRoute } from 'vue-router'; ...@@ -274,6 +269,7 @@ import { useRoute } from 'vue-router';
const route = useRoute(); const route = useRoute();
const areaList = ref([]) const areaList = ref([])
const personId = ref(route.query.personId || "Y000064"); const personId = ref(route.query.personId || "Y000064");
const wordloading=ref(false)
const handleGetAreaType = async () => { const handleGetAreaType = async () => {
try { try {
const res = await getareaType(); const res = await getareaType();
...@@ -367,7 +363,7 @@ const getendTime=(item)=>{ ...@@ -367,7 +363,7 @@ const getendTime=(item)=>{
if(item.endTime==null){ if(item.endTime==null){
return item.endTimeStatus==0?'至今':'未知' return item.endTimeStatus==0?'至今':'未知'
}else{ }else{
return item.endTime return item.endTime.split('-')[0]
} }
} }
...@@ -435,9 +431,10 @@ const getCharacterViewFn = async () => { ...@@ -435,9 +431,10 @@ const getCharacterViewFn = async () => {
personId: personId.value personId: personId.value
}; };
if (numActive.value !== '全部') { if (numActive.value !== '全部时间') {
params.year = numActive.value; params.year = numActive.value;
} }
wordloading.value=false
try { try {
const res = await getCharacterView(params); const res = await getCharacterView(params);
if (res.code === 200) { if (res.code === 200) {
...@@ -449,6 +446,7 @@ const getCharacterViewFn = async () => { ...@@ -449,6 +446,7 @@ const getCharacterViewFn = async () => {
value: item.count value: item.count
}; };
}); });
wordloading.value=true
} }
} }
} catch (error) { } catch (error) {
...@@ -459,8 +457,9 @@ const getCharacterViewFn = async () => { ...@@ -459,8 +457,9 @@ const getCharacterViewFn = async () => {
const handleCharacterView = async () => { const handleCharacterView = async () => {
await getCharacterViewFn(); await getCharacterViewFn();
const wordCloudChart = getWordCloudChart(characterView.value); // const wordCloudChart = getWordCloudChart(characterView.value);
setChart(wordCloudChart, "wordCloudChart");
// setChart(wordCloudChart, "wordCloudChart");
}; };
const handleChangeYear = () => { const handleChangeYear = () => {
...@@ -587,8 +586,8 @@ const info = ref(["人物详情", "人物关系"]); ...@@ -587,8 +586,8 @@ const info = ref(["人物详情", "人物关系"]);
// const info = ref(["人物详情", "人物关系", "相关情况"]); // const info = ref(["人物详情", "人物关系", "相关情况"]);
const infoActive = ref("人物详情"); const infoActive = ref("人物详情");
const num = ref(['全部', "2025", "2024", "2023", "2022", "2021", "2020"]); const num = ref(['全部时间' ]);
const numActive = ref("全部"); const numActive = ref("全部时间");
const dialogVisible = ref(false); const dialogVisible = ref(false);
const currentTag = ref(null) const currentTag = ref(null)
...@@ -597,183 +596,19 @@ const handleClickTag = async (tag) => { ...@@ -597,183 +596,19 @@ const handleClickTag = async (tag) => {
dialogVisible.value = true dialogVisible.value = true
await getCharacterFieldViewFn(tag) await getCharacterFieldViewFn(tag)
} }
const newList = ref([ const initTime=()=>{
{ const currentYear = new Date().getFullYear();
id: 1,
title: "能源才是真正的货币。我预测,人工智能和机器人的普及将在 3 年内导致美国出现通缩,并在 20 年内让工作成为可选项。我们需要提前改革教育和社会保障体系,为这一转变做好准备。", const lastFiveYears = [];
titleEn: for (let i = 0; i < 5; i++) {
"Energy is the real currency. I predict that the widespread adoption of AI and robots will lead to deflation in the US within 3 years and make work optional within 20 years. We need to reform the education and social security systems in advance to prepare for this shift.", lastFiveYears.push(String(currentYear - i));
pie: ["人工智能"], }
origin: "X",
time: "2025年10月10日", num.value = ['全部时间', ...lastFiveYears]
type: 1 }
},
{
id: 2,
title: "Neuralink 首位受试者二次植入脑机接口,目标实现下肢自主活动",
content:
"2025 年 11 月 21 日,马斯克宣布 Neuralink 首位人类受试者将接受第二次脑机接口植入手术。新版植入物电极数升级至 3000 个,信号延迟缩短至 15 毫秒,通过 “脑 - 脊信号桥接” 技术激活下肢神经,有望让脊髓损伤患者重新站立,标志着技术从 “操控外部设备” 迈入 “修复自身运动功能” 阶段。",
pie: ["量子科技"],
origin: "华尔街日报",
time: "2025年10月10日",
type: 2
},
{
id: 3,
title: "马斯克预判 AI 财富流向:英伟达、谷歌成关键,入口掌控终极价值",
content:
"2025 年 11 月 30 日,马斯克在播客采访中指出,AI 财富将沿 “芯片→平台→系统→入口” 路径流动。他点名英伟达(算力工具)和谷歌(数据生态)具备核心价值,同时透露 X 平台将打造 “AI 时代超级入口”,整合支付、社交、任务执行等功能,实现 “WeChat++” 愿景。",
pie: ["新能源"],
origin: "金融时报",
time: "2025年10月09日",
type: 2
},
{
id: 4,
title: "特斯拉人形机器人复数定名 “Optimi”,马斯克透露年产亿台目标",
content:
"2025 年 12 月 1 日,马斯克在社交平台确认特斯拉人形机器人 Optimus 的复数形式为 “Optimi”。他重申该机器人将成为特斯拉产量最高的产品,长期年产能目标达数亿台,目前计划在加州、得州工厂分别搭建年产 100 万台和数千万台的生产线。",
pie: ["量子科技"],
origin: "泰晤士报",
time: "2025年10月09日",
type: 2
},
{
id: 5,
title: "究竟要有多少无辜的人死去,美国才能进行改革?我们需要采取激进行动。错误地给非暴力人士贴上 “法西斯” 或 “纳粹” 的标签,应被视为煽动谋杀。这种扣帽子的行为往往被用来为施暴找借口,对社会而言极为危险。",
titleEn:
"How many innocent people have to die before the US reforms? We need radical action. Falsely labeling non-violent people as “fascist” or “Nazi” should be considered incitement to murder. This kind of name-calling is often used to rationalize violence, which is extremely dangerous for society.",
pie: ["人工智能"],
origin: "X",
time: "2025年10月08日",
type: 1
},
{
id: 6,
title: "Grok5 将于 2026 年与《英雄联盟》顶级人类战队对决,规则严格:无数据直读,仅通过摄像头输入;反应时间和点击速度不超过人类水平;必须通过阅读说明书自主学习玩法。目的是测试人工智能的策略与学习能力。",
titleEn:
"Grok5 will play League of Legends against top human teams in 2026, with strict constraints: no data access, only camera input; reaction time and click speed capped at human levels; must learn by reading the manual. The purpose is to test the strategic and learning abilities of AI.",
pie: ["集成电路"],
origin: "X",
time: "2025年10月08日",
type: 1
},
{
id: 7,
title: "SpaceX 频谱收购获 FCC 认可,美国下一代连接技术领先优势强化",
content:
"美国联邦通信委员会(FCC)正式表态支持 SpaceX 收购回声星通信频谱交易。FCC 指出,此次 170 亿美元交易将极大增强行业竞争力,助力美国在下一代连接技术领域巩固领先地位,同时为偏远地区提供更可靠的移动网络覆盖。",
pie: ["量子科技"],
origin: "华盛顿邮报",
time: "2025年10月07日",
type: 2
}
]);
const companyList = ref([
{
id: 1,
cn: "太空探索技术公司",
name: "SpaceX",
logo: spaceX
},
{
id: 2,
cn: "特斯拉",
name: "Tesla",
logo: tesla
},
{
id: 3,
cn: "星链",
name: "Starlink",
logo: starlink
},
{
id: 4,
cn: "X 平台(原推特)",
name: "X (formerly Twitter)",
logo: X
}
]);
// 个人履历
const resumeList = ref([
{
id: 1,
time: "2022-至今",
title: "Twitter/X | 所有者、CTO",
content: "以440亿美元收购Twitter,并担任首席技术官,进行大规模重组和品牌重塑为X。",
door: "旋转门:从硬件科技到社交媒体"
},
{
id: 2,
time: "2004-至今",
title: "Tesla | 董事长、产品架构师,后任CEO",
content: "早期投资者并加入特斯拉,领导产品设计和开发,推动电动汽车革命。推出了多款畅销电动汽车。",
door: "旋转门:从互联网支付到汽车制造"
},
{
id: 3,
time: "2002-至今",
title: "SpaceX | 创始人、CEO兼CTO",
content: "创立太空探索技术公司,目标是降低太空运输成本,最终实现火星殖民。开发了猎鹰系列火箭和龙飞船。",
door: "旋转门:从互联网支付到太空科技"
},
{
id: 4,
time: "1999-2002",
title: "PayPal | 创始人、CEO",
content: "创立太空探索技术公司,目标是降低太空运输成本,最终实现火星殖民。开发了猎鹰系列火箭和龙飞船。",
door: ""
},
{
id: 5,
time: "1995-1999",
title: "Zip2 | 联合创始人",
content: "与弟弟金巴尔·马斯克共同创立了Zip2,为报纸行业提供在线城市指南。1999年康柏以3.07亿美元收购了Zip2。",
door: ""
}
]);
// 弹框数据
const dialogData = ref([
{
id: 1,
name: "山姆・奥特曼",
content: "主张分级监管,反对 “一刀切” 审批;警告美国领先优势有限,电力与基建是关键瓶颈。",
img: img1,
job: "OpenAI CEO"
},
{
id: 2,
name: "布拉德・史密斯",
content: "主张全技术栈监管与基础建设投入,强调美国需在芯片、算法、数据各层保持领先。",
img: img2,
job: "微软副董事长"
},
{
id: 3,
name: "黄仁勋",
content: "认为 AI 处于 “智能基建起步期”,大模型为基础,代理式 AI 为下一阶段;否定 AI 泡沫,强调与实体经济融合。",
img: img3,
job: "NVIDIA CEO"
},
{
id: 4,
name: "杰弗里・辛顿",
content: "担忧 AI 发展过快,警告 “涌现能力” 超预期,可能导致存在性风险;呼吁放缓研发、加强安全。",
img: img4,
job: "图灵奖得主、深度学习先驱"
},
{
id: 5,
name: "李飞飞",
content: "关注 AI 伦理与公平性,强调安全与普惠并重,对 AGI 时间表持谨慎态度。",
img: img5,
job: "斯坦福大学教授"
}
]);
onMounted(() => { onMounted(() => {
initTime()
getCharacterGlobalInfoFn(); getCharacterGlobalInfoFn();
getCharacterBasicInfoFn(); getCharacterBasicInfoFn();
// getCharacterViewFn(); // getCharacterViewFn();
...@@ -791,7 +626,10 @@ onMounted(() => { ...@@ -791,7 +626,10 @@ onMounted(() => {
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
:deep(.el-select-dropdown__item) {
text-align: center !important;
justify-content: center;
}
.tech-leader { .tech-leader {
width: 1600px; width: 1600px;
margin: 0 auto; margin: 0 auto;
...@@ -853,7 +691,8 @@ onMounted(() => { ...@@ -853,7 +691,8 @@ onMounted(() => {
.domain { .domain {
font-size: 14px; font-size: 14px;
display: flex;
gap: 8px;
p { p {
display: inline-block; display: inline-block;
padding: 1px 8px; padding: 1px 8px;
...@@ -1012,7 +851,7 @@ onMounted(() => { ...@@ -1012,7 +851,7 @@ onMounted(() => {
padding-right: 50px; padding-right: 50px;
position: relative; position: relative;
z-index: 110; z-index: 110;
margin-top: 10px;
.main-item { .main-item {
width: 1014px; width: 1014px;
margin-bottom: 40px; margin-bottom: 40px;
...@@ -1490,6 +1329,7 @@ onMounted(() => { ...@@ -1490,6 +1329,7 @@ onMounted(() => {
font-weight: 700; font-weight: 700;
color: rgba(59, 65, 75, 1); color: rgba(59, 65, 75, 1);
line-height: 24px; line-height: 24px;
margin-left: 2px;
} }
.viewpoint-close { .viewpoint-close {
......
...@@ -131,15 +131,13 @@ const createCircularImage = (imageUrl, size) => { ...@@ -131,15 +131,13 @@ const createCircularImage = (imageUrl, size) => {
ctx.drawImage(img, 0, 0, size, size); ctx.drawImage(img, 0, 0, size, size);
resolve(canvas.toDataURL('image/png')); resolve(canvas.toDataURL('image/png'));
} catch (error) { } catch (error) {
console.log("[v0] Canvas error:", error);
resolve(imageUrl); resolve(imageUrl);
} }
}; };
img.onload = onLoadSuccess; img.onload = onLoadSuccess;
img.onerror = () => { img.onerror = () => {
console.log("[v0] Image failed to load, trying without crossOrigin:", imageUrl);
// 如果加载失败,尝试不带 crossOrigin 的方式 // 如果加载失败,尝试不带 crossOrigin 的方式
const img2 = new Image(); const img2 = new Image();
img2.onload = () => { img2.onload = () => {
...@@ -156,13 +154,11 @@ const createCircularImage = (imageUrl, size) => { ...@@ -156,13 +154,11 @@ const createCircularImage = (imageUrl, size) => {
ctx.drawImage(img2, 0, 0, size, size); ctx.drawImage(img2, 0, 0, size, size);
resolve(canvas.toDataURL('image/png')); resolve(canvas.toDataURL('image/png'));
} catch (error) { } catch (error) {
console.log("[v0] Fallback canvas error:", error);
resolve(imageUrl); resolve(imageUrl);
} }
}; };
img2.onerror = () => { img2.onerror = () => {
console.log("[v0] Image loading failed completely:", imageUrl);
resolve(imageUrl); resolve(imageUrl);
}; };
img2.src = imageUrl; img2.src = imageUrl;
...@@ -212,15 +208,11 @@ const getCharacterRelationFn = async () => { ...@@ -212,15 +208,11 @@ const getCharacterRelationFn = async () => {
} }
} }
// 创建圆形头像 // 创建圆形头像
console.log("[v0] characterInfo.value:", characterInfo.value);
console.log("[v0] characterInfo.value.imageUrl:", characterInfo.value.imageUrl);
const centerImageUrl = getProxyUrl(characterInfo.value.imageUrl); const centerImageUrl = getProxyUrl(characterInfo.value.imageUrl);
console.log("[v0] centerImageUrl:", centerImageUrl);
const centerCircularImage = await createCircularImage(centerImageUrl, 160); const centerCircularImage = await createCircularImage(centerImageUrl, 160);
console.log("[v0] centerCircularImage:", centerCircularImage?.substring(0, 50));
if(CharacterRelation.value.length > 0){ if(CharacterRelation.value.length > 0){
// 并行创建所有圆形头像 // 并行创建所有圆形头像
......
...@@ -24,16 +24,16 @@ ...@@ -24,16 +24,16 @@
<div class="info-content" v-show="infoActive === '人物详情'"> <div class="info-content" v-show="infoActive === '人物详情'">
<div class="left"> <div class="left">
<AnalysisBox title="核心观点" width="1064px" height="300px" :show-all-btn="false" class="left-top"> <AnalysisBox title="核心观点" width="1064px" height="300px" :show-all-btn="false" class="left-top">
<template #headerBtn> <template #header-btn>
<el-select v-model="numActive" class="tab-select" :teleported="true" @change="handleChangeYear"> <el-select v-model="numActive" class="tab-select" :teleported="true" @change="handleChangeYear">
<el-option label="全部" value="全部" /> <el-option label="全部" value="全部" />
<el-option v-for="item in num" :key="item" :label="item" :value="item" /> <el-option v-for="item in num" :key="item" :label="item" :value="item" />
</el-select> </el-select>
</template> </template>
<div class="echarts" id="wordCloudChart"></div> <div class="echarts" > <WordCloudChart v-if="wordLoading" :data="characterView" /></div>
</AnalysisBox> </AnalysisBox>
<AnalysisBox title="最新动态" width="1064px" height="1617px" :show-all-btn="false" class="left-bottom"> <AnalysisBox title="最新动态" width="1064px" height="1617px" :show-all-btn="false" class="left-bottom">
<template #headerBtn> <template #header-btn>
<div class="input"><input type="checkbox" v-model="isChecked" @change="handleChange" />只看涉华动态 <div class="input"><input type="checkbox" v-model="isChecked" @change="handleChange" />只看涉华动态
</div> </div>
</template> </template>
...@@ -168,6 +168,7 @@ import CharacterRelationships from "./components/characterRelationships/index.vu ...@@ -168,6 +168,7 @@ import CharacterRelationships from "./components/characterRelationships/index.vu
import RelevantSituation from "./components/relevantSituation/index.vue"; import RelevantSituation from "./components/relevantSituation/index.vue";
import HistoricalProposal from "./components/historicalProposal/components/NewsTracker.vue"; import HistoricalProposal from "./components/historicalProposal/components/NewsTracker.vue";
import getWordCloudChart from "../../utils/worldCloudChart"; import getWordCloudChart from "../../utils/worldCloudChart";
import WordCloudChart from "@/components/base/WordCloundChart/index.vue"
import { import {
getCharacterGlobalInfo, getCharacterGlobalInfo,
getCharacterBasicInfo, getCharacterBasicInfo,
...@@ -249,7 +250,7 @@ const getCharacterBasicInfoFn = async () => { ...@@ -249,7 +250,7 @@ const getCharacterBasicInfoFn = async () => {
console.error(error); console.error(error);
} }
}; };
const wordLoading=ref(false)
const characterView = ref({}); const characterView = ref({});
const getCharacterViewFn = async () => { const getCharacterViewFn = async () => {
const params = { const params = {
...@@ -259,6 +260,7 @@ const getCharacterViewFn = async () => { ...@@ -259,6 +260,7 @@ const getCharacterViewFn = async () => {
if(numActive.value!='全部'){ if(numActive.value!='全部'){
params['year']=numActive.value params['year']=numActive.value
} }
wordLoading.value=false
try { try {
const res = await getCharacterView(params); const res = await getCharacterView(params);
if (res.code === 200) { if (res.code === 200) {
...@@ -271,6 +273,7 @@ const getCharacterViewFn = async () => { ...@@ -271,6 +273,7 @@ const getCharacterViewFn = async () => {
}; };
}); });
} }
wordLoading.value=true
} }
} catch (error) { } catch (error) {
...@@ -280,8 +283,8 @@ const getCharacterViewFn = async () => { ...@@ -280,8 +283,8 @@ const getCharacterViewFn = async () => {
const handleCharacterView = async () => { const handleCharacterView = async () => {
await getCharacterViewFn(); await getCharacterViewFn();
const wordCloudChart = getWordCloudChart(characterView.value); // const wordCloudChart = getWordCloudChart(characterView.value);
setChart(wordCloudChart, "wordCloudChart"); // setChart(wordCloudChart, "wordCloudChart");
}; };
const handleChangeYear = (value) => { const handleChangeYear = (value) => {
...@@ -415,7 +418,7 @@ onMounted(() => { ...@@ -415,7 +418,7 @@ onMounted(() => {
const info = ref(["人物详情", "成果报告", "人物关系" ]); const info = ref(["人物详情", "成果报告", "人物关系" ]);
// const info = ref(["人物详情", "成果报告", "人物关系", "相关情况"]); // const info = ref(["人物详情", "成果报告", "人物关系", "相关情况"]);
const infoActive = ref("人物详情"); const infoActive = ref("人物详情");
const num = ref(["2025", "2024", "2023", "2022", "2021", "2020"]); const num = ref(["2026","2025", "2024", "2023", "2022", "2021" ]);
const numActive = ref("全部"); const numActive = ref("全部");
......
...@@ -608,7 +608,7 @@ export const raderOption1 = (data) => { ...@@ -608,7 +608,7 @@ export const raderOption1 = (data) => {
const indicatorNames = data.map(item => item.areaName); const indicatorNames = data.map(item => item.areaName);
const indicatorValues = data.map(item => item.areaValue); const indicatorValues = data.map(item => item.areaValue);
// 动态生成雷���图的 indicator 配置 // indicator 配置
const indicators = indicatorNames.map((name, index) => ({ const indicators = indicatorNames.map((name, index) => ({
name, name,
max: Math.max(...indicatorValues) * 1.2 // 设置最大值为所有值的最大值的1.2倍 max: Math.max(...indicatorValues) * 1.2 // 设置最大值为所有值的最大值的1.2倍
......
...@@ -175,10 +175,43 @@ ...@@ -175,10 +175,43 @@
</div> </div>
</div> </div>
</div> --> </div> -->
<NewsList :newsList="newsList" @item-click="handleNewsInfoClick" @more-click="handleToMoreNews" <NewsList style="margin-right:16px" :newsList="newsList" @item-click="handleNewsInfoClick" @more-click="handleToMoreNews"
img="newsImage" title="newsTitle" content="newsContent" /> img="newsImage" title="newsTitle" content="newsContent" />
<MessageBubble :messageList="messageList" imageUrl="personImage" @more-click="handleToSocialDetail" <div class="social-media-wrapper">
name="personName" content="remarks" source="orgName" /> <!-- 翻页按钮 - 绝对定位到右上角 -->
<div class="pagination-btns">
<button
class="page-btn"
:class="{ disabled: currentPage <= 1 }"
:disabled="currentPage <= 1"
@click="prevPage"
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M10 12L6 8L10 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<button
class="page-btn"
:class="{ disabled: currentPage >= totalPages }"
:disabled="currentPage >= totalPages"
@click="nextPage"
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M6 4L10 8L6 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div>
<!-- MessageBubble 组件 -->
<MessageBubble
:messageList="paginatedList"
imageUrl="personImage"
@more-click="handleToSocialDetail"
name="personName"
content="remarks"
source="orgName"
/>
</div>
<!-- <div class="box4"> <!-- <div class="box4">
<div class="box4-header"> <div class="box4-header">
<div class="header-icon"> <div class="header-icon">
...@@ -356,7 +389,31 @@ const handleCurrentChange = page => { ...@@ -356,7 +389,31 @@ const handleCurrentChange = page => {
currentPage.value = page; currentPage.value = page;
handleFindListBySubjectTypeId(); handleFindListBySubjectTypeId();
}; };
// 计算总页数
const totalPages = computed(() => {
return Math.ceil(messageList.value.length / pageSize.value) || 1;
});
// 当前页的数据
const paginatedList = computed(() => {
const start = (currentPage.value - 1) * pageSize.value;
const end = start + pageSize.value;
return messageList.value.slice(start, end);
});
// 上一页
const prevPage = () => {
if (currentPage.value > 1) {
currentPage.value--;
}
};
// 下一页
const nextPage = () => {
if (currentPage.value < totalPages.value) {
currentPage.value++;
}
};
// 地理分布 // 地理分布
const distributionList = ref([ const distributionList = ref([
{ {
...@@ -568,38 +625,22 @@ const handleGetNews = async () => { ...@@ -568,38 +625,22 @@ const handleGetNews = async () => {
// 社交媒体 // 社交媒体
const messageList = ref([ const messageList = ref([
{
img: Message1,
name: "贾森·史密斯",
time: "15:23 · 发布于真实社交",
content: `埃隆·马斯克在强力支持我竞选总统之前,早就知道我强烈反对‘电动汽车强制令’。这太荒谬了,这一直是我竞选活动的主要部分。电动汽车没问题,但不应该强迫每个人都拥有一辆。埃隆获得的补贴可能远远超过历史上任何一个人。如果没有补贴,埃隆可能不得不关门大吉,回到南非老家。`
},
{
img: Message2,
name: "詹姆斯·布莱尔",
time: "14:49 · 发布于X",
content: `如果这个疯狂的支出法案获得通过,‘美国党’将在第二天成立。`
},
{
img: Message3,
name: "塞巴斯蒂安·马拉比",
time: "11:05 · 发布于X",
content: `提出特朗普政府的AI政策强调技术开放与快速应用,但可能以牺牲安全防范为代价,开启了“潘多拉魔盒”。`
}
]); ]);
const handleGetSocialMediaInfo = async () => { const handleGetSocialMediaInfo = async () => {
try { try {
const res = await getSocialMediaInfo(); const res = await getSocialMediaInfo();
console.log("社交媒体", res); console.log("社交媒体", res);
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
messageList.value = res.data.map(item => ({ messageList.value = res.data.map(item => ({
...item, ...item,
time: item.time.replace(/T/, " "), time: item.time.replace(/T/, " "),
})); }));
} currentPage.value = 1;
} catch (error) { }
console.error("获取社交媒体error", error); } catch (error) {
} console.error("获取社交媒体error", error);
}
}; };
// 政令涉及领域 // 政令涉及领域
...@@ -2382,4 +2423,45 @@ onMounted(async () => { ...@@ -2382,4 +2423,45 @@ onMounted(async () => {
:deep(.table-row) { :deep(.table-row) {
height: 64px; height: 64px;
} }
.social-media-wrapper {
position: relative;
display: inline-block;
}
.pagination-btns {
position: absolute;
top: 10px;
right: 20px;
display: flex;
align-items: center;
gap: 8px;
z-index: 10;
}
.page-btn {
width: 28px;
height: 28px;
border: 1px solid #055fc2;
border-radius: 4px;
background: #fff;
color: #055fc2;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
padding: 0;
&:hover:not(.disabled) {
background: #055fc2;
color: #fff;
}
&.disabled {
cursor: not-allowed;
opacity: 0.4;
border-color: #c0c4cc;
color: #c0c4cc;
}
}
</style> </style>
\ No newline at end of file
<template>
<div class="cases-timeline">
<div class="timeline-line-row">
<div class="top-line">
<div class="top-line1"></div>
</div>
</div>
<div class="cases-carousel">
<LeftBtn @click="prevCases" />
<div class="cases-items">
<div v-for="(item, index) in visibleCases" :key="index" class="case-item">
<div class="case-date">{{ item.date }}</div>
<div class="node-dot"></div>
<div class="connector-line"></div>
<div class="case-card">
<div class="case-tag" :class="item.tagType">{{ item.tag }}</div>
<div class="case-partner">合作主体:{{ item.partner }}</div>
<div class="case-title">{{ item.title }}</div>
</div>
</div>
</div>
<RightBtn @click="nextCases" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import LeftBtn from '@/components/base/PageBtn/LeftBtn.vue'
import RightBtn from '@/components/base/PageBtn/RightBtn.vue'
interface CaseItem {
date: string
tag: string
tagType: 'project' | 'patent' | 'paper'
partner: string
title: string
}
const props = withDefaults(defineProps<{
data?: CaseItem[]
pageSize?: number
}>(), {
pageSize: 4
})
const defaultData: CaseItem[] = [
{ date: '2025年3月12日', tag: '项目合作', tagType: 'project', partner: '清华大学', title: '"气候变化、碳中和与能源智联"(CNEST)研讨会' },
{ date: '2025年5月7日', tag: '专利合作', tagType: 'patent', partner: '北京航空航天大学', title: '北航-哈佛 Wood 实验室联合研制鲤鱼仿生吸盘机器人' },
{ date: '2025年6月23日', tag: '论文合作', tagType: 'paper', partner: '山西省古建筑与彩塑壁画保护研究院', title: '山西古建院-哈佛中国艺术实验室合作' },
{ date: '2025年10月19日', tag: '项目合作', tagType: 'project', partner: '复旦大学、四川大学', title: '哈佛中国健康伙伴计划多机构合作' },
{ date: '2024年12月5日', tag: '论文合作', tagType: 'paper', partner: '北京大学', title: '人工智能在医疗诊断中的应用研究' },
{ date: '2024年9月18日', tag: '专利合作', tagType: 'patent', partner: '中国科学院', title: '新型纳米材料在能源存储中的应用专利' },
{ date: '2024年7月22日', tag: '项目合作', tagType: 'project', partner: '上海交通大学', title: '智能制造与工业4.0联合研究项目' },
{ date: '2024年4月10日', tag: '论文合作', tagType: 'paper', partner: '浙江大学', title: '量子计算在密码学中的突破性进展' },
{ date: '2024年2月28日', tag: '专利合作', tagType: 'patent', partner: '南京大学', title: '生物降解塑料新型合成技术专利' },
{ date: '2023年11月15日', tag: '项目合作', tagType: 'project', partner: '武汉大学', title: '长江流域生态环境保护联合研究' }
]
const casesData = computed(() => props.data && props.data.length > 0 ? props.data : defaultData)
const currentPage = ref(0)
const visibleCases = computed(() => {
const start = currentPage.value * props.pageSize
return casesData.value.slice(start, start + props.pageSize)
})
const totalPages = computed(() => Math.ceil(casesData.value.length / props.pageSize))
// 上一页
const prevCases = () => {
if (currentPage.value > 0) {
currentPage.value--
}
}
// 下一页
const nextCases = () => {
if (currentPage.value < totalPages.value - 1) {
currentPage.value++
}
}
</script>
<style lang="scss" scoped>
.cases-timeline {
padding: 24px 16px;
position: relative;
.timeline-line-row {
position: absolute;
left: 48px;
right: 48px;
top: calc(24px + 22px + 8px + 11px - 4px);
height: 8px;
z-index: 0;
.top-line {
width: 100%;
height: 8px;
background: url('../assets/top-line-icon.png') repeat-x;
background-size: auto 8px;
.top-line1 {
display: none;
}
}
}
.cases-carousel {
display: flex;
align-items: center;
gap: 60px;
.cases-items {
flex: 1;
display: flex;
gap: 60px;
overflow: hidden;
margin-top: 9px;
.case-item {
flex-shrink: 0;
width: 273px;
display: flex;
flex-direction: column;
align-items: flex-start;
position: relative;
z-index: 1;
.case-date {
font-size: 14px;
font-weight: 700;
color: #055FC2;
margin-bottom: 8px;
padding-left: 2px;
}
.node-dot {
width: 11px;
height: 11px;
border-radius: 50%;
background: #fff;
border: 4px solid #1677FF;
box-sizing: border-box;
position: relative;
z-index: 2;
}
.connector-line {
width: 1px;
height: 20px;
background: #1677FF;
margin-left: 5px;
}
.case-card {
width: 273px;
min-height: 210px;
padding: 16px;
background: #FFFFFF;
border: 1px solid #E5E7EB;
border-radius: 4px;
display: flex;
flex-direction: column;
gap: 8px;
.case-tag {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
width: fit-content;
&.project {
background: #FFF1F0;
color: #F5222D;
border: 1px solid #FFA39E;
}
&.patent {
background: #FFFBE6;
color: #FAAD14;
border: 1px solid #FFE58F;
}
&.paper {
background: #F0F5FF;
color: #2F54EB;
border: 1px solid #ADC6FF;
}
}
.case-partner {
font-size: 14px;
color: #5F656C;
}
.case-title {
font-size: 16px;
font-weight: 700;
color: #3B414B;
line-height: 24px;
}
}
}
}
}
}
</style>
<template>
<div class="semi-donut-chart-container">
<div ref="chartRef" class="chart"></div>
<div class="chart-legend">
<div v-for="(item, index) in legendData" :key="index" class="legend-item">
<span class="legend-dot" :style="{ background: colors[index] }"></span>
<span class="legend-label">{{ item.name }}</span>
<span class="legend-value">{{ item.value }}%</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, shallowRef, computed } from 'vue'
import * as echarts from 'echarts'
import { semiDonutOption } from './bindEcharts'
const props = defineProps<{
data: {
names: string[]
values: number[]
}
}>()
const chartRef = ref<HTMLDivElement>()
const chartInstance = shallowRef<echarts.ECharts>()
const colors = ['rgba(105, 177, 255, 1)', 'rgba(255, 192, 105, 1)', 'rgba(135, 232, 222, 1)']
const legendData = computed(() => {
return props.data.names.map((name, index) => ({
name,
value: props.data.values[index]
}))
})
function initChart() {
if (!chartRef.value) return
chartInstance.value = echarts.init(chartRef.value)
updateChart()
}
function updateChart() {
if (!chartInstance.value) return
if (!props.data?.names?.length) return
const option = semiDonutOption(props.data)
chartInstance.value.setOption(option)
}
function handleResize() {
chartInstance.value?.resize()
}
onMounted(() => {
initChart()
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
chartInstance.value?.dispose()
})
watch(() => props.data, updateChart, { deep: true })
</script>
<style scoped>
.semi-donut-chart-container {
display: flex;
align-items: center;
justify-content: center;
gap: 40px;
padding: 24px 40px;
height: 320px;
.chart {
width: 240px;
height: 240px;
flex-shrink: 0;
}
.chart-legend {
display: flex;
flex-direction: column;
gap: 16px;
flex-shrink: 0;
.legend-item {
display: flex;
align-items: center;
gap: 8px;
white-space: nowrap;
.legend-dot {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
}
.legend-label {
font-size: 14px;
color: #3B414B;
}
.legend-value {
font-size: 14px;
font-weight: 700;
color: #3B414B;
margin-left: 8px;
}
}
}
}
</style>
// echarts 图表配置
export const pieOption = (data: { names: string[]; values: number[]; total: number }) => {
const seriesData = data.names.map((name, index) => ({
value: data.values[index],
name: name
}))
return {
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 'right',
data: data.names
},
series: [
{
name: '合作类型',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: [8, 8, 0, 0]
},
label: {
show: false
},
emphasis: {
label: {
show: true
}
},
labelLine: {
show: false
},
data: seriesData,
color: ['rgba(105, 177, 255, 1)', 'rgba(255, 192, 105, 1)', 'rgba(135, 232, 222, 1)']
}
]
}
}
// 完整环形饼图配置
export const semiDonutOption = (data: { names: string[]; values: number[] }) => {
const seriesData = data.names.map((name, index) => ({
value: data.values[index],
name: name
}))
return {
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
show: false
},
series: [
{
name: '合作类型',
type: 'pie',
center: ['50%', '50%'],
radius: ['55%', '80%'],
avoidLabelOverlap: false,
itemStyle: {
borderWidth: 2,
borderColor: '#fff'
},
label: {
show: false
},
emphasis: {
label: {
show: false
}
},
labelLine: {
show: false
},
data: seriesData,
color: ['rgba(105, 177, 255, 1)', 'rgba(255, 192, 105, 1)', 'rgba(135, 232, 222, 1)']
}
]
}
}
...@@ -61,6 +61,7 @@ ...@@ -61,6 +61,7 @@
<!-- 科研实力Tab --> <!-- 科研实力Tab -->
<ResearchStrength v-else-if="activeTab === '科研实力'" /> <ResearchStrength v-else-if="activeTab === '科研实力'" />
<<<<<<< HEAD
<!-- 合作情况Tab --> <!-- 合作情况Tab -->
<Cooperation v-else-if="activeTab === '合作情况'" /> <Cooperation v-else-if="activeTab === '合作情况'" />
...@@ -68,6 +69,11 @@ ...@@ -68,6 +69,11 @@
<div class="placeholder-content" v-else> <div class="placeholder-content" v-else>
<div class="placeholder-text">{{ activeTab }}内容开发中...</div> <div class="placeholder-text">{{ activeTab }}内容开发中...</div>
</div> </div>
=======
<Cooperation v-else-if="activeTab === '合作情况'" />
<OtherInfo v-else-if="activeTab === '其他情况'" />
>>>>>>> c71276c18548779e8e676aea3b390352568ff20b
</div> </div>
</div> </div>
</div> </div>
...@@ -79,6 +85,10 @@ import AreaTag from '@/components/base/AreaTag/index.vue' ...@@ -79,6 +85,10 @@ import AreaTag from '@/components/base/AreaTag/index.vue'
import SchoolDetail from './tabs/SchoolDetail.vue' import SchoolDetail from './tabs/SchoolDetail.vue'
import ResearchStrength from './tabs/ResearchStrength.vue' import ResearchStrength from './tabs/ResearchStrength.vue'
import Cooperation from './tabs/Cooperation.vue' import Cooperation from './tabs/Cooperation.vue'
<<<<<<< HEAD
=======
import OtherInfo from './tabs/OtherInfo.vue'
>>>>>>> c71276c18548779e8e676aea3b390352568ff20b
// 大学基本信息 // 大学基本信息
const universityInfo = ref({ const universityInfo = ref({
...@@ -225,7 +235,11 @@ const latestDynamics = ref([ ...@@ -225,7 +235,11 @@ const latestDynamics = ref([
date: '9月15日', date: '9月15日',
title: '艾伦·M·加伯接任临时校长', title: '艾伦·M·加伯接任临时校长',
content: '原教务长艾伦·加伯(Alan M. Garber)出任哈佛第30任校长(临时),并完整复学术诚信与校园团结。', content: '原教务长艾伦·加伯(Alan M. Garber)出任哈佛第30任校长(临时),并完整复学术诚信与校园团结。',
<<<<<<< HEAD
tags: [{ name: '集成��路', type: 'tag6' }], tags: [{ name: '集成��路', type: 'tag6' }],
=======
tags: [{ name: '集成电路', type: 'tag6' }],
>>>>>>> c71276c18548779e8e676aea3b390352568ff20b
isHighlight: true isHighlight: true
}, },
{ {
...@@ -342,9 +356,18 @@ const handleVisitWebsite = () => { ...@@ -342,9 +356,18 @@ const handleVisitWebsite = () => {
} }
.header-section { .header-section {
<<<<<<< HEAD
padding: 80px 160px 30px; padding: 80px 160px 30px;
.header-content { .header-content {
=======
padding: 80px;
display: flex;
justify-content: center;
.header-content {
width: 1600px;
>>>>>>> c71276c18548779e8e676aea3b390352568ff20b
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
gap: 24px; gap: 24px;
...@@ -429,11 +452,22 @@ const handleVisitWebsite = () => { ...@@ -429,11 +452,22 @@ const handleVisitWebsite = () => {
.nav-tabs { .nav-tabs {
display: flex; display: flex;
<<<<<<< HEAD
margin: 0 160px 16px; margin: 0 160px 16px;
=======
justify-content: center;
margin: 0 auto 16px;
>>>>>>> c71276c18548779e8e676aea3b390352568ff20b
background: rgba(255, 255, 255, 0.65); background: rgba(255, 255, 255, 0.65);
border-radius: 10px; border-radius: 10px;
padding: 5px; padding: 5px;
box-shadow: 0px 0px 20px rgba(25, 69, 130, 0.1); box-shadow: 0px 0px 20px rgba(25, 69, 130, 0.1);
<<<<<<< HEAD
=======
width: 1600px;
margin-left: auto;
margin-right: auto;
>>>>>>> c71276c18548779e8e676aea3b390352568ff20b
.nav-tab { .nav-tab {
flex: 1; flex: 1;
...@@ -464,7 +498,16 @@ const handleVisitWebsite = () => { ...@@ -464,7 +498,16 @@ const handleVisitWebsite = () => {
} }
.main-content { .main-content {
<<<<<<< HEAD
padding: 0 160px; padding: 0 160px;
=======
display: flex;
justify-content: center;
> * {
width: 1600px;
}
>>>>>>> c71276c18548779e8e676aea3b390352568ff20b
} }
.placeholder-content { .placeholder-content {
......
<template> <template>
<div class="cooperation-content"> <div class="cooperation-content">
<<<<<<< HEAD
<!-- 悬浮提示框 --> <!-- 悬浮提示框 -->
=======
>>>>>>> c71276c18548779e8e676aea3b390352568ff20b
<Teleport to="body"> <Teleport to="body">
<div v-if="tooltipVisible" class="chart-tooltip" <div v-if="tooltipVisible" class="chart-tooltip"
:style="{ left: tooltipPosition.x + 10 + 'px', top: tooltipPosition.y - 30 + 'px' }"> :style="{ left: tooltipPosition.x + 10 + 'px', top: tooltipPosition.y - 30 + 'px' }">
...@@ -9,9 +12,13 @@ ...@@ -9,9 +12,13 @@
</div> </div>
</Teleport> </Teleport>
<<<<<<< HEAD
<!-- 第一行:数量变化 + 类型变化 --> <!-- 第一行:数量变化 + 类型变化 -->
<div class="chart-row"> <div class="chart-row">
<!-- 与中国合作数量变化 - 面积图 --> <!-- 与中国合作数量变化 - 面积图 -->
=======
<div class="chart-row">
>>>>>>> c71276c18548779e8e676aea3b390352568ff20b
<AnalysisBox title="与中国合作数量变化" width="100%" height="500px" :show-all-btn="false" class="chart-box"> <AnalysisBox title="与中国合作数量变化" width="100%" height="500px" :show-all-btn="false" class="chart-box">
<div class="chart-container"> <div class="chart-container">
<div class="area-chart"> <div class="area-chart">
...@@ -32,7 +39,10 @@ ...@@ -32,7 +39,10 @@
</defs> </defs>
<path :d="quantityAreaPath" fill="url(#coopAreaGradient1)" /> <path :d="quantityAreaPath" fill="url(#coopAreaGradient1)" />
<path :d="quantityLinePath" fill="none" stroke="#FF954D" stroke-width="2" /> <path :d="quantityLinePath" fill="none" stroke="#FF954D" stroke-width="2" />
<<<<<<< HEAD
<!-- 8px圆圈节点 --> <!-- 8px圆圈节点 -->
=======
>>>>>>> c71276c18548779e8e676aea3b390352568ff20b
<g class="data-points"> <g class="data-points">
<circle v-for="(point, idx) in quantityPoints" :key="idx" <circle v-for="(point, idx) in quantityPoints" :key="idx"
:cx="point.x" :cy="point.y" r="4" :cx="point.x" :cy="point.y" r="4"
...@@ -50,14 +60,19 @@ ...@@ -50,14 +60,19 @@
</div> </div>
<ChartSummary text="近十年哈佛大学与中国合作项目数量呈'前期稳步增多、2020年后敏感领域收缩、非敏感领域小幅复苏'的趋势。" /> <ChartSummary text="近十年哈佛大学与中国合作项目数量呈'前期稳步增多、2020年后敏感领域收缩、非敏感领域小幅复苏'的趋势。" />
</AnalysisBox> </AnalysisBox>
<<<<<<< HEAD
<!-- 与中国合作类型变化 - 半环形饼图 --> <!-- 与中国合作类型变化 - 半环形饼图 -->
=======
>>>>>>> c71276c18548779e8e676aea3b390352568ff20b
<AnalysisBox title="与中国合作类型变化" width="100%" height="500px" :show-all-btn="false" class="chart-box"> <AnalysisBox title="与中国合作类型变化" width="100%" height="500px" :show-all-btn="false" class="chart-box">
<template #header-btn> <template #header-btn>
<el-select v-model="selectedYear" size="small" style="width: 80px;"> <el-select v-model="selectedYear" size="small" style="width: 80px;">
<el-option v-for="year in yearOptions" :key="year" :label="year" :value="year" /> <el-option v-for="year in yearOptions" :key="year" :label="year" :value="year" />
</el-select> </el-select>
</template> </template>
<<<<<<< HEAD
<div class="chart-container"> <div class="chart-container">
<div class="semi-donut-chart"> <div class="semi-donut-chart">
<div class="donut-wrapper"> <div class="donut-wrapper">
...@@ -87,14 +102,25 @@ ...@@ -87,14 +102,25 @@
</div> </div>
</div> </div>
</div> </div>
=======
<SemiDonutChart :data="{
names: ['项目合作', '论文合作', '专利合作'],
values: [57, 25, 18]
}" />
>>>>>>> c71276c18548779e8e676aea3b390352568ff20b
<ChartSummary text="哈佛大学与中国的合作当中,大部分是以项目合作为主" /> <ChartSummary text="哈佛大学与中国的合作当中,大部分是以项目合作为主" />
</AnalysisBox> </AnalysisBox>
</div> </div>
<<<<<<< HEAD
<!-- 第二行:领域变化 + 经费变化 --> <!-- 第二行:领域变化 + 经费变化 -->
<div class="chart-row"> <div class="chart-row">
<!-- 与中国合作领域变化 - 堆叠柱状图 --> <!-- 与中国合作领域变化 - 堆叠柱状图 -->
<AnalysisBox title="与中国合作领域变化" width="100%" height="500px" :show-all-btn="false" class="chart-box"> <AnalysisBox title="与中国合作领域变化" width="100%" height="500px" :show-all-btn="false" class="chart-box">
=======
<div class="chart-row">
<AnalysisBox title="与中国合作领域变化" width="100%" height="500px" :show-all-btn="false" class="chart-box">
>>>>>>> c71276c18548779e8e676aea3b390352568ff20b
<div class="chart-container"> <div class="chart-container">
<div class="stacked-bar-chart"> <div class="stacked-bar-chart">
<!-- 图例 --> <!-- 图例 -->
...@@ -129,8 +155,12 @@ ...@@ -129,8 +155,12 @@
<ChartSummary text="近十年哈佛大学与中国合作项目中,各领域分布均匀,其中以人工智能与能源领域为主。" /> <ChartSummary text="近十年哈佛大学与中国合作项目中,各领域分布均匀,其中以人工智能与能源领域为主。" />
</AnalysisBox> </AnalysisBox>
<<<<<<< HEAD
<!-- 与中国合作经费变化 - 面积图 --> <!-- 与中国合作经费变化 - 面积图 -->
<AnalysisBox title="与中国合作经费变化" width="100%" height="500px" :show-all-btn="false" class="chart-box"> <AnalysisBox title="与中国合作经费变化" width="100%" height="500px" :show-all-btn="false" class="chart-box">
=======
<AnalysisBox title="与中国合作经费变化" width="100%" height="500px" :show-all-btn="false" class="chart-box">
>>>>>>> c71276c18548779e8e676aea3b390352568ff20b
<div class="chart-container"> <div class="chart-container">
<div class="area-chart"> <div class="area-chart">
<div class="y-axis"> <div class="y-axis">
...@@ -170,6 +200,7 @@ ...@@ -170,6 +200,7 @@
</AnalysisBox> </AnalysisBox>
</div> </div>
<<<<<<< HEAD
<!-- 第三行:与中国合作事例 --> <!-- 第三行:与中国合作事例 -->
<AnalysisBox title="与中国合作事例" width="100%" height="auto" :show-all-btn="false" class="cases-box"> <AnalysisBox title="与中国合作事例" width="100%" height="auto" :show-all-btn="false" class="cases-box">
<div class="cases-timeline"> <div class="cases-timeline">
...@@ -209,6 +240,10 @@ ...@@ -209,6 +240,10 @@
</button> </button>
</div> </div>
</div> </div>
=======
<AnalysisBox title="与中国合作事例" width="100%" height="auto" :show-all-btn="false" class="cases-box">
<CooperationCases />
>>>>>>> c71276c18548779e8e676aea3b390352568ff20b
</AnalysisBox> </AnalysisBox>
</div> </div>
</template> </template>
...@@ -217,6 +252,11 @@ ...@@ -217,6 +252,11 @@
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import AnalysisBox from '@/components/base/boxBackground/analysisBox.vue' import AnalysisBox from '@/components/base/boxBackground/analysisBox.vue'
import ChartSummary from '../components/ChartSummary.vue' import ChartSummary from '../components/ChartSummary.vue'
<<<<<<< HEAD
=======
import SemiDonutChart from '../components/SemiDonutChart.vue'
import CooperationCases from '../components/CooperationCases.vue'
>>>>>>> c71276c18548779e8e676aea3b390352568ff20b
// Tooltip 状态 // Tooltip 状态
const tooltipVisible = ref(false) const tooltipVisible = ref(false)
...@@ -308,6 +348,7 @@ const fieldData = [ ...@@ -308,6 +348,7 @@ const fieldData = [
]} ]}
] ]
<<<<<<< HEAD
// 合作事例数据 // 合作事例数据
const casesData = ref([ const casesData = ref([
{ date: '2025年3月12日', tag: '项目合作', tagType: 'project', partner: '清华大学', title: '"气候变化、碳中和与能源智联"(CNEST)研讨会' }, { date: '2025年3月12日', tag: '项目合作', tagType: 'project', partner: '清华大学', title: '"气候变化、碳中和与能源智联"(CNEST)研讨会' },
...@@ -335,6 +376,8 @@ const nextCases = () => { ...@@ -335,6 +376,8 @@ const nextCases = () => {
} }
} }
=======
>>>>>>> c71276c18548779e8e676aea3b390352568ff20b
// 面积图路径计算 // 面积图路径计算
const createAreaPath = (data, maxValue = 400) => { const createAreaPath = (data, maxValue = 400) => {
const width = 700 const width = 700
...@@ -379,6 +422,7 @@ const getDataPoints = (data, maxValue = 400) => { ...@@ -379,6 +422,7 @@ const getDataPoints = (data, maxValue = 400) => {
const quantityPoints = computed(() => getDataPoints(quantityData)) const quantityPoints = computed(() => getDataPoints(quantityData))
const fundingPoints = computed(() => getDataPoints(fundingData)) const fundingPoints = computed(() => getDataPoints(fundingData))
<<<<<<< HEAD
// 半环形饼图路径计算 // 半环形饼图路径计算
const getDonutPath = (startPercent, endPercent) => { const getDonutPath = (startPercent, endPercent) => {
const cx = 100 const cx = 100
...@@ -402,6 +446,8 @@ const getDonutPath = (startPercent, endPercent) => { ...@@ -402,6 +446,8 @@ const getDonutPath = (startPercent, endPercent) => {
return `M${x1},${y1} A${outerR},${outerR} 0 ${largeArc} 1 ${x2},${y2} L${x3},${y3} A${innerR},${innerR} 0 ${largeArc} 0 ${x4},${y4} Z` return `M${x1},${y1} A${outerR},${outerR} 0 ${largeArc} 1 ${x2},${y2} L${x3},${y3} A${innerR},${innerR} 0 ${largeArc} 0 ${x4},${y4} Z`
} }
=======
>>>>>>> c71276c18548779e8e676aea3b390352568ff20b
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
...@@ -427,8 +473,12 @@ const getDonutPath = (startPercent, endPercent) => { ...@@ -427,8 +473,12 @@ const getDonutPath = (startPercent, endPercent) => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
<<<<<<< HEAD
// 面积图样式 // 面积图样式
=======
>>>>>>> c71276c18548779e8e676aea3b390352568ff20b
.area-chart { .area-chart {
flex: 1; flex: 1;
display: flex; display: flex;
...@@ -472,6 +522,7 @@ const getDonutPath = (startPercent, endPercent) => { ...@@ -472,6 +522,7 @@ const getDonutPath = (startPercent, endPercent) => {
} }
} }
} }
<<<<<<< HEAD
// 半环形饼图样式 // 半环形饼图样式
.semi-donut-chart { .semi-donut-chart {
...@@ -526,6 +577,9 @@ const getDonutPath = (startPercent, endPercent) => { ...@@ -526,6 +577,9 @@ const getDonutPath = (startPercent, endPercent) => {
} }
// 堆叠柱状图样式 // 堆叠柱状图样式
=======
>>>>>>> c71276c18548779e8e676aea3b390352568ff20b
.stacked-bar-chart { .stacked-bar-chart {
flex: 1; flex: 1;
display: flex; display: flex;
...@@ -581,10 +635,17 @@ const getDonutPath = (startPercent, endPercent) => { ...@@ -581,10 +635,17 @@ const getDonutPath = (startPercent, endPercent) => {
.bar-stack { .bar-stack {
display: flex; display: flex;
<<<<<<< HEAD
flex-direction: column-reverse; // 从底部开始堆叠 flex-direction: column-reverse; // 从底部开始堆叠
align-items: center; align-items: center;
width: 20px; width: 20px;
height: 240px; // 必须设置高度,子元素百分比才能生效 height: 240px; // 必须设置高度,子元素百分比才能生效
=======
flex-direction: column-reverse;
align-items: center;
width: 20px;
height: 240px;
>>>>>>> c71276c18548779e8e676aea3b390352568ff20b
position: relative; position: relative;
.bar-segment { .bar-segment {
...@@ -608,13 +669,18 @@ const getDonutPath = (startPercent, endPercent) => { ...@@ -608,13 +669,18 @@ const getDonutPath = (startPercent, endPercent) => {
} }
} }
} }
<<<<<<< HEAD
// 合作事例样式 // 合作事例样式
=======
>>>>>>> c71276c18548779e8e676aea3b390352568ff20b
.cases-box { .cases-box {
:deep(.wrapper-main) { :deep(.wrapper-main) {
overflow: visible; overflow: visible;
} }
} }
<<<<<<< HEAD
.cases-timeline { .cases-timeline {
padding: 24px; padding: 24px;
...@@ -760,4 +826,6 @@ const getDonutPath = (startPercent, endPercent) => { ...@@ -760,4 +826,6 @@ const getDonutPath = (startPercent, endPercent) => {
} }
} }
} }
=======
>>>>>>> c71276c18548779e8e676aea3b390352568ff20b
</style> </style>
<template>
<div class="other-info-content">
<AnalysisBox title="重点实验室" width="100%" height="auto" :show-all-btn="false" class="labs-box">
<div class="labs-grid">
<div v-for="(lab, index) in labsData" :key="index" class="lab-card">
<div class="lab-logo">
<img :src="lab.logo" :alt="lab.name" />
</div>
<div class="lab-name">{{ lab.name }}</div>
<div class="lab-description">{{ lab.description }}</div>
<div class="lab-tags">
<AreaTag v-for="(tag, tagIdx) in lab.tags" :key="tagIdx" :tag-name="tag.name" :type="tag.type" />
</div>
</div>
</div>
</AnalysisBox>
<AnalysisBox title="政策文件" width="100%" height="auto" :show-all-btn="false" class="policy-box">
<div class="policy-grid">
<div v-for="(policy, index) in policyData" :key="index" class="policy-card">
<div class="policy-icon">
<!-- <img src="/images/policy-icon.png" alt="policy" /> -->
</div>
<div class="policy-content">
<div class="policy-title">{{ policy.title }}</div>
<div class="policy-description">{{ policy.description }}</div>
</div>
</div>
</div>
<div class="pagination-area">
<div class="total-count">{{ totalCount }}篇合作动态</div>
<div class="pagination">
<LeftBtn @click="prevPage" />
<span v-for="page in displayPages" :key="page" :class="['page-num', { active: page === currentPage }]" @click="goToPage(page)">
{{ page === '...' ? '...' : page }}
</span>
<RightBtn @click="nextPage" />
</div>
</div>
</AnalysisBox>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import AnalysisBox from '@/components/base/boxBackground/analysisBox.vue'
import LeftBtn from '@/components/base/PageBtn/LeftBtn.vue'
import RightBtn from '@/components/base/PageBtn/RightBtn.vue'
import AreaTag from '@/components/base/AreaTag/index.vue'
// 重点实验室数据
const labsData = ref([
{
logo: '/images/lab-wyss.png',
name: '怀斯生物启发工程研究所',
description: '成立于2009年,由汉斯约尔格·怀斯(Hansjörg Wyss)捐赠建立,聚焦仿生学与跨学科工程,推动医疗、机器人、材料等领域的突破。',
tags: [{ name: '生物科技', type: 'tag2' }]
},
{
logo: '/images/lab-rowland.png',
name: '罗兰研究所',
description: '原为独立研究机构,2002年并入哈佛,支持高风险、高回报的基础科学研究,尤其鼓励青年科学家。',
tags: [{ name: '物理学', type: 'tag6' }, { name: '化学', type: 'tag10' }]
},
{
logo: '/images/lab-quantum.png',
name: '哈佛量子计划',
description: '跨学院合作平台,整合物理、工程、计算机科学等资源,推动量子科学与技术发展。',
tags: [{ name: '物理', type: 'tag6' }]
},
{
logo: '/images/lab-broad.png',
name: '博德研究所',
description: '全球顶尖基因组学与生物医学研究中心,推动精准医学与疾病机制研究。',
tags: [{ name: '医学', type: 'tag9' }]
},
{
logo: '/images/lab-stem.png',
name: '哈佛干细胞研究所',
description: '成立于2004年,联合哈佛医学院、牙医学院、文理学院等,推动干细胞基础研究与临床转化。',
tags: [{ name: '医学', type: 'tag9' }]
},
{
logo: '/images/lab-cfa.png',
name: '哈佛大学天体物理中心',
description: '由哈佛大学与史密森尼学会于1973年联合成立,是全球规模最大、最活跃的天体物理研究机构之一。',
tags: [{ name: '天体物理', type: 'tag3' }]
}
])
// 政策文件数据
const policyData = ref([
{ title: '《哈佛大学权利与责任声明》', description: '阐明学生在言论自由、学术自由、正当程序、尊重他人等方面的权利与义务。' },
{ title: '《哈佛大学学术诚信政策》', description: '定义抄袭、作弊、伪造等学术不端行为,并规定处理流程。' },
{ title: '《哈佛大学反歧视与反骚扰政策》', description: '禁止基于种族、性别、性取向、宗教、残疾等的歧视与骚扰,明确举报与调查机制。' },
{ title: '《研究合规与人类受试者保护政策》', description: '规范涉及人类受试者的研究(如医学、心理学、社会学实验),确保符合联邦法规(如Common Rule)。' },
{ title: '《哈佛法学院学术政策手册》', description: '详述J.D./LL.M./S.J.D.学位要求、课程规则、成绩制度、书面作业要求、出勤规定、荣誉毕业标准等。' },
{ title: '《哈佛文理研究生院学生手册》', description: '涵盖博士生资格考试、论文提交、助教职责、奖学金续期、学术进展评估等。' }
])
// 分页相关
const currentPage = ref(5)
const totalCount = ref(105)
const totalPages = computed(() => Math.ceil(totalCount.value / 10))
const displayPages = computed(() => {
const pages: (number | string)[] = []
if (totalPages.value <= 7) {
for (let i = 1; i <= totalPages.value; i++) pages.push(i)
} else {
pages.push(1)
if (currentPage.value > 3) pages.push('...')
const start = Math.max(2, currentPage.value - 1)
const end = Math.min(totalPages.value - 1, currentPage.value + 1)
for (let i = start; i <= end; i++) pages.push(i)
if (currentPage.value < totalPages.value - 2) pages.push('...')
pages.push(totalPages.value)
}
return pages
})
// 上一页
const prevPage = () => {
if (currentPage.value > 1) currentPage.value--
}
// 下一页
const nextPage = () => {
if (currentPage.value < totalPages.value) currentPage.value++
}
// 跳转到指定页
const goToPage = (page: number | string) => {
if (typeof page === 'number') currentPage.value = page
}
// 获取标签样式
</script>
<style lang="scss" scoped>
.other-info-content {
display: flex;
flex-direction: column;
gap: 16px;
}
.labs-box {
.labs-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
padding: 16px;
.lab-card {
padding: 16px;
background: #FFFFFF;
border: 1px solid #EAECEE;
border-radius: 4px;
display: flex;
flex-direction: column;
gap: 12px;
.lab-logo {
width: 40px;
height: 40px;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
.lab-name {
font-size: 16px;
font-weight: 700;
color: #3B414B;
line-height: 24px;
}
.lab-description {
font-size: 14px;
color: #5F656C;
line-height: 22px;
flex: 1;
}
.lab-tags {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
}
}
}
.policy-box {
.policy-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
padding: 16px;
.policy-card {
display: flex;
gap: 12px;
padding: 16px;
background: #FFFFFF;
border: 1px solid #EAECEE;
border-radius: 4px;
.policy-icon {
width: 40px;
height: 40px;
flex-shrink: 0;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
.policy-content {
flex: 1;
.policy-title {
font-size: 16px;
font-weight: 700;
color: #3B414B;
line-height: 24px;
margin-bottom: 8px;
}
.policy-description {
font-size: 14px;
color: #5F656C;
line-height: 22px;
}
}
}
}
.pagination-area {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-top: 1px solid #EAECEE;
.total-count {
font-size: 14px;
color: #84888E;
}
.pagination {
display: flex;
align-items: center;
gap: 8px;
.page-num {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: #3B414B;
cursor: pointer;
border-radius: 4px;
&:hover {
background: #F6FAFF;
}
&.active {
background: #055FC2;
color: #FFFFFF;
}
}
}
}
}
</style>
...@@ -288,7 +288,11 @@ const paperPoints = computed(() => getDataPoints(paperData)) ...@@ -288,7 +288,11 @@ const paperPoints = computed(() => getDataPoints(paperData))
const fundingPoints = computed(() => getDataPoints(fundingData)) const fundingPoints = computed(() => getDataPoints(fundingData))
// 雷达图数据和计算 // 雷达图数据和计算
<<<<<<< HEAD
const radarValues = [0.7, 0.9, 0.85, 0.6, 0.75, 0.65] // 各领域相对�� const radarValues = [0.7, 0.9, 0.85, 0.6, 0.75, 0.65] // 各领域相对��
=======
const radarValues = [0.7, 0.9, 0.85, 0.6, 0.75, 0.65] // 各领域
>>>>>>> c71276c18548779e8e676aea3b390352568ff20b
const getHexagonPoints = (cx, cy, r) => { const getHexagonPoints = (cx, cy, r) => {
const points = [] const points = []
......
...@@ -203,8 +203,12 @@ const infoList = computed(() => [ ...@@ -203,8 +203,12 @@ const infoList = computed(() => [
<style lang="scss" scoped> <style lang="scss" scoped>
.school-detail-content { .school-detail-content {
display: flex; display: flex;
<<<<<<< HEAD
gap: 16px; gap: 16px;
// align-items 默认 stretch,使左右等高 // align-items 默认 stretch,使左右等高
=======
gap: 16px;
>>>>>>> c71276c18548779e8e676aea3b390352568ff20b
align-items: stretch; align-items: stretch;
.left-section { .left-section {
...@@ -212,8 +216,12 @@ const infoList = computed(() => [ ...@@ -212,8 +216,12 @@ const infoList = computed(() => [
min-width: 0; min-width: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
<<<<<<< HEAD
// 让内部 AnalysisBox 撑满整列高度 // 让内部 AnalysisBox 撑满整列高度
=======
>>>>>>> c71276c18548779e8e676aea3b390352568ff20b
:deep(.dynamics-box), :deep(.dynamics-box),
:deep(.analysis-box-wrapper) { :deep(.analysis-box-wrapper) {
flex: 1; flex: 1;
...@@ -226,8 +234,12 @@ const infoList = computed(() => [ ...@@ -226,8 +234,12 @@ const infoList = computed(() => [
flex-direction: column; flex-direction: column;
} }
} }
<<<<<<< HEAD
// dynamics-list 填满剩余空间,分页固定在底部 // dynamics-list 填满剩余空间,分页固定在底部
=======
>>>>>>> c71276c18548779e8e676aea3b390352568ff20b
:deep(.dynamics-box) { :deep(.dynamics-box) {
height: 100%; height: 100%;
} }
...@@ -240,9 +252,13 @@ const infoList = computed(() => [ ...@@ -240,9 +252,13 @@ const infoList = computed(() => [
flex-direction: column; flex-direction: column;
gap: 16px; gap: 16px;
} }
<<<<<<< HEAD
} }
// 节点尺寸变量 // 节点尺寸变量
=======
}
>>>>>>> c71276c18548779e8e676aea3b390352568ff20b
$node-size: 24px; $node-size: 24px;
$axis-width: 2px; $axis-width: 2px;
...@@ -255,14 +271,22 @@ $axis-width: 2px; ...@@ -255,14 +271,22 @@ $axis-width: 2px;
display: flex; display: flex;
align-items: stretch; align-items: stretch;
gap: 0; gap: 0;
<<<<<<< HEAD
// 左侧日期列 // 左侧日期列
=======
>>>>>>> c71276c18548779e8e676aea3b390352568ff20b
.time-col { .time-col {
width: 70px; width: 70px;
flex-shrink: 0; flex-shrink: 0;
text-align: right; text-align: right;
<<<<<<< HEAD
padding-right: 16px; padding-right: 16px;
// 日期与节点圆顶部对齐 // 日期与节点圆顶部对齐
=======
padding-right: 16px;
>>>>>>> c71276c18548779e8e676aea3b390352568ff20b
padding-top: calc(#{$node-size} / 2 - 12px); padding-top: calc(#{$node-size} / 2 - 12px);
.year, .date { .year, .date {
...@@ -273,8 +297,12 @@ $axis-width: 2px; ...@@ -273,8 +297,12 @@ $axis-width: 2px;
line-height: 24px; line-height: 24px;
} }
} }
<<<<<<< HEAD
// 中间轴线列:上段线 + 圆形节点 + 下段线,完全垂直居中对齐 // 中间轴线列:上段线 + 圆形节点 + 下段线,完全垂直居中对齐
=======
>>>>>>> c71276c18548779e8e676aea3b390352568ff20b
.axis-col { .axis-col {
width: $node-size; width: $node-size;
flex-shrink: 0; flex-shrink: 0;
...@@ -321,8 +349,12 @@ $axis-width: 2px; ...@@ -321,8 +349,12 @@ $axis-width: 2px;
} }
} }
} }
<<<<<<< HEAD
// 右侧内容列 // 右侧内容列
=======
>>>>>>> c71276c18548779e8e676aea3b390352568ff20b
.content-col { .content-col {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
...@@ -480,6 +512,7 @@ $axis-width: 2px; ...@@ -480,6 +512,7 @@ $axis-width: 2px;
color: rgb(59, 65, 75); color: rgb(59, 65, 75);
margin-bottom: 12px; margin-bottom: 12px;
} }
<<<<<<< HEAD
// 最多展示 2 行 (每行2个),超出可滚动 // 最多展示 2 行 (每行2个),超出可滚动
.people-grid { .people-grid {
...@@ -496,6 +529,20 @@ $axis-width: 2px; ...@@ -496,6 +529,20 @@ $axis-width: 2px;
} }
// 滚动条样式 // 滚动条样式
=======
.people-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
max-height: 144px;
overflow-y: auto;
&.single-row {
max-height: 64px;
}
>>>>>>> c71276c18548779e8e676aea3b390352568ff20b
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 4px; width: 4px;
} }
......
...@@ -440,7 +440,7 @@ export default { ...@@ -440,7 +440,7 @@ export default {
}; };
chart.setOption(option); chart.setOption(option);
// 地图拖拽/缩放时重新计算 DOM 卡片位置(加防抖避免频繁���发)
let georoamTimer = null; let georoamTimer = null;
chart.on("georoam", () => { chart.on("georoam", () => {
if (georoamTimer) clearTimeout(georoamTimer); if (georoamTimer) clearTimeout(georoamTimer);
......
...@@ -233,8 +233,8 @@ ...@@ -233,8 +233,8 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="box5-main" id="box5Chart"></div> <div class="box5-main" ><WordCloudChart v-if="WordLoading" :data="CharacterOpinionWordCloud" /></div>
</div> </div>
<div class="box6"> <div class="box6">
<div class="box6-header" style="width: 790px"> <div class="box6-header" style="width: 790px">
...@@ -324,6 +324,7 @@ import scrollToTop from "@/utils/scrollToTop"; ...@@ -324,6 +324,7 @@ import scrollToTop from "@/utils/scrollToTop";
import DivideHeader from "@/components/DivideHeader.vue"; import DivideHeader from "@/components/DivideHeader.vue";
import OverviewMainBox from '@/components/base/boxBackground/overviewMainBox.vue' import OverviewMainBox from '@/components/base/boxBackground/overviewMainBox.vue'
import setChart from "@/utils/setChart"; import setChart from "@/utils/setChart";
import WordCloudChart from "@/components/base/WordCloundChart/index.vue"
import { import {
getnewsDynamics, getnewsDynamics,
getBillRiskSignal, getBillRiskSignal,
...@@ -444,7 +445,7 @@ const handlegetareaTypeFn = async () => { ...@@ -444,7 +445,7 @@ const handlegetareaTypeFn = async () => {
} catch (error) { } } catch (error) { }
}; };
const WordLoading=ref(false)
// 获取科技人物观点词云 // 获取科技人物观点词云
const CharacterOpinionWordCloud = ref([]); const CharacterOpinionWordCloud = ref([]);
...@@ -456,6 +457,7 @@ const handlegetCharacterOpinionWordCloudFn = async () => { ...@@ -456,6 +457,7 @@ const handlegetCharacterOpinionWordCloudFn = async () => {
if (wordCloudfield.value !== "0") { if (wordCloudfield.value !== "0") {
params.areaId = wordCloudfield.value; params.areaId = wordCloudfield.value;
} }
WordLoading.value=false
try { try {
const res = await getCharacterOpinionWordCloud(params); const res = await getCharacterOpinionWordCloud(params);
console.log("科技人物观点词云", res); console.log("科技人物观点词云", res);
...@@ -466,7 +468,9 @@ const handlegetCharacterOpinionWordCloudFn = async () => { ...@@ -466,7 +468,9 @@ const handlegetCharacterOpinionWordCloudFn = async () => {
name: item.option, name: item.option,
value: item.count value: item.count
} }
}); });
WordLoading.value=true
} }
} catch (error) { } } catch (error) { }
}; };
...@@ -608,15 +612,15 @@ const ChooseArea = value => { ...@@ -608,15 +612,15 @@ const ChooseArea = value => {
const handleBox5Change = async () => { const handleBox5Change = async () => {
await handlegetCharacterOpinionWordCloudFn(); await handlegetCharacterOpinionWordCloudFn();
const wordCloudChart = getWordCloudChart(CharacterOpinionWordCloud.value); // const wordCloudChart = getWordCloudChart(CharacterOpinionWordCloud.value);
console.log(wordCloudChart,'wordCloudChart') // console.log(wordCloudChart,'wordCloudChart')
setChart(wordCloudChart, "box5Chart"); // setChart(wordCloudChart, "box5Chart");
} }
const handleBox5areaChange = async () => { const handleBox5areaChange = async () => {
await handlegetCharacterOpinionWordCloudFn(); await handlegetCharacterOpinionWordCloudFn();
const wordCloudChart = getWordCloudChart(CharacterOpinionWordCloud.value); // const wordCloudChart = getWordCloudChart(CharacterOpinionWordCloud.value);
setChart(wordCloudChart, "box5Chart"); // setChart(wordCloudChart, "box5Chart");
} }
......
...@@ -52,6 +52,7 @@ export default defineConfig({ ...@@ -52,6 +52,7 @@ export default defineConfig({
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/reportData/, '') rewrite: (path) => path.replace(/^\/reportData/, '')
}, },
<<<<<<< HEAD
'/api': { '/api': {
target: 'http://8.140.26.4:9085/', target: 'http://8.140.26.4:9085/',
// target: 'http://192.168.0.4:28080/', // target: 'http://192.168.0.4:28080/',
...@@ -65,6 +66,15 @@ export default defineConfig({ ...@@ -65,6 +66,15 @@ export default defineConfig({
// rewrite: (path) => path.replace(/^\/api/, '') // rewrite: (path) => path.replace(/^\/api/, '')
// '/api': { // '/api': {
=======
'/api': {
target: 'http://8.140.26.4:9085/',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
// '/api': {
>>>>>>> c71276c18548779e8e676aea3b390352568ff20b
// target: 'http://10.119.133.162:28080/', // target: 'http://10.119.133.162:28080/',
// changeOrigin: true, // changeOrigin: true,
// rewrite: (path) => path.replace(/^\/api/, '') // rewrite: (path) => path.replace(/^\/api/, '')
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论