提交 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) {
/**
* 获取相似法案列表
* @param {Object} params - 查询参数
* @param {string} params.patternType - 政府结构类型,如 "统一政府"
* @param {string} params.proposalType - 提案类型,默认 "两党共同提案"
* @param {string} params.billIds - 当前法案的ID
* @param {string[]} params.domains - 领域名称列表
* @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 || '两党共同提案',
billIds: params.billIds,
domains: params.domains,
patternType: params.patternType ,
proposalType: params.proposalType ,
...params
}
})
......@@ -400,31 +404,42 @@ export function transformSimiBillsData(apiData) {
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
proposalDate: extractProposalDate(bill.poli_pattern_desc),
areas: bill.domain_name ? (Array.isArray(bill.domain_name) ? bill.domain_name : [bill.domain_name]) : [],
proposer: extractProposer(bill.bill_sponsors),
coProposers: bill.bill_proposal_desc || '',
governmentType: formatGovernmentType(bill.poli_pattern_type, bill.poli_pattern_desc),
passDays: calculateTotalDays(bill.bill_actions),
selected: true // 默认全选
}))
return { stats, bills: transformedBills }
}
/**
* 从法案动作中提取通过时间
* @param {Array} actions - 法案动作列表
* @returns {string} 通过时间
* 从政治格局描述中提取提案时间
* @param {string} desc - 政治格局描述
* @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()}日`
function extractProposalDate(desc) {
if (!desc) return ''
const match = desc.match(/(\d{4})-(\d{2})-(\d{2})/)
if (match) {
return `${match[1]}${parseInt(match[2])}${parseInt(match[3])}日`
}
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) {
}
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) || '执政初期/蜜月期',
// 涉及领域 TAG - 使用 domain_name
areas: Array.isArray(data.domain_name) ? data.domain_name : (data.domain_name ? [data.domain_name] : []),
// 政策领域完整选项列表 - 使用 bill_domain(用于筛选下拉框)
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,
// 提案人职务 - 需要从其他 API 获取
proposerPosition: data.proposer_position || '委员会主席',
// 政府结构类型
governmentType: formatGovernmentType(data.poli_pattern_type, data.poli_pattern_desc),
// 法案预算规模 - 需要从其他 API 获取
budgetScale: data.budget_scale || '4万亿美元',
// 政治格局类型(用于筛选条件默认值)
patternType: data.poli_pattern_type || '统一政府',
// 原始数据,供筛选条件使用
_raw: data
}
}
/**
* 从政治格局描述中提取日期
* @param {string} desc - 政治格局描述
......@@ -578,9 +594,7 @@ export function generateDefaultFilters(proposalInfo) {
// 提案人职务
proposerPosition: proposalInfo.proposerPosition === '委员会主席' ? ['chairman'] : [],
// 政府结构类型
governmentType: proposalInfo.governmentType.includes('一致') ? ['unified'] : ['divided'],
// 选举周期阶段
electionPhase: proposalInfo.electionPhase.includes('蜜月') ? ['honeymoon'] : [],
governmentType: proposalInfo.governmentType.includes('一致') ? ['unified'] : ['divided'],
// 法案预算规模
budgetScale: ['trillion_plus'],
// 对方党派提案人
......@@ -589,3 +603,19 @@ export function generateDefaultFilters(proposalInfo) {
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) {
})
}
// 思维导图
/**
* @param { id }
*/
export function getDecreeMindMap(params) {
return request({
method: 'GET',
url: `/api/administrativeOrderInfo/mindMap/${params.id}`,
})
}
// 相关实体
/**
* @param { id }
......
......@@ -91,10 +91,20 @@ export function getDecreeTypeList() {
}
// 关键机构
export function getKeyOrganization() {
export function getKeyOrganization(params) {
return request({
method: 'GET',
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) {
}
// 政令关键词云
export function getKeyWordUp() {
export function getKeyWordUp(params) {
return request({
method: 'GET',
url: `/api/element/getKeyWordUp/2025-01-01`,
url: `/api/administrativeOrderInfo/wordCloud/${params.id}`,
})
}
......
......@@ -138,120 +138,118 @@ export function getPersonList(params) {
params
})
}
//创新主体科研实力:专利数量统计
export function getPatentList(params) {
//合作情况:与中国合作数量变化
export function getCooperateNumWithChina(params) {
return request({
method: 'GET',
url: `/api/innovateSubject/patentList/${params.id}`,
url: `/api/innovateSubject/cooperateNumWithChina/${params.id}`,
params
})
}
// 创新主体科研实力:论文数量统计
export function getPaperList(params) {
// 合作情况:与中国合作类型变化
export function getCooperateTypeWithChina(params) {
return request({
method: 'GET',
url: `/api/innovateSubject/paperList/${params.id}`,
url: `/api/innovateSubject/cooperateTypeWithChina/${params.year}/${params.id}`,
params
})
}
//创新主体科研实力:领域实力分布
export function getStudyFieldList(params) {
// 合作情况:与中国合作领域变化
export function getCooperateAreaWithChina(params) {
return request({
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({
method: 'GET',
url: `/api/innovateSubject/fundGrowth/${params.id}`,
url: `/api/innovateSubject/cooperateFundWithChina/${params.id}`,
params
})
}
//创新主体科研实力:经费来源
export function getFundFromList(params) {
//合作情况:与中国合作事例
export function getCooperateExampleWithChina(params) {
return request({
method: 'GET',
url: `/api/innovateSubject/fundFromList/${params.id}`,
url: `/api/innovateSubject/cooperateExampleWithChina/${params.id}`,
params
})
}
//创新主体科研实力:经费分配
export function getFundToList(params) {
// 专利数量统计
export function getPatentList(orgId) {
return request({
method: 'GET',
url: `/api/innovateSubject/fundToList/${params.id}`,
params
method: "GET",
url: `/api/innovateSubject/patentList/${orgId}`
})
}
//合作情况:与中国合作数量变化
export function getCooperateNumWithChina(params) {
// 论文数量统计
export function getPaperList(orgId) {
return request({
method: 'GET',
url: `/api/innovateSubject/cooperateNumWithChina/${params.id}`,
params
method: "GET",
url: `/api/innovateSubject/paperList/${orgId}`
})
}
// 合作情况:与中国合作类型变化
export function getCooperateTypeWithChina(params) {
// 领域实力分布
export function getStudyFieldList(orgId) {
return request({
method: 'GET',
url: `/api/innovateSubject/cooperateTypeWithChina/${params.year}/${params.id}`,
params
method: "GET",
url: `/api/innovateSubject/studyFieldList/${orgId}`
})
}
// 合作情况:与中国合作领域变化
export function getCooperateAreaWithChina(params) {
// 经费增长情况
export function getFundGrowth(orgId) {
return request({
method: 'GET',
url: `/api/innovateSubject/cooperateAreaWithChina/${params.id}`,
params
method: "GET",
url: `/api/innovateSubject/fundGrowth/${orgId}`
})
}
//合作情况:与中国合作经费变化
export function getCooperateFundWithChina(params) {
// 经费来源
export function getFundFromList(orgId) {
return request({
method: 'GET',
url: `/api/innovateSubject/cooperateFundWithChina/${params.id}`,
params
method: "GET",
url: `/api/innovateSubject/fundFromList/${orgId}`
})
}
//合作情况:与中国合作事例
export function getCooperateExampleWithChina(params) {
// 经费分配
export function getFundToList(orgId) {
return request({
method: 'GET',
url: `/api/innovateSubject/cooperateExampleWithChina/${params.id}`,
params
method: "GET",
url: `/api/innovateSubject/fundToList/${orgId}`
})
}
//创新主体其他情况:重点实验室
export function getLabList(params) {
// 获取实验室列表
export function getLabList(orgId) {
return request({
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({
method: 'GET',
url: `/api/innovateSubject/policyList/${params.id}`,
params
url: `/api/innovateSubject/policyList/${orgId}`,
params: { currentPage, pageSize }
})
}
\ No newline at end of file
}
......@@ -9,7 +9,7 @@
<div class="switch-label switch-label-left">高亮实体</div>
<el-switch v-model="isTranslate" />
<div class="switch-label">文显示</div>
<div class="switch-label">文显示</div>
<div
v-for="action in headerActions"
......@@ -53,8 +53,8 @@
class="content-row"
:class="{ 'high-light': isHighlight }"
>
<div class="content-cn" :class="{ 'translate-cn': !isTranslate }" v-html="item.content" />
<div v-if="isTranslate" class="content-en" v-html="item.contentEn" />
<div class="content-en" v-html="item.contentEn" :class="{ 'translate-cn': !isTranslate }"></div>
<div class="content-cn" v-html="item.content" v-if="isTranslate"></div>
</div>
</el-scrollbar>
</div>
......@@ -150,11 +150,9 @@ const doUpdateWord = () => {
}
displayReportData.value = originReportData.value.map((item) => {
const cn = applyHighlightToText(item.content, term);
const en = isTranslate.value
? applyHighlightToText(item.contentEn, term)
: { html: item.contentEn, count: 0 };
findWordMax.value += cn.count + en.count;
const en = applyHighlightToText(item.contentEn, term);
const cn = isTranslate.value ? applyHighlightToText(item.content, term) : { html: item.content, count: 0 };
findWordMax.value += en.count + cn.count;
return {
...item,
content: cn.html,
......
......@@ -4,22 +4,20 @@ const InnovationInstitution = () => import('@/views/innovationSubject/innovative
const innovationSubjectRoutes = [
//创新主体
{
path: "/innovationSubject",
name: "InnovationSubject",
component: InnovationSubject,
meta: {
title: "M国主要创新主体分析概览"
title: "M "
}
},
{
path: "/InnovativeInstitutions/:id",
path: "/InnovativeInstitutions/:id/:type",
name: "InnovativeInstitutions",
component: InnovationInstitution,
// meta: {
// title: "学校详情"
// },
}
]
......
......@@ -9,6 +9,7 @@ const DecreeDeepDig = () => import('@/views/decree/decreeLayout/deepdig/index.vu
const DecreeInfluence = () => import('@/views/decree/decreeLayout/influence/index.vue')
const Institution = () => import('@/views/decree/institution/index.vue')
const DecreeOriginal = () => import('@/views/decree/decreeOriginal/index.vue')
const allOrganization = () => import('@/views/decree/allOrganization/index.vue')
const decreeRoutes = [
......@@ -93,11 +94,12 @@ const decreeRoutes = [
path: "/decree/decreeOriginal",
name: "DecreeOriginal",
component: DecreeOriginal,
// meta: {
// title: "政令原文"
// }
},
{
path: "/decree/allOrganization",
name: "allOrganization",
component: allOrganization,
},
]
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图表
import getMonthRange from './getMonthRange'
import * as echarts from 'echarts'
import 'echarts-wordcloud';
import router from '@/router/index'
......@@ -18,15 +19,25 @@ const setChart = (option, chartId, allowClick, selectParam) => {
if (params.componentType === 'series' && params.seriesType === 'pie') {
console.log('点击的扇形名称:', params.name);
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 === '议院委员会') {
if (params.name === '众议院' || params.name === '参议院') {
selectParam.selectedCongress = params.name
selectParam.selectedOrg = ''
if (selectParam.selectedDate.length === 4) {
selectParam.selectedDate = JSON.stringify([selectParam.selectedDate + '-01-01', selectParam.selectedDate + '-12-31'])
}
} else {
selectParam.selectedOrg = params.name
selectParam.selectedCongress = ''
if (selectParam.selectedDate.length === 4) {
selectParam.selectedDate = JSON.stringify([selectParam.selectedDate + '-01-01', selectParam.selectedDate + '-12-31'])
}
}
}
const route = router.resolve({
......@@ -36,15 +47,35 @@ const setChart = (option, chartId, allowClick, selectParam) => {
window.open(route.href, "_blank");
} else if (params.componentType === 'series' && params.seriesType === 'bar') {
if (params.name === '已立法') {
selectParam.selectedStauts = 1
selectParam.selectedStatus = 1
} 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({
path: "/dataLibrary/countryBill",
query: selectParam
});
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 () => {
return p ? ((pass / p) * 100).toFixed(2) : 0;
});
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 () => {
if (t1 !== t2) return t1 - t2;
return (b.value ?? 0) - (a.value ?? 0);
});
const selectParam = {
moduleType: '国会法案',
key: '议院委员会',
......@@ -997,8 +1004,9 @@ const handleBox9Data = async () => {
);
const selectParam = {
moduleType: '国会法案',
key: '领域',
selectedDate: box9selectetedTime.value,
status: box9LegislativeStatus.value === '提出法案' ? 0 : 1,
selectedStatus: box9LegislativeStatus.value === '提出法案' ? 0 : 1,
isInvolveCn: 1
}
box9ChartInstance = setChart(box9Chart, "box9Chart", true, selectParam);
......@@ -1179,7 +1187,7 @@ const handleBox8Data = async () => {
const selectParam = {
moduleType: '国会法案',
key: '领域',
key: '所处阶段',
selectedDate: box8selectetedTime.value,
isInvolveCn: 1
}
......@@ -1212,7 +1220,7 @@ const handleBox8Data = async () => {
box8StageList.value = data.stages;
await nextTick();
const box8Chart = getBox8ChartOption(data.stages);
box8ChartInstance = setChart(box8Chart, "box8Chart" , true, selectParam);
box8ChartInstance = setChart(box8Chart, "box8Chart", true, selectParam);
} else {
box8HasData.value = false;
box8Summary.value = 0;
......
......@@ -3,20 +3,20 @@ import * as echarts from 'echarts'
const getMultiLineChart = (dataX, dataY1, dataY2, dataY3) => {
return {
tooltip: {
trigger: 'axis',
trigger: 'item',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
},
formatter: function (params) {
let res = params[0].name + '<br/>';
params.forEach(item => {
res += item.marker + item.seriesName + ': ' + item.value + (item.seriesName === '通过率' ? '%' : '') + '<br/>';
});
return res;
}
// formatter: function (params) {
// let res = params[0].name + '<br/>';
// params.forEach(item => {
// res += item.marker + item.seriesName + ': ' + item.value + (item.seriesName === '通过率' ? '%' : '') + '<br/>';
// });
// return res;
// }
},
grid: {
width: '96%',
......
// 模拟 API 延迟
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
// 步骤数据类型
export interface Step {
id: number
......@@ -12,13 +9,17 @@ export interface Step {
export interface ProposalInfo {
title: string
date: string
areas: string[]
areas: string[] // 涉及领域 TAG(domain_name)
billDomain: string[] // 政策领域完整选项列表(bill_domain)
defaultDomains: string[] // 政策领域默认已选项(domain_name)
electionPhase: string
proposer: string
coProposers: string
proposerPosition: string
governmentType: string
patternType: string
budgetScale: string
proposalType: string
}
// 筛选条件类型
......@@ -36,128 +37,24 @@ export interface FilterField {
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 const STEPS: Step[] = [
{
id: 1,
title: '设置筛选条件',
description: '基于核心相似度维度构建预测基准'
},
{
id: 2,
title: '筛选同类法案',
description: '形成历史相似同类法案预测基准集'
},
{
id: 3,
title: '阶段预测分析',
description: '基于相似法案的历史数据预测模型'
}
}
// 获取筛选字段配置
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 {
......@@ -173,31 +70,12 @@ export interface FilterStats {
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
tags: string[]
passTime: string
totalDays: 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'
......@@ -214,6 +92,7 @@ export interface PredictionPhase {
progressLevel: number
estimatedDays: string
modelInputs: string[]
predictionBasis?: string
supportingFacts: {
title: string
basedOn: string
......@@ -230,212 +109,3 @@ export interface PredictionAnalysis {
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
}
]
}
......@@ -50,12 +50,9 @@ const emit = defineEmits<{
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>
......
......@@ -2,59 +2,192 @@
<section class="filter-section">
<div class="section-header">
<div class="header-left">
<img src="../assets/fitller.svg" />
<img src="../assets/fitller.svg" />
<h2 class="section-title text-title-3-bold">核心相似度维度筛选</h2>
</div>
<button class="btn-outline" @click="emit('setAsCurrent')">设置为当前提案</button>
<button class="btn-outline" @click="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 class="field-item">
<span class="field-label text-tip-1 text-primary-65-clor">政策领域:</span>
<div class="field-content">
<el-select
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 class="field-item">
<span class="field-label text-tip-1 text-primary-65-clor">政府结构类型:</span>
<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>
<el-select
v-model="localValues.governmentType"
multiple
placeholder="请选择"
style="width: 420px"
@change="handleChange"
>
<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 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>
</section>
</template>
<script setup lang="ts">
import type { FilterField } from '../api'
import FilterSelect from './FilterSelect.vue'
import { ref, computed, watch } from 'vue'
import type { ProposalInfo } from '../api'
const props = defineProps<{
fields: FilterField[]
proposalInfo?: ProposalInfo | null
defaultFilters?: Record<string, string[]>
}>()
const emit = defineEmits<{
'update:fields': [fields: FilterField[]]
setAsCurrent: []
}>()
// 本地 v-model 值
const localValues = ref({
policyArea: [] as string[],
governmentType: [] as string[],
oppositionProposer: [] as string[],
proposalTime: [] as string[],
})
// 更新字段选中值
function handleFieldUpdate(fieldId: string, newValues: string[]) {
const updatedFields = props.fields.map(f =>
f.id === fieldId
? { ...f, selectedValues: newValues }
: f
)
emit('update:fields', updatedFields)
// 根据 proposalInfo 计算初始筛选值(即"设置为当前提案"的目标状态)
function buildInitialValues(info?: ProposalInfo | null): Record<string, string[]> {
if (!info) return { policyArea: [], governmentType: [], oppositionProposer: [], proposalTime: [] }
return {
policyArea: info.defaultDomains?.length ? [...info.defaultDomains] : [...(info.areas || [])],
governmentType: info.patternType ? [info.patternType] : [],
oppositionProposer: [],
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>
<style scoped>
......@@ -75,12 +208,6 @@ function handleFieldUpdate(fieldId: string, newValues: string[]) {
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);
......@@ -105,7 +232,7 @@ function handleFieldUpdate(fieldId: string, newValues: string[]) {
.fields-grid {
display: flex;
flex-wrap: wrap;
gap: 24px 24px;
gap: 24px;
padding-left: 30px;
}
......@@ -113,15 +240,14 @@ function handleFieldUpdate(fieldId: string, newValues: string[]) {
width: 580px;
display: flex;
align-items: flex-start;
}
.field-label-wrapper {
padding-top: 4px;
gap: 0;
}
.field-label {
display: block;
width: 150px;
width: 140px;
flex-shrink: 0;
padding-top: 6px;
}
.field-content {
......@@ -135,11 +261,4 @@ function handleFieldUpdate(fieldId: string, newValues: string[]) {
align-items: center;
gap: 8px;
}
.hint-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
color: var(--text-primary-50-color);
}
</style>
......@@ -39,26 +39,19 @@
</p>
</div>
</div>
<div class="facts-section">
<div v-if="phase.predictionBasis" 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>
<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>{{ phase.supportingFacts.basedOn }}</span>
<span>此阶段预测基于以下观点</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 class="prediction-basis-content">
<p class="text-tip-2 text-primary-65-clor">{{ phase.predictionBasis }}</p>
</div>
</div>
</div>
......@@ -260,6 +253,16 @@ const riskLabel = computed(() => {
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 {
display: grid;
grid-template-columns: repeat(4, 1fr);
......
......@@ -33,13 +33,6 @@
<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>
......@@ -59,17 +52,14 @@ 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: '法案预算规模:' }
{ key: 'governmentType', label: '政府结构类型:' }
]
</script>
......
......@@ -7,9 +7,9 @@
<div class="content-wrapper">
<ProposalInfoSection v-if="currentProposalInfo" :info="currentProposalInfo" />
<FilterSection
:fields="filterFields"
@update:fields="filterFields = $event"
@set-as-current="handleSetAsCurrent"
ref="filterSectionRef"
:proposal-info="currentProposalInfo"
:default-filters="defaultFilters"
/>
</div>
<ActionButtons
......@@ -22,8 +22,7 @@
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import type { ProposalInfo, FilterField } from '../api'
import { fetchProposalInfo, fetchFilterFields } from '../api'
import type { ProposalInfo } from '../api'
import ProposalInfoSection from './ProposalInfoSection.vue'
import FilterSection from './FilterSection.vue'
import ActionButtons from './ActionButtons.vue'
......@@ -34,99 +33,42 @@ const props = defineProps<{
}>()
const emit = defineEmits<{
next: []
next: [selectedFilters: Record<string, string[]>]
}>()
// 提案信息(优先使用 props,否则从 API 获取
// 提案信息(优先使用 props)
const currentProposalInfo = ref<ProposalInfo | null>(null)
// 筛选字段列表
const filterFields = ref<FilterField[]>([])
// FilterSection 组件引用
const filterSectionRef = ref<InstanceType<typeof FilterSection> | null>(null)
// 加载状态
const loading = ref(true)
// 初始筛选字段(用于重置)
const initialFilterFields = ref<FilterField[]>([])
// 默认筛选条件
const defaultFilters = ref<Record<string, string[]>>({})
// 监听 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()
// 根据提案信息生成默认筛选条件(仅用于 Step1 内部状态跟踪,不再传给 FilterSection)
defaultFilters.value = {
policyArea: newInfo.defaultDomains?.length ? newInfo.defaultDomains : newInfo.areas,
governmentType: newInfo.patternType ? [newInfo.patternType] : [],
oppositionProposer: [],
proposalTime: [],
}
// 获取筛选字段配置
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() {
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 []
}
filterSectionRef.value?.reset()
}
// 下一步
// 下一步:获取已选的筛选条件并传给父组件
function handleNext() {
emit('next')
const selectedFilters = filterSectionRef.value?.getValues() || {}
emit('next', selectedFilters)
}
</script>
......
......@@ -77,13 +77,12 @@
<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: []
next: [selectedBills: any[]]
}>()
// 从父组件注入筛选参数
......@@ -93,6 +92,8 @@ const filterParams = inject<any>('filterParams', null)
const stats = ref<FilterStats | null>(null)
// 法案列表
const bills = ref<BillInfo[]>([])
// 原始法案数据(用于传给第三页)
const rawBillsData = ref<any[]>([])
// 加载状态
const loading = ref(true)
......@@ -121,36 +122,30 @@ const selectedCount = computed(() => {
async function loadData() {
loading.value = true
try {
// 优先使用真实 API
// 使用真实 API,传入 billIds、domains、patternType、proposalType
const params = {
patternType: filterParams?.governmentType || '统一政府',
proposalType: '两党共同提案'
billIds: filterParams?.value.billIds,
domains:JSON.stringify(filterParams?.value.domains) || [],
patternType: filterParams?.value.patternType || '统一政府',
proposalType: filterParams?.value.proposalType || '两党共同提案'
}
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)
stats.value = apiStats
bills.value = apiBills
} else {
// 如果 API 失败,使用模拟数据
const [statsData, billsData] = await Promise.all([
fetchFilterStats(),
fetchBillList()
])
stats.value = statsData
bills.value = billsData
stats.value = null
bills.value = []
}
} catch (error) {
console.error('获取相似法案失败:', error)
// 使用模拟数据作为 fallback
const [statsData, billsData] = await Promise.all([
fetchFilterStats(),
fetchBillList()
])
stats.value = statsData
bills.value = billsData
stats.value = null
bills.value = []
} finally {
loading.value = false
}
......@@ -189,7 +184,11 @@ function handleBack() {
// 开始预测分析
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>
......
<template>
<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-left flex-display-start">
......@@ -27,8 +30,8 @@
<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>
<p class="highlight-title text-regular">该法案的通过概率为 {{ predictionData?.overallProbability || '—' }}</p>
<p class="highlight-text text-regular">{{ predictionData?.highlightText }}</p>
</div>
</div>
<div v-if="predictionData?.phases?.length" class="phases-list">
......@@ -59,23 +62,22 @@
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { fetchPredictionAnalysis, type PredictionAnalysis } from '../api'
import { ref, computed } from 'vue'
import type { PredictionAnalysis } from '../api'
import PredictionPhaseCard from './PredictionPhaseCard.vue'
const props = defineProps<{
data?: PredictionAnalysis | null
loading?: boolean
}>()
const emit = defineEmits<{
(e: 'prev'): void
(e: 'repredict'): void
}>()
// 预测分析数据
const predictionData = ref<PredictionAnalysis | null>(null)
// 获取预测分析数据
onMounted(async () => {
const data = await fetchPredictionAnalysis()
predictionData.value = data
})
// 预测分析数据 - 从 props 获取,暂无真实接口
const predictionData = computed(() => props.data || null)
// 根据索引和progressLevel返回进度条格子的类名
function getOverallSegmentClass(index: number) {
......@@ -140,15 +142,16 @@ function handleRepredict() {
</script>
<style scoped>
/* .text-title-2-bold{
color: #3b414b;
} */
.step-container {
display: flex;
flex-direction: column;
height: 100%;
}
.loading-wrapper {
flex: 1;
}
.content-wrapper {
flex: 1;
overflow: auto;
......@@ -157,7 +160,6 @@ function handleRepredict() {
.header-section {
justify-content: space-between;
align-items: flex-start;
/* margin-bottom: 24px; */
}
.header-left {
......
......@@ -14,15 +14,17 @@
v-if="currentStep === 1"
:proposal-info="proposalInfo"
:default-filters="defaultFilters"
@next="goToNextStep"
@next="handleStep1Next"
/>
<Step2FilterBills
v-else-if="currentStep === 2"
@previous="goToPreviousStep"
@next="goToNextStep"
@next="handleStep2Next"
/>
<Step3PredictionAnalysis
v-else-if="currentStep === 3"
:data="predictionResult"
:loading="predictionLoading"
@prev="goToPreviousStep"
@repredict="handleRepredict"
/>
......@@ -40,8 +42,8 @@
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 { STEPS } from './api'
import { getProgressPrediction, parseBillId, transformProposalInfo, generateDefaultFilters, getPassProd } from '@/api/bill/billHome.js'
import PageHeader from './components/PageHeader.vue'
import StepSidebar from './components/StepSidebar.vue'
import Step1FilterCondition from './components/Step1FilterCondition.vue'
......@@ -62,35 +64,51 @@ const proposalInfo = ref<ProposalInfo | null>(null)
const defaultFilters = ref<Record<string, string[]>>({})
// 法案ID
const billId = ref('')
// 当前筛选参数(传递给 Step2)
const currentFilterParams = ref<any>(null)
// 第一页接口原始数据(包含 bill_actions、bill_sponsors)
const rawProposalData = ref<any>(null)
// 第三页预测结果
const predictionResult = ref<any>(null)
// 预测加载状态
const predictionLoading = ref(false)
// 向子组件提供提案信息
// 向子组件提供提案信息和筛选参数
provide('proposalInfo', proposalInfo)
provide('defaultFilters', defaultFilters)
provide('filterParams', currentFilterParams)
// 页面初始化时加载数据
onMounted(async () => {
try {
// 步骤数据使用固定配置
steps.value = STEPS
// 获取路由参数中的 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(' 获取预测数据失败:', err)
return null
})
])
steps.value = stepsData
// 获取提案信息
const predictionData = await getProgressPrediction(billId.value).catch(err => {
console.error('[v0] 获取预测数据失败:', err)
return null
})
if (predictionData) {
// 保存原始数据(包含 bill_actions、bill_sponsors 等)
rawProposalData.value = predictionData.data
// 转换 API 数据为组件所需格式
proposalInfo.value = transformProposalInfo(predictionData)
// 根据提案信息生成默认筛选条件
if (proposalInfo.value) {
defaultFilters.value = generateDefaultFilters(proposalInfo.value)
// 初始化筛选参数(Step2 会用到)
currentFilterParams.value = {
billIds: billId.value,
domains: proposalInfo.value.areas || [],
patternType: proposalInfo.value.patternType || '统一政府',
proposalType: '两党共同提案'
}
}
}
} finally {
......@@ -105,6 +123,285 @@ function goToNextStep() {
}
}
// Step1 下一步:更新筛选参数中的 domains
function handleStep1Next(selectedFilters: Record<string, string[]>) {
// 用已选的政策领域更新 currentFilterParams
if (currentFilterParams.value) {
currentFilterParams.value.domains = selectedFilters.policyArea || []
}
// 进入下一步
goToNextStep()
}
// Step2 下一步:调用预测接口
async function handleStep2Next(selectedBills: any[]) {
predictionLoading.value = true
predictionResult.value = null
// 先进入第三页显示加载状态
goToNextStep()
try {
// TODO: 暂行版本 - 使用固定参数,等后端接口完善后替换为真实数据
// 真实参数应从 rawProposalData 和 selectedBills 获取
const params = {
"bill_name": "《人工智能监督法案》(AI OVERWATCH Act)",
"bill_actions": [
{
"action_date": "2025-12-18",
"action_type": "IntroReferral",
"org_name": "House floor actions",
"action_content": "Referred to the House Committee on Foreign Affairs."
},
{
"action_date": "2025-12-18",
"action_type": "IntroReferral",
"org_name": "Library of Congress",
"action_content": "Introduced in House"
},
{
"action_date": "2025-12-18",
"action_type": "IntroReferral",
"org_name": "Library of Congress",
"action_content": "Introduced in House"
}
],
"bill_sponsors": [
{
"person_name": "Moolenaar, John R.",
"sponsor_type": "共同提案人",
"party_name": "共和党"
},
{
"person_name": "Huizenga, Bill",
"sponsor_type": "共同提案人",
"party_name": "共和党"
},
{
"person_name": "LaHood, Darin",
"sponsor_type": "共同提案人",
"party_name": "共和党"
},
{
"person_name": "Mast, Brian J.",
"sponsor_type": "提案人",
"party_name": "共和党"
},
{
"person_name": "Kim, Young",
"sponsor_type": "共同提案人",
"party_name": "共和党"
},
{
"person_name": "Crawford, Eric A. &quot;Rick&quot;",
"sponsor_type": "共同提案人",
"party_name": "共和党"
},
{
"person_name": "Self, Keith",
"sponsor_type": "共同提案人",
"party_name": "共和党"
},
{
"person_name": "McCaul, Michael T.",
"sponsor_type": "共同提案人",
"party_name": "共和党"
},
{
"person_name": "Davidson, Warren",
"sponsor_type": "共同提案人",
"party_name": "共和党"
},
{
"person_name": "Kean, Thomas H.",
"sponsor_type": "共同提案人",
"party_name": "共和党"
},
{
"person_name": "Baird, James R.",
"sponsor_type": "共同提案人",
"party_name": "共和党"
},
{
"person_name": "Moylan, James C.",
"sponsor_type": "共同提案人",
"party_name": "共和党"
},
{
"person_name": "Fine, Randy",
"sponsor_type": "共同提案人",
"party_name": "共和党"
},
{
"person_name": "Moran, Nathaniel",
"sponsor_type": "共同提案人",
"party_name": "共和党"
},
{
"person_name": "Miller, Max L.",
"sponsor_type": "共同提案人",
"party_name": "共和党"
},
{
"person_name": "Lawler, Michael",
"sponsor_type": "共同提案人",
"party_name": "共和党"
},
{
"person_name": "Meeks, Gregory W.",
"sponsor_type": "共同提案人",
"party_name": "民主党"
},
{
"person_name": "Kamlager-Dove, Sydney",
"sponsor_type": "共同提案人",
"party_name": "民主党"
},
{
"person_name": "Sherman, Brad",
"sponsor_type": "共同提案人",
"party_name": "民主党"
},
{
"person_name": "Newhouse, Dan",
"sponsor_type": "共同提案人",
"party_name": "共和党"
},
{
"person_name": "Biggs, Sheri",
"sponsor_type": "共同提案人",
"party_name": "共和党"
},
{
"person_name": "Salazar, Maria Elvira",
"sponsor_type": "共同提案人",
"party_name": "共和党"
},
{
"person_name": "Nunn, Zachary",
"sponsor_type": "共同提案人",
"party_name": "共和党"
},
{
"person_name": "Radewagen, Aumua Amata Coleman",
"sponsor_type": "共同提案人",
"party_name": "共和党"
}
],
"bill_proposal_type": "两党共同提案",
"poli_pattern_type": "统一政府",
"poli_pattern_desc": "2025年12月18日,美国处于统一政府:共和党籍总统就职,共和党在参议院以53席对47席掌握多数,在众议院以220席对215席占据多数。",
"simi_bills": [
{
"bill_name": "2025年NASA过渡授权法案",
"bill_actions": [
{
"action_date": "2025-03-11",
"action_type": "IntroReferral",
"org_name": "Senate",
"action_content": "Read twice and referred to the Committee on Commerce, Science, and Transportation."
},
{
"action_date": "2025-03-11",
"action_type": "IntroReferral",
"org_name": "Library of Congress",
"action_content": "Introduced in Senate"
},
{
"action_date": "2025-03-11",
"action_type": "IntroReferral",
"org_name": "Senate",
"action_content": "Read twice and referred to the Committee on Commerce, Science, and Transportation."
},
{
"action_date": "2025-03-11",
"action_type": "IntroReferral",
"org_name": "Library of Congress",
"action_content": "Introduced in Senate"
},
{
"action_date": "2026-03-04",
"action_type": "Committee",
"org_name": "Senate",
"action_content": "Committee on Commerce, Science, and Transportation. Ordered to be reported with an amendment in the nature of a substitute favorably."
}
],
"bill_proposal_type": "两党共同提案",
"bill_proposal_desc": "两党共同提案",
"poli_pattern_type": "统一政府",
"poli_pattern_desc": "2025-03-11,共和党总统唐纳德&middot;特朗普执政;参议院共和党53席、民主党47席;众议院共和党220席、民主党215席(含空缺)。"
}
]
}
const response = await getPassProd(params)
if (response && response.data) {
// 转换预测结果为组件所需格式
predictionResult.value = transformPredictionResult(response.data)
}
} catch (error) {
console.error('[v0] 获取预测分析失败:', error)
} finally {
predictionLoading.value = false
}
}
// 根据通过概率转换为风险等级
function probabilityToRisk(probability: string): 'high' | 'medium' | 'low' {
const num = parseInt(probability) || 0
if (num >= 70) return 'high'
if (num >= 40) return 'medium'
return 'low'
}
// 根据通过概率转换为进度等级 (1-3)
function probabilityToProgressLevel(probability: string): number {
const num = parseInt(probability) || 0
if (num >= 70) return 3
if (num >= 40) return 2
return 1
}
// 转换预测结果为组件所需格式
function transformPredictionResult(data: any) {
const overall = data?.overall_analysis || {}
const stages = data?.stage_analysis || []
const factors = data?.factor_analysis || []
// 转换阶段分析
const phases = stages.map((stage: any, index: number) => ({
id: index + 1,
title: `阶段${index + 1}${stage.stage}`,
description: stage.stage,
riskLevel: probabilityToRisk(stage.predicted_pass_probability),
progressLevel: probabilityToProgressLevel(stage.predicted_pass_probability),
estimatedDays: `预计耗时${stage.predicted_passing_time}天`,
modelInputs: [stage.analysis],
supportingFacts: {
title: '通过性预测依据',
basedOn: '此阶段预测基于以下观点',
stats: [
{ value: `${stage.predicted_pass_probability}`, label: '通过概率' },
{ value: `${stage.predicted_passing_time}天`, label: '预计耗时' }
]
},
predictionBasis: stage.prediction_basis
}))
return {
title: '立法进展阶段预测分析',
subtitle: `基于相似法案的历史数据与预测模型`,
overallRisk: probabilityToRisk(overall.predicted_pass_probability),
overallProgressLevel: probabilityToProgressLevel(overall.predicted_pass_probability),
overallEstimatedDays: `预计耗时${overall.predicted_passing_time || '—'}天`,
highlightText: overall.analysis || '',
overallProbability: overall.predicted_pass_probability || '',
phases,
factorAnalysis: factors
}
}
// 上一步
function goToPreviousStep() {
if (currentStep.value > 1) {
......
......@@ -25,8 +25,8 @@
</div>
<div class="meta-row">
<span class="meta-label">相关领域:</span>
<div class="meta-tags">
<TagBadge v-for="item in bill.industryList" :key="item.industryName" :label="item.industryName" tag-class="tag3" />
<div class="meta-tags">
<AreaTag v-for="item in bill.industryList" :key="item.industryName" :tagName="item.industryName" />
</div>
</div>
<div class="meta-row">
......@@ -44,8 +44,7 @@
<script setup>
import { computed } from 'vue'
import DocumentPreview from './DocumentPreview.vue'
import TagBadge from './TagBadge.vue'
import DocumentPreview from './DocumentPreview.vue'
import ProgressBar from './ProgressBar.vue'
const props = defineProps({
......@@ -179,23 +178,25 @@ const currentStageIndex = computed(() => {
.bill-card-meta {
width: 100%;
flex: 1;
position: relative;
display: flex;
flex-direction: column;
gap: 12px;
min-height: 0;
overflow: hidden;
}
.meta-row {
display: flex;
align-items: center;
position: absolute;
left: 0;
align-items: flex-start;
width: 100%;
gap: 12px;
}
.meta-row:nth-child(1) { top: 0; }
.meta-row:nth-child(2) { top: 36px; }
.meta-row:nth-child(3) { top: 72px; }
.meta-row:nth-child(4) { top: 108px; }
.meta-row:nth-child(5) { top: 144px; }
.meta-row:nth-child(1) { }
.meta-row:nth-child(2) { }
.meta-row:nth-child(3) { }
.meta-row:nth-child(4) { }
.meta-row:nth-child(5) { }
.meta-label {
font-size: 16px;
......@@ -206,6 +207,7 @@ const currentStageIndex = computed(() => {
white-space: nowrap;
flex-shrink: 0;
width: 100px;
padding-top: 2px;
}
.meta-value {
......@@ -213,6 +215,8 @@ const currentStageIndex = computed(() => {
font-weight: 400;
color: rgba(95, 101, 108, 1);
line-height: 24px;
flex: 1;
min-width: 0;
}
.sponsor-name {
......@@ -225,11 +229,11 @@ const currentStageIndex = computed(() => {
gap: 8px;
flex-wrap: wrap;
align-items: center;
flex: 1;
min-width: 0;
}
.meta-row-progress {
left: 0;
right: 0;
width: 100%;
}
.meta-row-progress :deep(.progress-bar) {
......
......@@ -84,7 +84,7 @@
</div>
</div>
</div>
<div class="line-test"></div>
<!-- <div class="line-test"></div> -->
</div>
<div class="pagination">
<div class="total">{{ `共 ${total} 项` }}</div>
......@@ -853,10 +853,12 @@ onMounted(() => {
z-index: 110;
margin-top: 10px;
.main-item {
width: 1014px;
margin-bottom: 40px;
display: flex;
width: 1014px;
display: flex;
flex-direction: row;
align-items: flex-start;
margin-bottom: 20px;
position: relative;
.time {
width: 77px;
box-sizing: border-box;
......@@ -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 {
position: absolute;
top: 10px;
......
......@@ -69,7 +69,7 @@
</div>
</div>
</div>
<div class="line-test"></div>
<!-- <div class="line-test"></div> -->
</div>
<div class="pagination">
<div class="total">{{ `共 ${total} 项` }}</div>
......@@ -605,10 +605,13 @@ const companyList = ref([
z-index: 110;
.main-item {
width: 100%;
margin-bottom: 40px;
width: 100%;
display: flex;
display: flex;
flex-direction: row;
align-items: flex-start;
margin-bottom: 20px;
position: relative;
.time {
width: 77px;
box-sizing: border-box;
......@@ -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 {
position: absolute;
top: 10px;
......
......@@ -16,7 +16,7 @@
<SelectBox v-if="isFolderAll" :placeholder-name="memberPlaceHolder" select-title="提出议员"
:select-list="memberList" :select-name="selectedmember" @update:select-text="handleSelectMember" />
<SelectBox v-if="isFolderAll" :placeholder-name="statusPlaceHolder" select-title="所处阶段"
:select-list="statusList" :select-name="selectedStauts" @update:select-text="handleSelectStauts" />
:select-list="statusList" :select-name="selectedStatus" @update:select-text="handleSelectStauts" />
<div class="check-box">
<div class="check-box-left text-tip-1">
{{ '是否涉华:' }}
......@@ -34,7 +34,8 @@
@close="handleCloseCurTag(tag, index)" />
</div>
<div class="header-footer-right">
<HeaderBtnBox :isShowAll="isFolderAll" @show-all="handleSwitchFolderAll" />
<HeaderBtnBox :isShowAll="isFolderAll" @show-all="handleSwitchFolderAll" @clear="handleClear"
@confirm="handleConfirm" />
</div>
</div>
</div>
......@@ -60,12 +61,12 @@
</ChartHeader>
</div>
<div class="content-main">
<ChartContainer chartTitle="美国会法案提出数量随时间变化趋势" :chartTypeList="curChartTypeList"
<ChartContainer :chartTitle="curChartTitle" :chartTypeList="curChartTypeList"
@clickChartItem="handleSwitchActiveChart">
<template #chart-box>
<LineChart v-if="activeChart === '折线图'" :lineChartData="curChartData" />
<PieChart v-if="activeChart === '饼状图'" :pieChartData="curChartData" />
<BarChart v-if="activeChart === '柱状图'" :barChartData="curChartData" />
<PieChart v-if="activeChart === '饼状图'" :pieChartData="curChartData" />
<RaderChart v-if="activeChart === '雷达图'" :radarChartData="curChartData" />
</template>
</ChartContainer>
......@@ -97,7 +98,7 @@
<div class="icon">
<img src="../../assets/icons/download.svg" alt="">
</div>
<div class="text text-tip-1">{{ '导出' }}</div>
<div class="text text-tip-1" @click="handleExport">{{ '导出' }}</div>
</div>
<div class="header-right-item2 item2">
<el-select v-model="curOperation" placeholder="批量操作" style="width: 120px">
......@@ -122,7 +123,8 @@
<el-table-column type="selection" width="40" />
<el-table-column label="法案名称" width="455">
<template #default="scope">
<span class="title-item text-compact-bold">{{ scope.row.title }}</span>
<span class="title-item text-compact-bold" @click="handleClickToDetail(scope.row)">{{ scope.row.title
}}</span>
</template>
</el-table-column>
<el-table-column label="日期" width="120" class-name="date-column">
......@@ -130,7 +132,8 @@
</el-table-column>
<el-table-column label="提出人" width="480">
<template #default="scope">
<span class="person-item text-compact">{{ scope.row.sponsorPersonName }}</span>
<span class="person-item text-compact" @click="handlePerClick(scope.row)">{{ scope.row.sponsorPersonName
}}</span>
</template>
</el-table-column>
<el-table-column property="affiliation" label="所属党派" width="120" />
......@@ -159,20 +162,33 @@ import BarChart from '../../components/BarChart/index.vue'
import RaderChart from '../../components/RadarChart/idnex.vue'
import SelectBox from '../../components/SelectBox/index.vue'
import { useRoute } from "vue-router";
import router from '@/router'
import { getPostOrgList, getPostMemberList } from '@/api/bill/billHome'
import { search } from '@/api/comprehensiveSearch'
import { ElMessage } from 'element-plus'
import getDateRange from '@/utils/getDateRange'
const route = useRoute();
// 图表/数据
const isShowChart = ref(false)
// 点击切换数据/图表
const handleSwitchChartData = () => {
isShowChart.value = !isShowChart.value
if (isShowChart.value) {
const curDemensionItem = staticsDemensionList.value.filter(item => {
return item.name === curDemension.value
})[0]
setTimeout(() => {
activeChart.value = curDemensionItem.chartTypeList[0]
curChartData.value = curDemensionItem.data
})
}
}
// 总计数据
const totalNum = ref(12)
const totalNum = ref(0)
// 统计维度列表
const staticsDemensionList = ref([
......@@ -180,39 +196,51 @@ const staticsDemensionList = ref([
name: '提案时间',
active: true,
chartTypeList: ['折线图', '柱状图'],
chartTitle: '美国会法案提出数量随时间变化趋势',
data: {
dataX: [],
dataY: []
},
quatarData: {
},
yearData: {
}
},
{
name: '科技领域',
active: false,
chartTypeList: ['饼状图'],
chartTitle: '美国会法案领域分布',
data: []
},
{
name: '提出议院',
active: false,
chartTypeList: ['饼状图'],
chartTitle: '美国会法案议院分布',
data: []
},
{
name: '提出委员会',
active: false,
chartTypeList: ['饼状图'],
chartTitle: '美国会法案委员会分布',
data: []
},
{
name: '提出议员党派',
active: false,
chartTypeList: ['饼状图'],
chartTitle: '美国会法案党派分布',
data: []
},
{
name: '立法阶段',
active: false,
chartTypeList: ['饼状图'],
chartTitle: '美国会法案立法阶段分布',
data: []
},
])
......@@ -223,6 +251,11 @@ const curChartTypeList = computed(() => {
return arr[0].chartTypeList
})
const curChartTitle = computed(() => {
let arr = staticsDemensionList.value.filter(item => item.active)
return arr[0].chartTitle
})
// 当前维度
const curDemension = ref('提案时间')
......@@ -243,16 +276,16 @@ const handleClickDemensionItem = (val) => {
const selectedTime = ref('按月统计')
const timeList = ref([
{
label: '按年统计',
value: '按年统计'
label: '按年统计',
value: '按年统计'
},
{
label: '按季统计',
value: '按季统计'
label: '按季统计',
value: '按季统计'
},
{
label: '按月统计',
value: '按月统计'
label: '按月统计',
value: '按月统计'
},
])
......@@ -262,8 +295,8 @@ const activeTagList = computed(() => {
if (selectedArea.value && selectedArea.value !== '全部领域') {
arr.push(selectedArea.value)
}
if (selectedDate.value) {
arr.push(selectedDate.value)
if (selectedDate.value === '自定义') {
arr.push(customTime.value.join('至'))
}
if (selectedParty.value && selectedParty.value !== '全部党派') {
arr.push(selectedParty.value)
......@@ -277,8 +310,8 @@ const activeTagList = computed(() => {
if (selectedmember.value && selectedmember.value !== '全部议员') {
arr.push(selectedmember.value)
}
if (selectedStauts.value && selectedStauts.value !== '全部阶段') {
arr.push(selectedStauts.value)
if (selectedStatus.value && selectedStatus.value !== '全部阶段') {
arr.push(selectedStatus.value)
}
if (isInvolveCn.value) {
const involveStr = '涉华'
......@@ -294,7 +327,7 @@ const handleCloseCurTag = (tag, index) => {
activeTagList.value.splice(index, 1)
}
const activeChart = ref('')
const activeChart = ref('') // 当前激活的图表
const handleSwitchActiveChart = val => {
activeChart.value = val.name
......@@ -448,27 +481,23 @@ const dateList = ref([
id: '自定义'
},
{
name: '2026',
id: '2026'
},
{
name: '2025',
id: '2025'
name: '近一年',
id: '近一年'
},
{
name: '2024',
id: '2024'
name: '近半年',
id: '近半年'
},
{
name: '2023',
id: '2023'
name: '近三月',
id: '近三月'
},
{
name: '2022',
id: '2022'
},
name: '近一月',
id: '近一月'
}
])
const customTime = ref('') // 自定义时间
const customTime = ref([]) // 自定义时间
const handleCustomDate = value => {
// console.log('value', value);
customTime.value = value
......@@ -476,6 +505,9 @@ const handleCustomDate = value => {
const handleSelectDate = (value) => {
selectedDate.value = value
if (selectedDate.value !== '自定义') {
customTime.value = getDateRange(selectedDate.value)
}
}
// 所属党派
......@@ -572,7 +604,8 @@ const handleGetMemberList = async () => {
memberList.value = res.data.map(item => {
return {
name: item.memberName,
id: item.memberName
id: item.memberName,
memberId: item.memberId
}
})
memberList.value = [{ name: '全部议员', id: '全部议员' }, ...memberList.value]
......@@ -600,16 +633,33 @@ const statusList = ref([
}
])
const selectedStauts = ref('全部阶段')
const selectedStatus = ref('全部阶段')
const statusPlaceHolder = ref('请选择立法阶段')
const handleSelectStauts = value => {
selectedStauts.value = value
selectedStatus.value = value
}
// 是否涉华
const isInvolveCn = ref(true)
const isInvolveCn = ref(false)
const handleInvolveCnChange = () => {
}
// 清空条件
const handleClear = () => {
selectedArea.value = '全部领域'
selectedDate.value = ''
selectedParty.value = '全部党派'
selectedCongress.value = '全部议院'
selectedOrg.value = '全部委员会'
selectedmember.value = '全部议员'
selectedStatus.value = '全部阶段'
isInvolveCn.value = false
ElMessage.success('已清空条件!')
}
// 确定
const handleConfirm = () => {
fetchTableData()
}
// 展开全部 / 收起
......@@ -642,7 +692,7 @@ const isSort = ref(true); // true 倒序 false 升序
// isSort.value = !isSort.value;
// };
const handlePxChange = val => {
fetchTableData()
};
const currentPage = ref(1);
......@@ -656,6 +706,8 @@ const selectedCount = computed(() => selectedMap.value.size)
// 获取表格数据(示例)
const fetchTableData = async () => {
isSelectedAll.value = false
selectedMap.value.clear()
// 调用接口获取数据...
const params = {
page: currentPage.value,
......@@ -663,15 +715,15 @@ const fetchTableData = async () => {
keyword: '',
type: 1, // type 1= 法案 2= 政令 3 =智库 4=智库报告 5=实体清单【制裁记录】 6= 人物 7= 机构 8=新闻 9= 社媒
domains: selectedArea.value === '全部领域' ? null : [selectedArea.value],
proposedDateStart: selectedDate.value ? selectedDate.value : null,
proposedDateEnd: null,
proposedDateStart: customTime.value[0],
proposedDateEnd: customTime.value[1],
affiliation: selectedParty.value === '全部党派' ? null : selectedParty.value,
originChamber: selectedCongress.value === '全部议院' ? null : selectedCongress.value,
originDepart: selectedOrg.value === '全部委员会' ? null : selectedOrg.value,
sponsorPersonName: selectedmember.value === '全部议员' ? null : selectedmember.value,
status: selectedStauts.value === '通过' ? 1 : 0,
sleStatus: isInvolveCn ? 1 : 0,
description: isSort.value ? 0 : 1 // 0 先按分数降序 后按时间降序 1 先按分数降序,再按时间升序
status: selectedStatus.value === '通过' ? 1 : 0,
sleStatus: isInvolveCn ? 'Y' : 'N',
sort: isSort.value ? 0 : 1 // 0 先按分数降序 后按时间降序 1 先按分数降序,再按时间升序
}
try {
const res = await search(params)
......@@ -679,8 +731,6 @@ const fetchTableData = async () => {
if (res.code === 200 && res.data) {
tableData.value = res.data.records
totalNum.value = res.data.total
staticsDemensionList.value[0].data = {
dataX: Object.keys(res.data.aggregationsDate),
dataY: Object.values(res.data.aggregationsDate).map(value => Number(value))
......@@ -708,6 +758,20 @@ const fetchTableData = async () => {
}
const curDemensionItem = staticsDemensionList.value.filter(item => {
return item.name === curDemension.value
})[0]
setTimeout(() => {
activeChart.value = curDemensionItem.chartTypeList[0]
curChartData.value = curDemensionItem.data
})
} catch (error) {
}
......@@ -836,14 +900,93 @@ const curChartData = ref(null)
// 跳转到当前页 初始化筛选条件
const initParam = () => {
selectedArea.value = route.query.domains ? JSON.parse(route.query.domains)[0] : '全部领域'
selectedDate.value = route.query.selectedDate,
const hasQuery = Object.keys(route.query).length > 0;
if (hasQuery) {
selectedArea.value = route.query.domains ? route.query.domains : '全部领域'
if (Array.isArray(JSON.parse(route.query.selectedDate)) && JSON.parse(route.query.selectedDate).length) {
selectedDate.value = '自定义'
customTime.value = JSON.parse(route.query.selectedDate)
}
isInvolveCn.value = route.query.isInvolveCn ? true : false
selectedStauts.value = route.query.status === '1' ? '通过' : '提出'
selectedCongress.value = route.query.selectedCongress ? route.query.selectedCongress : '全部议院'
selectedOrg.value = route.query.selectedOrg ? route.query.selectedOrg : '全部委员会'
if (route.query.selectedStatus) {
selectedStatus.value = route.query.selectedStatus === '1' ? '通过' : '提出'
} else {
selectedStatus.value = '全部阶段'
}
selectedCongress.value = route.query.selectedCongress ? route.query.selectedCongress : '全部议院'
selectedOrg.value = route.query.selectedOrg ? route.query.selectedOrg : '全部委员会'
const query = route.query;
if (Object.keys(query).length > 0) {
sessionStorage.setItem('routeQuery', JSON.stringify(query));
}
} else {
const savedQuery = JSON.parse(sessionStorage.getItem('routeQuery') || '{}');
selectedArea.value = savedQuery.domains ? savedQuery.domains : '全部领域'
if (Array.isArray(JSON.parse(savedQuery.selectedDate)) && JSON.parse(savedQuery.selectedDate).length) {
selectedDate.value = '自定义'
customTime.value = JSON.parse(savedQuery.selectedDate)
}
isInvolveCn.value = savedQuery.isInvolveCn ? true : false
if (savedQuery.selectedStatus) {
selectedStatus.value = savedQuery.selectedStatus === '1' ? '通过' : '提出'
} else {
selectedStatus.value = '全部阶段'
}
selectedCongress.value = savedQuery.selectedCongress ? savedQuery.selectedCongress : '全部议院'
selectedOrg.value = savedQuery.selectedOrg ? savedQuery.selectedOrg : '全部委员会'
}
}
// 跳转法案详情
const handleClickToDetail = (curBill) => {
console.log('curBill', curBill);
window.sessionStorage.setItem("billId", curBill.id);
window.sessionStorage.setItem("curTabName", curBill.title);
const route = router.resolve({
path: "/billLayout",
query: {
billId: curBill.id
}
});
window.open(route.href, "_blank");
};
// 跳转人物详情
const handlePerClick = item => {
window.sessionStorage.setItem("curTabName", item.sponsorPersonName);
const route = router.resolve({
path: "/characterPage",
query: {
type: 2,
personId: item.personId
}
});
window.open(route.href, "_blank");
};
// 导出
const handleExport = () => {
console.log(selectedMap.value);
const arr = Array.from(selectedMap.value);
const jsonStr = JSON.stringify(arr, null, 2);
const blob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'export.json';
link.click();
URL.revokeObjectURL(url);
};
onMounted(async () => {
handleGetOrgList()
handleGetMemberList()
......@@ -855,9 +998,6 @@ onMounted(async () => {
await fetchTableData()
activeChart.value = staticsDemensionList.value[0].chartTypeList[0]
curChartData.value = staticsDemensionList.value[0].data
})
</script>
......@@ -1108,9 +1248,21 @@ onMounted(async () => {
.title-item {
color: var(--text-primary-80-color);
cursor: pointer;
&:hover {
color: var(--color-primary-100);
text-decoration: underline;
}
}
.person-item {
color: var(--color-primary-100);
cursor: pointer;
&:hover {
font-weight: bold;
text-decoration: underline;
}
}
</style>
\ No newline at end of file
......@@ -78,7 +78,7 @@ const props = defineProps({
type: String,
default: ''
},
chartTypeList : {
chartTypeList: {
type: Array,
default: []
}
......@@ -88,15 +88,19 @@ const chartItemList = computed(() => {
let arr = []
props.chartTypeList.forEach(item => {
defaultChartTypeList.forEach(val => {
if(val.name === item) {
if (val.name === item) {
arr.push(val)
}
})
})
arr.forEach(item => {
item.active = false
})
arr[0].active = true
// console.log('arr', arr);
return arr
})
......
<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-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-option label="全部领域" value="全部领域" /> -->
<el-option v-for="item in selectList" :key="item.id" :label="item.name" :value="item.id" />
</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" />
<!-- <el-date-picker v-model="customTimeValue" type="daterange" value-format="YYYY-MM-DD">
</el-date-picker> -->
</div>
</div>
</template>
......@@ -61,6 +63,7 @@ const customTimeValue = computed({
justify-content: space-between;
align-items: center;
gap: 8px;
.select-left {
width: 100px;
height: 24px;
......@@ -72,15 +75,18 @@ const customTimeValue = computed({
display: flex;
gap: 8px;
justify-content: space-between;
:deep(.el-input__wrapper) {
border-radius: 4px;
}
}
.select-right-custom{
.select-right-custom {
width: 630px;
}
}
.select-wrapper-custom{
.select-wrapper-custom {
width: 738px;
}
</style>
\ No newline at end of file
<template>
<div class="countrybill-wrapper">
<div class="header-box">我是政令</div>
<div class="main-box">
<div class="header-box">
<div class="header-top">
<SelectBox :placeholder-name="areaPlaceHolder" select-title="科技领域" :select-list="areaList"
:select-name="selectedArea" @update:select-text="handleSelectArea" />
<SelectBox :placeholder-name="DatePlaceHolder" select-title="提出时间" :select-list="dateList"
:select-name="selectedDate" :custom-time="customTime" @update:select-text="handleSelectDate"
@update:custom-time="handleCustomDate" />
<SelectBox v-if="isFolderAll" :placeholder-name="insPlaceHolder" select-title="发布机构" :select-list="institutionList"
:select-name="selectedIns" @update:select-text="handleSelectIns" />
<SelectBox v-if="isFolderAll" :placeholder-name="decreeTypePlaceHolder" select-title="政令类型"
:select-list="decreeTypeList" :select-name="selectedDecreeType" @update:select-text="handleSelectDecreeType" />
<div class="check-box">
<div class="check-box-left text-tip-1">
{{ '是否涉华:' }}
</div>
<div class="check-box-right">
<el-checkbox v-model="isInvolveCn" class="involve-checkbox" @change="handleInvolveCnChange">
{{ '只看涉华法案' }}
</el-checkbox>
</div>
</div>
<div class="check-box">
<div class="check-box-left text-tip-1">
{{ '科技相关:' }}
</div>
<div class="check-box-right">
<el-checkbox v-model="isInvolveTechnology" class="involve-checkbox">
{{ '只看科技相关' }}
</el-checkbox>
</div>
</div>
</div>
<div class="header-footer">
<div class="header-footer-left">
<ActiveTag v-for="tag, index in activeTagList" :key="index" :tagName="tag"
@close="handleCloseCurTag(tag, index)" />
</div>
<div class="header-footer-right">
<HeaderBtnBox :isShowAll="isFolderAll" @show-all="handleSwitchFolderAll" @clear="handleClear"
@confirm="handleConfirm" />
</div>
</div>
</div>
<div class="chart-main-box" v-if="isShowChart">
<div class="info-box">
<div class="switch-box" @click="handleSwitchChartData">
<img v-if="!isShowChart" src="@/views/dataLibrary/assets/icons/chart-active.svg" alt="">
<img v-else src="@/views/dataLibrary/assets/icons/data-active.svg" alt="">
<img src="@/views/dataLibrary/assets/icons/chart-active.svg" alt="">
</div>
<div class="num-box text-title-3-bold">
{{ `共 ${totalNum} 条数据` }}
</div>
</div>
<div class="content-box">
<div class="content-header">
<ChartHeader :list="staticsDemensionList" @clickItem="handleClickDemensionItem">
<template #chart-header-right>
<el-select v-model="selectedTime" placeholder="选择时间" style="width: 150px"
v-show="curDemension === '提案时间'">
<el-option v-for="item in timeList" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</template>
</ChartHeader>
</div>
<div class="content-main">
<ChartContainer :chartTitle="curChartTitle" :chartTypeList="curChartTypeList"
@clickChartItem="handleSwitchActiveChart">
<template #chart-box>
<LineChart v-if="activeChart === '折线图'" :lineChartData="curChartData" />
<PieChart v-if="activeChart === '饼状图'" :pieChartData="curChartData" />
<BarChart v-if="activeChart === '柱状图'" :barChartData="curChartData" />
<RaderChart v-if="activeChart === '雷达图'" :radarChartData="curChartData" />
</template>
</ChartContainer>
</div>
</div>
</div>
<div class="data-main-box" v-else>
<div class="data-main-box-header">
<div class="switch-box" @click="handleSwitchChartData">
<img src="@/views/dataLibrary/assets/icons/data-active.svg" alt="">
</div>
<div class="num-box text-title-3-bold">
{{ `共 ${totalNum} 条数据` }}
</div>
</div>
<div class="content-box"></div>
<div class="data-main-box-main">
<div class="data-main-box-main-header">
<div class="header-left">
<div class="header-left-item1">
<el-checkbox v-model="isSelectedAll" label="全选" @change="handleSelectAllChange" size="large" />
</div>
<div class="header-left-item2 text-tip-1">{{ `已选择${selectedCount}项` }}</div>
<div class="header-left-item2 text-tip-1 cancel" @click="handleClearAll" v-show="selectedCount">{{ '取消' }}
</div>
</div>
<div class="header-right">
<div class="header-right-item item1">
<div class="icon">
<img src="../assets/icons/download.svg" alt="">
</div>
<div class="text text-tip-1" @click="handleExport">{{ '导出' }}</div>
</div>
<div class="header-right-item2 item2">
<el-select v-model="curOperation" placeholder="批量操作" style="width: 120px">
<el-option v-for="item in operationList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</div>
<div class="header-right-item3 item3">
<el-select v-model="isSort" placeholder="发布时间" style="width: 166px" @change="handlePxChange">
<template #prefix>
<div style="display: flex; align-items: center; height: 100%">
<img src="../../assets/icons/desc-icon.svg" style="width: 14px; height: 14px" />
</div>
</template>
<el-option v-for="item in releaseTimeList" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
</div>
</div>
<div class="data-main-box-main-content">
<el-table ref="tableRef" :data="tableData" row-key="id" @selection-change="handleSelectionChange"
@select="handleSelect" @select-all="handleSelectAll" style="width: 100%" :row-style="{ height: '52px' }">
<el-table-column type="selection" width="40" />
<el-table-column label="法案名称" width="455">
<template #default="scope">
<span class="title-item text-compact-bold" @click="handleClickToDetail(scope.row)">{{ scope.row.title
}}</span>
</template>
</el-table-column>
<el-table-column label="日期" width="120" class-name="date-column">
<template #default="scope">{{ scope.row.date }}</template>
</el-table-column>
<el-table-column label="提出人" width="480">
<template #default="scope">
<span class="person-item text-compact" @click="handlePerClick(scope.row)">{{ scope.row.sponsorPersonName
}}</span>
</template>
</el-table-column>
<el-table-column property="affiliation" label="所属党派" width="120" />
<el-table-column property="originDepart" label="提出委员会" width="180" />
<el-table-column property="status" label="所处阶段" width="120" />
</el-table>
</div>
</div>
<div class="data-main-box-footer">
<el-pagination background layout="prev, pager, next" :total="totalNum" v-model:current-page="currentPage"
:page-size="pageSize" @current-change="handleCurrentChange" />
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ref, computed, watch, onMounted, nextTick } from 'vue'
import ChartContainer from '../components/ChartContainer/index.vue'
import ChartHeader from '../components/ChartHeader/index.vue'
import ActiveTag from '../components/ActiveTag/index.vue'
import HeaderBtnBox from '../components/HeaderBtnBox/index.vue'
import LineChart from '../components/LineChart/index.vue'
import PieChart from '../components/PieChart/index.vue'
import BarChart from '../components/BarChart/index.vue'
import RaderChart from '../components/RadarChart/idnex.vue'
import SelectBox from '../components/SelectBox/index.vue'
import { useRoute } from "vue-router";
import router from '@/router'
// 图表/数据
const isShowChart = ref(true)
import { getPostOrgList, getPostMemberList } from '@/api/bill/billHome'
import { search } from '@/api/comprehensiveSearch'
import { ElMessage } from 'element-plus'
import getDateRange from '@/utils/getDateRange'
const route = useRoute();
// 图表/数据
const isShowChart = ref(false)
// 点击切换数据/图表
const handleSwitchChartData = () => {
isShowChart.value = !isShowChart.value
if (isShowChart.value) {
const curDemensionItem = staticsDemensionList.value.filter(item => {
return item.name === curDemension.value
})[0]
setTimeout(() => {
activeChart.value = curDemensionItem.chartTypeList[0]
curChartData.value = curDemensionItem.data
})
}
}
// 总计数据
const totalNum = ref(12)
const totalNum = ref(0)
// 统计维度列表
const staticsDemensionList = ref([
{
name: '发布时间',
active: true,
chartTypeList: ['折线图', '柱状图'],
chartTitle: '政令发布时间趋势变化',
data: {
dataX: [],
dataY: []
},
quatarData:{
},
yearData: {
}
},
{
name: '科技领域',
active: false,
chartTypeList: ['饼状图'],
chartTitle: '政令科技领域分布',
data: []
},
{
name: '发布机构',
active: false,
chartTypeList: ['饼状图'],
chartTitle: '政令发布机构分布',
data: []
}
])
// 当前维度下的图表列表
const curChartTypeList = computed(() => {
let arr = staticsDemensionList.value.filter(item => item.active)
return arr[0].chartTypeList
})
// 当前图表标题
const curChartTitle = computed(() => {
let arr = staticsDemensionList.value.filter(item => item.active)
return arr[0].chartTitle
})
// 当前维度
const curDemension = ref('提案时间')
// 点击维度item
const handleClickDemensionItem = (val) => {
activeChart.value = ''
staticsDemensionList.value.forEach(item => {
item.active = false
})
val.active = true
curDemension.value = val.name
setTimeout(() => {
activeChart.value = val.chartTypeList[0]
curChartData.value = val.data
})
}
const selectedTime = ref('按月统计')
const timeList = ref([
{
label: '按年度统计',
value: '按年度统计'
},
{
label: '按季度统计',
value: '按季度统计'
},
{
label: '按月度统计',
value: '按月度统计'
},
])
// 激活的标签列表
const activeTagList = computed(() => {
const arr = []
if (selectedArea.value && selectedArea.value !== '全部领域') {
arr.push(selectedArea.value)
}
if (selectedDate.value === '自定义') {
arr.push(customTime.value.join('至'))
}
if (selectedIns.value && selectedIns.value !== '全部机构') {
arr.push(selectedIns.value)
}
if (selectedDecreeType.value && selectedDecreeType.value !== '全部类型') {
arr.push(selectedDecreeType.value)
}
if (isInvolveCn.value) {
const involveStr = '涉华'
arr.push(involveStr)
}
return arr
})
const handleCloseCurTag = (tag, index) => {
// alert(tag.name)
activeTagList.value.splice(index, 1)
}
const activeChart = ref('')
const handleSwitchActiveChart = val => {
activeChart.value = val.name
}
const radarChartData = ref({
title: [
{
name: '航空航天',
max: 10
},
{
name: '先进制造',
max: 10
},
{
name: '量子科技',
max: 10
},
{
name: '人工智能',
max: 10
},
{
name: '新材料',
max: 10
},
{
name: '集成电路',
max: 10
},
],
data: [
{
name: "337调查",
value: [10, 5, 2, 8, 5, 7
]
},
{
name: "232调查",
value: [2, 5, 3, 8, 10, 2]
},
{
name: "301调查",
value: [5, 8, 2, 9, 1, 5]
}
]
}
)
// 数据- 是否全选
const isSelectedAll = ref(false)
const curOperation = ref('')
const operationList = ref([
{
name: 'aaa',
id: 'aaa'
},
{
name: 'bbb',
id: 'bbb'
},
{
name: 'ccc',
id: 'ccc'
},
])
// 科技领域
const areaPlaceHolder = ref('请选择领域')
const selectedArea = ref('全部领域')
const areaList = ref([
{
name: '全部领域',
id: '全部领域'
},
{
name: '人工智能',
id: '人工智能'
},
{
name: '生物科技',
id: '生物科技'
},
{
name: '新一代通信网络',
id: '新一代通信网络'
},
{
name: '量子科技',
id: '量子科技'
},
{
name: '新能源',
id: '新能源'
},
{
name: '集成电路',
id: '集成电路'
},
{
name: '海洋',
id: '海洋'
},
{
name: '先进制造',
id: '先进制造'
},
{
name: '新材料',
id: '新材料'
},
{
name: '航空航天',
id: '航空航天'
},
{
name: '太空',
id: '太空'
},
{
name: '深海',
id: '深海'
},
{
name: '极地',
id: '极地'
},
{
name: '核',
id: '核'
},
{
name: '其他',
id: '其他'
},
])
const handleSelectArea = (value) => {
selectedArea.value = value
}
// 发布时间
const DatePlaceHolder = ref('请选择时间')
const selectedDate = ref('')
const dateList = ref([
{
name: '自定义',
id: '自定义'
},
{
name: '近一年',
id: '近一年'
},
{
name: '近半年',
id: '近半年'
},
{
name: '近三月',
id: '近三月'
},
{
name: '近一月',
id: '近一月'
}
])
const customTime = ref([]) // 自定义时间
const handleCustomDate = value => {
// console.log('value', value);
customTime.value = value
}
const handleSelectDate = (value) => {
selectedDate.value = value
if (selectedDate.value !== '自定义') {
customTime.value = getDateRange(selectedDate.value)
}
}
// 发布机构 机构列表
const institutionList = ref([
{
name: '全部机构',
id: '全部机构'
},
{
name: '机构1',
id: '机构1'
},
{
name: '机构2',
id: '机构2'
},
{
name: '机构3',
id: '机构3'
},
])
const selectedIns = ref('全部机构')
const insPlaceHolder = ref('请选择机构')
const handleSelectIns = value => {
selectedIns.value = value
}
// 政令类型
const decreeTypeList = ref([
{
name: '全部类型',
id: '全部类型'
},
{
name: '行政命令',
id: '行政命令'
},
{
name: '备忘录',
id: '备忘录'
}
])
const selectedDecreeType = ref('全部类型')
const decreeTypePlaceHolder = ref('请选择议院')
const handleSelectDecreeType = value => {
selectedDecreeType.value = value
}
// 是否涉华
const isInvolveCn = ref(false)
const handleInvolveCnChange = () => {
}
// 是否科技相关
const isInvolveTechnology = ref(false)
// 清空条件
const handleClear = () => {
selectedArea.value = '全部领域'
selectedDate.value = ''
selectedIns.value = '全部机构'
selectedDecreeType.value = '全部类型'
isInvolveCn.value = false
ElMessage.success('已清空条件!')
}
// 确定
const handleConfirm = () => {
fetchTableData()
}
// 展开全部 / 收起
const isFolderAll = ref(false)
const handleSwitchFolderAll = () => {
isFolderAll.value = !isFolderAll.value
}
const tableRef = ref(null)
// 表格数据
const tableData = ref([
])
const releaseTimeList = ref([
{
label: "按发布时间倒序",
value: true
},
{
label: "按发布时间升序",
value: false
}
]);
const isSort = ref(true); // true 倒序 false 升序
// const handleSwithSort = () => {
// isSort.value = !isSort.value;
// };
const handlePxChange = val => {
fetchTableData()
};
const currentPage = ref(1);
const pageSize = ref(10)
// 存储选中的数据(跨页)[citation:3][citation:8]
const selectedMap = ref(new Map()) // 使用 Map 存储,key 为唯一 id
// 计算已选中的条数
const selectedCount = computed(() => selectedMap.value.size)
// 获取表格数据(示例)
const fetchTableData = async () => {
isSelectedAll.value = false
selectedMap.value.clear()
// 调用接口获取数据...
const params = {
page: currentPage.value,
size: pageSize.value,
keyword: '',
type: 1, // type 1= 法案 2= 政令 3 =智库 4=智库报告 5=实体清单【制裁记录】 6= 人物 7= 机构 8=新闻 9= 社媒
domains: selectedArea.value === '全部领域' ? null : [selectedArea.value],
proposedDateStart: customTime.value[0],
proposedDateEnd: customTime.value[1],
affiliation: selectedIns.value === '全部机构' ? null : selectedIns.value,
originChamber: selectedDecreeType.value === '全部类型' ? null : selectedDecreeType.value,
sleStatus: isInvolveCn ? 'Y' : 'N',
sort: isSort.value ? 0 : 1 // 0 先按分数降序 后按时间降序 1 先按分数降序,再按时间升序
}
try {
const res = await search(params)
console.log('搜索结果', res);
if (res.code === 200 && res.data) {
tableData.value = res.data.records
totalNum.value = res.data.total
staticsDemensionList.value[0].data = {
dataX: Object.keys(res.data.aggregationsDate),
dataY: Object.values(res.data.aggregationsDate).map(value => Number(value))
}
staticsDemensionList.value[1].data = Object.entries(res.data.aggregationsDomain).map(([key, value]) => ({
name: key,
value: Number(value)
}))
staticsDemensionList.value[2].data = Object.entries(res.data.aggregationsOriginChamber).map(([key, value]) => ({
name: key,
value: Number(value)
}))
staticsDemensionList.value[3].data = Object.entries(res.data.aggregationsOriginDepart).map(([key, value]) => ({
name: key,
value: Number(value)
}))
staticsDemensionList.value[4].data = Object.entries(res.data.aggregationsAffiliation).map(([key, value]) => ({
name: key,
value: Number(value)
}))
staticsDemensionList.value[5].data = Object.entries(res.data.aggregationsStatus).map(([key, value]) => ({
name: key === '1' ? '通过' : '提出',
value: Number(value)
}))
}
const curDemensionItem = staticsDemensionList.value.filter(item => {
return item.name === curDemension.value
})[0]
setTimeout(() => {
activeChart.value = curDemensionItem.chartTypeList[0]
curChartData.value = curDemensionItem.data
})
} catch (error) {
}
// tableData.value = res.data
// total.value = res.total
// 数据加载后,回显已选中的行
nextTick(() => {
tableData.value.forEach(row => {
if (selectedMap.value.has(row.id)) {
tableRef.value?.toggleRowSelection(row, true)
}
})
})
}
// 单选事件
const handleSelect = (selection, row) => {
if (selection.some(item => item.id === row.id)) {
// 选中:添加到 Map
selectedMap.value.set(row.id, row)
} else {
// 取消选中:从 Map 移除
selectedMap.value.delete(row.id)
}
}
// 全选/全不选事件
const handleSelectAll = (selection) => {
if (selection.length > 0) {
// 全选:将当前页所有数据添加到 Map
tableData.value.forEach(row => {
if (!selectedMap.value.has(row.id)) {
selectedMap.value.set(row.id, row)
}
})
} else {
// 全不选:从 Map 中移除当前页的所有数据
tableData.value.forEach(row => {
selectedMap.value.delete(row.id)
})
}
}
// 处理选择变化(用于统计)
const handleSelectionChange = (val) => {
// 这里可以做一些额外的处理,但主要统计使用 selectedMap
console.log('当前页选中数量:', val.length)
}
// 全选当前页按钮
const handleSelectAllPage = () => {
if (tableData.value.length === 0) return
// 检查当前页是否已全选
const currentPageSelected = tableData.value.every(row =>
selectedMap.value.has(row.id)
)
if (currentPageSelected) {
// 已全选,则取消当前页的全选
tableData.value.forEach(row => {
tableRef.value.toggleRowSelection(row, false)
selectedMap.value.delete(row.id)
})
} else {
// 未全选,则全选当前页
tableData.value.forEach(row => {
tableRef.value.toggleRowSelection(row, true)
if (!selectedMap.value.has(row.id)) {
selectedMap.value.set(row.id, row)
}
})
}
}
// 处理全选
const handleSelectAllChange = () => {
if (isSelectedAll.value) {
handleSelectAllPage()
} else {
handleClearAll()
}
// if (isSelectedAll.value) {
// // 全选:将当前页所有数据添加到 Map
// tableData.value.forEach(row => {
// if (!selectedMap.value.has(row.id)) {
// selectedMap.value.set(row.id, row)
// }
// })
// } else {
// // 全不选:从 Map 中移除当前页的所有数据
// tableData.value.forEach(row => {
// selectedMap.value.delete(row.id)
// })
// }
}
// 清空所有选择
const handleClearAll = () => {
selectedMap.value.clear()
tableRef.value?.clearSelection()
}
// 翻页
const handleCurrentChange = (val) => {
currentPage.value = val
fetchTableData()
}
// 监听数据变化,回显选中状态 [citation:4][citation:8]
watch(tableData, () => {
nextTick(() => {
tableData.value.forEach(row => {
if (selectedMap.value.has(row.id)) {
tableRef.value?.toggleRowSelection(row, true)
}
})
})
})
// 当前图表数据
const curChartData = ref(null)
// 跳转到当前页 初始化筛选条件
const initParam = () => {
const hasQuery = Object.keys(route.query).length > 0;
if (hasQuery) {
selectedArea.value = route.query.domains ? route.query.domains : '全部领域'
if (Array.isArray(JSON.parse(route.query.selectedDate)) && JSON.parse(route.query.selectedDate).length) {
selectedDate.value = '自定义'
customTime.value = JSON.parse(route.query.selectedDate)
}
isInvolveCn.value = route.query.isInvolveCn ? true : false
selectedDecreeType.value = route.query.selectedDecreeType ? route.query.selectedDecreeType : '全部类型'
const query = route.query;
if (Object.keys(query).length > 0) {
sessionStorage.setItem('routeQuery', JSON.stringify(query));
}
} else {
const savedQuery = JSON.parse(sessionStorage.getItem('routeQuery') || '{}');
selectedArea.value = savedQuery.domains ? savedQuery.domains : '全部领域'
if (Array.isArray(JSON.parse(savedQuery.selectedDate)) && JSON.parse(savedQuery.selectedDate).length) {
selectedDate.value = '自定义'
customTime.value = JSON.parse(savedQuery.selectedDate)
}
isInvolveCn.value = savedQuery.isInvolveCn ? true : false
selectedDecreeType.value = savedQuery.selectedDecreeType ? savedQuery.selectedDecreeType : '全部类型'
}
}
// 跳转法案详情
const handleClickToDetail = (curBill) => {
console.log('curBill', curBill);
window.sessionStorage.setItem("billId", curBill.id);
window.sessionStorage.setItem("curTabName", curBill.title);
const route = router.resolve({
path: "/billLayout",
query: {
billId: curBill.id
}
});
window.open(route.href, "_blank");
};
// 跳转人物详情
const handlePerClick = item => {
window.sessionStorage.setItem("curTabName", item.sponsorPersonName);
const route = router.resolve({
path: "/characterPage",
query: {
type: 2,
personId: item.personId
}
});
window.open(route.href, "_blank");
};
// 导出
const handleExport = () => {
console.log(selectedMap.value);
const arr = Array.from(selectedMap.value);
const jsonStr = JSON.stringify(arr, null, 2);
const blob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'export.json';
link.click();
URL.revokeObjectURL(url);
};
onMounted(async () => {
handleGetOrgList()
handleGetMemberList()
initParam()
// 初始化
await fetchTableData()
})
</script>
......@@ -35,6 +885,8 @@ const totalNum = ref(12)
.countrybill-wrapper {
width: 1600px;
height: 968px;
overflow: hidden;
overflow-y: auto;
.headere-box {
width: 1568px;
......@@ -44,19 +896,53 @@ const totalNum = ref(12)
border: 1px solid var(--bg-black-5);
margin: 16px auto;
}
.header-box {
width: 1568px;
height: 112px;
border-radius: 10px;
background: rgb(255, 255, 255);
border: 1px solid var(--bg-black-5);
margin: 16px auto;
.header-box {
width: 1568px;
min-height: 112px;
border-radius: 10px;
background: rgb(255, 255, 255);
border: 1px solid var(--bg-black-5);
margin: 16px auto;
box-sizing: border-box;
padding: 16px 24px;
.header-top {
min-height: 28px;
display: flex;
flex-wrap: wrap;
gap: 12px 42px;
// transition: all ease 1s;
.check-box {
display: flex;
width: 348px;
height: 28px;
align-items: center;
gap: 8px;
.check-box-left {
width: 100px;
color: var(--text-primary-65-color);
}
}
}
.main-box {
.header-footer {
margin-top: 16px;
display: flex;
justify-content: space-between;
.header-footer-left {
display: flex;
justify-content: space-between;
gap: 8px;
}
}
}
.chart-main-box {
.info-box {
margin: 0 auto;
width: 1568px;
......@@ -90,7 +976,172 @@ const totalNum = ref(12)
border-radius: 10px;
background: rgba(255, 255, 255);
border: 1px solid var(--bg-black-5);
.content-header {
margin: 16px 24px;
width: 1520px;
height: 28px;
}
.content-main {
width: 1520px;
margin: 0 auto;
}
}
}
.data-main-box {
width: 1568px;
height: 810px;
border-radius: 10px;
background: var(--bg-white-100);
margin: 0 auto;
overflow: hidden;
.data-main-box-header {
margin: 16px auto;
width: 1520px;
height: 30px;
display: flex;
justify-content: space-between;
.switch-box {
width: 160px;
border-radius: 20px;
border: 1px solid var(--color-primary-100);
height: 30px;
cursor: pointer;
img {
width: 100%;
height: 100%;
}
}
.num-box {
box-sizing: border-box;
padding: 2px 0;
color: var(--color-red-100);
}
}
.data-main-box-main {
width: 1520px;
// height: 633px;
height: 680px;
border-radius: 10px;
border: 1px solid var(--bg-black-5);
margin: 0 auto;
.data-main-box-main-header {
height: 48px;
background: var(--bg-black-2);
display: flex;
justify-content: space-between;
.header-left {
margin-left: 16px;
height: 48px;
display: flex;
gap: 16px;
align-items: center;
.header-left-item2 {
color: var(--color-primary-100);
}
.cancel {
cursor: pointer;
}
}
.header-right {
margin-right: 16px;
display: flex;
gap: 8px;
align-items: center;
.header-right-item {
height: 30px;
padding: 5px 16px;
border: 1px solid var(--bg-black-10);
border-radius: 4px;
background: var(--bg-white-100);
cursor: pointer;
}
.item1 {
display: flex;
gap: 2px;
justify-content: center;
align-items: center;
&:hover {
background: var(--color-primary-2);
}
.icon {
width: 16px;
height: 16px;
img {
width: 100%;
height: 100%;
}
}
.text {
color: var(--text-primary-65-color);
}
}
.item2 {}
.item3 {}
}
}
.data-main-box-main-content {}
}
.data-main-box-footer {
margin: 16px auto 0;
height: 40px;
width: 1520px;
display: flex;
align-items: center;
justify-content: flex-end;
}
}
}
.date-column {
background-color: #ecf5ff;
}
.date-column .cell {
color: #409eff !important;
font-weight: 500;
}
.title-item {
color: var(--text-primary-80-color);
cursor: pointer;
&:hover {
color: var(--color-primary-100);
text-decoration: underline;
}
}
.person-item {
color: var(--color-primary-100);
cursor: pointer;
&:hover {
font-weight: bold;
text-decoration: underline;
}
}
</style>
\ No newline at end of file
......@@ -478,12 +478,13 @@ const handleClickToolBox = () => {
onMounted(() => {
const path = route.path
console.log(decodeURI(route.fullPath));
switch (path) {
case '/dataLibrary/countryBill':
siderList.value[0].active = true
siderList.value[0].isExpanded = true
siderList.value[0].children[0].active = true
break
case '/dataLibrary/stateBill':
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 @@
<div class="item-footer">分析报告</div>
</div>
</div> -->
<div class="date-box" v-if="govInsList.length">
<div class="date-box" v-if="keyOrganizationList.length">
<div class="date-icon">
<img :src="tipsTcon" alt="">
</div>
<div class="date-text">近期美国各联邦政府机构发布涉华政令数量汇总</div>
<TimeTabPane @time-click="handleGetDepartmentList" />
<TimeTabPane @time-click="onKeyOrganization" />
</div>
<div class="home-main-header-item-box" v-if="govInsList.length">
<div class="organization-item" v-for="(item, index) in govInsList.slice(0, 7)" :key="index" @click="handleToInstitution(item)">
<div class="home-main-header-item-box" v-if="keyOrganizationList.length">
<div class="organization-item" v-for="(item, index) in keyOrganizationList" :key="index" @click="handleToInstitution(item)">
<div class="item-left">
<img :src="item.orgImage || DefaultIcon2" alt="" />
<img :src="item.imgUrl || DefaultIcon2" alt="" />
</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>
<div class="item-dot" v-if="item.totalRecent">+{{item.totalRecent}}</div>
<div class="item-dot" v-if="item.recentOrderNum">+{{item.recentOrderNum}}</div>
</div>
<div class="organization-item">
<div class="organization-item" @click="onNavigateTo()">
<div class="item-more">查看全部机构 ({{govInsList.length+1}}家)</div>
<el-icon color="var(--color-primary-100)"><ArrowRightBold /></el-icon>
</div>
......@@ -164,7 +164,6 @@
<DivideHeader id="position2" class="divide2" :titleText="'资讯要闻'"></DivideHeader>
<div class="center-center">
<NewsList :newsList="newsList" @item-click="handleToNewsAnalysis" @more-click="handleToMoreNews" />
<!-- <NewsList :newsList="newsList" /> -->
<MessageBubble :messageList="messageList" @person-click="handleClickPerson" @info-click="handleGetMessage"
imageUrl="img" @more-click="handleToSocialDetail" />
</div>
......@@ -194,7 +193,7 @@
</el-select>
</div>
</div>
<div class="box5-main">
<div class="box5-main" v-loading="box5Params.loading">
<div class="box5-chart" id="chart1"></div>
</div>
<div class="data-origin-box">
......@@ -227,7 +226,7 @@
</el-select>
</div>
</div>
<div class="box5-main">
<div class="box5-main" v-loading="box6Params.loading">
<div class="box5-chart" id="chart2"></div>
</div>
<div class="data-origin-box">
......@@ -267,7 +266,7 @@
</el-select>
</div>
</div>
<div class="box7-main">
<div class="box7-main" v-loading="box7Params.loading">
<div class="box7-list">
<div class="box7-item" v-for="(item, index) in keyDecreeList" :key="index" @click="handleKeyDecree(item)">
<div class="icon">
......@@ -315,7 +314,7 @@
</el-select>
</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%" />
</div>
<div class="data-origin-box">
......@@ -485,6 +484,8 @@ import DefaultIcon2 from "@/assets/icons/default-icon2.png";
import tipsTcon from "./assets/images/tips-icon.png";
import { ElMessage } from "element-plus";
import { useGotoNewsDetail } from '@/router/modules/news';
const containerRef = ref(null);
const { isShow } = useContainerScroll(containerRef);
const currentPage = ref(1);
......@@ -499,14 +500,9 @@ const handleCurrentChange = page => {
// 机构列表
const govInsList = ref([]);
const checkedGovIns = ref([]);
const handleGetDepartmentList = async (event) => {
let day = 7
if (event?.time === '近一周') day = 7
if (event?.time === '近一月') day = 30
if (event?.time === '近一年') day = 365
const handleGetDepartmentList = async () => {
try {
const res = await getDepartmentList({day});
const res = await getDepartmentList({day:7});
console.log("机构列表", res);
if (res.code === 200 && res.data) {
govInsList.value = res.data;
......@@ -526,6 +522,10 @@ const handleToInstitution = item => {
});
window.open(curRoute.href, "_blank");
};
// 跳转全部机构页面
const onNavigateTo = () => {
router.push({name: "allOrganization"});
}
// 查看更多风险信号
const handleToMoreRiskSignal = () => {
......@@ -657,7 +657,6 @@ const handleGetNews = async () => {
const res = await getNews({moduleId: "0101"});
console.log("新闻资讯", res);
if (res.code === 200 && res.data) {
// newsList.value = res.data || []
newsList.value = (res.data ?? []).map(item => {
return {
newsId: item.newsId,
......@@ -673,14 +672,9 @@ const handleGetNews = async () => {
}
};
// 点击新闻条目,跳转到新闻分析页
const gotoNewsDetail = useGotoNewsDetail()
const handleToNewsAnalysis = news => {
const route = router.resolve({
path: "/newsAnalysis",
query: {
newsId: news.newsId
}
});
window.open(route.href, "_blank");
gotoNewsDetail(news.newsId)
};
// 社交媒体
......@@ -799,9 +793,11 @@ const box5Params = reactive({
year: yearList[0].value,
domainId: '',
proposeName: '',
loading: false,
})
const summarize1 = ref()
const handleGetDecreeYearOrder = async () => {
box5Params.loading = true
try {
let { year, domainId, proposeName } = box5Params;
const res = await getDecreeYearOrder({
......@@ -822,6 +818,7 @@ const handleGetDecreeYearOrder = async () => {
} catch (error) {
console.error("行政令发布频度error", error);
}
box5Params.loading = false
};
// AI智能总结
const onChartInterpretation = async (text) => {
......@@ -882,9 +879,11 @@ const chart2Data = ref([
const box6Params = reactive({
year: yearList[0].value,
proposeName: '',
loading: false,
});
const summarize2 = ref()
const handleGetDecreeArea = async () => {
box6Params.loading = true
try {
let { year, proposeName } = box6Params;
const res = await getDecreeArea({
......@@ -904,6 +903,7 @@ const handleGetDecreeArea = async () => {
} catch (error) {
console.error("政令科技领域error", error);
}
box6Params.loading = false
};
const handleBox6 = async () => {
await handleGetDecreeArea();
......@@ -926,8 +926,10 @@ const box7Params = reactive({
year: yearList[0].value,
domainId: '',
proposeName: '',
loading: false,
})
const handleGetKeyDecree = async () => {
box7Params.loading = true
try {
let { year, domainId, proposeName } = box7Params;
const res = await getKeyDecree({
......@@ -950,6 +952,7 @@ const handleGetKeyDecree = async () => {
});
}
} catch (error) { }
box7Params.loading = false
};
// 政令重点条款
......@@ -958,8 +961,10 @@ const box8Params = reactive({
year: yearList[0].value,
domainId: '',
proposeName: '',
loading: false,
})
const handleGetDecreeKeyInstruction = async () => {
box8Params.loading = true
try {
let { year, domainId, proposeName } = box8Params;
const res = await getDecreeKeyInstruction({
......@@ -972,6 +977,7 @@ const handleGetDecreeKeyInstruction = async () => {
} catch (error) {
console.error("政令重点条款error", error);
}
box8Params.loading = false
};
// 资源库
......@@ -1197,12 +1203,16 @@ const handleSearch = () => {
// 关键机构
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 {
const res = await getKeyOrganization();
const res = await getKeyOrganization({day});
console.log("关键机构", res);
if (res.code === 200) {
keyOrganizationList.value = res.data.map(item => ({ orgName:item.orgName, orgId:item.id }));
keyOrganizationList.value = res.data;
}
} catch (error) { }
}
......@@ -1423,15 +1433,13 @@ onMounted(async () => {
.home-main-header-item-box {
margin: 20px 0 64px;
width: 1600px;
display: flex;
flex-wrap: wrap;
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 80px;
gap: 16px;
font-family: Microsoft YaHei;
.organization-item {
width: 20%;
flex: auto;
height: 80px;
padding: 0 16px;
display: flex;
box-sizing: border-box;
......@@ -2194,6 +2202,7 @@ onMounted(async () => {
.box5-main {
flex: auto;
height: 20px;
padding-top: 16px;
.box5-chart {
height: 100%;
}
......
......@@ -170,7 +170,6 @@ const onWordWrap = (word, num) => {
}
const handleClickDecree = decree => {
window.sessionStorage.setItem("curTabName", decree.name);
const route = router.resolve({
path: "/decreeLayout",
query: {
......@@ -184,101 +183,101 @@ const handleClickDecree = decree => {
const graphContainer = ref(null);
let graph = null;
const onRelationChart = () => {
const container = graphContainer.value;
const nodeWidth = 180;
const width = container.clientWidth;
const height = container.clientHeight;
const centerX = width / 2
const centerY = height / 2 - 40
const leftNodeX = centerX - nodeWidth;
const rightNodeX = centerX + nodeWidth;
const container = graphContainer.value;
const nodeWidth = 180;
const width = container.clientWidth;
const height = container.clientHeight;
const centerX = width / 2
const centerY = height / 2 - 40
const leftNodeX = centerX - nodeWidth;
const rightNodeX = centerX + nodeWidth;
const data = {
nodes: [
{
id: mainInfo.value.id+'', label: `${mainInfo.value.time}\n${onWordWrap(mainInfo.value.label, 15)}`,
size: [250, 80], x: leftNodeX, y: centerY,
const data = {
nodes: [
{
id: mainInfo.value.id+'', label: `${mainInfo.value.time}\n${onWordWrap(mainInfo.value.label, 15)}`,
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],
},
{
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,
labelCfg: {
style: {
stroke: ["", "#B9DCFF", "#87E8DE", "#FFCCC7"][1],
},
labelCfg: {
style: {
fill: ["", "#055fc2", "#13A8A8", "#CE4F51"][1],
background: {
fill: ["", "#eef7ff", "#E6FFFB", "#FFE0E0"][1],
}
fill: ["", "#055fc2", "#13A8A8", "#CE4F51"][1],
background: {
fill: ["", "#eef7ff", "#E6FFFB", "#FFE0E0"][1],
}
}
}
]
};
}
]
};
// 创建图实例
if (!graph) {
graph = new G6.Graph({
container: container,
width,
height,
defaultNode: {
type: 'rect',
anchorPoints: [[0, 0.5], [1, 0.5]],
style: {
// 创建图实例
if (!graph) {
graph = new G6.Graph({
container: container,
width,
height,
defaultNode: {
type: 'rect',
anchorPoints: [[0, 0.5], [1, 0.5]],
style: {
cursor: "pointer",
radius: 4,
fill: '#f6faff',
stroke: '#B9DCFF',
},
labelCfg: {
style: {
cursor: "pointer",
radius: 4,
fill: '#f6faff',
stroke: '#B9DCFF',
},
labelCfg: {
style: {
cursor: "pointer",
fill: "#333333",
fontSize: 15,
fontWeight: "bold",
fontFamily: "Microsoft YaHei",
textAlign: "center",
}
fill: "#333333",
fontSize: 15,
fontWeight: "bold",
fontFamily: "Microsoft YaHei",
textAlign: "center",
}
}
},
defaultEdge: {
type: "line",
style: {
lineWidth: 1,
endArrow: true,
},
defaultEdge: {
type: "line",
labelCfg: {
autoRotate: true,
style: {
lineWidth: 1,
endArrow: true,
},
labelCfg: {
autoRotate: true,
style: {
fontSize: 14,
fontFamily: 'Microsoft YaHei',
background: {
padding: [6, 4, 4, 4],
}
fontSize: 14,
fontFamily: 'Microsoft YaHei',
background: {
padding: [6, 4, 4, 4],
}
}
},
layout: null,
modes: { default: [] },
fitView: false,
});
}
},
layout: null,
modes: { default: [] },
fitView: false,
});
graph.on('node:click', (evt) => {
let node = siderList.value.find(item => item.id==evt.item._cfg.model.id)
if (node) handleClickDecree(node)
});
}
graph.on('node:click', (evt) => {
let node = siderList.value.find(item => item.id==evt.item._cfg.model.id)
if (node) handleClickDecree(node)
});
}
// 加载数据并渲染
graph.data(data);
graph.render();
// 加载数据并渲染
graph.data(data);
graph.render();
}
onMounted(() => {
......
......@@ -75,7 +75,7 @@
</div>
</template>
<div class="box2-main">
<AiTips :tips="tips" />
<!-- <AiTips :tips="tips" /> -->
<div class="graph-box" v-if="contentType==1">
<ChartChain :listData="fishbone.list" :baseData="fishbone.base" />
</div>
......
......@@ -46,7 +46,7 @@
<div class="custom-collapse-title">
<div class="custom-collapse-index">{{ index + 1 }}</div>
<div class="custom-collapse-name one-line-ellipsis">
<span class="text-click-hover" @click.stop="handleClickDecree(item)">{{ item.title }}</span>
<span class="text-click-hover" @click.stop="handleClickBull(item)">{{ item.title }}</span>
</div>
</div>
</template>
......@@ -73,9 +73,9 @@
<div class="time-line-icon">
<img style="width: 100%; height: 100%;" :src="item.orgImage || DefaultIcon1" alt="">
</div>
<div class="time-line-name" @click="handleToInstitution(item)">{{ item.proposeOrgName }}</div>
<div class="time-line-name text-click-hover" @click="handleToInstitution(item)">{{ item.proposeOrgName }}</div>
</div>
<div class="timeline-content">{{ item.describe }}</div>
<div class="timeline-content" @click="handleClickDecree(item)">{{ item.describe }}</div>
</div>
</el-timeline-item>
</el-timeline>
......@@ -167,15 +167,24 @@ const handleGetPrev = async () => {
};
// 跳转行政机构主页
const handleToInstitution = item => {
window.sessionStorage.setItem("curTabName", item.proposeOrgName);
const curRoute = router.resolve({
path: "/institution",
query: {
id: item.id
id: item.orgId
}
});
window.open(curRoute.href, "_blank");
};
// 跳转科技政令详情页
const handleClickDecree = item => {
const route = router.resolve({
path: "/decreeLayout",
query: {
id: item.id
}
});
window.open(route.href, "_blank");
};
// 法律依据
const dependList = ref([]);
......@@ -186,7 +195,7 @@ const handleGetLaws = async () => {
console.log("法律依据", res);
if (res.code === 200 && res.data) {
dependList.value = res.data;
dependActive.value = res.data.map(item => item.billId);
// dependActive.value = res.data.map(item => item.billId);
} else {
dependList.value = [];
}
......@@ -196,7 +205,7 @@ const handleGetLaws = async () => {
}
};
// 跳转科技法案详情页
const handleClickDecree = decree => {
const handleClickBull = decree => {
window.sessionStorage.setItem("billId", decree.billId);
window.sessionStorage.setItem("curTabName", decree.title);
const route = router.resolve({
......@@ -414,7 +423,6 @@ onMounted(() => {
.time-line-name {
color: rgba(59, 65, 75, 1);
font-size: 15px;
cursor: pointer;
}
}
......@@ -445,6 +453,7 @@ onMounted(() => {
font-size: 16px;
font-weight: 400;
line-height: 26px;
cursor: pointer;
}
}
}
......
......@@ -83,7 +83,7 @@
<img :src="item.imageUrl || DefaultIconNews" alt="" />
</div>
<div class="item-center">
<div class="bubble-header" @click="handleClickToNewsDetail(item)">
<div class="bubble-header" @click="gotoNewsDetail(item.id)">
<span class="name">{{ item.sjbt }}</span>
<span class="meta">{{ item.sjsj }} · {{ item.source }}</span>
</div>
......@@ -177,6 +177,9 @@ import DefaultIcon1 from "@/assets/icons/default-icon1.png";
import DefaultIcon2 from "@/assets/icons/default-icon2.png";
import DefaultIconNews from "@/assets/icons/default-icon-news.png";
import { useGotoNewsDetail } from '@/router/modules/news';
const gotoNewsDetail = useGotoNewsDetail()
const route = useRoute();
const decreeId = ref(route.query.id);
......@@ -235,7 +238,7 @@ handleGetBasicInfo();
const wordCloudData = ref([])
const onKeyWordUp = async () => {
try {
const res = await getKeyWordUp();
const res = await getKeyWordUp({id: decreeId.value});
console.log("政令关键词云", res);
wordCloudData.value = res.data.slice(0, 10).map(item => ({name: item.name, value: item.count}));
} catch (error) {
......@@ -271,15 +274,6 @@ const handleGetRelateEvents = async () => {
console.error("获取相关事件数据失败", error);
}
};
const handleClickToNewsDetail = news => {
const route = router.resolve({
path: "/newsAnalysis",
query: {
newsId: news.id
}
});
window.open(route.href, "_blank");
};
// 发布机构
const box3TopTopData = ref({
......@@ -476,7 +470,6 @@ onMounted(() => {
font-weight: 400;
font-size: 16px;
line-height: 30px;
min-height: 300px;
}
}
......
......@@ -3,6 +3,14 @@
<div class="page-left">
<div class="box1">
<AnalysisBox title="主要指令" :showAllBtn="false">
<template #header-btn>
<div class="mind-bnt" @click="onDecreeMindMap()">
<div class="mind-icon">
<img src="./assets/images/edit-line.png" alt="">
</div>
<div class="mind-text">思维导图</div>
</div>
</template>
<div class="analysis-box">
<div class="analysis-top">
<el-select v-model="areaType" :empty-values="[null, undefined]" @change="onMainContentData()" style="width: 200px;">
......@@ -92,7 +100,7 @@
</div>
</div>
</div>
<div class="organization-button">
<div class="organization-button" @click="handleClickToolBox()">
<div class="button-text">查看政令执行情况</div>
<el-icon size="16"><Right /></el-icon>
</div>
......@@ -114,16 +122,29 @@
</AnalysisBox>
</div>
</div>
<el-dialog v-model="isTreeDialog" width="1540px" top="8vh" class="viewpoint-dialog" destroy-on-close>
<template #header>
<div class="viewpoint-header">
<div class="viewpoint-title">政令举措思维导图</div>
</div>
</template>
<div class="viewpoint-body">
<MindGraph ref="refMindGraph"></MindGraph>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted, reactive } from "vue";
import { useRoute } from "vue-router";
import { ElMessage } from "element-plus";
import router from "@/router";
import { Search } from '@element-plus/icons-vue'
import MindGraph from "@/views/decree/com/MindGraph.vue"
import { getDecreeOrganization } from "@/api/decree/introduction";
import { getDecreeRelatedEntity, getDecreeMainContent } from "@/api/decree/background";
import { getDecreeRelatedEntity, getDecreeMainContent, getDecreeMindMap } from "@/api/decree/background";
import { getDecreehylyList } from "@/api/decree/home";
import ActionButton from '@/components/base/ActionButton/index.vue'
import DefaultIcon1 from "@/assets/icons/default-icon1.png";
......@@ -132,6 +153,10 @@ import defaultCom from "@/views/coopRestriction/assets/images/default-icon2.png"
const route = useRoute();
const handleClickToolBox = () => {
ElMessage.warning("当前功能正在开发中,敬请期待!");
};
// 科技领域
const areaType = ref("");
const areaList = ref([]);
......@@ -272,6 +297,50 @@ const simpleNumToChinese = (num) => {
}
}
// 思维导图
const isTreeDialog = ref(false);
const refMindGraph = ref(null);
const onDecreeMindMap = async () => {
let labelCfg = {
position: 'left',
offset: -20,
style: {
fontWeight: "normal",
fontSize: 16,
textAlign: 'left',
autoWrap: true
}
}
isTreeDialog.value = true;
try {
let res = await getDecreeMindMap({id: route.query.id});
console.log("思维导图", res);
if (res.code === 200) {
let nodes = []
let edges = []
Object.keys(res.data||{}).forEach((label, count) => {
nodes.push({ id:`node-${count}`, label })
})
Object.values(res.data||{}).forEach((list, count) => {
list.forEach((item, index) => {
if (item.execAgent?.length) {
nodes.push({ id:`node-${count}-${index}`, label:item.textZh, maxWidth:600, labelCfg })
item.execAgent.forEach((label, num) => {
nodes.push({ id:`node-${count}-${index}-${num}`, label })
edges.push({ id:`edge1-${count}-${index}-${num}`, source:`node-${count}-${index}-${num}`, target:`node-${count}-${index}` })
edges.push({ id:`edge2-${count}-${index}-${num}`, source:`node-${count}`, target:`node-${count}-${index}-${num}` })
})
}
})
})
setTimeout(() => { refMindGraph.value.onMindGraphData(nodes, edges) }, 100)
}
} catch (error) {
console.error("获取思维导图数据失败:", error);
}
}
// 相关实体
const entityList = ref([]);
const onRelatedEntityData = async () => {
......@@ -359,6 +428,31 @@ onMounted(() => {
flex: auto;
.box1 {
.mind-bnt {
background-color: var(--color-primary-10);
height: 28px;
border-radius: 14px;
display: flex;
align-items: center;
padding: 0 16px;
cursor: pointer;
.mind-icon {
width: 16px;
height: 13px;
font-size: 0px;
img {
width: 100%;
height: 100%;
}
}
.mind-text {
color: var(--color-primary-100);
font-family: Source Han Sans CN;
font-size: 16px;
line-height: 16px;
margin-left: 6px;
}
}
.analysis-box {
display: flex;
......@@ -776,5 +870,42 @@ onMounted(() => {
}
}
}
// 修改element-plus弹出框样式
:deep(.viewpoint-dialog) {
padding: 0;
border-radius: 4px;
.el-dialog__body {
padding: 0;
}
.el-dialog__header {
padding: 0;
margin: 0;
position: relative;
height: 48px;
}
.el-dialog__headerbtn {
top: 50%;
transform: translateY(-50%);
right: 12px;
}
.viewpoint-header {
height: 48px;
display: flex;
align-items: center;
padding: 0 24px;
border-bottom: 1px solid rgb(234, 236, 238);
}
.viewpoint-title {
font-size: 16px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 24px;
}
.viewpoint-body {
padding: 16px;
height: 77vh;
}
}
}
</style>
\ No newline at end of file
......@@ -187,11 +187,11 @@ const handleMouseLeave = () => {
isInChart.value = false;
};
const handleNodeClick = (node) => {
const handleNodeClick = node => {
selectedNode.value = node;
};
const handleLayoutChange = (type) => {
const handleLayoutChange = type => {
controlActive.value = type;
if (type !== 2) {
isInChart.value = true;
......@@ -201,9 +201,8 @@ const handleLayoutChange = (type) => {
};
const updateGraphData = () => {
const data = rightActiveTab.value === 'supplyChain'
? singleSanctionEntitySupplyChainData.value
: singleSanctionEntityEquityData.value;
const data =
rightActiveTab.value === "supplyChain" ? singleSanctionEntitySupplyChainData.value : singleSanctionEntityEquityData.value;
if (!data) return;
......@@ -231,7 +230,7 @@ const updateGraphData = () => {
links.push({
source: `p-${item.id || index}`,
target: "0",
name: rightActiveTab.value === 'supplyChain' ? "供应商" : (item.type || "持股")
name: rightActiveTab.value === "supplyChain" ? "供应商" : item.type || "持股"
});
});
......@@ -248,14 +247,14 @@ const updateGraphData = () => {
links.push({
source: "0",
target: `c-${item.id || index}`,
name: rightActiveTab.value === 'supplyChain' ? "客户" : (item.type || "投资")
name: rightActiveTab.value === "supplyChain" ? "客户" : item.description || "投资"
});
});
graphData.value = { nodes, links };
};
const updateTreeData = (data) => {
const updateTreeData = data => {
if (!data) return;
treeData.value = {
......@@ -336,17 +335,17 @@ const getSingleSanctionEntityListRequest = async () => {
}
};
watch(rightActiveTab, async (newTab) => {
if (newTab === 'supplyChain') {
watch(rightActiveTab, async newTab => {
if (newTab === "supplyChain") {
await getSingleSanctionEntitySupplyChainRequest();
} else {
await getSingleSanctionEntityEquityRequest();
}
});
watch(activeEntityId, async (newId) => {
watch(activeEntityId, async newId => {
if (newId) {
if (rightActiveTab.value === 'supplyChain') {
if (rightActiveTab.value === "supplyChain") {
await getSingleSanctionEntitySupplyChainRequest();
} else {
await getSingleSanctionEntityEquityRequest();
......@@ -355,7 +354,7 @@ watch(activeEntityId, async (newId) => {
});
watch(is50PercentRule, async () => {
if (rightActiveTab.value === 'equity') {
if (rightActiveTab.value === "equity") {
await getSingleSanctionEntityEquityRequest();
}
});
......
......@@ -71,21 +71,19 @@
v-for="item in listData"
:key="item.id"
class="resource-card"
@click="goToInstitution(item.id)"
>
<div class="card-logo">
<img :src="item.logoUrl || defaultLogo" :alt="item.name" />
<img :src="item.logo || defaultLogo" :alt="item.name" />
</div>
<div class="card-name">{{ item.name }}</div>
<div class="card-location">{{ item.address }}</div>
<div class="card-location">{{ item.location }}</div>
<div class="card-majors">{{ item.majors }}</div>
<div class="card-tags">
<span
v-for="tag in item.tags"
:key="tag"
class="tag"
>
{{ tag }}
</span>
<AreaTag v-for="tag in item.tags"
:key="tag" :tagName="tag" />
</div>
</div>
</template>
......@@ -134,7 +132,10 @@
import { ref, computed, onMounted, watch } from 'vue'
import { getIndustryKeyList } from '@/api/bill/billHome.js'
import { getSubjectList } from '@/api/characterPage/characterPage.js'
import AreaTag from '@/components/base/AreaTag/index.vue'
import { useRouter } from 'vue-router'
const router = useRouter()
// Props
const props = defineProps<{
initialTab?: string
......@@ -142,7 +143,9 @@ const props = defineProps<{
// 默认logo
const defaultLogo = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj48cmVjdCB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIGZpbGw9IiNhMzE4MWIiIHJ4PSI0Ii8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTggOGgxNnYxNkg4eiIvPjwvc3ZnPg=='
const goOrigin=(id)=>{
router.push(`/InnovativeInstitutions/${id}`)
}
// 标签配置 - 添加 subjectTypeId
const tabs = ref([
{ label: '大学', value: 'university', subjectTypeId: 1 },
......@@ -165,7 +168,17 @@ const fields = ref<{ label: string; value: string }[]>([
{ label: '全部领域', value: 'all' }
])
const fieldsLoading = ref(false)
const goToInstitution = (id) => {
router.push({
name: 'InnovativeInstitutions',
params: {
id: id,
type: 1
}
})
}
// 获取科技领域选项
async function fetchFieldOptions() {
fieldsLoading.value = true
......@@ -273,8 +286,7 @@ function goToPage(page: number | string) {
fetchData()
}
}
// 模拟API获取数据
async function fetchData() {
loading.value = true
try {
......@@ -299,12 +311,12 @@ async function fetchData() {
if (res.code === 200 && res.data) {
// 映射 API 返回的数据到卡片显示格式
const mappedData = (res.data.content || []).map((item: any) => ({
id: item.id,
id: item.orgId,
name: item.orgName,
location: item.countryName ? `${item.countryName}·${item.provinceName}` : item.provinceName || '--',
location: item.address || '--',
majors: item.fieldList?.join('、') || '--',
tags: item.areaList?.map((a: any) => a.areaName) || [],
logo: defaultLogo
tags: item.taglist || [],
logo: item.logoUrl
}))
listData.value = mappedData
......
......@@ -70,13 +70,21 @@
</template>
<script setup>
import { ref } from 'vue'
import { ref, computed, provide } from 'vue'
import { useRoute } from 'vue-router'
import AreaTag from '@/components/base/AreaTag/index.vue'
import SchoolDetail from './tabs/SchoolDetail.vue'
import ResearchStrength from './tabs/ResearchStrength.vue'
import Cooperation from './tabs/Cooperation.vue'
import OtherInfo from './tabs/OtherInfo.vue'
// 从路由获取 orgId
const route = useRoute()
const orgId = computed(() => route.params.id || '')
// 提供给子组件使用
provide('orgId', orgId)
// 大学基本信息
const universityInfo = ref({
name: '哈佛大学',
......
......@@ -42,100 +42,127 @@
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, computed, onMounted, watch } 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'
import { getLabList, getPolicyList } from '@/api/innovationSubject/overview.js'
const props = defineProps<{
orgId?: string
}>()
const areaTypeMap: Record<string, string> = {
'人工智能': 'tag1',
'生物科技': 'tag2',
'太空': 'tag3',
'航空航天': 'tag4',
'先进制造': 'tag5',
'物理学': 'tag6',
'海洋': 'tag7',
'新能源': 'tag8',
'医学': 'tag9',
'化学': 'tag10',
'新一代通信网络': 'tag11',
'其他': 'tag12'
}
// 重点实验室数据
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 labsData = ref<Array<{
logo: string
name: string
description: string
tags: Array<{ name: string; type: string }>
}>>([])
const policyData = ref<Array<{
title: string
description: string
}>>([])
const currentPage = ref(1)
const pageSize = ref(6)
const totalCount = ref(0)
const totalPages = ref(0)
const loading = ref(false)
const fetchLabList = async () => {
const orgId = props.orgId || '6789'
try {
const result = await getLabList(orgId)
if (result.success && result.data) {
labsData.value = result.data.map((item: any) => ({
logo: item.logoUrl || '/images/lab-default.png',
name: item.labName,
description: item.introduction,
tags: (item.arealist || []).map((area: string) => ({
name: area,
type: areaTypeMap[area] || 'tag12'
}))
}))
}
} catch (error) {
console.error('获取实验室列表失败:', error)
}
])
// 政策文件数据
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 fetchPolicyList = async () => {
const orgId = props.orgId || '6789'
loading.value = true
try {
const result = await getPolicyList(orgId, currentPage.value, pageSize.value)
if (result.success && result.data) {
policyData.value = result.data.content.map((item: any) => ({
title: `《${item.name}》`,
description: item.introduction
}))
totalCount.value = result.data.totalElements
totalPages.value = result.data.totalPages
}
} catch (error) {
console.error('获取政策文件列表失败:', error)
} finally {
loading.value = false
}
}
watch(currentPage, () => {
fetchPolicyList()
})
onMounted(() => {
fetchLabList()
fetchPolicyList()
})
const displayPages = computed(() => {
const pages: (number | string)[] = []
if (totalPages.value <= 7) {
for (let i = 1; i <= totalPages.value; i++) pages.push(i)
const total = totalPages.value
if (total <= 7) {
for (let i = 1; i <= total; 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)
const end = Math.min(total - 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)
if (currentPage.value < total - 2) pages.push('...')
pages.push(total)
}
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>
......
......@@ -10,22 +10,18 @@
</Teleport>
<!-- 第一行 -->
<div class="chart-row">
<!-- 专利数量统计 - 柱状图 -->
<!-- 专利数量统计 - 柱状图 -->
<AnalysisBox title="专利数量统计" width="100%" height="500px" :show-all-btn="false" class="chart-box">
<div class="chart-container">
<div class="bar-chart">
<div class="y-axis">
<span>400</span>
<span>300</span>
<span>200</span>
<span>100</span>
<span>0</span>
<span v-for="tick in patentYAxisTicks" :key="tick">{{ tick }}</span>
</div>
<div class="chart-area">
<div class="bars">
<div v-for="(item, index) in patentData" :key="index" class="bar-item">
<span class="bar-label">{{ item.year }}</span>
<div class="bar" :style="{ height: `${item.value / 400 * 100}%`, background: getPatentBarGradient() }"></div>
<div class="bar" :style="{ height: `${item.value / patentMaxValue * 100}%`, background: getPatentBarGradient() }"></div>
</div>
</div>
</div>
......@@ -34,16 +30,12 @@
<ChartSummary text="哈佛大学近十年专利数量呈现稳定增长趋势,尤其在生物技术和人工智能领域表现突出,2025年达到历史新高。" />
</AnalysisBox>
<!-- 论文数量统计 - 面积图 -->
<!-- 论文数量统计 - 面积图 -->
<AnalysisBox title="论文数量统计" width="100%" height="500px" :show-all-btn="false" class="chart-box">
<div class="chart-container">
<div class="area-chart">
<div class="y-axis">
<span>400</span>
<span>300</span>
<span>200</span>
<span>100</span>
<span>0</span>
<span v-for="tick in paperYAxisTicks" :key="tick">{{ tick }}</span>
</div>
<div class="chart-area">
<svg class="area-svg" viewBox="0 0 700 240" preserveAspectRatio="none">
......@@ -61,12 +53,12 @@
:cx="point.x" :cy="point.y" r="4"
fill="#FFFFFF" stroke="#FF954D" stroke-width="2"
class="data-point"
@mouseenter="showTooltip($event, chartYears[idx], paperData[idx])"
@mouseenter="showTooltip($event, paperYears[idx], paperData[idx]?.value)"
@mouseleave="hideTooltip" />
</g>
</svg>
<div class="x-labels">
<span v-for="year in chartYears" :key="year">{{ year }}</span>
<span v-for="year in paperYears" :key="year">{{ year }}</span>
</div>
</div>
</div>
......@@ -116,16 +108,12 @@
<ChartSummary text="哈佛大学在生物科技和人工智能领域实力最为突出,同时在量子科技和能源领域也有显著优势,体现了其跨学科研究能力。" />
</AnalysisBox>
<!-- 大学经费增长情况 - 面积图 -->
<!-- 大学经费增长情况 - 面积图 -->
<AnalysisBox title="大学经费增长情况" width="100%" height="500px" :show-all-btn="false" class="chart-box">
<div class="chart-container">
<div class="area-chart">
<div class="y-axis">
<span>400</span>
<span>300</span>
<span>200</span>
<span>100</span>
<span>0</span>
<span v-for="tick in fundingYAxisTicks" :key="tick">{{ tick }}</span>
</div>
<div class="chart-area">
<svg class="area-svg" viewBox="0 0 700 240" preserveAspectRatio="none">
......@@ -143,12 +131,12 @@
:cx="point.x" :cy="point.y" r="4"
fill="#FFFFFF" stroke="#22C55E" stroke-width="2"
class="data-point"
@mouseenter="showTooltip($event, chartYears[idx], fundingData[idx])"
@mouseenter="showTooltip($event, fundingYears[idx], fundingData[idx]?.value)"
@mouseleave="hideTooltip" />
</g>
</svg>
<div class="x-labels">
<span v-for="year in chartYears" :key="year">{{ year }}</span>
<span v-for="year in fundingYears" :key="year">{{ year }}</span>
</div>
</div>
</div>
......@@ -207,9 +195,13 @@
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed, onMounted, inject, watch } from 'vue'
import AnalysisBox from '@/components/base/boxBackground/analysisBox.vue'
import ChartSummary from '../components/ChartSummary.vue'
import { getPatentList, getPaperList, getStudyFieldList, getFundGrowth } from '@/api/innovationSubject/overview.js'
// 从父组件注入 orgId
const orgId = inject('orgId', ref(''))
// Tooltip 状态
const tooltipVisible = ref(false)
......@@ -227,40 +219,86 @@ const hideTooltip = () => {
}
// 专利数据 - 柱状图
const patentData = [
{ year: '2016', value: 80 },
{ year: '2017', value: 90 },
{ year: '2018', value: 120 },
{ year: '2019', value: 180 },
{ year: '2020', value: 220 },
{ year: '2021', value: 250 },
{ year: '2022', value: 280 },
{ year: '2023', value: 300 },
{ year: '2024', value: 340 },
{ year: '2025', value: 360 }
]
// 图表年份
const chartYears = ['2016', '2017', '2018', '2019', '2020', '2021', '2022', '2023', '2024', '2025']
const patentData = ref([])
// 论文数据
const paperData = [120, 140, 150, 160, 180, 200, 220, 280, 350, 400]
const paperData = ref([])
// 图表年份(从论文数据中提取)
const chartYears = computed(() => paperData.value.map(item => String(item.year)))
// 经费数据
const fundingData = [80, 100, 120, 150, 180, 220, 260, 300, 340, 380]
const fundingData = ref([])
// 领域实力分布数据
const studyFieldData = ref([])
// 加载数据
const loadData = async () => {
if (!orgId.value) return
try {
// 并行请求所有数据
const [patentRes, paperRes, fieldRes, fundRes] = await Promise.all([
getPatentList(orgId.value),
getPaperList(orgId.value),
getStudyFieldList(orgId.value),
getFundGrowth(orgId.value)
])
if (patentRes.success && patentRes.data) {
patentData.value = patentRes.data.map(item => ({
year: String(item.year),
value: item.countNum
}))
}
if (paperRes.success && paperRes.data) {
paperData.value = paperRes.data.map(item => ({
year: item.year,
value: item.countNum
}))
}
if (fieldRes.success && fieldRes.data) {
studyFieldData.value = fieldRes.data
}
if (fundRes.success && fundRes.data) {
fundingData.value = fundRes.data.map(item => ({
year: item.year,
value: item.fundAmount
}))
}
} catch (error) {
console.error('加载科研实力数据失败:', error)
}
}
// 监听 orgId 变化时重新加载数据
watch(orgId, (newVal) => {
if (newVal) {
loadData()
}
}, { immediate: true })
// 计算面积图路径
const createAreaPath = (data, maxValue = 400) => {
const createAreaPath = (data, maxValue) => {
if (!data || data.length === 0) return { linePath: '', areaPath: '' }
const width = 700
const height = 240
const step = width / (data.length - 1)
const step = data.length > 1 ? width / (data.length - 1) : width
const max = maxValue || Math.max(...data.map(d => d.value || d)) * 1.2 || 400
let linePath = `M 0 ${height - (data[0] / maxValue) * height}`
let areaPath = `M 0 ${height} L 0 ${height - (data[0] / maxValue) * height}`
const getValue = (item) => typeof item === 'object' ? item.value : item
data.forEach((value, index) => {
let linePath = `M 0 ${height - (getValue(data[0]) / max) * height}`
let areaPath = `M 0 ${height} L 0 ${height - (getValue(data[0]) / max) * height}`
data.forEach((item, index) => {
const x = index * step
const y = height - (value / maxValue) * height
const y = height - (getValue(item) / max) * height
linePath += ` L ${x} ${y}`
areaPath += ` L ${x} ${y}`
})
......@@ -270,22 +308,61 @@ const createAreaPath = (data, maxValue = 400) => {
return { linePath, areaPath }
}
const { linePath: paperLinePath, areaPath: paperAreaPath } = createAreaPath(paperData)
const { linePath: fundingLinePath, areaPath: fundingAreaPath } = createAreaPath(fundingData)
// 论文图表路径
const paperLinePath = computed(() => createAreaPath(paperData.value).linePath)
const paperAreaPath = computed(() => createAreaPath(paperData.value).areaPath)
// 经费图表路径
const fundingLinePath = computed(() => createAreaPath(fundingData.value).linePath)
const fundingAreaPath = computed(() => createAreaPath(fundingData.value).areaPath)
// 计算节点坐标
const getDataPoints = (data, maxValue = 400) => {
const getDataPoints = (data, maxValue) => {
if (!data || data.length === 0) return []
const width = 700
const height = 240
const step = width / (data.length - 1)
return data.map((value, index) => ({
const step = data.length > 1 ? width / (data.length - 1) : width
const max = maxValue || Math.max(...data.map(d => d.value || d)) * 1.2 || 400
const getValue = (item) => typeof item === 'object' ? item.value : item
return data.map((item, index) => ({
x: index * step,
y: height - (value / maxValue) * height
y: height - (getValue(item) / max) * height
}))
}
const paperPoints = computed(() => getDataPoints(paperData))
const fundingPoints = computed(() => getDataPoints(fundingData))
const paperPoints = computed(() => getDataPoints(paperData.value))
const fundingPoints = computed(() => getDataPoints(fundingData.value))
// 论文/经费年份
const paperYears = computed(() => paperData.value.map(item => String(item.year)))
const fundingYears = computed(() => fundingData.value.map(item => String(item.year)))
// 计算Y轴刻度
const calculateYAxisTicks = (data, valueKey = 'value') => {
if (!data || data.length === 0) return [0, 100, 200, 300, 400].reverse()
const values = data.map(d => d[valueKey] || d)
const max = Math.max(...values)
const niceMax = Math.ceil(max / 100) * 100 || 400
const step = niceMax / 4
return [niceMax, niceMax - step, niceMax - step * 2, niceMax - step * 3, 0]
}
// 专利Y轴刻度和最大值
const patentMaxValue = computed(() => {
if (!patentData.value || patentData.value.length === 0) return 400
const max = Math.max(...patentData.value.map(d => d.value))
return Math.ceil(max / 100) * 100 || 400
})
const patentYAxisTicks = computed(() => calculateYAxisTicks(patentData.value))
// 论文Y轴刻度
const paperYAxisTicks = computed(() => calculateYAxisTicks(paperData.value))
// 经费Y轴刻度
const fundingYAxisTicks = computed(() => calculateYAxisTicks(fundingData.value))
// 雷达图数据和计算
const radarValues = [0.7, 0.9, 0.85, 0.6, 0.75, 0.65] // 各领域
......@@ -644,6 +721,7 @@ const getPatentBarGradient = () => {
}
}
}
// 图表底部摘要
</style>
......@@ -53,8 +53,9 @@ export default defineConfig({
rewrite: (path) => path.replace(/^\/reportData/, '')
},
'/api': {
'/api': {
target: 'http://8.140.26.4:9085/',
// target: 'http://192.168.0.4:28080/',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论