提交 8148dacc authored 作者: 张伊明's avatar 张伊明

合并分支 'liuyuqi' 到 'pre'

Liuyuqi 查看合并请求 !227
流水线 #57 已通过 于阶段
in 6 分 15 秒
...@@ -328,16 +328,20 @@ export function getProgressPrediction(billId) { ...@@ -328,16 +328,20 @@ export function getProgressPrediction(billId) {
/** /**
* 获取相似法案列表 * 获取相似法案列表
* @param {Object} params - 查询参数 * @param {Object} params - 查询参数
* @param {string} params.patternType - 政府结构类型,如 "统一政府" * @param {string} params.billIds - 当前法案的ID
* @param {string} params.proposalType - 提案类型,默认 "两党共同提案" * @param {string[]} params.domains - 领域名称列表
* @param {string} params.patternType - 政府结构类型,如 "统一政府"/"分裂政府"/"微弱多数"
* @param {string} params.proposalType - 提案类型,如 "两党共同提案"/"单政党提案(共和党提案)"/"单政党提案(民主党提案)"
* @returns {Promise<Object>} 相似法案列表 * @returns {Promise<Object>} 相似法案列表
*/ */
export function getSimiBills(params = {}) { export function getSimiBills(params = {}) {
return request('/api/BillProgressPrediction/simiBills', { return request('/api/BillProgressPrediction/simiBills', {
method: 'GET', method: 'GET',
params: { params: {
patternType: params.patternType || '统一政府', billIds: params.billIds,
proposalType: params.proposalType || '两党共同提案', domains: params.domains,
patternType: params.patternType ,
proposalType: params.proposalType ,
...params ...params
} }
}) })
...@@ -400,31 +404,42 @@ export function transformSimiBillsData(apiData) { ...@@ -400,31 +404,42 @@ export function transformSimiBillsData(apiData) {
const transformedBills = bills.map(bill => ({ const transformedBills = bills.map(bill => ({
id: bill.bill_id, id: bill.bill_id,
title: bill.bill_name || bill.bill_id, title: bill.bill_name || bill.bill_id,
tags: [bill.poli_pattern_type, bill.bill_proposal_type].filter(Boolean), proposalDate: extractProposalDate(bill.poli_pattern_desc),
passTime: extractPassTime(bill.bill_actions), areas: bill.domain_name ? (Array.isArray(bill.domain_name) ? bill.domain_name : [bill.domain_name]) : [],
totalDays: calculateTotalDays(bill.bill_actions), proposer: extractProposer(bill.bill_sponsors),
selected: true, // 默认全选 coProposers: bill.bill_proposal_desc || '',
_raw: bill governmentType: formatGovernmentType(bill.poli_pattern_type, bill.poli_pattern_desc),
passDays: calculateTotalDays(bill.bill_actions),
selected: true // 默认全选
})) }))
return { stats, bills: transformedBills } return { stats, bills: transformedBills }
} }
/** /**
* 从法案动作中提取通过时间 * 从政治格局描述中提取提案时间
* @param {Array} actions - 法案动作列表 * @param {string} desc - 政治格局描述
* @returns {string} 通过时间 * @returns {string} 提案时间
*/ */
function extractPassTime(actions) { function extractProposalDate(desc) {
if (!actions || actions.length === 0) return '' if (!desc) return ''
// 找到最后一个动作的日期 const match = desc.match(/(\d{4})-(\d{2})-(\d{2})/)
const lastAction = actions[actions.length - 1] if (match) {
if (lastAction && lastAction.action_date) { return `${match[1]}${parseInt(match[2])}${parseInt(match[3])}日`
const date = new Date(lastAction.action_date)
return `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}日`
} }
return '' return ''
} }
/**
* 从提案人列表中提取主提案人
* @param {Array} sponsors - 提案人列表
* @returns {string} 主提案人姓名
*/
function extractProposer(sponsors) {
if (!sponsors || sponsors.length === 0) return ''
const mainProposer = sponsors.find(s => s.sponsor_type === '提案人')
return mainProposer ? mainProposer.person_name : ''
}
/**
/** /**
* 计算法案总耗时 * 计算法案总耗时
...@@ -488,29 +503,30 @@ export function transformProposalInfo(apiData) { ...@@ -488,29 +503,30 @@ export function transformProposalInfo(apiData) {
} }
return { return {
// 提案标题 - 需要从其他 API 获取或使用默认值 // 提案标题
title: data.bill_title || 'H.R.1-大而美法案', title: data.bill_title || 'H.R.1-大而美法案',
// 提案时间 - 从政治格局描述中提取或使用默认值 // 提案时间 - 从政治格局描述中提取
date: extractDateFromDesc(data.poli_pattern_desc) || '2025年5月20日', date: extractDateFromDesc(data.poli_pattern_desc) || '2025年5月20日',
// 涉及领域 // 涉及领域 TAG - 使用 domain_name
areas: data.domain_name || [], areas: Array.isArray(data.domain_name) ? data.domain_name : (data.domain_name ? [data.domain_name] : []),
// 选举周期阶段 - 从政治格局描述推断 // 政策领域完整选项列表 - 使用 bill_domain(用于筛选下拉框)
electionPhase: inferElectionPhase(data.poli_pattern_desc) || '执政初期/蜜月期', billDomain: Array.isArray(data.bill_domain) ? data.bill_domain : [],
// 政策领域默认已选项 - 使用 domain_name
defaultDomains: Array.isArray(data.domain_name) ? data.domain_name : (data.domain_name ? [data.domain_name] : []),
// 提案人 // 提案人
proposer: mainProposer ? `${mainProposer.person_name}` : '', proposer: mainProposer ? mainProposer.person_name : '',
// 共同提案人 // 共同提案人
coProposers: coProposersDesc, coProposers: coProposersDesc,
// 提案人职务 - 需要从其他 API 获取
proposerPosition: data.proposer_position || '委员会主席',
// 政府结构类型 // 政府结构类型
governmentType: formatGovernmentType(data.poli_pattern_type, data.poli_pattern_desc), governmentType: formatGovernmentType(data.poli_pattern_type, data.poli_pattern_desc),
// 法案预算规模 - 需要从其他 API 获取 // 政治格局类型(用于筛选条件默认值)
budgetScale: data.budget_scale || '4万亿美元', patternType: data.poli_pattern_type || '统一政府',
// 原始数据,供筛选条件使用 // 原始数据,供筛选条件使用
_raw: data _raw: data
} }
} }
/** /**
* 从政治格局描述中提取日期 * 从政治格局描述中提取日期
* @param {string} desc - 政治格局描述 * @param {string} desc - 政治格局描述
...@@ -578,9 +594,7 @@ export function generateDefaultFilters(proposalInfo) { ...@@ -578,9 +594,7 @@ export function generateDefaultFilters(proposalInfo) {
// 提案人职务 // 提案人职务
proposerPosition: proposalInfo.proposerPosition === '委员会主席' ? ['chairman'] : [], proposerPosition: proposalInfo.proposerPosition === '委员会主席' ? ['chairman'] : [],
// 政府结构类型 // 政府结构类型
governmentType: proposalInfo.governmentType.includes('一致') ? ['unified'] : ['divided'], governmentType: proposalInfo.governmentType.includes('一致') ? ['unified'] : ['divided'],
// 选举周期阶段
electionPhase: proposalInfo.electionPhase.includes('蜜月') ? ['honeymoon'] : [],
// 法案预算规模 // 法案预算规模
budgetScale: ['trillion_plus'], budgetScale: ['trillion_plus'],
// 对方党派提案人 // 对方党派提案人
...@@ -589,3 +603,19 @@ export function generateDefaultFilters(proposalInfo) { ...@@ -589,3 +603,19 @@ export function generateDefaultFilters(proposalInfo) {
proposalTime: ['recent_5'] proposalTime: ['recent_5']
} }
} }
/**
* 获取预测分析结果
* @param {Object} params - 请求参数
* @param {string} params.bill_id - 当前法案ID
* @param {string} params.bill_name - 当前法案名称
* @param {Array} params.bill_actions - 当前法案动作列表
* @param {Array} params.bill_sponsors - 当前法案提案人列表
* @param {Array} params.simi_bills - 用户勾选的相似法案列表
* @returns {Promise<Object>} 预测分析结果
*/
export function getPassProd(params) {
return request('/api/BillProgressPrediction/passProd', {
method: 'POST',
data: params
})
}
\ No newline at end of file
...@@ -138,120 +138,118 @@ export function getPersonList(params) { ...@@ -138,120 +138,118 @@ export function getPersonList(params) {
params params
}) })
} }
//创新主体科研实力:专利数量统计 //合作情况:与中国合作数量变化
export function getPatentList(params) { export function getCooperateNumWithChina(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/innovateSubject/patentList/${params.id}`, url: `/api/innovateSubject/cooperateNumWithChina/${params.id}`,
params params
}) })
} }
// 合作情况:与中国合作类型变化
// 创新主体科研实力:论文数量统计 export function getCooperateTypeWithChina(params) {
export function getPaperList(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/innovateSubject/paperList/${params.id}`, url: `/api/innovateSubject/cooperateTypeWithChina/${params.year}/${params.id}`,
params params
}) })
} }
//创新主体科研实力:领域实力分布 // 合作情况:与中国合作领域变化
export function getStudyFieldList(params) { export function getCooperateAreaWithChina(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/innovateSubject/studyFieldList/${params.id}`, url: `/api/innovateSubject/cooperateAreaWithChina/${params.id}`,
params
}) })
} }
//创新主体科研实力:经费增长情况 //合作情况:与中国合作经费变化
export function getFundGrowth(params) { export function getCooperateFundWithChina(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/innovateSubject/fundGrowth/${params.id}`, url: `/api/innovateSubject/cooperateFundWithChina/${params.id}`,
params params
}) })
} }
//创新主体科研实力:经费来源 //合作情况:与中国合作事例
export function getFundFromList(params) { export function getCooperateExampleWithChina(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/innovateSubject/fundFromList/${params.id}`, url: `/api/innovateSubject/cooperateExampleWithChina/${params.id}`,
params params
}) })
} }
//创新主体科研实力:经费分配
export function getFundToList(params) {
// 专利数量统计
export function getPatentList(orgId) {
return request({ return request({
method: 'GET', method: "GET",
url: `/api/innovateSubject/fundToList/${params.id}`, url: `/api/innovateSubject/patentList/${orgId}`
params
}) })
} }
//合作情况:与中国合作数量变化 // 论文数量统计
export function getCooperateNumWithChina(params) { export function getPaperList(orgId) {
return request({ return request({
method: 'GET', method: "GET",
url: `/api/innovateSubject/cooperateNumWithChina/${params.id}`, url: `/api/innovateSubject/paperList/${orgId}`
params
}) })
} }
// 合作情况:与中国合作类型变化 // 领域实力分布
export function getCooperateTypeWithChina(params) { export function getStudyFieldList(orgId) {
return request({ return request({
method: 'GET', method: "GET",
url: `/api/innovateSubject/cooperateTypeWithChina/${params.year}/${params.id}`, url: `/api/innovateSubject/studyFieldList/${orgId}`
params
}) })
} }
// 合作情况:与中国合作领域变化 // 经费增长情况
export function getCooperateAreaWithChina(params) { export function getFundGrowth(orgId) {
return request({ return request({
method: 'GET', method: "GET",
url: `/api/innovateSubject/cooperateAreaWithChina/${params.id}`, url: `/api/innovateSubject/fundGrowth/${orgId}`
params
}) })
} }
//合作情况:与中国合作经费变化 // 经费来源
export function getCooperateFundWithChina(params) { export function getFundFromList(orgId) {
return request({ return request({
method: 'GET', method: "GET",
url: `/api/innovateSubject/cooperateFundWithChina/${params.id}`, url: `/api/innovateSubject/fundFromList/${orgId}`
params
}) })
} }
//合作情况:与中国合作事例 // 经费分配
export function getCooperateExampleWithChina(params) { export function getFundToList(orgId) {
return request({ return request({
method: 'GET', method: "GET",
url: `/api/innovateSubject/cooperateExampleWithChina/${params.id}`, url: `/api/innovateSubject/fundToList/${orgId}`
params
}) })
} }
// 获取实验室列表
//创新主体其他情况:重点实验室 export function getLabList(orgId) {
export function getLabList(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/innovateSubject/labList/${params.id}`, url: `/api/innovateSubject/labList/${orgId}`
}) })
} }
//创新主体其他情况:政策文件 // 获取政策文件列表
export function getPolicyList(params) { export function getPolicyList(orgId, currentPage = 1, pageSize = 6) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/innovateSubject/policyList/${params.id}`, url: `/api/innovateSubject/policyList/${orgId}`,
params params: { currentPage, pageSize }
}) })
} }
\ No newline at end of file
...@@ -4,22 +4,20 @@ const InnovationInstitution = () => import('@/views/innovationSubject/innovative ...@@ -4,22 +4,20 @@ const InnovationInstitution = () => import('@/views/innovationSubject/innovative
const innovationSubjectRoutes = [ const innovationSubjectRoutes = [
//创新主体
{ {
path: "/innovationSubject", path: "/innovationSubject",
name: "InnovationSubject", name: "InnovationSubject",
component: InnovationSubject, component: InnovationSubject,
meta: { meta: {
title: "M国主要创新主体分析概览" title: "M "
} }
}, },
{ {
path: "/InnovativeInstitutions/:id", path: "/InnovativeInstitutions/:id/:type",
name: "InnovativeInstitutions", name: "InnovativeInstitutions",
component: InnovationInstitution, component: InnovationInstitution,
// meta: {
// title: "学校详情"
// },
} }
] ]
......
...@@ -50,12 +50,9 @@ const emit = defineEmits<{ ...@@ -50,12 +50,9 @@ const emit = defineEmits<{
const billFields = [ const billFields = [
{ key: 'proposalDate', label: '提案时间:' }, { key: 'proposalDate', label: '提案时间:' },
{ key: 'areas', label: '涉及领域:' }, { key: 'areas', label: '涉及领域:' },
{ key: 'electionPhase', label: '选举周期阶段:' },
{ key: 'proposer', label: '提案人:' }, { key: 'proposer', label: '提案人:' },
{ key: 'coProposers', label: '共同提案人:' }, { key: 'coProposers', label: '共同提案人:' },
{ key: 'proposerPosition', label: '提案人职务:' },
{ key: 'governmentType', label: '政府结构类型:' }, { key: 'governmentType', label: '政府结构类型:' },
{ key: 'budgetScale', label: '法案预算规模:' },
{ key: 'passDays', label: '通过耗时:' } { key: 'passDays', label: '通过耗时:' }
] ]
</script> </script>
......
...@@ -2,59 +2,192 @@ ...@@ -2,59 +2,192 @@
<section class="filter-section"> <section class="filter-section">
<div class="section-header"> <div class="section-header">
<div class="header-left"> <div class="header-left">
<img src="../assets/fitller.svg" /> <img src="../assets/fitller.svg" />
<h2 class="section-title text-title-3-bold">核心相似度维度筛选</h2> <h2 class="section-title text-title-3-bold">核心相似度维度筛选</h2>
</div> </div>
<button class="btn-outline" @click="emit('setAsCurrent')">设置为当前提案</button> <button class="btn-outline" @click="setAsCurrent">设置为当前提案</button>
</div> </div>
<div class="divider" /> <div class="divider" />
<div class="fields-grid"> <div class="fields-grid">
<div
v-for="field in fields" <!-- 政策领域 -->
:key="field.id" <div class="field-item">
class="field-item" <span class="field-label text-tip-1 text-primary-65-clor">政策领域:</span>
> <div class="field-content">
<div class="field-label-wrapper"> <el-select
<span class="field-label text-tip-1 text-primary-65-clor">{{ field.label }}</span> v-model="localValues.policyArea"
multiple
placeholder="请选择"
style="width: 420px"
@change="handleChange"
>
<el-option
v-for="opt in fields.policyArea.options"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</div> </div>
</div>
<!-- 政府结构类型 -->
<div class="field-item">
<span class="field-label text-tip-1 text-primary-65-clor">政府结构类型:</span>
<div class="field-content"> <div class="field-content">
<FilterSelect <el-select
:options="field.options" v-model="localValues.governmentType"
:model-value="field.selectedValues" multiple
@update:model-value="(val) => handleFieldUpdate(field.id, val)" placeholder="请选择"
/> style="width: 420px"
<div v-if="field.hint" class="field-hint"> @change="handleChange"
<img src="../assets/importent.svg" /> >
<span class="text-tip-2 text-primary-50-clor">{{ field.hint }}</span> <el-option
v-for="opt in fields.governmentType.options"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<div v-if="fields.governmentType.hint" class="field-hint">
<img src="../assets/importent.svg" />
<span class="text-tip-2 text-primary-50-clor">{{ fields.governmentType.hint }}</span>
</div> </div>
</div> </div>
</div> </div>
<!-- 对方党派提案人 -->
<div class="field-item">
<span class="field-label text-tip-1 text-primary-65-clor">对方党派提案人:</span>
<div class="field-content">
<el-select
v-model="localValues.oppositionProposer"
multiple
placeholder="请选择"
style="width: 420px"
@change="handleChange"
>
<el-option
v-for="opt in fields.oppositionProposer.options"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</div>
</div>
<!-- 提案时间 -->
<div class="field-item">
<span class="field-label text-tip-1 text-primary-65-clor">提案时间:</span>
<div class="field-content">
<el-select
v-model="localValues.proposalTime"
multiple
placeholder="请选择"
style="width: 420px"
@change="handleChange"
>
<el-option
v-for="opt in fields.proposalTime.options"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</div>
</div>
</div> </div>
</section> </section>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { FilterField } from '../api' import { ref, computed, watch } from 'vue'
import FilterSelect from './FilterSelect.vue' import type { ProposalInfo } from '../api'
const props = defineProps<{ const props = defineProps<{
fields: FilterField[] proposalInfo?: ProposalInfo | null
defaultFilters?: Record<string, string[]>
}>() }>()
const emit = defineEmits<{ // 本地 v-model 值
'update:fields': [fields: FilterField[]] const localValues = ref({
setAsCurrent: [] policyArea: [] as string[],
}>() governmentType: [] as string[],
oppositionProposer: [] as string[],
proposalTime: [] as string[],
})
// 更新字段选中值 // 根据 proposalInfo 计算初始筛选值(即"设置为当前提案"的目标状态)
function handleFieldUpdate(fieldId: string, newValues: string[]) { function buildInitialValues(info?: ProposalInfo | null): Record<string, string[]> {
const updatedFields = props.fields.map(f => if (!info) return { policyArea: [], governmentType: [], oppositionProposer: [], proposalTime: [] }
f.id === fieldId return {
? { ...f, selectedValues: newValues } policyArea: info.defaultDomains?.length ? [...info.defaultDomains] : [...(info.areas || [])],
: f governmentType: info.patternType ? [info.patternType] : [],
) oppositionProposer: [],
emit('update:fields', updatedFields) proposalTime: [],
}
} }
// 监听 proposalInfo 首次传入时设置初始值
watch(
() => props.proposalInfo,
(newInfo) => {
Object.assign(localValues.value, buildInitialValues(newInfo))
},
{ immediate: true }
)
// 重置:清空所有已选项
function reset() {
localValues.value = {
policyArea: [],
governmentType: [],
oppositionProposer: [],
proposalTime: [],
}
}
// 设置为当前提案:恢复为 proposalInfo 的初始状态
function setAsCurrent() {
Object.assign(localValues.value, buildInitialValues(props.proposalInfo))
}
defineExpose({ reset, setAsCurrent, getValues: () => localValues.value })
// 固定的字段配置,options 由 proposalInfo 动态注入
const fields = computed(() => {
const billDomain = props.proposalInfo?.billDomain
const domainOptions = Array.isArray(billDomain) ? billDomain.map(d => ({ value: d, label: d })) : []
return {
policyArea: { options: domainOptions },
governmentType: {
options: [
{ value: '统一政府', label: '统一政府' },
{ value: '分裂政府', label: '分裂政府' },
{ value: '微弱多数', label: '微弱多数' }
],
hint: '总统所属政党同时控制国会参众两院'
},
oppositionProposer: {
options: [
{ value: '两党共同提案', label: '两党共同提案' },
{ value: '单政党提案(共和党提案)', label: '单政党提案(共和党提案)' },
{ value: '单政党提案(民主党提案)', label: '单政党提案(民主党提案)' }
]
},
proposalTime: {
options: [
{ value: '近五年', label: '近五年' },
{ value: '近十年', label: '近十年' },
{ value: '全部', label: '全部' }
]
}
}
})
function handleChange() {}
</script> </script>
<style scoped> <style scoped>
...@@ -75,12 +208,6 @@ function handleFieldUpdate(fieldId: string, newValues: string[]) { ...@@ -75,12 +208,6 @@ function handleFieldUpdate(fieldId: string, newValues: string[]) {
gap: 12px; gap: 12px;
} }
.section-icon {
width: 16px;
height: 16px;
color: var(--text-primary-80-color);
}
.btn-outline { .btn-outline {
padding: 6px 16px; padding: 6px 16px;
border: 1px solid var(--bg-black-10); border: 1px solid var(--bg-black-10);
...@@ -105,7 +232,7 @@ function handleFieldUpdate(fieldId: string, newValues: string[]) { ...@@ -105,7 +232,7 @@ function handleFieldUpdate(fieldId: string, newValues: string[]) {
.fields-grid { .fields-grid {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 24px 24px; gap: 24px;
padding-left: 30px; padding-left: 30px;
} }
...@@ -113,15 +240,14 @@ function handleFieldUpdate(fieldId: string, newValues: string[]) { ...@@ -113,15 +240,14 @@ function handleFieldUpdate(fieldId: string, newValues: string[]) {
width: 580px; width: 580px;
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
} gap: 0;
.field-label-wrapper {
padding-top: 4px;
} }
.field-label { .field-label {
display: block; display: block;
width: 150px; width: 140px;
flex-shrink: 0;
padding-top: 6px;
} }
.field-content { .field-content {
...@@ -135,11 +261,4 @@ function handleFieldUpdate(fieldId: string, newValues: string[]) { ...@@ -135,11 +261,4 @@ function handleFieldUpdate(fieldId: string, newValues: string[]) {
align-items: center; align-items: center;
gap: 8px; gap: 8px;
} }
.hint-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
color: var(--text-primary-50-color);
}
</style> </style>
...@@ -39,26 +39,19 @@ ...@@ -39,26 +39,19 @@
</p> </p>
</div> </div>
</div> </div>
<div class="facts-section"> <div v-if="phase.predictionBasis" class="facts-section">
<div class="box-header flex-display-start"> <div class="box-header flex-display-start">
<div class="box-title-row flex-display-center"> <div class="box-title-row flex-display-center">
<img src="../assets/icon1.svg"/> <img src="../assets/icon1.svg"/>
<span class="text-compact-bold">{{ phase.supportingFacts.title }}</span> <span class="text-compact-bold">通过性预测依据</span>
</div> </div>
<div class="box-hint flex-display-center text-tip-2 text-primary-50-clor"> <div class="box-hint flex-display-center text-tip-2 text-primary-50-clor">
<img src="../assets/importent.svg"/> <img src="../assets/importent.svg"/>
<span>{{ phase.supportingFacts.basedOn }}</span> <span>此阶段预测基于以下观点</span>
</div> </div>
</div> </div>
<div class="stats-grid"> <div class="prediction-basis-content">
<div <p class="text-tip-2 text-primary-65-clor">{{ phase.predictionBasis }}</p>
v-for="(stat, index) in phase.supportingFacts.stats"
:key="index"
class="stat-card"
>
<div class="stat-value main-color">{{ stat.value }}</div>
<div class="text-tip-3 text-primary-65-clor">{{ stat.label }}</div>
</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -260,6 +253,16 @@ const riskLabel = computed(() => { ...@@ -260,6 +253,16 @@ const riskLabel = computed(() => {
line-height: 1.6; line-height: 1.6;
} }
.prediction-basis-content {
background-color: var(--bg-black-2);
border-radius: var(--radius-10);
padding: 16px;
}
.prediction-basis-content p {
line-height: 1.6;
}
.stats-grid { .stats-grid {
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
......
...@@ -33,13 +33,6 @@ ...@@ -33,13 +33,6 @@
<span class="info-label text-body-1">{{ field.label }}</span> <span class="info-label text-body-1">{{ field.label }}</span>
<template v-if="field.key === 'areas'"> <template v-if="field.key === 'areas'">
<div class="area-tags"> <div class="area-tags">
<!-- <span
v-for="area in info.areas"
:key="area"
class="area-tag"
>
{{ area }}
</span> -->
<AreaTag v-for="area in info.areas" :key="area" :tagName="area" /> <AreaTag v-for="area in info.areas" :key="area" :tagName="area" />
</div> </div>
...@@ -59,17 +52,14 @@ defineProps<{ ...@@ -59,17 +52,14 @@ defineProps<{
info: ProposalInfo info: ProposalInfo
}>() }>()
// 信息字段配置 // 信息字段配置 - 只保留:提案标题、提案时间、涉及领域、提案人、共同提案人、政府结构类型
const infoFields = [ const infoFields = [
{ key: 'title', label: '提案标题:' }, { key: 'title', label: '提案标题:' },
{ key: 'date', label: '提案时间:' }, { key: 'date', label: '提案时间:' },
{ key: 'areas', label: '涉及领域:' }, { key: 'areas', label: '涉及领域:' },
{ key: 'electionPhase', label: '选举周期阶段:' },
{ key: 'proposer', label: '提案人:' }, { key: 'proposer', label: '提案人:' },
{ key: 'coProposers', label: '共同提案人:' }, { key: 'coProposers', label: '共同提案人:' },
{ key: 'proposerPosition', label: '提案人职务:' }, { key: 'governmentType', label: '政府结构类型:' }
{ key: 'governmentType', label: '政府结构类型:' },
{ key: 'budgetScale', label: '法案预算规模:' }
] ]
</script> </script>
......
...@@ -7,9 +7,9 @@ ...@@ -7,9 +7,9 @@
<div class="content-wrapper"> <div class="content-wrapper">
<ProposalInfoSection v-if="currentProposalInfo" :info="currentProposalInfo" /> <ProposalInfoSection v-if="currentProposalInfo" :info="currentProposalInfo" />
<FilterSection <FilterSection
:fields="filterFields" ref="filterSectionRef"
@update:fields="filterFields = $event" :proposal-info="currentProposalInfo"
@set-as-current="handleSetAsCurrent" :default-filters="defaultFilters"
/> />
</div> </div>
<ActionButtons <ActionButtons
...@@ -22,8 +22,7 @@ ...@@ -22,8 +22,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, watch } from 'vue' import { ref, onMounted, watch } from 'vue'
import type { ProposalInfo, FilterField } from '../api' import type { ProposalInfo } from '../api'
import { fetchProposalInfo, fetchFilterFields } from '../api'
import ProposalInfoSection from './ProposalInfoSection.vue' import ProposalInfoSection from './ProposalInfoSection.vue'
import FilterSection from './FilterSection.vue' import FilterSection from './FilterSection.vue'
import ActionButtons from './ActionButtons.vue' import ActionButtons from './ActionButtons.vue'
...@@ -34,99 +33,42 @@ const props = defineProps<{ ...@@ -34,99 +33,42 @@ const props = defineProps<{
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
next: [] next: [selectedFilters: Record<string, string[]>]
}>() }>()
// 提案信息(优先使用 props,否则从 API 获取 // 提案信息(优先使用 props)
const currentProposalInfo = ref<ProposalInfo | null>(null) const currentProposalInfo = ref<ProposalInfo | null>(null)
// 筛选字段列表 // FilterSection 组件引用
const filterFields = ref<FilterField[]>([]) const filterSectionRef = ref<InstanceType<typeof FilterSection> | null>(null)
// 加载状态 // 加载状态
const loading = ref(true) const loading = ref(true)
// 初始筛选字段(用于重置) // 默认筛选条件
const initialFilterFields = ref<FilterField[]>([]) const defaultFilters = ref<Record<string, string[]>>({})
// 监听 props.proposalInfo 变化 // 监听 props.proposalInfo 变化
watch(() => props.proposalInfo, (newInfo) => { watch(() => props.proposalInfo, (newInfo) => {
if (newInfo) { if (newInfo) {
currentProposalInfo.value = newInfo currentProposalInfo.value = newInfo
} // 根据提案信息生成默认筛选条件(仅用于 Step1 内部状态跟踪,不再传给 FilterSection)
}, { immediate: true }) defaultFilters.value = {
policyArea: newInfo.defaultDomains?.length ? newInfo.defaultDomains : newInfo.areas,
// 页面初始化时加载数据 governmentType: newInfo.patternType ? [newInfo.patternType] : [],
onMounted(async () => { oppositionProposer: [],
try { proposalTime: [],
// 如果没有从 props 获取到提案信息,则从 API 获取
if (!props.proposalInfo) {
currentProposalInfo.value = await fetchProposalInfo()
} }
// 获取筛选字段配置
const fields = await fetchFilterFields()
// 如果有默认筛选条件,应用到筛选字段
if (props.defaultFilters && Object.keys(props.defaultFilters).length > 0) {
filterFields.value = fields.map(field => ({
...field,
selectedValues: props.defaultFilters?.[field.id] || field.selectedValues
}))
} else {
filterFields.value = fields
}
// 保存初始状态用于重置
initialFilterFields.value = JSON.parse(JSON.stringify(filterFields.value))
} finally {
loading.value = false
} }
}) loading.value = false
}, { immediate: true })
// 重置所有筛选条件 // 重置所有筛选条件:清空所有已选项
function handleReset() { function handleReset() {
filterFields.value = JSON.parse(JSON.stringify(initialFilterFields.value)) filterSectionRef.value?.reset()
}
// 设置为当前提案
function handleSetAsCurrent() {
// 根据当前提案信息重新设置筛选条件
if (currentProposalInfo.value) {
filterFields.value = filterFields.value.map(field => {
// 根据提案信息设置默认值
const defaultValues = getDefaultValuesForField(field.id, currentProposalInfo.value!)
return {
...field,
selectedValues: defaultValues.length > 0 ? defaultValues : field.selectedValues
}
})
}
}
// 根据字段 ID 和提案信息获取默认值
function getDefaultValuesForField(fieldId: string, info: ProposalInfo): string[] {
const areaMap: Record<string, string> = {
'能源': 'energy',
'集成电路': 'ic',
'人工智能': 'ai',
'生物技术': 'biotech'
}
switch (fieldId) {
case 'policyArea':
return info.areas.map(area => areaMap[area] || area.toLowerCase())
case 'proposerPosition':
return info.proposerPosition === '委员会主席' ? ['chairman'] : []
case 'governmentType':
return info.governmentType.includes('一致') ? ['unified'] : ['divided']
case 'electionPhase':
return info.electionPhase.includes('蜜月') ? ['honeymoon'] : []
default:
return []
}
} }
// 下一步 // 下一步:获取已选的筛选条件并传给父组件
function handleNext() { function handleNext() {
emit('next') const selectedFilters = filterSectionRef.value?.getValues() || {}
emit('next', selectedFilters)
} }
</script> </script>
......
...@@ -77,13 +77,12 @@ ...@@ -77,13 +77,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, inject, watch } from 'vue' import { ref, computed, onMounted, inject, watch } from 'vue'
import type { FilterStats, BillInfo } from '../api' import type { FilterStats, BillInfo } from '../api'
import { fetchFilterStats, fetchBillList } from '../api'
import { getSimiBills, transformSimiBillsData } from '@/api/bill/billHome' import { getSimiBills, transformSimiBillsData } from '@/api/bill/billHome'
import BillCard from './BillCard.vue' import BillCard from './BillCard.vue'
const emit = defineEmits<{ const emit = defineEmits<{
previous: [] previous: []
next: [] next: [selectedBills: any[]]
}>() }>()
// 从父组件注入筛选参数 // 从父组件注入筛选参数
...@@ -93,6 +92,8 @@ const filterParams = inject<any>('filterParams', null) ...@@ -93,6 +92,8 @@ const filterParams = inject<any>('filterParams', null)
const stats = ref<FilterStats | null>(null) const stats = ref<FilterStats | null>(null)
// 法案列表 // 法案列表
const bills = ref<BillInfo[]>([]) const bills = ref<BillInfo[]>([])
// 原始法案数据(用于传给第三页)
const rawBillsData = ref<any[]>([])
// 加载状态 // 加载状态
const loading = ref(true) const loading = ref(true)
...@@ -121,36 +122,30 @@ const selectedCount = computed(() => { ...@@ -121,36 +122,30 @@ const selectedCount = computed(() => {
async function loadData() { async function loadData() {
loading.value = true loading.value = true
try { try {
// 优先使用真实 API // 使用真实 API,传入 billIds、domains、patternType、proposalType
const params = { const params = {
patternType: filterParams?.governmentType || '统一政府', billIds: filterParams?.value.billIds,
proposalType: '两党共同提案' domains:JSON.stringify(filterParams?.value.domains) || [],
patternType: filterParams?.value.patternType || '统一政府',
proposalType: filterParams?.value.proposalType || '两党共同提案'
} }
const response = await getSimiBills(params) const response = await getSimiBills(params)
if (response && response.success && response.data) { if (response && response.data) {
// 保存原始数据
rawBillsData.value = response.data
const { stats: apiStats, bills: apiBills } = transformSimiBillsData(response) const { stats: apiStats, bills: apiBills } = transformSimiBillsData(response)
stats.value = apiStats stats.value = apiStats
bills.value = apiBills bills.value = apiBills
} else { } else {
// 如果 API 失败,使用模拟数据 stats.value = null
const [statsData, billsData] = await Promise.all([ bills.value = []
fetchFilterStats(),
fetchBillList()
])
stats.value = statsData
bills.value = billsData
} }
} catch (error) { } catch (error) {
console.error('获取相似法案失败:', error) console.error('获取相似法案失败:', error)
// 使用模拟数据作为 fallback stats.value = null
const [statsData, billsData] = await Promise.all([ bills.value = []
fetchFilterStats(),
fetchBillList()
])
stats.value = statsData
bills.value = billsData
} finally { } finally {
loading.value = false loading.value = false
} }
...@@ -189,7 +184,11 @@ function handleBack() { ...@@ -189,7 +184,11 @@ function handleBack() {
// 开始预测分析 // 开始预测分析
function handleStartAnalysis() { function handleStartAnalysis() {
emit('next') // 获取用户勾选的法案ID列表
const selectedIds = bills.value.filter(b => b.selected).map(b => b.id)
// 从原始数据中筛选出用户勾选的法案
const selectedBills = rawBillsData.value.filter(b => selectedIds.includes(b.bill_id))
emit('next', selectedBills)
} }
</script> </script>
......
<template> <template>
<div class="step-container"> <div class="step-container">
<div v-if="predictionData" class="content-wrapper"> <div v-if="props.loading" class="loading-wrapper flex-display-center">
<span class="text-tip-2 text-primary-50-clor">预测分析中...</span>
</div>
<div v-else-if="predictionData" class="content-wrapper">
<div class="header-section flex-display-start"> <div class="header-section flex-display-start">
<div class="header-left flex-display-start"> <div class="header-left flex-display-start">
...@@ -27,8 +30,8 @@ ...@@ -27,8 +30,8 @@
<div class="highlight-box text-regular"> <div class="highlight-box text-regular">
<div class="highlight-wave text-regular"></div> <div class="highlight-wave text-regular"></div>
<div class="highlight-content text-regular"> <div class="highlight-content text-regular">
<p class="highlight-title text-regular">《大而美法案》的通过概率非常高</p> <p class="highlight-title text-regular">该法案的通过概率为 {{ predictionData?.overallProbability || '—' }}</p>
<p class="highlight-text text-regular">该法案由众议院共和党领导层在5月正式提出,作为特朗普第二任期核心经济议程,旨在通过一揽子税收、贸易和预算改革提振经济。到6月初,法案已快速通过关键的筹款委员会和预算委员会审议,进入众议院全院辩论阶段。共和党当时控制众议院,且党内团结支持;白宫已明确表示强烈支持。虽然民主党普遍反对,但共和党凭借席位优势足以在众议院通过。主要不确定性在于参议院,但预计部分温和民主党议员可能支持,或通过预算和解程序(只需简单多数)规避阻挠议事。因此,该法案在6月初已势在必行,最终成法几无悬念。</p> <p class="highlight-text text-regular">{{ predictionData?.highlightText }}</p>
</div> </div>
</div> </div>
<div v-if="predictionData?.phases?.length" class="phases-list"> <div v-if="predictionData?.phases?.length" class="phases-list">
...@@ -59,23 +62,22 @@ ...@@ -59,23 +62,22 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, computed } from 'vue' import { ref, computed } from 'vue'
import { fetchPredictionAnalysis, type PredictionAnalysis } from '../api' import type { PredictionAnalysis } from '../api'
import PredictionPhaseCard from './PredictionPhaseCard.vue' import PredictionPhaseCard from './PredictionPhaseCard.vue'
const props = defineProps<{
data?: PredictionAnalysis | null
loading?: boolean
}>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'prev'): void (e: 'prev'): void
(e: 'repredict'): void (e: 'repredict'): void
}>() }>()
// 预测分析数据 // 预测分析数据 - 从 props 获取,暂无真实接口
const predictionData = ref<PredictionAnalysis | null>(null) const predictionData = computed(() => props.data || null)
// 获取预测分析数据
onMounted(async () => {
const data = await fetchPredictionAnalysis()
predictionData.value = data
})
// 根据索引和progressLevel返回进度条格子的类名 // 根据索引和progressLevel返回进度条格子的类名
function getOverallSegmentClass(index: number) { function getOverallSegmentClass(index: number) {
...@@ -140,15 +142,16 @@ function handleRepredict() { ...@@ -140,15 +142,16 @@ function handleRepredict() {
</script> </script>
<style scoped> <style scoped>
/* .text-title-2-bold{
color: #3b414b;
} */
.step-container { .step-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
} }
.loading-wrapper {
flex: 1;
}
.content-wrapper { .content-wrapper {
flex: 1; flex: 1;
overflow: auto; overflow: auto;
...@@ -157,7 +160,6 @@ function handleRepredict() { ...@@ -157,7 +160,6 @@ function handleRepredict() {
.header-section { .header-section {
justify-content: space-between; justify-content: space-between;
align-items: flex-start; align-items: flex-start;
/* margin-bottom: 24px; */
} }
.header-left { .header-left {
......
...@@ -25,8 +25,8 @@ ...@@ -25,8 +25,8 @@
</div> </div>
<div class="meta-row"> <div class="meta-row">
<span class="meta-label">相关领域:</span> <span class="meta-label">相关领域:</span>
<div class="meta-tags"> <div class="meta-tags">
<TagBadge v-for="item in bill.industryList" :key="item.industryName" :label="item.industryName" tag-class="tag3" /> <AreaTag v-for="item in bill.industryList" :key="item.industryName" :tagName="item.industryName" />
</div> </div>
</div> </div>
<div class="meta-row"> <div class="meta-row">
...@@ -44,8 +44,7 @@ ...@@ -44,8 +44,7 @@
<script setup> <script setup>
import { computed } from 'vue' import { computed } from 'vue'
import DocumentPreview from './DocumentPreview.vue' import DocumentPreview from './DocumentPreview.vue'
import TagBadge from './TagBadge.vue'
import ProgressBar from './ProgressBar.vue' import ProgressBar from './ProgressBar.vue'
const props = defineProps({ const props = defineProps({
...@@ -179,23 +178,25 @@ const currentStageIndex = computed(() => { ...@@ -179,23 +178,25 @@ const currentStageIndex = computed(() => {
.bill-card-meta { .bill-card-meta {
width: 100%; width: 100%;
flex: 1; flex: 1;
position: relative; display: flex;
flex-direction: column;
gap: 12px;
min-height: 0; min-height: 0;
overflow: hidden;
} }
.meta-row { .meta-row {
display: flex; display: flex;
align-items: center; align-items: flex-start;
position: absolute;
left: 0;
width: 100%; width: 100%;
gap: 12px;
} }
.meta-row:nth-child(1) { top: 0; } .meta-row:nth-child(1) { }
.meta-row:nth-child(2) { top: 36px; } .meta-row:nth-child(2) { }
.meta-row:nth-child(3) { top: 72px; } .meta-row:nth-child(3) { }
.meta-row:nth-child(4) { top: 108px; } .meta-row:nth-child(4) { }
.meta-row:nth-child(5) { top: 144px; } .meta-row:nth-child(5) { }
.meta-label { .meta-label {
font-size: 16px; font-size: 16px;
...@@ -206,6 +207,7 @@ const currentStageIndex = computed(() => { ...@@ -206,6 +207,7 @@ const currentStageIndex = computed(() => {
white-space: nowrap; white-space: nowrap;
flex-shrink: 0; flex-shrink: 0;
width: 100px; width: 100px;
padding-top: 2px;
} }
.meta-value { .meta-value {
...@@ -213,6 +215,8 @@ const currentStageIndex = computed(() => { ...@@ -213,6 +215,8 @@ const currentStageIndex = computed(() => {
font-weight: 400; font-weight: 400;
color: rgba(95, 101, 108, 1); color: rgba(95, 101, 108, 1);
line-height: 24px; line-height: 24px;
flex: 1;
min-width: 0;
} }
.sponsor-name { .sponsor-name {
...@@ -225,11 +229,11 @@ const currentStageIndex = computed(() => { ...@@ -225,11 +229,11 @@ const currentStageIndex = computed(() => {
gap: 8px; gap: 8px;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
flex: 1;
min-width: 0;
} }
.meta-row-progress { .meta-row-progress {
left: 0;
right: 0;
width: 100%; width: 100%;
} }
.meta-row-progress :deep(.progress-bar) { .meta-row-progress :deep(.progress-bar) {
......
...@@ -84,7 +84,7 @@ ...@@ -84,7 +84,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="line-test"></div> <!-- <div class="line-test"></div> -->
</div> </div>
<div class="pagination"> <div class="pagination">
<div class="total">{{ `共 ${total} 项` }}</div> <div class="total">{{ `共 ${total} 项` }}</div>
...@@ -853,10 +853,12 @@ onMounted(() => { ...@@ -853,10 +853,12 @@ onMounted(() => {
z-index: 110; z-index: 110;
margin-top: 10px; margin-top: 10px;
.main-item { .main-item {
width: 1014px; width: 1014px;
margin-bottom: 40px; display: flex;
display: flex; flex-direction: row;
align-items: flex-start;
margin-bottom: 20px;
position: relative;
.time { .time {
width: 77px; width: 77px;
box-sizing: border-box; box-sizing: border-box;
...@@ -995,7 +997,16 @@ onMounted(() => { ...@@ -995,7 +997,16 @@ onMounted(() => {
} }
} }
} }
.main-item::after {
content: '';
position: absolute;
left: 109px;
top: 24px;
bottom: -20px;
width: 1px;
background-color: rgb(230, 231, 232);
z-index: -1;
}
.line-test { .line-test {
position: absolute; position: absolute;
top: 10px; top: 10px;
......
...@@ -69,7 +69,7 @@ ...@@ -69,7 +69,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="line-test"></div> <!-- <div class="line-test"></div> -->
</div> </div>
<div class="pagination"> <div class="pagination">
<div class="total">{{ `共 ${total} 项` }}</div> <div class="total">{{ `共 ${total} 项` }}</div>
...@@ -605,10 +605,13 @@ const companyList = ref([ ...@@ -605,10 +605,13 @@ const companyList = ref([
z-index: 110; z-index: 110;
.main-item { .main-item {
width: 100%; width: 100%;
margin-bottom: 40px;
display: flex; display: flex;
display: flex;
flex-direction: row;
align-items: flex-start;
margin-bottom: 20px;
position: relative;
.time { .time {
width: 77px; width: 77px;
box-sizing: border-box; box-sizing: border-box;
...@@ -717,7 +720,16 @@ const companyList = ref([ ...@@ -717,7 +720,16 @@ const companyList = ref([
} }
} }
} }
.main-item::after {
content: '';
position: absolute;
left: 109px;
top: 24px;
bottom: -20px;
width: 1px;
background-color: rgb(230, 231, 232);
z-index: -1;
}
.line-test { .line-test {
position: absolute; position: absolute;
top: 10px; top: 10px;
......
...@@ -187,11 +187,11 @@ const handleMouseLeave = () => { ...@@ -187,11 +187,11 @@ const handleMouseLeave = () => {
isInChart.value = false; isInChart.value = false;
}; };
const handleNodeClick = (node) => { const handleNodeClick = node => {
selectedNode.value = node; selectedNode.value = node;
}; };
const handleLayoutChange = (type) => { const handleLayoutChange = type => {
controlActive.value = type; controlActive.value = type;
if (type !== 2) { if (type !== 2) {
isInChart.value = true; isInChart.value = true;
...@@ -201,9 +201,8 @@ const handleLayoutChange = (type) => { ...@@ -201,9 +201,8 @@ const handleLayoutChange = (type) => {
}; };
const updateGraphData = () => { const updateGraphData = () => {
const data = rightActiveTab.value === 'supplyChain' const data =
? singleSanctionEntitySupplyChainData.value rightActiveTab.value === "supplyChain" ? singleSanctionEntitySupplyChainData.value : singleSanctionEntityEquityData.value;
: singleSanctionEntityEquityData.value;
if (!data) return; if (!data) return;
...@@ -231,7 +230,7 @@ const updateGraphData = () => { ...@@ -231,7 +230,7 @@ const updateGraphData = () => {
links.push({ links.push({
source: `p-${item.id || index}`, source: `p-${item.id || index}`,
target: "0", target: "0",
name: rightActiveTab.value === 'supplyChain' ? "供应商" : (item.type || "持股") name: rightActiveTab.value === "supplyChain" ? "供应商" : item.type || "持股"
}); });
}); });
...@@ -248,14 +247,14 @@ const updateGraphData = () => { ...@@ -248,14 +247,14 @@ const updateGraphData = () => {
links.push({ links.push({
source: "0", source: "0",
target: `c-${item.id || index}`, target: `c-${item.id || index}`,
name: rightActiveTab.value === 'supplyChain' ? "客户" : (item.type || "投资") name: rightActiveTab.value === "supplyChain" ? "客户" : item.description || "投资"
}); });
}); });
graphData.value = { nodes, links }; graphData.value = { nodes, links };
}; };
const updateTreeData = (data) => { const updateTreeData = data => {
if (!data) return; if (!data) return;
treeData.value = { treeData.value = {
...@@ -336,17 +335,17 @@ const getSingleSanctionEntityListRequest = async () => { ...@@ -336,17 +335,17 @@ const getSingleSanctionEntityListRequest = async () => {
} }
}; };
watch(rightActiveTab, async (newTab) => { watch(rightActiveTab, async newTab => {
if (newTab === 'supplyChain') { if (newTab === "supplyChain") {
await getSingleSanctionEntitySupplyChainRequest(); await getSingleSanctionEntitySupplyChainRequest();
} else { } else {
await getSingleSanctionEntityEquityRequest(); await getSingleSanctionEntityEquityRequest();
} }
}); });
watch(activeEntityId, async (newId) => { watch(activeEntityId, async newId => {
if (newId) { if (newId) {
if (rightActiveTab.value === 'supplyChain') { if (rightActiveTab.value === "supplyChain") {
await getSingleSanctionEntitySupplyChainRequest(); await getSingleSanctionEntitySupplyChainRequest();
} else { } else {
await getSingleSanctionEntityEquityRequest(); await getSingleSanctionEntityEquityRequest();
...@@ -355,7 +354,7 @@ watch(activeEntityId, async (newId) => { ...@@ -355,7 +354,7 @@ watch(activeEntityId, async (newId) => {
}); });
watch(is50PercentRule, async () => { watch(is50PercentRule, async () => {
if (rightActiveTab.value === 'equity') { if (rightActiveTab.value === "equity") {
await getSingleSanctionEntityEquityRequest(); await getSingleSanctionEntityEquityRequest();
} }
}); });
......
...@@ -71,21 +71,19 @@ ...@@ -71,21 +71,19 @@
v-for="item in listData" v-for="item in listData"
:key="item.id" :key="item.id"
class="resource-card" class="resource-card"
@click="goToInstitution(item.id)"
> >
<div class="card-logo"> <div class="card-logo">
<img :src="item.logoUrl || defaultLogo" :alt="item.name" /> <img :src="item.logo || defaultLogo" :alt="item.name" />
</div> </div>
<div class="card-name">{{ 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-majors">{{ item.majors }}</div>
<div class="card-tags"> <div class="card-tags">
<span
v-for="tag in item.tags" <AreaTag v-for="tag in item.tags"
:key="tag" :key="tag" :tagName="tag" />
class="tag"
>
{{ tag }}
</span>
</div> </div>
</div> </div>
</template> </template>
...@@ -134,7 +132,10 @@ ...@@ -134,7 +132,10 @@
import { ref, computed, onMounted, watch } from 'vue' import { ref, computed, onMounted, watch } from 'vue'
import { getIndustryKeyList } from '@/api/bill/billHome.js' import { getIndustryKeyList } from '@/api/bill/billHome.js'
import { getSubjectList } from '@/api/characterPage/characterPage.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 // Props
const props = defineProps<{ const props = defineProps<{
initialTab?: string initialTab?: string
...@@ -142,7 +143,9 @@ const props = defineProps<{ ...@@ -142,7 +143,9 @@ const props = defineProps<{
// 默认logo // 默认logo
const defaultLogo = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj48cmVjdCB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIGZpbGw9IiNhMzE4MWIiIHJ4PSI0Ii8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTggOGgxNnYxNkg4eiIvPjwvc3ZnPg==' const defaultLogo = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj48cmVjdCB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIGZpbGw9IiNhMzE4MWIiIHJ4PSI0Ii8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTggOGgxNnYxNkg4eiIvPjwvc3ZnPg=='
const goOrigin=(id)=>{
router.push(`/InnovativeInstitutions/${id}`)
}
// 标签配置 - 添加 subjectTypeId // 标签配置 - 添加 subjectTypeId
const tabs = ref([ const tabs = ref([
{ label: '大学', value: 'university', subjectTypeId: 1 }, { label: '大学', value: 'university', subjectTypeId: 1 },
...@@ -165,7 +168,17 @@ const fields = ref<{ label: string; value: string }[]>([ ...@@ -165,7 +168,17 @@ const fields = ref<{ label: string; value: string }[]>([
{ label: '全部领域', value: 'all' } { label: '全部领域', value: 'all' }
]) ])
const fieldsLoading = ref(false) const fieldsLoading = ref(false)
const goToInstitution = (id) => {
router.push({
name: 'InnovativeInstitutions',
params: {
id: id,
type: 1
}
})
}
// 获取科技领域选项 // 获取科技领域选项
async function fetchFieldOptions() { async function fetchFieldOptions() {
fieldsLoading.value = true fieldsLoading.value = true
...@@ -273,8 +286,7 @@ function goToPage(page: number | string) { ...@@ -273,8 +286,7 @@ function goToPage(page: number | string) {
fetchData() fetchData()
} }
} }
// 模拟API获取数据
async function fetchData() { async function fetchData() {
loading.value = true loading.value = true
try { try {
...@@ -299,12 +311,12 @@ async function fetchData() { ...@@ -299,12 +311,12 @@ async function fetchData() {
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
// 映射 API 返回的数据到卡片显示格式 // 映射 API 返回的数据到卡片显示格式
const mappedData = (res.data.content || []).map((item: any) => ({ const mappedData = (res.data.content || []).map((item: any) => ({
id: item.id, id: item.orgId,
name: item.orgName, name: item.orgName,
location: item.countryName ? `${item.countryName}·${item.provinceName}` : item.provinceName || '--', location: item.address || '--',
majors: item.fieldList?.join('、') || '--', majors: item.fieldList?.join('、') || '--',
tags: item.areaList?.map((a: any) => a.areaName) || [], tags: item.taglist || [],
logo: defaultLogo logo: item.logoUrl
})) }))
listData.value = mappedData listData.value = mappedData
......
...@@ -70,13 +70,21 @@ ...@@ -70,13 +70,21 @@
</template> </template>
<script setup> <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 AreaTag from '@/components/base/AreaTag/index.vue'
import SchoolDetail from './tabs/SchoolDetail.vue' import SchoolDetail from './tabs/SchoolDetail.vue'
import ResearchStrength from './tabs/ResearchStrength.vue' import ResearchStrength from './tabs/ResearchStrength.vue'
import Cooperation from './tabs/Cooperation.vue' import Cooperation from './tabs/Cooperation.vue'
import OtherInfo from './tabs/OtherInfo.vue' import OtherInfo from './tabs/OtherInfo.vue'
// 从路由获取 orgId
const route = useRoute()
const orgId = computed(() => route.params.id || '')
// 提供给子组件使用
provide('orgId', orgId)
// 大学基本信息 // 大学基本信息
const universityInfo = ref({ const universityInfo = ref({
name: '哈佛大学', name: '哈佛大学',
......
...@@ -42,100 +42,127 @@ ...@@ -42,100 +42,127 @@
</template> </template>
<script setup lang="ts"> <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 AnalysisBox from '@/components/base/boxBackground/analysisBox.vue'
import LeftBtn from '@/components/base/pageBtn/leftBtn.vue' import LeftBtn from '@/components/base/pageBtn/leftBtn.vue'
import RightBtn from '@/components/base/pageBtn/rightBtn.vue' import RightBtn from '@/components/base/pageBtn/rightBtn.vue'
import AreaTag from '@/components/base/AreaTag/index.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<Array<{
const labsData = ref([ logo: string
{ name: string
logo: '/images/lab-wyss.png', description: string
name: '怀斯生物启发工程研究所', tags: Array<{ name: string; type: string }>
description: '成立于2009年,由汉斯约尔格·怀斯(Hansjörg Wyss)捐赠建立,聚焦仿生学与跨学科工程,推动医疗、机器人、材料等领域的突破。', }>>([])
tags: [{ name: '生物科技', type: 'tag2' }]
}, const policyData = ref<Array<{
{ title: string
logo: '/images/lab-rowland.png', description: string
name: '罗兰研究所', }>>([])
description: '原为独立研究机构,2002年并入哈佛,支持高风险、高回报的基础科学研究,尤其鼓励青年科学家。',
tags: [{ name: '物理学', type: 'tag6' }, { name: '化学', type: 'tag10' }] const currentPage = ref(1)
}, const pageSize = ref(6)
{ const totalCount = ref(0)
logo: '/images/lab-quantum.png', const totalPages = ref(0)
name: '哈佛量子计划', const loading = ref(false)
description: '跨学院合作平台,整合物理、工程、计算机科学等资源,推动量子科学与技术发展。',
tags: [{ name: '物理', type: 'tag6' }] const fetchLabList = async () => {
}, const orgId = props.orgId || '6789'
{ try {
logo: '/images/lab-broad.png', const result = await getLabList(orgId)
name: '博德研究所', if (result.success && result.data) {
description: '全球顶尖基因组学与生物医学研究中心,推动精准医学与疾病机制研究。', labsData.value = result.data.map((item: any) => ({
tags: [{ name: '医学', type: 'tag9' }] logo: item.logoUrl || '/images/lab-default.png',
}, name: item.labName,
{ description: item.introduction,
logo: '/images/lab-stem.png', tags: (item.arealist || []).map((area: string) => ({
name: '哈佛干细胞研究所', name: area,
description: '成立于2004年,联合哈佛医学院、牙医学院、文理学院等,推动干细胞基础研究与临床转化。', type: areaTypeMap[area] || 'tag12'
tags: [{ name: '医学', type: 'tag9' }] }))
}, }))
{ }
logo: '/images/lab-cfa.png', } catch (error) {
name: '哈佛大学天体物理中心', console.error('获取实验室列表失败:', error)
description: '由哈佛大学与史密森尼学会于1973年联合成立,是全球规模最大、最活跃的天体物理研究机构之一。',
tags: [{ name: '天体物理', type: 'tag3' }]
} }
]) }
// 政策文件数据 const fetchPolicyList = async () => {
const policyData = ref([ const orgId = props.orgId || '6789'
{ title: '《哈佛大学权利与责任声明》', description: '阐明学生在言论自由、学术自由、正当程序、尊重他人等方面的权利与义务。' }, loading.value = true
{ title: '《哈佛大学学术诚信政策》', description: '定义抄袭、作弊、伪造等学术不端行为,并规定处理流程。' }, try {
{ title: '《哈佛大学反歧视与反骚扰政策》', description: '禁止基于种族、性别、性取向、宗教、残疾等的歧视与骚扰,明确举报与调查机制。' }, const result = await getPolicyList(orgId, currentPage.value, pageSize.value)
{ title: '《研究合规与人类受试者保护政策》', description: '规范涉及人类受试者的研究(如医学、心理学、社会学实验),确保符合联邦法规(如Common Rule)。' }, if (result.success && result.data) {
{ title: '《哈佛法学院学术政策手册》', description: '详述J.D./LL.M./S.J.D.学位要求、课程规则、成绩制度、书面作业要求、出勤规定、荣誉毕业标准等。' }, policyData.value = result.data.content.map((item: any) => ({
{ title: '《哈佛文理研究生院学生手册》', description: '涵盖博士生资格考试、论文提交、助教职责、奖学金续期、学术进展评估等。' } title: `《${item.name}》`,
]) description: item.introduction
}))
// 分页相关 totalCount.value = result.data.totalElements
const currentPage = ref(5) totalPages.value = result.data.totalPages
const totalCount = ref(105) }
const totalPages = computed(() => Math.ceil(totalCount.value / 10)) } catch (error) {
console.error('获取政策文件列表失败:', error)
} finally {
loading.value = false
}
}
watch(currentPage, () => {
fetchPolicyList()
})
onMounted(() => {
fetchLabList()
fetchPolicyList()
})
const displayPages = computed(() => { const displayPages = computed(() => {
const pages: (number | string)[] = [] const pages: (number | string)[] = []
if (totalPages.value <= 7) { const total = totalPages.value
for (let i = 1; i <= totalPages.value; i++) pages.push(i) if (total <= 7) {
for (let i = 1; i <= total; i++) pages.push(i)
} else { } else {
pages.push(1) pages.push(1)
if (currentPage.value > 3) pages.push('...') if (currentPage.value > 3) pages.push('...')
const start = Math.max(2, currentPage.value - 1) 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) for (let i = start; i <= end; i++) pages.push(i)
if (currentPage.value < totalPages.value - 2) pages.push('...') if (currentPage.value < total - 2) pages.push('...')
pages.push(totalPages.value) pages.push(total)
} }
return pages return pages
}) })
// 上一页
const prevPage = () => { const prevPage = () => {
if (currentPage.value > 1) currentPage.value-- if (currentPage.value > 1) currentPage.value--
} }
// 下一页
const nextPage = () => { const nextPage = () => {
if (currentPage.value < totalPages.value) currentPage.value++ if (currentPage.value < totalPages.value) currentPage.value++
} }
// 跳转到指定页
const goToPage = (page: number | string) => { const goToPage = (page: number | string) => {
if (typeof page === 'number') currentPage.value = page if (typeof page === 'number') currentPage.value = page
} }
// 获取标签样式
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论