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

刘宇琪 法案预测

上级 247def06
...@@ -335,14 +335,18 @@ export function getProgressPrediction(billId) { ...@@ -335,14 +335,18 @@ export function getProgressPrediction(billId) {
* @returns {Promise<Object>} 相似法案列表 * @returns {Promise<Object>} 相似法案列表
*/ */
export function getSimiBills(params = {}) { export function getSimiBills(params = {}) {
// domains 如果是数组则用逗号拼接
const domains = Array.isArray(params.domains)
? params.domains.join(',')
: params.domains
return request('/api/BillProgressPrediction/simiBills', { return request('/api/BillProgressPrediction/simiBills', {
method: 'GET', method: 'GET',
params: { params: {
billIds: params.billIds, billIds: params.billIds,
domains: params.domains, domains: domains,
patternType: params.patternType , patternType: params.patternType,
proposalType: params.proposalType , proposalType: params.proposalType
...params
} }
}) })
} }
...@@ -353,80 +357,51 @@ export function getSimiBills(params = {}) { ...@@ -353,80 +357,51 @@ export function getSimiBills(params = {}) {
* @returns {Object} 转换后的统计数据和法案列表 * @returns {Object} 转换后的统计数据和法案列表
*/ */
export function transformSimiBillsData(apiData) { export function transformSimiBillsData(apiData) {
if (!apiData || !apiData.data || !Array.isArray(apiData.data)) { if (!apiData || !apiData.data) {
return { stats: null, bills: [] } return { stats: null, bills: [] }
} }
const bills = apiData.data const data = apiData.data
const simiBills = data.simi_bills || []
// 计算统计数据 // 直接使用 API 返回的统计数据
let becameLaw = 0
let notPassedOrShelved = 0
let totalDays = 0
let completedBills = 0
bills.forEach(bill => {
const actions = bill.bill_actions || []
const hasBecameLaw = actions.some(a =>
a.action_type === 'BecameLaw' ||
a.action_type === 'President' && a.action_desc?.includes('签署')
)
if (hasBecameLaw) {
becameLaw++
// 计算耗时
if (actions.length >= 2) {
const firstDate = new Date(actions[0].action_date)
const lastDate = new Date(actions[actions.length - 1].action_date)
const days = Math.ceil((lastDate - firstDate) / (1000 * 60 * 60 * 24))
if (days > 0) {
totalDays += days
completedBills++
}
}
} else {
notPassedOrShelved++
}
})
const medianDays = completedBills > 0 ? Math.round(totalDays / completedBills) : 223
const passRate = bills.length > 0 ? ((becameLaw / bills.length) * 100).toFixed(1) : '0'
const stats = { const stats = {
totalBills: bills.length, totalBills: data.count || simiBills.length,
becameLaw, becameLaw: data.become_law || 0,
notPassedOrShelved, notPassedOrShelved: (data.count || simiBills.length) - (data.become_law || 0),
medianDays, medianDays: data.become_law_avg_days || 0,
passRate passRate: data.become_law_prop ? (data.become_law_prop * 100).toFixed(1) : '0'
} }
// 转换法案列表格式 // 转换法案列表格式
const transformedBills = bills.map(bill => ({ const transformedBills = simiBills.map(bill => ({
id: bill.bill_id, id: bill.bill_id,
title: bill.bill_name || bill.bill_id, title: bill.bill_name || bill.bill_id,
proposalDate: extractProposalDate(bill.poli_pattern_desc), proposalDate: bill.proposed_date ? formatDate(bill.proposed_date) : '',
areas: bill.domain_name ? (Array.isArray(bill.domain_name) ? bill.domain_name : [bill.domain_name]) : [], areas: bill.bill_domain ? (Array.isArray(bill.bill_domain) ? bill.bill_domain : [bill.bill_domain]) : [],
proposer: extractProposer(bill.bill_sponsors), proposer: bill.key_sponsor_name || extractProposer(bill.bill_sponsors),
coProposers: bill.bill_proposal_desc || '', proposerParty: bill.key_sponsor_party || '',
coProposers: bill.co_sponsor_desc || bill.bill_proposal_desc || '',
proposalType: bill.bill_proposal_type || '',
governmentType: formatGovernmentType(bill.poli_pattern_type, bill.poli_pattern_desc), governmentType: formatGovernmentType(bill.poli_pattern_type, bill.poli_pattern_desc),
passDays: calculateTotalDays(bill.bill_actions), patternType: bill.poli_pattern_type || '',
billStatus: bill.bill_status || '',
passDays: bill.bill_action_days || calculateTotalDays(bill.bill_actions),
yearsDifference: bill.years_difference || 0,
billActions: bill.bill_actions || [],
billSponsors: bill.bill_sponsors || [],
selected: true // 默认全选 selected: true // 默认全选
})) }))
return { stats, bills: transformedBills } return { stats, bills: transformedBills }
} }
/** function formatDate(dateStr) {
* 从政治格局描述中提取提案时间 if (!dateStr) return ''
* @param {string} desc - 政治格局描述 const match = dateStr.match(/(\d{4})-(\d{2})-(\d{2})/)
* @returns {string} 提案时间
*/
function extractProposalDate(desc) {
if (!desc) return ''
const match = desc.match(/(\d{4})-(\d{2})-(\d{2})/)
if (match) { if (match) {
return `${match[1]}${parseInt(match[2])}${parseInt(match[3])}日` return `${match[1]}${parseInt(match[2])}${parseInt(match[3])}日`
} }
return '' return dateStr
} }
/** /**
* 从提案人列表中提取主提案人 * 从提案人列表中提取主提案人
...@@ -504,9 +479,9 @@ export function transformProposalInfo(apiData) { ...@@ -504,9 +479,9 @@ export function transformProposalInfo(apiData) {
return { return {
// 提案标题 // 提案标题
title: data.bill_title || 'H.R.1-大而美法案', title: data.bill_name_zh ,
// 提案时间 - 从政治格局描述中提取 // 提案时间 - 从政治格局描述中提取
date: extractDateFromDesc(data.poli_pattern_desc) || '2025年5月20日', date: extractDateFromDesc(data.poli_pattern_desc) ,
// 涉及领域 TAG - 使用 domain_name // 涉及领域 TAG - 使用 domain_name
areas: Array.isArray(data.domain_name) ? data.domain_name : (data.domain_name ? [data.domain_name] : []), areas: Array.isArray(data.domain_name) ? data.domain_name : (data.domain_name ? [data.domain_name] : []),
// 政策领域完整选项列表 - 使用 bill_domain(用于筛选下拉框) // 政策领域完整选项列表 - 使用 bill_domain(用于筛选下拉框)
......
...@@ -62,7 +62,6 @@ ...@@ -62,7 +62,6 @@
<div class="field-content"> <div class="field-content">
<el-select <el-select
v-model="localValues.oppositionProposer" v-model="localValues.oppositionProposer"
multiple
placeholder="请选择" placeholder="请选择"
style="width: 420px" style="width: 420px"
@change="handleChange" @change="handleChange"
...@@ -83,7 +82,6 @@ ...@@ -83,7 +82,6 @@
<div class="field-content"> <div class="field-content">
<el-select <el-select
v-model="localValues.proposalTime" v-model="localValues.proposalTime"
multiple
placeholder="请选择" placeholder="请选择"
style="width: 420px" style="width: 420px"
@change="handleChange" @change="handleChange"
...@@ -115,18 +113,18 @@ const props = defineProps<{ ...@@ -115,18 +113,18 @@ const props = defineProps<{
const localValues = ref({ const localValues = ref({
policyArea: [] as string[], policyArea: [] as string[],
governmentType: [] as string[], governmentType: [] as string[],
oppositionProposer: [] as string[], oppositionProposer: '' as string,
proposalTime: [] as string[], proposalTime: '' as string,
}) })
// 根据 proposalInfo 计算初始筛选值(即"设置为当前提案"的目标状态) // 根据 proposalInfo 计算初始筛选值(即"设置为当前提案"的目标状态)
function buildInitialValues(info?: ProposalInfo | null): Record<string, string[]> { function buildInitialValues(info?: ProposalInfo | null): Record<string, string[] | string> {
if (!info) return { policyArea: [], governmentType: [], oppositionProposer: [], proposalTime: [] } if (!info) return { policyArea: [], governmentType: [], oppositionProposer: '', proposalTime: '' }
return { return {
policyArea: info.defaultDomains?.length ? [...info.defaultDomains] : [...(info.areas || [])], policyArea: info.defaultDomains?.length ? [...info.defaultDomains] : [...(info.areas || [])],
governmentType: info.patternType ? [info.patternType] : [], governmentType: info.patternType ? [info.patternType] : [],
oppositionProposer: [], oppositionProposer: '',
proposalTime: [], proposalTime: '',
} }
} }
...@@ -144,8 +142,8 @@ function reset() { ...@@ -144,8 +142,8 @@ function reset() {
localValues.value = { localValues.value = {
policyArea: [], policyArea: [],
governmentType: [], governmentType: [],
oppositionProposer: [], oppositionProposer: '',
proposalTime: [], proposalTime: '',
} }
} }
......
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
<div v-if="phase" class="phase-card" :class="borderColorClass"> <div v-if="phase" class="phase-card" :class="borderColorClass">
<div class="phase-header flex-display-start"> <div class="phase-header flex-display-start">
<div> <div>
<h3 class="phase-title main-color text-title-2-bold">{{ phase.title }}</h3> <div class="phase-title main-color text-title-2-bold">{{ phase.title }}</div>
<p class="text-tip-2 text-primary-50-clor">{{ phase.description }}</p> <div class="text-tip-2 text-primary-50-clor">{{ phase.description }}</div>
</div> </div>
<div class="phase-status"> <div class="phase-status">
<span class="risk-badge" :class="riskColorClass">{{ riskLabel }}</span> <span class="risk-badge" :class="riskColorClass">{{ riskLabel }}</span>
...@@ -18,32 +18,41 @@ ...@@ -18,32 +18,41 @@
<p v-if="phase.riskLevel !== 'passed'" class="text-tip-2 text-primary-50-clor">{{ phase.estimatedDays }}</p> <p v-if="phase.riskLevel !== 'passed'" class="text-tip-2 text-primary-50-clor">{{ phase.estimatedDays }}</p>
</div> </div>
</div> </div>
<div style="display: flex;">
<div class="box-title-row"><img src="../assets/input.svg" />
<span class="text-compact-bold text-primary-80-clor" style="margin-left: 8px;">预测模型数据输入</span></div>
<div class="box-hint flex-display-center text-tip-2 text-primary-50-clor" style="margin-left: auto;">
<img src="../assets/importent.svg"/>
<span>此阶段预测基于以下多维特征</span>
</div>
</div>
<div class="model-inputs-box"> <div class="model-inputs-box">
<div class="box-header flex-display-start"> <div class="box-header flex-display-start">
<div class="box-title-row flex-display-center"> <div class="box-title-row flex-display-center">
<img src="../assets/input.svg" />
<span class="text-compact-bold">预测模型数据输入</span>
</div>
<div class="box-hint flex-display-center text-tip-2 text-primary-50-clor">
<img src="../assets/importent.svg"/>
<span>此阶段预测基于以下多维特征</span>
</div> </div>
</div> </div>
<div class="model-inputs"> <div class="model-inputs">
<p <div
v-for="(input, index) in phase.modelInputs" v-for="(input, index) in phase.modelInputs"
:key="index" :key="index"
class="text-tip-2 text-primary-65-clor" class="text-tip-2 text-primary-65-clor"
> >
{{ input }} {{ input }}
</p> </div>
</div> </div>
</div> </div>
<div v-if="phase.predictionBasis" class="facts-section"> <div v-if="phase.predictionBasis" class="facts-section">
<div class="box-header flex-display-start"> <div class="box-header flex-display-start">
<div class="box-title-row flex-display-center"> <div class="box-title-row flex-display-center">
<img src="../assets/icon1.svg"/> <img src="../assets/icon1.svg"/>
<span class="text-compact-bold">通过性预测依据</span> <span class="text-compact-bold text-primary-80-clor">通过性预测依据</span>
</div> </div>
<div class="box-hint flex-display-center text-tip-2 text-primary-50-clor"> <div class="box-hint flex-display-center text-tip-2 text-primary-50-clor">
<img src="../assets/importent.svg"/> <img src="../assets/importent.svg"/>
...@@ -51,9 +60,11 @@ ...@@ -51,9 +60,11 @@
</div> </div>
</div> </div>
<div class="prediction-basis-content"> <div class="prediction-basis-content">
<p class="text-tip-2 text-primary-65-clor">{{ phase.predictionBasis }}</p> <div class="text-tip-2 text-primary-65-clor">{{ phase.predictionBasis }}</div>
</div> </div>
</div> </div>
<!-- 底部虚线分隔 -->
<div class="phase-divider"></div>
</div> </div>
</template> </template>
...@@ -144,7 +155,8 @@ const riskLabel = computed(() => { ...@@ -144,7 +155,8 @@ const riskLabel = computed(() => {
<style scoped> <style scoped>
.phase-card { .phase-card {
padding-left: 24px; padding-left: 24px;
padding-bottom: 32px; /* padding-bottom: 16px; */
/* padding-top: 16px; */
} }
.border-primary { .border-primary {
...@@ -166,7 +178,7 @@ const riskLabel = computed(() => { ...@@ -166,7 +178,7 @@ const riskLabel = computed(() => {
.phase-header { .phase-header {
justify-content: space-between; justify-content: space-between;
align-items: flex-start; align-items: flex-start;
margin-bottom: 16px; /* margin-bottom: 16px; */
} }
.phase-header > div:first-child { .phase-header > div:first-child {
...@@ -288,4 +300,10 @@ const riskLabel = computed(() => { ...@@ -288,4 +300,10 @@ const riskLabel = computed(() => {
height: 16px; height: 16px;
color: var(--text-primary-65-color); color: var(--text-primary-65-color);
} }
/* 底部虚线分隔 */
.phase-divider {
margin-top: 16px;
border-bottom: 1px solid var(--bg-black-10, #e5e5e5);
}
</style> </style>
...@@ -125,22 +125,23 @@ async function loadData() { ...@@ -125,22 +125,23 @@ async function loadData() {
// 使用真实 API,传入 billIds、domains、patternType、proposalType // 使用真实 API,传入 billIds、domains、patternType、proposalType
const params = { const params = {
billIds: filterParams?.value.billIds, billIds: filterParams?.value.billIds,
domains:JSON.stringify(filterParams?.value.domains) || [], domains: filterParams?.value.domains || [],
patternType: filterParams?.value.patternType || '统一政府', patternType: filterParams?.value.patternType || '统一政府',
proposalType: filterParams?.value.proposalType || '两党共同提案' proposalType: filterParams?.value.proposalType || '两党共同提案'
} }
const response = await getSimiBills(params) const response = await getSimiBills(params)
if (response && response.data) { if (response && response.data) {
// 保存原始数据 // 保存原始数据(新 API 返回结构中 simi_bills 是法案数组)
rawBillsData.value = response.data rawBillsData.value = response.data.simi_bills || []
const { stats: apiStats, bills: apiBills } = transformSimiBillsData(response) const { stats: apiStats, bills: apiBills } = transformSimiBillsData(response)
stats.value = apiStats stats.value = apiStats
bills.value = apiBills bills.value = apiBills
} else { } else {
stats.value = null stats.value = null
bills.value = [] bills.value = []
rawBillsData.value = []
} }
} catch (error) { } catch (error) {
console.error('获取相似法案失败:', error) console.error('获取相似法案失败:', error)
......
...@@ -370,24 +370,26 @@ function transformPredictionResult(data: any) { ...@@ -370,24 +370,26 @@ function transformPredictionResult(data: any) {
const factors = data?.factor_analysis || [] const factors = data?.factor_analysis || []
// 转换阶段分析 // 转换阶段分析
const phases = stages.map((stage: any, index: number) => ({ const chineseNum = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十']
id: index + 1,
title: `阶段${index + 1}${stage.stage}`, const phases = stages.map((stage: any, index: number) => ({
description: stage.stage, id: index + 1,
riskLevel: probabilityToRisk(stage.predicted_pass_probability), title: `阶段${chineseNum[index] || index + 1}${stage.stage}`,
progressLevel: probabilityToProgressLevel(stage.predicted_pass_probability), description: stage.stage,
estimatedDays: `预计耗时${stage.predicted_passing_time}天`, riskLevel: probabilityToRisk(stage.predicted_pass_probability),
modelInputs: [stage.analysis], progressLevel: probabilityToProgressLevel(stage.predicted_pass_probability),
supportingFacts: { estimatedDays: `预计耗时${stage.predicted_passing_time}天`,
title: '通过性预测依据', modelInputs: [stage.analysis],
basedOn: '此阶段预测基于以下观点', supportingFacts: {
stats: [ title: '通过性预测依据',
{ value: `${stage.predicted_pass_probability}`, label: '通过概率' }, basedOn: '此阶段预测基于以下观点',
{ value: `${stage.predicted_passing_time}天`, label: '预计耗时' } stats: [
] { value: `${stage.predicted_pass_probability}`, label: '通过概率' },
}, { value: `${stage.predicted_passing_time}天`, label: '预计耗时' }
predictionBasis: stage.prediction_basis ]
})) },
predictionBasis: stage.prediction_basis
}))
return { return {
title: '立法进展阶段预测分析', title: '立法进展阶段预测分析',
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论