提交 76c3cef8 authored 作者: 张伊明's avatar 张伊明

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

...@@ -328,16 +328,20 @@ export function getProgressPrediction(billId) { ...@@ -328,16 +328,20 @@ export function getProgressPrediction(billId) {
/** /**
* 获取相似法案列表 * 获取相似法案列表
* @param {Object} params - 查询参数 * @param {Object} params - 查询参数
* @param {string} params.patternType - 政府结构类型,如 "统一政府" * @param {string} params.billIds - 当前法案的ID
* @param {string} params.proposalType - 提案类型,默认 "两党共同提案" * @param {string[]} params.domains - 领域名称列表
* @param {string} params.patternType - 政府结构类型,如 "统一政府"/"分裂政府"/"微弱多数"
* @param {string} params.proposalType - 提案类型,如 "两党共同提案"/"单政党提案(共和党提案)"/"单政党提案(民主党提案)"
* @returns {Promise<Object>} 相似法案列表 * @returns {Promise<Object>} 相似法案列表
*/ */
export function getSimiBills(params = {}) { export function getSimiBills(params = {}) {
return request('/api/BillProgressPrediction/simiBills', { return request('/api/BillProgressPrediction/simiBills', {
method: 'GET', method: 'GET',
params: { params: {
patternType: params.patternType || '统一政府', billIds: params.billIds,
proposalType: params.proposalType || '两党共同提案', domains: params.domains,
patternType: params.patternType ,
proposalType: params.proposalType ,
...params ...params
} }
}) })
...@@ -400,31 +404,42 @@ export function transformSimiBillsData(apiData) { ...@@ -400,31 +404,42 @@ export function transformSimiBillsData(apiData) {
const transformedBills = bills.map(bill => ({ const transformedBills = bills.map(bill => ({
id: bill.bill_id, id: bill.bill_id,
title: bill.bill_name || bill.bill_id, title: bill.bill_name || bill.bill_id,
tags: [bill.poli_pattern_type, bill.bill_proposal_type].filter(Boolean), proposalDate: extractProposalDate(bill.poli_pattern_desc),
passTime: extractPassTime(bill.bill_actions), areas: bill.domain_name ? (Array.isArray(bill.domain_name) ? bill.domain_name : [bill.domain_name]) : [],
totalDays: calculateTotalDays(bill.bill_actions), proposer: extractProposer(bill.bill_sponsors),
selected: true, // 默认全选 coProposers: bill.bill_proposal_desc || '',
_raw: bill governmentType: formatGovernmentType(bill.poli_pattern_type, bill.poli_pattern_desc),
passDays: calculateTotalDays(bill.bill_actions),
selected: true // 默认全选
})) }))
return { stats, bills: transformedBills } return { stats, bills: transformedBills }
} }
/** /**
* 从法案动作中提取通过时间 * 从政治格局描述中提取提案时间
* @param {Array} actions - 法案动作列表 * @param {string} desc - 政治格局描述
* @returns {string} 通过时间 * @returns {string} 提案时间
*/ */
function extractPassTime(actions) { function extractProposalDate(desc) {
if (!actions || actions.length === 0) return '' if (!desc) return ''
// 找到最后一个动作的日期 const match = desc.match(/(\d{4})-(\d{2})-(\d{2})/)
const lastAction = actions[actions.length - 1] if (match) {
if (lastAction && lastAction.action_date) { return `${match[1]}${parseInt(match[2])}${parseInt(match[3])}日`
const date = new Date(lastAction.action_date)
return `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}日`
} }
return '' return ''
} }
/**
* 从提案人列表中提取主提案人
* @param {Array} sponsors - 提案人列表
* @returns {string} 主提案人姓名
*/
function extractProposer(sponsors) {
if (!sponsors || sponsors.length === 0) return ''
const mainProposer = sponsors.find(s => s.sponsor_type === '提案人')
return mainProposer ? mainProposer.person_name : ''
}
/**
/** /**
* 计算法案总耗时 * 计算法案总耗时
...@@ -488,29 +503,30 @@ export function transformProposalInfo(apiData) { ...@@ -488,29 +503,30 @@ export function transformProposalInfo(apiData) {
} }
return { return {
// 提案标题 - 需要从其他 API 获取或使用默认值 // 提案标题
title: data.bill_title || 'H.R.1-大而美法案', title: data.bill_title || 'H.R.1-大而美法案',
// 提案时间 - 从政治格局描述中提取或使用默认值 // 提案时间 - 从政治格局描述中提取
date: extractDateFromDesc(data.poli_pattern_desc) || '2025年5月20日', date: extractDateFromDesc(data.poli_pattern_desc) || '2025年5月20日',
// 涉及领域 // 涉及领域 TAG - 使用 domain_name
areas: data.domain_name || [], areas: Array.isArray(data.domain_name) ? data.domain_name : (data.domain_name ? [data.domain_name] : []),
// 选举周期阶段 - 从政治格局描述推断 // 政策领域完整选项列表 - 使用 bill_domain(用于筛选下拉框)
electionPhase: inferElectionPhase(data.poli_pattern_desc) || '执政初期/蜜月期', billDomain: Array.isArray(data.bill_domain) ? data.bill_domain : [],
// 政策领域默认已选项 - 使用 domain_name
defaultDomains: Array.isArray(data.domain_name) ? data.domain_name : (data.domain_name ? [data.domain_name] : []),
// 提案人 // 提案人
proposer: mainProposer ? `${mainProposer.person_name}` : '', proposer: mainProposer ? mainProposer.person_name : '',
// 共同提案人 // 共同提案人
coProposers: coProposersDesc, coProposers: coProposersDesc,
// 提案人职务 - 需要从其他 API 获取
proposerPosition: data.proposer_position || '委员会主席',
// 政府结构类型 // 政府结构类型
governmentType: formatGovernmentType(data.poli_pattern_type, data.poli_pattern_desc), governmentType: formatGovernmentType(data.poli_pattern_type, data.poli_pattern_desc),
// 法案预算规模 - 需要从其他 API 获取 // 政治格局类型(用于筛选条件默认值)
budgetScale: data.budget_scale || '4万亿美元', patternType: data.poli_pattern_type || '统一政府',
// 原始数据,供筛选条件使用 // 原始数据,供筛选条件使用
_raw: data _raw: data
} }
} }
/** /**
* 从政治格局描述中提取日期 * 从政治格局描述中提取日期
* @param {string} desc - 政治格局描述 * @param {string} desc - 政治格局描述
...@@ -578,9 +594,7 @@ export function generateDefaultFilters(proposalInfo) { ...@@ -578,9 +594,7 @@ export function generateDefaultFilters(proposalInfo) {
// 提案人职务 // 提案人职务
proposerPosition: proposalInfo.proposerPosition === '委员会主席' ? ['chairman'] : [], proposerPosition: proposalInfo.proposerPosition === '委员会主席' ? ['chairman'] : [],
// 政府结构类型 // 政府结构类型
governmentType: proposalInfo.governmentType.includes('一致') ? ['unified'] : ['divided'], governmentType: proposalInfo.governmentType.includes('一致') ? ['unified'] : ['divided'],
// 选举周期阶段
electionPhase: proposalInfo.electionPhase.includes('蜜月') ? ['honeymoon'] : [],
// 法案预算规模 // 法案预算规模
budgetScale: ['trillion_plus'], budgetScale: ['trillion_plus'],
// 对方党派提案人 // 对方党派提案人
...@@ -589,3 +603,19 @@ export function generateDefaultFilters(proposalInfo) { ...@@ -589,3 +603,19 @@ export function generateDefaultFilters(proposalInfo) {
proposalTime: ['recent_5'] proposalTime: ['recent_5']
} }
} }
/**
* 获取预测分析结果
* @param {Object} params - 请求参数
* @param {string} params.bill_id - 当前法案ID
* @param {string} params.bill_name - 当前法案名称
* @param {Array} params.bill_actions - 当前法案动作列表
* @param {Array} params.bill_sponsors - 当前法案提案人列表
* @param {Array} params.simi_bills - 用户勾选的相似法案列表
* @returns {Promise<Object>} 预测分析结果
*/
export function getPassProd(params) {
return request('/api/BillProgressPrediction/passProd', {
method: 'POST',
data: params
})
}
\ No newline at end of file
...@@ -60,6 +60,17 @@ export function getDecreeMainContent(params) { ...@@ -60,6 +60,17 @@ export function getDecreeMainContent(params) {
}) })
} }
// 思维导图
/**
* @param { id }
*/
export function getDecreeMindMap(params) {
return request({
method: 'GET',
url: `/api/administrativeOrderInfo/mindMap/${params.id}`,
})
}
// 相关实体 // 相关实体
/** /**
* @param { id } * @param { id }
......
...@@ -91,10 +91,20 @@ export function getDecreeTypeList() { ...@@ -91,10 +91,20 @@ export function getDecreeTypeList() {
} }
// 关键机构 // 关键机构
export function getKeyOrganization() { export function getKeyOrganization(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/commonFeature/keyOrganization`, url: `/api/commonFeature/keyOrganization`,
params
})
}
// 所有机构
export function getAllOrganization(params) {
return request({
method: 'POST',
url: `/api/administrativeOrderOverview/orderCount`,
data: params
}) })
} }
......
...@@ -80,10 +80,10 @@ export function getDecreeReport(params) { ...@@ -80,10 +80,10 @@ export function getDecreeReport(params) {
} }
// 政令关键词云 // 政令关键词云
export function getKeyWordUp() { export function getKeyWordUp(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/element/getKeyWordUp/2025-01-01`, url: `/api/administrativeOrderInfo/wordCloud/${params.id}`,
}) })
} }
......
...@@ -138,120 +138,118 @@ export function getPersonList(params) { ...@@ -138,120 +138,118 @@ export function getPersonList(params) {
params params
}) })
} }
//创新主体科研实力:专利数量统计 //合作情况:与中国合作数量变化
export function getPatentList(params) { export function getCooperateNumWithChina(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/innovateSubject/patentList/${params.id}`, url: `/api/innovateSubject/cooperateNumWithChina/${params.id}`,
params params
}) })
} }
// 合作情况:与中国合作类型变化
// 创新主体科研实力:论文数量统计 export function getCooperateTypeWithChina(params) {
export function getPaperList(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/innovateSubject/paperList/${params.id}`, url: `/api/innovateSubject/cooperateTypeWithChina/${params.year}/${params.id}`,
params params
}) })
} }
//创新主体科研实力:领域实力分布 // 合作情况:与中国合作领域变化
export function getStudyFieldList(params) { export function getCooperateAreaWithChina(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/innovateSubject/studyFieldList/${params.id}`, url: `/api/innovateSubject/cooperateAreaWithChina/${params.id}`,
params
}) })
} }
//创新主体科研实力:经费增长情况 //合作情况:与中国合作经费变化
export function getFundGrowth(params) { export function getCooperateFundWithChina(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/innovateSubject/fundGrowth/${params.id}`, url: `/api/innovateSubject/cooperateFundWithChina/${params.id}`,
params params
}) })
} }
//创新主体科研实力:经费来源 //合作情况:与中国合作事例
export function getFundFromList(params) { export function getCooperateExampleWithChina(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/innovateSubject/fundFromList/${params.id}`, url: `/api/innovateSubject/cooperateExampleWithChina/${params.id}`,
params params
}) })
} }
//创新主体科研实力:经费分配
export function getFundToList(params) {
// 专利数量统计
export function getPatentList(orgId) {
return request({ return request({
method: 'GET', method: "GET",
url: `/api/innovateSubject/fundToList/${params.id}`, url: `/api/innovateSubject/patentList/${orgId}`
params
}) })
} }
//合作情况:与中国合作数量变化 // 论文数量统计
export function getCooperateNumWithChina(params) { export function getPaperList(orgId) {
return request({ return request({
method: 'GET', method: "GET",
url: `/api/innovateSubject/cooperateNumWithChina/${params.id}`, url: `/api/innovateSubject/paperList/${orgId}`
params
}) })
} }
// 合作情况:与中国合作类型变化 // 领域实力分布
export function getCooperateTypeWithChina(params) { export function getStudyFieldList(orgId) {
return request({ return request({
method: 'GET', method: "GET",
url: `/api/innovateSubject/cooperateTypeWithChina/${params.year}/${params.id}`, url: `/api/innovateSubject/studyFieldList/${orgId}`
params
}) })
} }
// 合作情况:与中国合作领域变化 // 经费增长情况
export function getCooperateAreaWithChina(params) { export function getFundGrowth(orgId) {
return request({ return request({
method: 'GET', method: "GET",
url: `/api/innovateSubject/cooperateAreaWithChina/${params.id}`, url: `/api/innovateSubject/fundGrowth/${orgId}`
params
}) })
} }
//合作情况:与中国合作经费变化 // 经费来源
export function getCooperateFundWithChina(params) { export function getFundFromList(orgId) {
return request({ return request({
method: 'GET', method: "GET",
url: `/api/innovateSubject/cooperateFundWithChina/${params.id}`, url: `/api/innovateSubject/fundFromList/${orgId}`
params
}) })
} }
//合作情况:与中国合作事例 // 经费分配
export function getCooperateExampleWithChina(params) { export function getFundToList(orgId) {
return request({ return request({
method: 'GET', method: "GET",
url: `/api/innovateSubject/cooperateExampleWithChina/${params.id}`, url: `/api/innovateSubject/fundToList/${orgId}`
params
}) })
} }
// 获取实验室列表
//创新主体其他情况:重点实验室 export function getLabList(orgId) {
export function getLabList(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/innovateSubject/labList/${params.id}`, url: `/api/innovateSubject/labList/${orgId}`
}) })
} }
//创新主体其他情况:政策文件 // 获取政策文件列表
export function getPolicyList(params) { export function getPolicyList(orgId, currentPage = 1, pageSize = 6) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/innovateSubject/policyList/${params.id}`, url: `/api/innovateSubject/policyList/${orgId}`,
params params: { currentPage, pageSize }
}) })
} }
\ No newline at end of file
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
<div class="switch-label switch-label-left">高亮实体</div> <div class="switch-label switch-label-left">高亮实体</div>
<el-switch v-model="isTranslate" /> <el-switch v-model="isTranslate" />
<div class="switch-label">文显示</div> <div class="switch-label">文显示</div>
<div <div
v-for="action in headerActions" v-for="action in headerActions"
...@@ -53,8 +53,8 @@ ...@@ -53,8 +53,8 @@
class="content-row" class="content-row"
:class="{ 'high-light': isHighlight }" :class="{ 'high-light': isHighlight }"
> >
<div class="content-cn" :class="{ 'translate-cn': !isTranslate }" v-html="item.content" /> <div class="content-en" v-html="item.contentEn" :class="{ 'translate-cn': !isTranslate }"></div>
<div v-if="isTranslate" class="content-en" v-html="item.contentEn" /> <div class="content-cn" v-html="item.content" v-if="isTranslate"></div>
</div> </div>
</el-scrollbar> </el-scrollbar>
</div> </div>
...@@ -150,11 +150,9 @@ const doUpdateWord = () => { ...@@ -150,11 +150,9 @@ const doUpdateWord = () => {
} }
displayReportData.value = originReportData.value.map((item) => { displayReportData.value = originReportData.value.map((item) => {
const cn = applyHighlightToText(item.content, term); const en = applyHighlightToText(item.contentEn, term);
const en = isTranslate.value const cn = isTranslate.value ? applyHighlightToText(item.content, term) : { html: item.content, count: 0 };
? applyHighlightToText(item.contentEn, term) findWordMax.value += en.count + cn.count;
: { html: item.contentEn, count: 0 };
findWordMax.value += cn.count + en.count;
return { return {
...item, ...item,
content: cn.html, content: cn.html,
......
...@@ -4,22 +4,20 @@ const InnovationInstitution = () => import('@/views/innovationSubject/innovative ...@@ -4,22 +4,20 @@ const InnovationInstitution = () => import('@/views/innovationSubject/innovative
const innovationSubjectRoutes = [ const innovationSubjectRoutes = [
//创新主体
{ {
path: "/innovationSubject", path: "/innovationSubject",
name: "InnovationSubject", name: "InnovationSubject",
component: InnovationSubject, component: InnovationSubject,
meta: { meta: {
title: "M国主要创新主体分析概览" title: "M "
} }
}, },
{ {
path: "/InnovativeInstitutions/:id", path: "/InnovativeInstitutions/:id/:type",
name: "InnovativeInstitutions", name: "InnovativeInstitutions",
component: InnovationInstitution, component: InnovationInstitution,
// meta: {
// title: "学校详情"
// },
} }
] ]
......
...@@ -9,6 +9,7 @@ const DecreeDeepDig = () => import('@/views/decree/decreeLayout/deepdig/index.vu ...@@ -9,6 +9,7 @@ const DecreeDeepDig = () => import('@/views/decree/decreeLayout/deepdig/index.vu
const DecreeInfluence = () => import('@/views/decree/decreeLayout/influence/index.vue') const DecreeInfluence = () => import('@/views/decree/decreeLayout/influence/index.vue')
const Institution = () => import('@/views/decree/institution/index.vue') const Institution = () => import('@/views/decree/institution/index.vue')
const DecreeOriginal = () => import('@/views/decree/decreeOriginal/index.vue') const DecreeOriginal = () => import('@/views/decree/decreeOriginal/index.vue')
const allOrganization = () => import('@/views/decree/allOrganization/index.vue')
const decreeRoutes = [ const decreeRoutes = [
...@@ -93,11 +94,12 @@ const decreeRoutes = [ ...@@ -93,11 +94,12 @@ const decreeRoutes = [
path: "/decree/decreeOriginal", path: "/decree/decreeOriginal",
name: "DecreeOriginal", name: "DecreeOriginal",
component: DecreeOriginal, component: DecreeOriginal,
// meta: {
// title: "政令原文"
// }
}, },
{
path: "/decree/allOrganization",
name: "allOrganization",
component: allOrganization,
},
] ]
export default decreeRoutes export default decreeRoutes
\ No newline at end of file
// 使用 day.js(推荐,轻量级)
import dayjs from 'dayjs';
// 获取日期范围
const getDateRange = (type) => {
const endDate = dayjs(); // 当前日期
let startDate;
switch(type) {
case '近一月':
startDate = dayjs().subtract(1, 'month');
break;
case '近三月':
startDate = dayjs().subtract(3, 'month');
break;
case '近半年':
startDate = dayjs().subtract(6, 'month');
break;
case '近一年':
startDate = dayjs().subtract(1, 'year');
break;
default:
return null;
}
return [
startDate.format('YYYY-MM-DD'),
endDate.format('YYYY-MM-DD')
];
}
export default getDateRange
// 使用示例
// console.log(getDateRange('近一个月')); // ['2026-02-25', '2026-03-25']
// console.log(getDateRange('近三个月')); // ['2025-12-25', '2026-03-25']
// console.log(getDateRange('近半年')); // ['2025-09-25', '2026-03-25']
// console.log(getDateRange('近一年')); // ['2025-03-25', '2026-03-25']
\ No newline at end of file
const getMonthRange = (yearMonth) => {
// yearMonth 格式:'2026-03'
const [year, month] = yearMonth.split('-');
// 当月第一天
const firstDay = `${year}-${month}-01`;
// 当月最后一天:先获取下个月的第0天,即当月的最后一天
const lastDay = new Date(Number(year), Number(month), 0);
const lastDayStr = `${year}-${month}-${lastDay.getDate()}`;
return [firstDay, lastDayStr];
}
export default getMonthRange
// 绘制echarts图表 // 绘制echarts图表
import getMonthRange from './getMonthRange'
import * as echarts from 'echarts' import * as echarts from 'echarts'
import 'echarts-wordcloud'; import 'echarts-wordcloud';
import router from '@/router/index' import router from '@/router/index'
...@@ -18,15 +19,25 @@ const setChart = (option, chartId, allowClick, selectParam) => { ...@@ -18,15 +19,25 @@ const setChart = (option, chartId, allowClick, selectParam) => {
if (params.componentType === 'series' && params.seriesType === 'pie') { if (params.componentType === 'series' && params.seriesType === 'pie') {
console.log('点击的扇形名称:', params.name); console.log('点击的扇形名称:', params.name);
if (selectParam.key === '领域') { if (selectParam.key === '领域') {
selectParam.domains = JSON.stringify([params.name]) selectParam.domains = params.name
if (selectParam.selectedDate.length === 4) {
selectParam.selectedDate = JSON.stringify([selectParam.selectedDate + '-01-01', selectParam.selectedDate + '-12-31'])
}
} else if (selectParam.key === '议院委员会') { } else if (selectParam.key === '议院委员会') {
if (params.name === '众议院' || params.name === '参议院') { if (params.name === '众议院' || params.name === '参议院') {
selectParam.selectedCongress = params.name selectParam.selectedCongress = params.name
selectParam.selectedOrg = '' selectParam.selectedOrg = ''
if (selectParam.selectedDate.length === 4) {
selectParam.selectedDate = JSON.stringify([selectParam.selectedDate + '-01-01', selectParam.selectedDate + '-12-31'])
}
} else { } else {
selectParam.selectedOrg = params.name selectParam.selectedOrg = params.name
selectParam.selectedCongress = '' selectParam.selectedCongress = ''
if (selectParam.selectedDate.length === 4) {
selectParam.selectedDate = JSON.stringify([selectParam.selectedDate + '-01-01', selectParam.selectedDate + '-12-31'])
}
} }
} }
const route = router.resolve({ const route = router.resolve({
...@@ -36,15 +47,35 @@ const setChart = (option, chartId, allowClick, selectParam) => { ...@@ -36,15 +47,35 @@ const setChart = (option, chartId, allowClick, selectParam) => {
window.open(route.href, "_blank"); window.open(route.href, "_blank");
} else if (params.componentType === 'series' && params.seriesType === 'bar') { } else if (params.componentType === 'series' && params.seriesType === 'bar') {
if (params.name === '已立法') { if (params.name === '已立法') {
selectParam.selectedStauts = 1 selectParam.selectedStatus = 1
} else { } else {
selectParam.selectedStauts = 0 selectParam.selectedStatus = 0
}
if (selectParam.selectedDate.length === 4) {
selectParam.selectedDate = JSON.stringify([selectParam.selectedDate + '-01-01', selectParam.selectedDate + '-12-31'])
} }
const route = router.resolve({ const route = router.resolve({
path: "/dataLibrary/countryBill", path: "/dataLibrary/countryBill",
query: selectParam query: selectParam
}); });
window.open(route.href, "_blank"); window.open(route.href, "_blank");
} else {
console.log('当前点击', selectParam, params.seriesName, params.name);
if (params.seriesName !== '通过率') {
selectParam.selectedDate = JSON.stringify(getMonthRange(params.name))
if (params.seriesName === '通过法案') {
selectParam.selectedStatus = 1
} else {
selectParam.selectedStatus = null
}
const route = router.resolve({
path: "/dataLibrary/countryBill",
query: selectParam
});
window.open(route.href, "_blank");
}
} }
} }
......
...@@ -821,7 +821,14 @@ const handleBox5 = async () => { ...@@ -821,7 +821,14 @@ const handleBox5 = async () => {
return p ? ((pass / p) * 100).toFixed(2) : 0; return p ? ((pass / p) * 100).toFixed(2) : 0;
}); });
const box5Chart = getMultiLineChart(box5Data.value.title, proposed, passed, rate); const box5Chart = getMultiLineChart(box5Data.value.title, proposed, passed, rate);
setChart(box5Chart, "box5Chart"); const domain = categoryList.value.filter(item => {
return item.id === box5Select.value
})[0]?.name
const selectParam = {
moduleType: '国会法案',
domains: domain
}
setChart(box5Chart, "box5Chart", true, selectParam);
} }
}; };
...@@ -866,7 +873,7 @@ const handleBox7Data = async () => { ...@@ -866,7 +873,7 @@ const handleBox7Data = async () => {
if (t1 !== t2) return t1 - t2; if (t1 !== t2) return t1 - t2;
return (b.value ?? 0) - (a.value ?? 0); return (b.value ?? 0) - (a.value ?? 0);
}); });
const selectParam = { const selectParam = {
moduleType: '国会法案', moduleType: '国会法案',
key: '议院委员会', key: '议院委员会',
...@@ -997,8 +1004,9 @@ const handleBox9Data = async () => { ...@@ -997,8 +1004,9 @@ const handleBox9Data = async () => {
); );
const selectParam = { const selectParam = {
moduleType: '国会法案', moduleType: '国会法案',
key: '领域',
selectedDate: box9selectetedTime.value, selectedDate: box9selectetedTime.value,
status: box9LegislativeStatus.value === '提出法案' ? 0 : 1, selectedStatus: box9LegislativeStatus.value === '提出法案' ? 0 : 1,
isInvolveCn: 1 isInvolveCn: 1
} }
box9ChartInstance = setChart(box9Chart, "box9Chart", true, selectParam); box9ChartInstance = setChart(box9Chart, "box9Chart", true, selectParam);
...@@ -1179,7 +1187,7 @@ const handleBox8Data = async () => { ...@@ -1179,7 +1187,7 @@ const handleBox8Data = async () => {
const selectParam = { const selectParam = {
moduleType: '国会法案', moduleType: '国会法案',
key: '领域', key: '所处阶段',
selectedDate: box8selectetedTime.value, selectedDate: box8selectetedTime.value,
isInvolveCn: 1 isInvolveCn: 1
} }
...@@ -1212,7 +1220,7 @@ const handleBox8Data = async () => { ...@@ -1212,7 +1220,7 @@ const handleBox8Data = async () => {
box8StageList.value = data.stages; box8StageList.value = data.stages;
await nextTick(); await nextTick();
const box8Chart = getBox8ChartOption(data.stages); const box8Chart = getBox8ChartOption(data.stages);
box8ChartInstance = setChart(box8Chart, "box8Chart" , true, selectParam); box8ChartInstance = setChart(box8Chart, "box8Chart", true, selectParam);
} else { } else {
box8HasData.value = false; box8HasData.value = false;
box8Summary.value = 0; box8Summary.value = 0;
......
...@@ -3,20 +3,20 @@ import * as echarts from 'echarts' ...@@ -3,20 +3,20 @@ import * as echarts from 'echarts'
const getMultiLineChart = (dataX, dataY1, dataY2, dataY3) => { const getMultiLineChart = (dataX, dataY1, dataY2, dataY3) => {
return { return {
tooltip: { tooltip: {
trigger: 'axis', trigger: 'item',
axisPointer: { axisPointer: {
type: 'cross', type: 'cross',
label: { label: {
backgroundColor: '#6a7985' backgroundColor: '#6a7985'
} }
}, },
formatter: function (params) { // formatter: function (params) {
let res = params[0].name + '<br/>'; // let res = params[0].name + '<br/>';
params.forEach(item => { // params.forEach(item => {
res += item.marker + item.seriesName + ': ' + item.value + (item.seriesName === '通过率' ? '%' : '') + '<br/>'; // res += item.marker + item.seriesName + ': ' + item.value + (item.seriesName === '通过率' ? '%' : '') + '<br/>';
}); // });
return res; // return res;
} // }
}, },
grid: { grid: {
width: '96%', width: '96%',
......
...@@ -50,12 +50,9 @@ const emit = defineEmits<{ ...@@ -50,12 +50,9 @@ const emit = defineEmits<{
const billFields = [ const billFields = [
{ key: 'proposalDate', label: '提案时间:' }, { key: 'proposalDate', label: '提案时间:' },
{ key: 'areas', label: '涉及领域:' }, { key: 'areas', label: '涉及领域:' },
{ key: 'electionPhase', label: '选举周期阶段:' },
{ key: 'proposer', label: '提案人:' }, { key: 'proposer', label: '提案人:' },
{ key: 'coProposers', label: '共同提案人:' }, { key: 'coProposers', label: '共同提案人:' },
{ key: 'proposerPosition', label: '提案人职务:' },
{ key: 'governmentType', label: '政府结构类型:' }, { key: 'governmentType', label: '政府结构类型:' },
{ key: 'budgetScale', label: '法案预算规模:' },
{ key: 'passDays', label: '通过耗时:' } { key: 'passDays', label: '通过耗时:' }
] ]
</script> </script>
......
...@@ -2,59 +2,192 @@ ...@@ -2,59 +2,192 @@
<section class="filter-section"> <section class="filter-section">
<div class="section-header"> <div class="section-header">
<div class="header-left"> <div class="header-left">
<img src="../assets/fitller.svg" /> <img src="../assets/fitller.svg" />
<h2 class="section-title text-title-3-bold">核心相似度维度筛选</h2> <h2 class="section-title text-title-3-bold">核心相似度维度筛选</h2>
</div> </div>
<button class="btn-outline" @click="emit('setAsCurrent')">设置为当前提案</button> <button class="btn-outline" @click="setAsCurrent">设置为当前提案</button>
</div> </div>
<div class="divider" /> <div class="divider" />
<div class="fields-grid"> <div class="fields-grid">
<div
v-for="field in fields" <!-- 政策领域 -->
:key="field.id" <div class="field-item">
class="field-item" <span class="field-label text-tip-1 text-primary-65-clor">政策领域:</span>
> <div class="field-content">
<div class="field-label-wrapper"> <el-select
<span class="field-label text-tip-1 text-primary-65-clor">{{ field.label }}</span> v-model="localValues.policyArea"
multiple
placeholder="请选择"
style="width: 420px"
@change="handleChange"
>
<el-option
v-for="opt in fields.policyArea.options"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</div> </div>
</div>
<!-- 政府结构类型 -->
<div class="field-item">
<span class="field-label text-tip-1 text-primary-65-clor">政府结构类型:</span>
<div class="field-content"> <div class="field-content">
<FilterSelect <el-select
:options="field.options" v-model="localValues.governmentType"
:model-value="field.selectedValues" multiple
@update:model-value="(val) => handleFieldUpdate(field.id, val)" placeholder="请选择"
/> style="width: 420px"
<div v-if="field.hint" class="field-hint"> @change="handleChange"
<img src="../assets/importent.svg" /> >
<span class="text-tip-2 text-primary-50-clor">{{ field.hint }}</span> <el-option
v-for="opt in fields.governmentType.options"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<div v-if="fields.governmentType.hint" class="field-hint">
<img src="../assets/importent.svg" />
<span class="text-tip-2 text-primary-50-clor">{{ fields.governmentType.hint }}</span>
</div> </div>
</div> </div>
</div> </div>
<!-- 对方党派提案人 -->
<div class="field-item">
<span class="field-label text-tip-1 text-primary-65-clor">对方党派提案人:</span>
<div class="field-content">
<el-select
v-model="localValues.oppositionProposer"
multiple
placeholder="请选择"
style="width: 420px"
@change="handleChange"
>
<el-option
v-for="opt in fields.oppositionProposer.options"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</div>
</div>
<!-- 提案时间 -->
<div class="field-item">
<span class="field-label text-tip-1 text-primary-65-clor">提案时间:</span>
<div class="field-content">
<el-select
v-model="localValues.proposalTime"
multiple
placeholder="请选择"
style="width: 420px"
@change="handleChange"
>
<el-option
v-for="opt in fields.proposalTime.options"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</div>
</div>
</div> </div>
</section> </section>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { FilterField } from '../api' import { ref, computed, watch } from 'vue'
import FilterSelect from './FilterSelect.vue' import type { ProposalInfo } from '../api'
const props = defineProps<{ const props = defineProps<{
fields: FilterField[] proposalInfo?: ProposalInfo | null
defaultFilters?: Record<string, string[]>
}>() }>()
const emit = defineEmits<{ // 本地 v-model 值
'update:fields': [fields: FilterField[]] const localValues = ref({
setAsCurrent: [] policyArea: [] as string[],
}>() governmentType: [] as string[],
oppositionProposer: [] as string[],
proposalTime: [] as string[],
})
// 更新字段选中值 // 根据 proposalInfo 计算初始筛选值(即"设置为当前提案"的目标状态)
function handleFieldUpdate(fieldId: string, newValues: string[]) { function buildInitialValues(info?: ProposalInfo | null): Record<string, string[]> {
const updatedFields = props.fields.map(f => if (!info) return { policyArea: [], governmentType: [], oppositionProposer: [], proposalTime: [] }
f.id === fieldId return {
? { ...f, selectedValues: newValues } policyArea: info.defaultDomains?.length ? [...info.defaultDomains] : [...(info.areas || [])],
: f governmentType: info.patternType ? [info.patternType] : [],
) oppositionProposer: [],
emit('update:fields', updatedFields) proposalTime: [],
}
} }
// 监听 proposalInfo 首次传入时设置初始值
watch(
() => props.proposalInfo,
(newInfo) => {
Object.assign(localValues.value, buildInitialValues(newInfo))
},
{ immediate: true }
)
// 重置:清空所有已选项
function reset() {
localValues.value = {
policyArea: [],
governmentType: [],
oppositionProposer: [],
proposalTime: [],
}
}
// 设置为当前提案:恢复为 proposalInfo 的初始状态
function setAsCurrent() {
Object.assign(localValues.value, buildInitialValues(props.proposalInfo))
}
defineExpose({ reset, setAsCurrent, getValues: () => localValues.value })
// 固定的字段配置,options 由 proposalInfo 动态注入
const fields = computed(() => {
const billDomain = props.proposalInfo?.billDomain
const domainOptions = Array.isArray(billDomain) ? billDomain.map(d => ({ value: d, label: d })) : []
return {
policyArea: { options: domainOptions },
governmentType: {
options: [
{ value: '统一政府', label: '统一政府' },
{ value: '分裂政府', label: '分裂政府' },
{ value: '微弱多数', label: '微弱多数' }
],
hint: '总统所属政党同时控制国会参众两院'
},
oppositionProposer: {
options: [
{ value: '两党共同提案', label: '两党共同提案' },
{ value: '单政党提案(共和党提案)', label: '单政党提案(共和党提案)' },
{ value: '单政党提案(民主党提案)', label: '单政党提案(民主党提案)' }
]
},
proposalTime: {
options: [
{ value: '近五年', label: '近五年' },
{ value: '近十年', label: '近十年' },
{ value: '全部', label: '全部' }
]
}
}
})
function handleChange() {}
</script> </script>
<style scoped> <style scoped>
...@@ -75,12 +208,6 @@ function handleFieldUpdate(fieldId: string, newValues: string[]) { ...@@ -75,12 +208,6 @@ function handleFieldUpdate(fieldId: string, newValues: string[]) {
gap: 12px; gap: 12px;
} }
.section-icon {
width: 16px;
height: 16px;
color: var(--text-primary-80-color);
}
.btn-outline { .btn-outline {
padding: 6px 16px; padding: 6px 16px;
border: 1px solid var(--bg-black-10); border: 1px solid var(--bg-black-10);
...@@ -105,7 +232,7 @@ function handleFieldUpdate(fieldId: string, newValues: string[]) { ...@@ -105,7 +232,7 @@ function handleFieldUpdate(fieldId: string, newValues: string[]) {
.fields-grid { .fields-grid {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 24px 24px; gap: 24px;
padding-left: 30px; padding-left: 30px;
} }
...@@ -113,15 +240,14 @@ function handleFieldUpdate(fieldId: string, newValues: string[]) { ...@@ -113,15 +240,14 @@ function handleFieldUpdate(fieldId: string, newValues: string[]) {
width: 580px; width: 580px;
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
} gap: 0;
.field-label-wrapper {
padding-top: 4px;
} }
.field-label { .field-label {
display: block; display: block;
width: 150px; width: 140px;
flex-shrink: 0;
padding-top: 6px;
} }
.field-content { .field-content {
...@@ -135,11 +261,4 @@ function handleFieldUpdate(fieldId: string, newValues: string[]) { ...@@ -135,11 +261,4 @@ function handleFieldUpdate(fieldId: string, newValues: string[]) {
align-items: center; align-items: center;
gap: 8px; gap: 8px;
} }
.hint-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
color: var(--text-primary-50-color);
}
</style> </style>
...@@ -39,26 +39,19 @@ ...@@ -39,26 +39,19 @@
</p> </p>
</div> </div>
</div> </div>
<div class="facts-section"> <div v-if="phase.predictionBasis" class="facts-section">
<div class="box-header flex-display-start"> <div class="box-header flex-display-start">
<div class="box-title-row flex-display-center"> <div class="box-title-row flex-display-center">
<img src="../assets/icon1.svg"/> <img src="../assets/icon1.svg"/>
<span class="text-compact-bold">{{ phase.supportingFacts.title }}</span> <span class="text-compact-bold">通过性预测依据</span>
</div> </div>
<div class="box-hint flex-display-center text-tip-2 text-primary-50-clor"> <div class="box-hint flex-display-center text-tip-2 text-primary-50-clor">
<img src="../assets/importent.svg"/> <img src="../assets/importent.svg"/>
<span>{{ phase.supportingFacts.basedOn }}</span> <span>此阶段预测基于以下观点</span>
</div> </div>
</div> </div>
<div class="stats-grid"> <div class="prediction-basis-content">
<div <p class="text-tip-2 text-primary-65-clor">{{ phase.predictionBasis }}</p>
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> </div>
</div> </div>
...@@ -260,6 +253,16 @@ const riskLabel = computed(() => { ...@@ -260,6 +253,16 @@ const riskLabel = computed(() => {
line-height: 1.6; line-height: 1.6;
} }
.prediction-basis-content {
background-color: var(--bg-black-2);
border-radius: var(--radius-10);
padding: 16px;
}
.prediction-basis-content p {
line-height: 1.6;
}
.stats-grid { .stats-grid {
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
......
...@@ -33,13 +33,6 @@ ...@@ -33,13 +33,6 @@
<span class="info-label text-body-1">{{ field.label }}</span> <span class="info-label text-body-1">{{ field.label }}</span>
<template v-if="field.key === 'areas'"> <template v-if="field.key === 'areas'">
<div class="area-tags"> <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" /> <AreaTag v-for="area in info.areas" :key="area" :tagName="area" />
</div> </div>
...@@ -59,17 +52,14 @@ defineProps<{ ...@@ -59,17 +52,14 @@ defineProps<{
info: ProposalInfo info: ProposalInfo
}>() }>()
// 信息字段配置 // 信息字段配置 - 只保留:提案标题、提案时间、涉及领域、提案人、共同提案人、政府结构类型
const infoFields = [ const infoFields = [
{ key: 'title', label: '提案标题:' }, { key: 'title', label: '提案标题:' },
{ key: 'date', label: '提案时间:' }, { key: 'date', label: '提案时间:' },
{ key: 'areas', label: '涉及领域:' }, { key: 'areas', label: '涉及领域:' },
{ key: 'electionPhase', label: '选举周期阶段:' },
{ key: 'proposer', label: '提案人:' }, { key: 'proposer', label: '提案人:' },
{ key: 'coProposers', label: '共同提案人:' }, { key: 'coProposers', label: '共同提案人:' },
{ key: 'proposerPosition', label: '提案人职务:' }, { key: 'governmentType', label: '政府结构类型:' }
{ key: 'governmentType', label: '政府结构类型:' },
{ key: 'budgetScale', label: '法案预算规模:' }
] ]
</script> </script>
......
...@@ -7,9 +7,9 @@ ...@@ -7,9 +7,9 @@
<div class="content-wrapper"> <div class="content-wrapper">
<ProposalInfoSection v-if="currentProposalInfo" :info="currentProposalInfo" /> <ProposalInfoSection v-if="currentProposalInfo" :info="currentProposalInfo" />
<FilterSection <FilterSection
:fields="filterFields" ref="filterSectionRef"
@update:fields="filterFields = $event" :proposal-info="currentProposalInfo"
@set-as-current="handleSetAsCurrent" :default-filters="defaultFilters"
/> />
</div> </div>
<ActionButtons <ActionButtons
...@@ -22,8 +22,7 @@ ...@@ -22,8 +22,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, watch } from 'vue' import { ref, onMounted, watch } from 'vue'
import type { ProposalInfo, FilterField } from '../api' import type { ProposalInfo } from '../api'
import { fetchProposalInfo, fetchFilterFields } from '../api'
import ProposalInfoSection from './ProposalInfoSection.vue' import ProposalInfoSection from './ProposalInfoSection.vue'
import FilterSection from './FilterSection.vue' import FilterSection from './FilterSection.vue'
import ActionButtons from './ActionButtons.vue' import ActionButtons from './ActionButtons.vue'
...@@ -34,99 +33,42 @@ const props = defineProps<{ ...@@ -34,99 +33,42 @@ const props = defineProps<{
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
next: [] next: [selectedFilters: Record<string, string[]>]
}>() }>()
// 提案信息(优先使用 props,否则从 API 获取 // 提案信息(优先使用 props)
const currentProposalInfo = ref<ProposalInfo | null>(null) const currentProposalInfo = ref<ProposalInfo | null>(null)
// 筛选字段列表 // FilterSection 组件引用
const filterFields = ref<FilterField[]>([]) const filterSectionRef = ref<InstanceType<typeof FilterSection> | null>(null)
// 加载状态 // 加载状态
const loading = ref(true) const loading = ref(true)
// 初始筛选字段(用于重置) // 默认筛选条件
const initialFilterFields = ref<FilterField[]>([]) const defaultFilters = ref<Record<string, string[]>>({})
// 监听 props.proposalInfo 变化 // 监听 props.proposalInfo 变化
watch(() => props.proposalInfo, (newInfo) => { watch(() => props.proposalInfo, (newInfo) => {
if (newInfo) { if (newInfo) {
currentProposalInfo.value = newInfo currentProposalInfo.value = newInfo
} // 根据提案信息生成默认筛选条件(仅用于 Step1 内部状态跟踪,不再传给 FilterSection)
}, { immediate: true }) defaultFilters.value = {
policyArea: newInfo.defaultDomains?.length ? newInfo.defaultDomains : newInfo.areas,
// 页面初始化时加载数据 governmentType: newInfo.patternType ? [newInfo.patternType] : [],
onMounted(async () => { oppositionProposer: [],
try { proposalTime: [],
// 如果没有从 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
} }
}) loading.value = false
}, { immediate: true })
// 重置所有筛选条件 // 重置所有筛选条件:清空所有已选项
function handleReset() { function handleReset() {
filterFields.value = JSON.parse(JSON.stringify(initialFilterFields.value)) filterSectionRef.value?.reset()
}
// 设置为当前提案
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() { function handleNext() {
emit('next') const selectedFilters = filterSectionRef.value?.getValues() || {}
emit('next', selectedFilters)
} }
</script> </script>
......
...@@ -77,13 +77,12 @@ ...@@ -77,13 +77,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, inject, watch } from 'vue' import { ref, computed, onMounted, inject, watch } from 'vue'
import type { FilterStats, BillInfo } from '../api' import type { FilterStats, BillInfo } from '../api'
import { fetchFilterStats, fetchBillList } from '../api'
import { getSimiBills, transformSimiBillsData } from '@/api/bill/billHome' import { getSimiBills, transformSimiBillsData } from '@/api/bill/billHome'
import BillCard from './BillCard.vue' import BillCard from './BillCard.vue'
const emit = defineEmits<{ const emit = defineEmits<{
previous: [] previous: []
next: [] next: [selectedBills: any[]]
}>() }>()
// 从父组件注入筛选参数 // 从父组件注入筛选参数
...@@ -93,6 +92,8 @@ const filterParams = inject<any>('filterParams', null) ...@@ -93,6 +92,8 @@ const filterParams = inject<any>('filterParams', null)
const stats = ref<FilterStats | null>(null) const stats = ref<FilterStats | null>(null)
// 法案列表 // 法案列表
const bills = ref<BillInfo[]>([]) const bills = ref<BillInfo[]>([])
// 原始法案数据(用于传给第三页)
const rawBillsData = ref<any[]>([])
// 加载状态 // 加载状态
const loading = ref(true) const loading = ref(true)
...@@ -121,36 +122,30 @@ const selectedCount = computed(() => { ...@@ -121,36 +122,30 @@ const selectedCount = computed(() => {
async function loadData() { async function loadData() {
loading.value = true loading.value = true
try { try {
// 优先使用真实 API // 使用真实 API,传入 billIds、domains、patternType、proposalType
const params = { const params = {
patternType: filterParams?.governmentType || '统一政府', billIds: filterParams?.value.billIds,
proposalType: '两党共同提案' domains:JSON.stringify(filterParams?.value.domains) || [],
patternType: filterParams?.value.patternType || '统一政府',
proposalType: filterParams?.value.proposalType || '两党共同提案'
} }
const response = await getSimiBills(params) const response = await getSimiBills(params)
if (response && response.success && response.data) { if (response && response.data) {
// 保存原始数据
rawBillsData.value = response.data
const { stats: apiStats, bills: apiBills } = transformSimiBillsData(response) const { stats: apiStats, bills: apiBills } = transformSimiBillsData(response)
stats.value = apiStats stats.value = apiStats
bills.value = apiBills bills.value = apiBills
} else { } else {
// 如果 API 失败,使用模拟数据 stats.value = null
const [statsData, billsData] = await Promise.all([ bills.value = []
fetchFilterStats(),
fetchBillList()
])
stats.value = statsData
bills.value = billsData
} }
} catch (error) { } catch (error) {
console.error('获取相似法案失败:', error) console.error('获取相似法案失败:', error)
// 使用模拟数据作为 fallback stats.value = null
const [statsData, billsData] = await Promise.all([ bills.value = []
fetchFilterStats(),
fetchBillList()
])
stats.value = statsData
bills.value = billsData
} finally { } finally {
loading.value = false loading.value = false
} }
...@@ -189,7 +184,11 @@ function handleBack() { ...@@ -189,7 +184,11 @@ function handleBack() {
// 开始预测分析 // 开始预测分析
function handleStartAnalysis() { function handleStartAnalysis() {
emit('next') // 获取用户勾选的法案ID列表
const selectedIds = bills.value.filter(b => b.selected).map(b => b.id)
// 从原始数据中筛选出用户勾选的法案
const selectedBills = rawBillsData.value.filter(b => selectedIds.includes(b.bill_id))
emit('next', selectedBills)
} }
</script> </script>
......
<template> <template>
<div class="step-container"> <div class="step-container">
<div v-if="predictionData" class="content-wrapper"> <div v-if="props.loading" class="loading-wrapper flex-display-center">
<span class="text-tip-2 text-primary-50-clor">预测分析中...</span>
</div>
<div v-else-if="predictionData" class="content-wrapper">
<div class="header-section flex-display-start"> <div class="header-section flex-display-start">
<div class="header-left flex-display-start"> <div class="header-left flex-display-start">
...@@ -27,8 +30,8 @@ ...@@ -27,8 +30,8 @@
<div class="highlight-box text-regular"> <div class="highlight-box text-regular">
<div class="highlight-wave text-regular"></div> <div class="highlight-wave text-regular"></div>
<div class="highlight-content text-regular"> <div class="highlight-content text-regular">
<p class="highlight-title text-regular">《大而美法案》的通过概率非常高</p> <p class="highlight-title text-regular">该法案的通过概率为 {{ predictionData?.overallProbability || '—' }}</p>
<p class="highlight-text text-regular">该法案由众议院共和党领导层在5月正式提出,作为特朗普第二任期核心经济议程,旨在通过一揽子税收、贸易和预算改革提振经济。到6月初,法案已快速通过关键的筹款委员会和预算委员会审议,进入众议院全院辩论阶段。共和党当时控制众议院,且党内团结支持;白宫已明确表示强烈支持。虽然民主党普遍反对,但共和党凭借席位优势足以在众议院通过。主要不确定性在于参议院,但预计部分温和民主党议员可能支持,或通过预算和解程序(只需简单多数)规避阻挠议事。因此,该法案在6月初已势在必行,最终成法几无悬念。</p> <p class="highlight-text text-regular">{{ predictionData?.highlightText }}</p>
</div> </div>
</div> </div>
<div v-if="predictionData?.phases?.length" class="phases-list"> <div v-if="predictionData?.phases?.length" class="phases-list">
...@@ -59,23 +62,22 @@ ...@@ -59,23 +62,22 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, computed } from 'vue' import { ref, computed } from 'vue'
import { fetchPredictionAnalysis, type PredictionAnalysis } from '../api' import type { PredictionAnalysis } from '../api'
import PredictionPhaseCard from './PredictionPhaseCard.vue' import PredictionPhaseCard from './PredictionPhaseCard.vue'
const props = defineProps<{
data?: PredictionAnalysis | null
loading?: boolean
}>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'prev'): void (e: 'prev'): void
(e: 'repredict'): void (e: 'repredict'): void
}>() }>()
// 预测分析数据 // 预测分析数据 - 从 props 获取,暂无真实接口
const predictionData = ref<PredictionAnalysis | null>(null) const predictionData = computed(() => props.data || null)
// 获取预测分析数据
onMounted(async () => {
const data = await fetchPredictionAnalysis()
predictionData.value = data
})
// 根据索引和progressLevel返回进度条格子的类名 // 根据索引和progressLevel返回进度条格子的类名
function getOverallSegmentClass(index: number) { function getOverallSegmentClass(index: number) {
...@@ -140,15 +142,16 @@ function handleRepredict() { ...@@ -140,15 +142,16 @@ function handleRepredict() {
</script> </script>
<style scoped> <style scoped>
/* .text-title-2-bold{
color: #3b414b;
} */
.step-container { .step-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
} }
.loading-wrapper {
flex: 1;
}
.content-wrapper { .content-wrapper {
flex: 1; flex: 1;
overflow: auto; overflow: auto;
...@@ -157,7 +160,6 @@ function handleRepredict() { ...@@ -157,7 +160,6 @@ function handleRepredict() {
.header-section { .header-section {
justify-content: space-between; justify-content: space-between;
align-items: flex-start; align-items: flex-start;
/* margin-bottom: 24px; */
} }
.header-left { .header-left {
......
...@@ -25,8 +25,8 @@ ...@@ -25,8 +25,8 @@
</div> </div>
<div class="meta-row"> <div class="meta-row">
<span class="meta-label">相关领域:</span> <span class="meta-label">相关领域:</span>
<div class="meta-tags"> <div class="meta-tags">
<TagBadge v-for="item in bill.industryList" :key="item.industryName" :label="item.industryName" tag-class="tag3" /> <AreaTag v-for="item in bill.industryList" :key="item.industryName" :tagName="item.industryName" />
</div> </div>
</div> </div>
<div class="meta-row"> <div class="meta-row">
...@@ -44,8 +44,7 @@ ...@@ -44,8 +44,7 @@
<script setup> <script setup>
import { computed } from 'vue' import { computed } from 'vue'
import DocumentPreview from './DocumentPreview.vue' import DocumentPreview from './DocumentPreview.vue'
import TagBadge from './TagBadge.vue'
import ProgressBar from './ProgressBar.vue' import ProgressBar from './ProgressBar.vue'
const props = defineProps({ const props = defineProps({
...@@ -179,23 +178,25 @@ const currentStageIndex = computed(() => { ...@@ -179,23 +178,25 @@ const currentStageIndex = computed(() => {
.bill-card-meta { .bill-card-meta {
width: 100%; width: 100%;
flex: 1; flex: 1;
position: relative; display: flex;
flex-direction: column;
gap: 12px;
min-height: 0; min-height: 0;
overflow: hidden;
} }
.meta-row { .meta-row {
display: flex; display: flex;
align-items: center; align-items: flex-start;
position: absolute;
left: 0;
width: 100%; width: 100%;
gap: 12px;
} }
.meta-row:nth-child(1) { top: 0; } .meta-row:nth-child(1) { }
.meta-row:nth-child(2) { top: 36px; } .meta-row:nth-child(2) { }
.meta-row:nth-child(3) { top: 72px; } .meta-row:nth-child(3) { }
.meta-row:nth-child(4) { top: 108px; } .meta-row:nth-child(4) { }
.meta-row:nth-child(5) { top: 144px; } .meta-row:nth-child(5) { }
.meta-label { .meta-label {
font-size: 16px; font-size: 16px;
...@@ -206,6 +207,7 @@ const currentStageIndex = computed(() => { ...@@ -206,6 +207,7 @@ const currentStageIndex = computed(() => {
white-space: nowrap; white-space: nowrap;
flex-shrink: 0; flex-shrink: 0;
width: 100px; width: 100px;
padding-top: 2px;
} }
.meta-value { .meta-value {
...@@ -213,6 +215,8 @@ const currentStageIndex = computed(() => { ...@@ -213,6 +215,8 @@ const currentStageIndex = computed(() => {
font-weight: 400; font-weight: 400;
color: rgba(95, 101, 108, 1); color: rgba(95, 101, 108, 1);
line-height: 24px; line-height: 24px;
flex: 1;
min-width: 0;
} }
.sponsor-name { .sponsor-name {
...@@ -225,11 +229,11 @@ const currentStageIndex = computed(() => { ...@@ -225,11 +229,11 @@ const currentStageIndex = computed(() => {
gap: 8px; gap: 8px;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
flex: 1;
min-width: 0;
} }
.meta-row-progress { .meta-row-progress {
left: 0;
right: 0;
width: 100%; width: 100%;
} }
.meta-row-progress :deep(.progress-bar) { .meta-row-progress :deep(.progress-bar) {
......
...@@ -84,7 +84,7 @@ ...@@ -84,7 +84,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="line-test"></div> <!-- <div class="line-test"></div> -->
</div> </div>
<div class="pagination"> <div class="pagination">
<div class="total">{{ `共 ${total} 项` }}</div> <div class="total">{{ `共 ${total} 项` }}</div>
...@@ -853,10 +853,12 @@ onMounted(() => { ...@@ -853,10 +853,12 @@ onMounted(() => {
z-index: 110; z-index: 110;
margin-top: 10px; margin-top: 10px;
.main-item { .main-item {
width: 1014px; width: 1014px;
margin-bottom: 40px; display: flex;
display: flex; flex-direction: row;
align-items: flex-start;
margin-bottom: 20px;
position: relative;
.time { .time {
width: 77px; width: 77px;
box-sizing: border-box; box-sizing: border-box;
...@@ -995,7 +997,16 @@ onMounted(() => { ...@@ -995,7 +997,16 @@ onMounted(() => {
} }
} }
} }
.main-item::after {
content: '';
position: absolute;
left: 109px;
top: 24px;
bottom: -20px;
width: 1px;
background-color: rgb(230, 231, 232);
z-index: -1;
}
.line-test { .line-test {
position: absolute; position: absolute;
top: 10px; top: 10px;
......
...@@ -69,7 +69,7 @@ ...@@ -69,7 +69,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="line-test"></div> <!-- <div class="line-test"></div> -->
</div> </div>
<div class="pagination"> <div class="pagination">
<div class="total">{{ `共 ${total} 项` }}</div> <div class="total">{{ `共 ${total} 项` }}</div>
...@@ -605,10 +605,13 @@ const companyList = ref([ ...@@ -605,10 +605,13 @@ const companyList = ref([
z-index: 110; z-index: 110;
.main-item { .main-item {
width: 100%; width: 100%;
margin-bottom: 40px;
display: flex; display: flex;
display: flex;
flex-direction: row;
align-items: flex-start;
margin-bottom: 20px;
position: relative;
.time { .time {
width: 77px; width: 77px;
box-sizing: border-box; box-sizing: border-box;
...@@ -717,7 +720,16 @@ const companyList = ref([ ...@@ -717,7 +720,16 @@ const companyList = ref([
} }
} }
} }
.main-item::after {
content: '';
position: absolute;
left: 109px;
top: 24px;
bottom: -20px;
width: 1px;
background-color: rgb(230, 231, 232);
z-index: -1;
}
.line-test { .line-test {
position: absolute; position: absolute;
top: 10px; top: 10px;
......
...@@ -78,7 +78,7 @@ const props = defineProps({ ...@@ -78,7 +78,7 @@ const props = defineProps({
type: String, type: String,
default: '' default: ''
}, },
chartTypeList : { chartTypeList: {
type: Array, type: Array,
default: [] default: []
} }
...@@ -88,15 +88,19 @@ const chartItemList = computed(() => { ...@@ -88,15 +88,19 @@ const chartItemList = computed(() => {
let arr = [] let arr = []
props.chartTypeList.forEach(item => { props.chartTypeList.forEach(item => {
defaultChartTypeList.forEach(val => { defaultChartTypeList.forEach(val => {
if(val.name === item) { if (val.name === item) {
arr.push(val) arr.push(val)
} }
}) })
}) })
arr.forEach(item => { arr.forEach(item => {
item.active = false item.active = false
}) })
arr[0].active = true arr[0].active = true
// console.log('arr', arr);
return arr return arr
}) })
......
<template> <template>
<div class="select-wrapper" :class="{'select-wrapper-custom': selectValue === '自定义'}"> <div class="select-wrapper" :class="{ 'select-wrapper-custom': selectValue === '自定义' }">
<div class="select-left text-tip-1">{{ selectTitle + ':' }}</div> <div class="select-left text-tip-1">{{ selectTitle + ':' }}</div>
<div class="select-right" :class="{'select-right-custom': selectValue === '自定义'}"> <div class="select-right" :class="{ 'select-right-custom': selectValue === '自定义' }">
<el-select v-model="selectValue" :placeholder="placeholderName" style="width: 240px"> <el-select v-model="selectValue" :placeholder="placeholderName" style="width: 240px">
<!-- <el-option label="全部领域" value="全部领域" /> --> <!-- <el-option label="全部领域" value="全部领域" /> -->
<el-option v-for="item in selectList" :key="item.id" :label="item.name" :value="item.id" /> <el-option v-for="item in selectList" :key="item.id" :label="item.name" :value="item.id" />
</el-select> </el-select>
<el-date-picker style="width: 300px" v-if="selectValue === '自定义'" v-model="customTimeValue" type="daterange" <el-date-picker style="width: 300px" v-if="selectValue==='自定义'" v-model="customTimeValue" type="daterange" value-format="YYYY-MM-DD"
range-separator="--" start-placeholder="开始日期" end-placeholder="结束日期" size="default" /> range-separator="--" start-placeholder="开始日期" end-placeholder="结束日期" size="default" />
<!-- <el-date-picker v-model="customTimeValue" type="daterange" value-format="YYYY-MM-DD">
</el-date-picker> -->
</div> </div>
</div> </div>
</template> </template>
...@@ -61,6 +63,7 @@ const customTimeValue = computed({ ...@@ -61,6 +63,7 @@ const customTimeValue = computed({
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
.select-left { .select-left {
width: 100px; width: 100px;
height: 24px; height: 24px;
...@@ -72,15 +75,18 @@ const customTimeValue = computed({ ...@@ -72,15 +75,18 @@ const customTimeValue = computed({
display: flex; display: flex;
gap: 8px; gap: 8px;
justify-content: space-between; justify-content: space-between;
:deep(.el-input__wrapper) { :deep(.el-input__wrapper) {
border-radius: 4px; border-radius: 4px;
} }
} }
.select-right-custom{
.select-right-custom {
width: 630px; width: 630px;
} }
} }
.select-wrapper-custom{
.select-wrapper-custom {
width: 738px; width: 738px;
} }
</style> </style>
\ No newline at end of file
...@@ -478,12 +478,13 @@ const handleClickToolBox = () => { ...@@ -478,12 +478,13 @@ const handleClickToolBox = () => {
onMounted(() => { onMounted(() => {
const path = route.path const path = route.path
console.log(decodeURI(route.fullPath));
switch (path) { switch (path) {
case '/dataLibrary/countryBill': case '/dataLibrary/countryBill':
siderList.value[0].active = true siderList.value[0].active = true
siderList.value[0].isExpanded = true siderList.value[0].isExpanded = true
siderList.value[0].children[0].active = true siderList.value[0].children[0].active = true
break break
case '/dataLibrary/stateBill': case '/dataLibrary/stateBill':
siderList.value[0].active = true siderList.value[0].active = true
......
<template>
<div class="view-box">
<div class="container-box">
<div class="hard-box">
<div class="hard-name text-title-0-show">美国政府机构</div>
<div class="hard-num text-title-2-show">{{organizationInfo.total}}</div>
<div style="width: 0px; flex: auto;"></div>
<div class="hard-input">
<el-input v-model="organizationInfo.keyWord" @keyup.enter="onAllOrganization()" style="width:100%; height:100%;" :suffix-icon="Search" placeholder="搜索机构" />
</div>
<div class="hard-time">
<el-select v-model="organizationInfo.isSort" @change="onAllOrganization()" placeholder="发布时间" style="width:160px; margin-left:8px;">
<template #prefix>
<div class="icon1">
<img v-if="isSort" src="@/assets/icons/shengxu1.png" alt="" />
<img v-else src="@/assets/icons/jiangxu1.png" alt="" />
</div>
</template>
<el-option label="政令数量" :value="1" />
</el-select>
</div>
</div>
<div class="date-box">
<div class="date-icon">
<img :src="tipsTcon" alt="">
</div>
<div class="date-text">近期美国各联邦政府机构发布涉华政令数量汇总</div>
<TimeTabPane @time-click="handleDateChange" />
</div>
<div class="organization-list" ref="refOrganization" v-loading="organizationInfo.loading">
<div class="organization-item" v-for="(item, index) in organizationInfo.list" :key="index" @click="handleToInstitution(item)">
<div class="item-left">
<img :src="item.imgUrl || DefaultIcon2" alt="" />
</div>
<div class="item-right one-line-ellipsis">{{ item.orgName }}</div>
<div class="item-total">{{ item.totalOrderNum }}项</div>
<el-icon color="var(--color-primary-100)"><ArrowRightBold /></el-icon>
<div class="item-dot" v-if="item.recentOrderNum">+{{item.recentOrderNum}}</div>
</div>
</div>
<div class="pagination-box">
<el-pagination @current-change="onAllOrganization" :pageSize="organizationInfo.pageSize" :current-page="organizationInfo.pageNum" background layout="prev, pager, next" :total="organizationInfo.total" />
</div>
</div>
<div class="back-bnt" @click="router.back()">
<el-icon><Back /></el-icon>
<div style="margin-left: 6px;">返回</div>
</div>
</div>
</template>
<script setup name="index">
import {onMounted, reactive, ref} from "vue"
import { Search } from '@element-plus/icons-vue'
import router from "@/router";
import TimeTabPane from '@/components/base/TimeTabPane/index.vue';
import { getAllOrganization } from "@/api/decree/home";
import tipsTcon from "./assets/icons/tips-icon.png";
import DefaultIcon2 from "@/assets/icons/default-icon2.png";
const organizationInfo = reactive({
loading: false,
pageNum: 1,
pageSize: 8,
total: 0,
isSort: 1,
keyWord: "",
day: 7,
list: []
})
const onAllOrganization = async (num) => {
organizationInfo.pageNum = num || 1
organizationInfo.loading = true
try {
let {keyWord, pageNum, pageSize, day} = organizationInfo
const res = await getAllOrganization({day, pageNum:pageNum-1, pageSize, keyWord: keyWord||undefined});
console.log("机构列表", res);
if (res.code === 200) {
organizationInfo.list = res.data.orgList || [];
organizationInfo.total = res.data.total || 0;
}
} catch (error) {
console.error("获取机构列表数据失败", error);
organizationInfo.list = [];
organizationInfo.total = 0;
}
organizationInfo.loading = false
}
const handleDateChange = (event) => {
if (event?.time === '近一周') organizationInfo.day = 7
if (event?.time === '近一月') organizationInfo.day = 30
if (event?.time === '近一年') organizationInfo.day = 365
onAllOrganization()
}
// 跳转行政机构主页
const handleToInstitution = item => {
window.sessionStorage.setItem("curTabName", item.orgName);
const curRoute = router.resolve({
path: "/institution",
query: {
id: item.orgId
}
});
window.open(curRoute.href, "_blank");
};
const refOrganization = ref()
onMounted(() => {
// 根据元素的高度决定分页显示的机构数量
let height = 2;
if (refOrganization.value) {
height = Math.floor(refOrganization.value?.clientHeight/120)
}
organizationInfo.pageSize = height*4
onAllOrganization()
})
</script>
<style scoped lang="scss">
.view-box {
width: 100%;
height: 100%;
background: url("./assets/images/background.png"), linear-gradient(180deg, rgba(229, 241, 254, 1) 0%, rgba(246, 251, 255, 0) 30%);
background-size: 100% 100%;
display: flex;
justify-content: center;
position: relative;
.back-bnt {
position: absolute;
top: 16px;
left: 30px;
width: 86px;
height: 38px;
background-color: white;
border-radius: 19px;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-primary-65-color);
font-family: Source Han Sans CN;
font-size: 16px;
cursor: pointer;
}
.container-box {
width: 1600px;
padding: 50px 0 20px;
display: flex;
flex-direction: column;
.hard-box {
display: flex;
align-items: center;
width: 100%;
.hard-name {
color: var(--text-primary-90-color);
height: 62px;
line-height: 62px !important;
}
.hard-num {
height: 36px;
background-color: var(--color-primary-100);
color: var(--bg-white-100);
border-radius: 18px;
line-height: 36px !important;
padding: 0 16px;
margin-left: 16px;
}
.hard-input {
background-color: var(--el-fill-color-blank);
border-radius: var(--el-border-radius-base);
box-shadow: 0 0 0 1px var(--el-border-color) inset;
box-sizing: border-box;
margin-left: 20px;
width: 160px;
height: 32px;
}
.hard-time {
height: 42px;
padding: 5px 0;
.icon1 {
width: 11px;
height: 14px;
font-size: 0px;
img {
width: 100%;
height: 100%;
}
}
}
}
.date-box {
margin-top: 6px;
display: flex;
align-items: center;
width: 100%;
.date-icon {
width: 16px;
height: 16px;
font-size: 0px;
margin-right: 6px;
img {
width: 100%;
height: 100%;
}
}
.date-text {
width: 20px;
flex: auto;
font-size: 18px;
line-height: 18px;
font-family: Source Han Sans CN;
color: var(--text-primary-80-color);
}
}
.organization-list {
width: 100%;
height: 20px;
padding: 16px 0;
margin-top: 10px;
flex: auto;
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 104px;
gap: 16px;
font-family: Microsoft YaHei;
.organization-item {
padding: 0 16px;
display: flex;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.65);
border: 1px solid rgba(255, 255, 255, 1);
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
align-items: center;
justify-content: center;
cursor: pointer;
transition: transform 0.3s ease, box-shadow 0.3s ease;
position: relative;
&:hover {
transform: translateY(-3px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
.item-left {
width: 48px;
height: 48px;
font-size: 0px;
img {
width: 100%;
height: 100%;
}
}
.item-right {
width: 20px;
flex: auto;
color: rgba(59, 65, 75, 1);
font-size: 20px;
font-weight: 700;
line-height: 20px;
margin: 0 16px;
}
.item-total {
font-size: 20px;
margin-right: 2px;
white-space: nowrap;
font-weight: 700;
line-height: 20px;
color: var(--color-primary-100);
}
.item-more {
font-size: 16px;
margin-right: 12px;
white-space: nowrap;
font-weight: 700;
line-height: 16px;
color: var(--color-primary-100);
}
.item-dot {
position: absolute;
right: -13px;
top: -10px;
padding: 0 8px;
height: 26px;
background-color: #FF4D4F;
color: white;
font-size: 16px;
line-height: 26px;
font-family: Source Han Sans CN;
border-radius: 14px;
letter-spacing: 1px;
}
}
}
.pagination-box {
display: flex;
justify-content: center;
}
}
}
</style>
\ No newline at end of file
<template>
<div class="view-box">
<div ref="graphContainer" style="height: 100%; width: 100%;"></div>
</div>
</template>
<script setup name="MindGraph">
import { ref, onBeforeUnmount } from "vue"
import * as G6 from '@antv/g6';
// 初始化画布
const graphContainer = ref(null);
let graph = null;
const onInitGraph = () => {
const container = graphContainer.value;
const width = container.clientWidth;
const height = container.clientHeight;
graph = new G6.Graph({
container: container,
width, height,
// fitView: true,
// fitViewPadding: 50,
defaultNode: {
type: "rect",
size: [250, 45],
style: {
fill: "#F6FAFF",
stroke: "#B9DCFF",
lineWidth: 1
},
labelCfg: {
style: {
fill: "#055FC2",
fontSize: 18,
lineHeight: 25,
fontWeight: "bold",
fontFamily: "Source Han Sans CN",
}
}
},
defaultEdge: {
type: "cubic-horizontal",
style: {
stroke: "#B9DCFF",
lineWidth: 2,
endArrow: true,
}
},
layout: {
type: 'dagre', // 层次布局
rankdir: 'LR', // 布局从左向右
controlPoints: true, // 节点间连线的控制点
nodesep: 10, // 同一层节点之间的距离
ranksep: 50, // 不同层节点之间的距离
},
modes: {
default: [
'drag-canvas', // 鼠标拖拽移动画布
'zoom-canvas', // 鼠标滚轮缩放
// 'drag-node' // 可选:允许拖拽节点
]
},
});
}
// 加载思维导图数据
const onMindGraphData = (nodes=[], edges=[]) => {
let data = { nodes:[], edges }
nodes.forEach(node => {
if (node.maxWidth) onFormatLineFeed(node);
data.nodes.push(node);
})
if (!graph) onInitGraph();
graph.data(data);
graph.render();
}
// 获取文本宽度
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const getLabelWidth = (label, size, family) => {
ctx.font = `${size}px ${family}`;
return ctx.measureText(label).width;
}
// 文本插入换行符
const onFormatLineFeed = (node) => {
let size = node?.labelCfg?.style?.fontSize || '16'
let family = node?.labelCfg?.style?.fontFamily || 'Source Han Sans CN'
const lines = [];
let line = '';
for (let char of node.label) {
const testLine = line + char;
const width = getLabelWidth(testLine, size, family);
if (width > node.maxWidth-40) {
lines.push(line);
line = char;
} else {
line = testLine;
}
}
if (line) lines.push(line);
node.label = lines.join("\n")
node.size = [node.maxWidth, 25*lines.length+20]
}
defineExpose({ onMindGraphData })
onBeforeUnmount(() => {
graph?.destroy()
})
</script>
<style scoped lang="scss">
.view-box {
width: 100%;
height: 100%;
}
</style>
\ No newline at end of file
...@@ -29,24 +29,24 @@ ...@@ -29,24 +29,24 @@
<div class="item-footer">分析报告</div> <div class="item-footer">分析报告</div>
</div> </div>
</div> --> </div> -->
<div class="date-box" v-if="govInsList.length"> <div class="date-box" v-if="keyOrganizationList.length">
<div class="date-icon"> <div class="date-icon">
<img :src="tipsTcon" alt=""> <img :src="tipsTcon" alt="">
</div> </div>
<div class="date-text">近期美国各联邦政府机构发布涉华政令数量汇总</div> <div class="date-text">近期美国各联邦政府机构发布涉华政令数量汇总</div>
<TimeTabPane @time-click="handleGetDepartmentList" /> <TimeTabPane @time-click="onKeyOrganization" />
</div> </div>
<div class="home-main-header-item-box" v-if="govInsList.length"> <div class="home-main-header-item-box" v-if="keyOrganizationList.length">
<div class="organization-item" v-for="(item, index) in govInsList.slice(0, 7)" :key="index" @click="handleToInstitution(item)"> <div class="organization-item" v-for="(item, index) in keyOrganizationList" :key="index" @click="handleToInstitution(item)">
<div class="item-left"> <div class="item-left">
<img :src="item.orgImage || DefaultIcon2" alt="" /> <img :src="item.imgUrl || DefaultIcon2" alt="" />
</div> </div>
<div class="item-right one-line-ellipsis">{{ item.orgName }}</div> <div class="item-right one-line-ellipsis">{{ item.orgName }}</div>
<div class="item-total">{{ item.total }}</div> <div class="item-total">{{ item.totalOrderNum }}</div>
<el-icon color="var(--color-primary-100)"><ArrowRightBold /></el-icon> <el-icon color="var(--color-primary-100)"><ArrowRightBold /></el-icon>
<div class="item-dot" v-if="item.totalRecent">+{{item.totalRecent}}</div> <div class="item-dot" v-if="item.recentOrderNum">+{{item.recentOrderNum}}</div>
</div> </div>
<div class="organization-item"> <div class="organization-item" @click="onNavigateTo()">
<div class="item-more">查看全部机构 ({{govInsList.length+1}}家)</div> <div class="item-more">查看全部机构 ({{govInsList.length+1}}家)</div>
<el-icon color="var(--color-primary-100)"><ArrowRightBold /></el-icon> <el-icon color="var(--color-primary-100)"><ArrowRightBold /></el-icon>
</div> </div>
...@@ -164,7 +164,6 @@ ...@@ -164,7 +164,6 @@
<DivideHeader id="position2" class="divide2" :titleText="'资讯要闻'"></DivideHeader> <DivideHeader id="position2" class="divide2" :titleText="'资讯要闻'"></DivideHeader>
<div class="center-center"> <div class="center-center">
<NewsList :newsList="newsList" @item-click="handleToNewsAnalysis" @more-click="handleToMoreNews" /> <NewsList :newsList="newsList" @item-click="handleToNewsAnalysis" @more-click="handleToMoreNews" />
<!-- <NewsList :newsList="newsList" /> -->
<MessageBubble :messageList="messageList" @person-click="handleClickPerson" @info-click="handleGetMessage" <MessageBubble :messageList="messageList" @person-click="handleClickPerson" @info-click="handleGetMessage"
imageUrl="img" @more-click="handleToSocialDetail" /> imageUrl="img" @more-click="handleToSocialDetail" />
</div> </div>
...@@ -194,7 +193,7 @@ ...@@ -194,7 +193,7 @@
</el-select> </el-select>
</div> </div>
</div> </div>
<div class="box5-main"> <div class="box5-main" v-loading="box5Params.loading">
<div class="box5-chart" id="chart1"></div> <div class="box5-chart" id="chart1"></div>
</div> </div>
<div class="data-origin-box"> <div class="data-origin-box">
...@@ -227,7 +226,7 @@ ...@@ -227,7 +226,7 @@
</el-select> </el-select>
</div> </div>
</div> </div>
<div class="box5-main"> <div class="box5-main" v-loading="box6Params.loading">
<div class="box5-chart" id="chart2"></div> <div class="box5-chart" id="chart2"></div>
</div> </div>
<div class="data-origin-box"> <div class="data-origin-box">
...@@ -267,7 +266,7 @@ ...@@ -267,7 +266,7 @@
</el-select> </el-select>
</div> </div>
</div> </div>
<div class="box7-main"> <div class="box7-main" v-loading="box7Params.loading">
<div class="box7-list"> <div class="box7-list">
<div class="box7-item" v-for="(item, index) in keyDecreeList" :key="index" @click="handleKeyDecree(item)"> <div class="box7-item" v-for="(item, index) in keyDecreeList" :key="index" @click="handleKeyDecree(item)">
<div class="icon"> <div class="icon">
...@@ -315,7 +314,7 @@ ...@@ -315,7 +314,7 @@
</el-select> </el-select>
</div> </div>
</div> </div>
<div class="box8-content"> <div class="box8-content" v-loading="box8Params.loading">
<WordCloudChart v-if="wordCloudData?.length" :data="wordCloudData" width="100%" height="100%" /> <WordCloudChart v-if="wordCloudData?.length" :data="wordCloudData" width="100%" height="100%" />
</div> </div>
<div class="data-origin-box"> <div class="data-origin-box">
...@@ -485,6 +484,8 @@ import DefaultIcon2 from "@/assets/icons/default-icon2.png"; ...@@ -485,6 +484,8 @@ import DefaultIcon2 from "@/assets/icons/default-icon2.png";
import tipsTcon from "./assets/images/tips-icon.png"; import tipsTcon from "./assets/images/tips-icon.png";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { useGotoNewsDetail } from '@/router/modules/news';
const containerRef = ref(null); const containerRef = ref(null);
const { isShow } = useContainerScroll(containerRef); const { isShow } = useContainerScroll(containerRef);
const currentPage = ref(1); const currentPage = ref(1);
...@@ -499,14 +500,9 @@ const handleCurrentChange = page => { ...@@ -499,14 +500,9 @@ const handleCurrentChange = page => {
// 机构列表 // 机构列表
const govInsList = ref([]); const govInsList = ref([]);
const checkedGovIns = ref([]); const checkedGovIns = ref([]);
const handleGetDepartmentList = async (event) => { const handleGetDepartmentList = async () => {
let day = 7
if (event?.time === '近一周') day = 7
if (event?.time === '近一月') day = 30
if (event?.time === '近一年') day = 365
try { try {
const res = await getDepartmentList({day}); const res = await getDepartmentList({day:7});
console.log("机构列表", res); console.log("机构列表", res);
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
govInsList.value = res.data; govInsList.value = res.data;
...@@ -526,6 +522,10 @@ const handleToInstitution = item => { ...@@ -526,6 +522,10 @@ const handleToInstitution = item => {
}); });
window.open(curRoute.href, "_blank"); window.open(curRoute.href, "_blank");
}; };
// 跳转全部机构页面
const onNavigateTo = () => {
router.push({name: "allOrganization"});
}
// 查看更多风险信号 // 查看更多风险信号
const handleToMoreRiskSignal = () => { const handleToMoreRiskSignal = () => {
...@@ -657,7 +657,6 @@ const handleGetNews = async () => { ...@@ -657,7 +657,6 @@ const handleGetNews = async () => {
const res = await getNews({moduleId: "0101"}); const res = await getNews({moduleId: "0101"});
console.log("新闻资讯", res); console.log("新闻资讯", res);
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
// newsList.value = res.data || []
newsList.value = (res.data ?? []).map(item => { newsList.value = (res.data ?? []).map(item => {
return { return {
newsId: item.newsId, newsId: item.newsId,
...@@ -673,14 +672,9 @@ const handleGetNews = async () => { ...@@ -673,14 +672,9 @@ const handleGetNews = async () => {
} }
}; };
// 点击新闻条目,跳转到新闻分析页 // 点击新闻条目,跳转到新闻分析页
const gotoNewsDetail = useGotoNewsDetail()
const handleToNewsAnalysis = news => { const handleToNewsAnalysis = news => {
const route = router.resolve({ gotoNewsDetail(news.newsId)
path: "/newsAnalysis",
query: {
newsId: news.newsId
}
});
window.open(route.href, "_blank");
}; };
// 社交媒体 // 社交媒体
...@@ -799,9 +793,11 @@ const box5Params = reactive({ ...@@ -799,9 +793,11 @@ const box5Params = reactive({
year: yearList[0].value, year: yearList[0].value,
domainId: '', domainId: '',
proposeName: '', proposeName: '',
loading: false,
}) })
const summarize1 = ref() const summarize1 = ref()
const handleGetDecreeYearOrder = async () => { const handleGetDecreeYearOrder = async () => {
box5Params.loading = true
try { try {
let { year, domainId, proposeName } = box5Params; let { year, domainId, proposeName } = box5Params;
const res = await getDecreeYearOrder({ const res = await getDecreeYearOrder({
...@@ -822,6 +818,7 @@ const handleGetDecreeYearOrder = async () => { ...@@ -822,6 +818,7 @@ const handleGetDecreeYearOrder = async () => {
} catch (error) { } catch (error) {
console.error("行政令发布频度error", error); console.error("行政令发布频度error", error);
} }
box5Params.loading = false
}; };
// AI智能总结 // AI智能总结
const onChartInterpretation = async (text) => { const onChartInterpretation = async (text) => {
...@@ -882,9 +879,11 @@ const chart2Data = ref([ ...@@ -882,9 +879,11 @@ const chart2Data = ref([
const box6Params = reactive({ const box6Params = reactive({
year: yearList[0].value, year: yearList[0].value,
proposeName: '', proposeName: '',
loading: false,
}); });
const summarize2 = ref() const summarize2 = ref()
const handleGetDecreeArea = async () => { const handleGetDecreeArea = async () => {
box6Params.loading = true
try { try {
let { year, proposeName } = box6Params; let { year, proposeName } = box6Params;
const res = await getDecreeArea({ const res = await getDecreeArea({
...@@ -904,6 +903,7 @@ const handleGetDecreeArea = async () => { ...@@ -904,6 +903,7 @@ const handleGetDecreeArea = async () => {
} catch (error) { } catch (error) {
console.error("政令科技领域error", error); console.error("政令科技领域error", error);
} }
box6Params.loading = false
}; };
const handleBox6 = async () => { const handleBox6 = async () => {
await handleGetDecreeArea(); await handleGetDecreeArea();
...@@ -926,8 +926,10 @@ const box7Params = reactive({ ...@@ -926,8 +926,10 @@ const box7Params = reactive({
year: yearList[0].value, year: yearList[0].value,
domainId: '', domainId: '',
proposeName: '', proposeName: '',
loading: false,
}) })
const handleGetKeyDecree = async () => { const handleGetKeyDecree = async () => {
box7Params.loading = true
try { try {
let { year, domainId, proposeName } = box7Params; let { year, domainId, proposeName } = box7Params;
const res = await getKeyDecree({ const res = await getKeyDecree({
...@@ -950,6 +952,7 @@ const handleGetKeyDecree = async () => { ...@@ -950,6 +952,7 @@ const handleGetKeyDecree = async () => {
}); });
} }
} catch (error) { } } catch (error) { }
box7Params.loading = false
}; };
// 政令重点条款 // 政令重点条款
...@@ -958,8 +961,10 @@ const box8Params = reactive({ ...@@ -958,8 +961,10 @@ const box8Params = reactive({
year: yearList[0].value, year: yearList[0].value,
domainId: '', domainId: '',
proposeName: '', proposeName: '',
loading: false,
}) })
const handleGetDecreeKeyInstruction = async () => { const handleGetDecreeKeyInstruction = async () => {
box8Params.loading = true
try { try {
let { year, domainId, proposeName } = box8Params; let { year, domainId, proposeName } = box8Params;
const res = await getDecreeKeyInstruction({ const res = await getDecreeKeyInstruction({
...@@ -972,6 +977,7 @@ const handleGetDecreeKeyInstruction = async () => { ...@@ -972,6 +977,7 @@ const handleGetDecreeKeyInstruction = async () => {
} catch (error) { } catch (error) {
console.error("政令重点条款error", error); console.error("政令重点条款error", error);
} }
box8Params.loading = false
}; };
// 资源库 // 资源库
...@@ -1197,12 +1203,16 @@ const handleSearch = () => { ...@@ -1197,12 +1203,16 @@ const handleSearch = () => {
// 关键机构 // 关键机构
const keyOrganizationList = ref([]); const keyOrganizationList = ref([]);
const onKeyOrganization = async () => { const onKeyOrganization = async (event) => {
let day = 7
if (event?.time === '近一周') day = 7
if (event?.time === '近一月') day = 30
if (event?.time === '近一年') day = 365
try { try {
const res = await getKeyOrganization(); const res = await getKeyOrganization({day});
console.log("关键机构", res); console.log("关键机构", res);
if (res.code === 200) { if (res.code === 200) {
keyOrganizationList.value = res.data.map(item => ({ orgName:item.orgName, orgId:item.id })); keyOrganizationList.value = res.data;
} }
} catch (error) { } } catch (error) { }
} }
...@@ -1423,15 +1433,13 @@ onMounted(async () => { ...@@ -1423,15 +1433,13 @@ onMounted(async () => {
.home-main-header-item-box { .home-main-header-item-box {
margin: 20px 0 64px; margin: 20px 0 64px;
width: 1600px; width: 1600px;
display: flex; display: grid;
flex-wrap: wrap; grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 80px;
gap: 16px; gap: 16px;
font-family: Microsoft YaHei; font-family: Microsoft YaHei;
.organization-item { .organization-item {
width: 20%;
flex: auto;
height: 80px;
padding: 0 16px; padding: 0 16px;
display: flex; display: flex;
box-sizing: border-box; box-sizing: border-box;
...@@ -2194,6 +2202,7 @@ onMounted(async () => { ...@@ -2194,6 +2202,7 @@ onMounted(async () => {
.box5-main { .box5-main {
flex: auto; flex: auto;
height: 20px; height: 20px;
padding-top: 16px;
.box5-chart { .box5-chart {
height: 100%; height: 100%;
} }
......
...@@ -170,7 +170,6 @@ const onWordWrap = (word, num) => { ...@@ -170,7 +170,6 @@ const onWordWrap = (word, num) => {
} }
const handleClickDecree = decree => { const handleClickDecree = decree => {
window.sessionStorage.setItem("curTabName", decree.name);
const route = router.resolve({ const route = router.resolve({
path: "/decreeLayout", path: "/decreeLayout",
query: { query: {
...@@ -184,101 +183,101 @@ const handleClickDecree = decree => { ...@@ -184,101 +183,101 @@ const handleClickDecree = decree => {
const graphContainer = ref(null); const graphContainer = ref(null);
let graph = null; let graph = null;
const onRelationChart = () => { const onRelationChart = () => {
const container = graphContainer.value; const container = graphContainer.value;
const nodeWidth = 180; const nodeWidth = 180;
const width = container.clientWidth; const width = container.clientWidth;
const height = container.clientHeight; const height = container.clientHeight;
const centerX = width / 2 const centerX = width / 2
const centerY = height / 2 - 40 const centerY = height / 2 - 40
const leftNodeX = centerX - nodeWidth; const leftNodeX = centerX - nodeWidth;
const rightNodeX = centerX + nodeWidth; const rightNodeX = centerX + nodeWidth;
const data = { const data = {
nodes: [ nodes: [
{ {
id: mainInfo.value.id+'', label: `${mainInfo.value.time}\n${onWordWrap(mainInfo.value.label, 15)}`, id: mainInfo.value.id+'', label: `${mainInfo.value.time}\n${onWordWrap(mainInfo.value.label, 15)}`,
size: [250, 80], x: leftNodeX, y: centerY, size: [250, 80], x: leftNodeX, y: centerY,
},
{
id: nodeInfo.value.id+'', label: `${nodeInfo.value.time}\n${onWordWrap(nodeInfo.value.label, 15)}`,
size: [250, 80], x: rightNodeX, y: centerY,
},
],
edges: [
{
id: `edge-1`, target: nodeInfo.value.id+'', source: mainInfo.value.id+'', label: nodeInfo.value.relation,
style: {
stroke: ["", "#B9DCFF", "#87E8DE", "#FFCCC7"][1],
}, },
{ labelCfg: {
id: nodeInfo.value.id+'', label: `${nodeInfo.value.time}\n${onWordWrap(nodeInfo.value.label, 15)}`,
size: [250, 80], x: rightNodeX, y: centerY,
},
],
edges: [
{
id: `edge-1`, target: nodeInfo.value.id+'', source: mainInfo.value.id+'', label: nodeInfo.value.relation,
style: { style: {
stroke: ["", "#B9DCFF", "#87E8DE", "#FFCCC7"][1], fill: ["", "#055fc2", "#13A8A8", "#CE4F51"][1],
}, background: {
labelCfg: { fill: ["", "#eef7ff", "#E6FFFB", "#FFE0E0"][1],
style: {
fill: ["", "#055fc2", "#13A8A8", "#CE4F51"][1],
background: {
fill: ["", "#eef7ff", "#E6FFFB", "#FFE0E0"][1],
}
} }
} }
} }
] }
}; ]
};
// 创建图实例 // 创建图实例
if (!graph) { if (!graph) {
graph = new G6.Graph({ graph = new G6.Graph({
container: container, container: container,
width, width,
height, height,
defaultNode: { defaultNode: {
type: 'rect', type: 'rect',
anchorPoints: [[0, 0.5], [1, 0.5]], anchorPoints: [[0, 0.5], [1, 0.5]],
style: { style: {
cursor: "pointer",
radius: 4,
fill: '#f6faff',
stroke: '#B9DCFF',
},
labelCfg: {
style: {
cursor: "pointer", cursor: "pointer",
radius: 4, fill: "#333333",
fill: '#f6faff', fontSize: 15,
stroke: '#B9DCFF', fontWeight: "bold",
}, fontFamily: "Microsoft YaHei",
labelCfg: { textAlign: "center",
style: {
cursor: "pointer",
fill: "#333333",
fontSize: 15,
fontWeight: "bold",
fontFamily: "Microsoft YaHei",
textAlign: "center",
}
} }
}
},
defaultEdge: {
type: "line",
style: {
lineWidth: 1,
endArrow: true,
}, },
defaultEdge: { labelCfg: {
type: "line", autoRotate: true,
style: { style: {
lineWidth: 1, fontSize: 14,
endArrow: true, fontFamily: 'Microsoft YaHei',
}, background: {
labelCfg: { padding: [6, 4, 4, 4],
autoRotate: true,
style: {
fontSize: 14,
fontFamily: 'Microsoft YaHei',
background: {
padding: [6, 4, 4, 4],
}
} }
} }
}, }
layout: null, },
modes: { default: [] }, layout: null,
fitView: false, modes: { default: [] },
}); fitView: false,
});
graph.on('node:click', (evt) => { graph.on('node:click', (evt) => {
let node = siderList.value.find(item => item.id==evt.item._cfg.model.id) let node = siderList.value.find(item => item.id==evt.item._cfg.model.id)
if (node) handleClickDecree(node) if (node) handleClickDecree(node)
}); });
} }
// 加载数据并渲染 // 加载数据并渲染
graph.data(data); graph.data(data);
graph.render(); graph.render();
} }
onMounted(() => { onMounted(() => {
......
差异被折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论