提交 56da96ca authored 作者: 朱政's avatar 朱政

Merge branch 'pre' into zz-dev

stages:
- build
- deploy
# cache:
# key: "$CI_COMMIT_REF_SLUG"
# paths:
# - .npm/
build_pre:
stage: build
image: node:20-bullseye
tags:
- risk-monitor-frontend
only:
- pre
script:
- node -v
- npm -v
- npm config set cache .npm --global
- npm ci --prefer-offline --no-audit --no-fund
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 hour
deploy_pre:
stage: deploy
image: alpine:3.20
tags:
- risk-monitor-frontend
only:
- pre
dependencies:
- build_pre
script:
- apk add --no-cache rsync
- rsync -av --delete dist/ /nas/kjb_service/zm/pre-project/html/
\ No newline at end of file
......@@ -4241,7 +4241,7 @@
},
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"license": "MIT",
......@@ -7527,7 +7527,7 @@
},
"node_modules/thenify": {
"version": "3.3.1",
"resolved": "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/thenify/-/thenify-3.3.1.tgz",
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
"dev": true,
"license": "MIT",
......@@ -7537,7 +7537,7 @@
},
"node_modules/thenify-all": {
"version": "1.6.0",
"resolved": "https://registry.npmmirror.com/thenify-all/-/thenify-all-1.6.0.tgz",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/thenify-all/-/thenify-all-1.6.0.tgz",
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
"dev": true,
"license": "MIT",
......@@ -7555,7 +7555,7 @@
},
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"dev": true,
"license": "MIT",
......
......@@ -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}`,
})
}
......
......@@ -5,6 +5,8 @@ import request from "@/api/request.js";
* @param {Object} params
* @param {string} [params.id] - 行业领域id(全部领域不传)
* @param {string} [params.companyName] - 公司名称(搜索框为空不传)
* @returns {Array<{id: string, name: string, marketChange: number|null, chainCompanyId: string|number}>}
* 说明:右侧详情查询当前建议优先使用 chainCompanyId;若缺失可回退 id,兼容后续接口调整
*/
export function getCompanyList(params) {
return request({
......
......@@ -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
}
......@@ -2,67 +2,66 @@ import request from "@/api/request.js";
// 中美博弈概览V2:最新风险动态统计
export function getLatestRiskUpdates(params) {
return request({
method: 'GET',
url: `/api/rivalryIndexV2/LatestRiskUpdates`,
params: params
})
return request({
method: "GET",
url: `/api/rivalryIndexV2/LatestRiskUpdates`,
params: params
});
}
// 中美博弈概览V2:最新风险信号
export function getLatestRisks() {
return request({
method: 'GET',
url: `/api/rivalryIndexV2/LatestRisks`,
})
return request({
method: "GET",
url: `/api/rivalryIndexV2/LatestRisks`
});
}
// 中美博弈概览V2:美对华制裁措施数量趋势
export function geDomainContainmentTrend(params) {
return request({
method: 'GET',
url: `/api/rivalryIndexV2/DomainContainmentTrend`,
params: params
})
return request({
method: "GET",
url: `/api/rivalryIndexV2/DomainContainmentTrend`,
params: params
});
}
// 中美博弈概况:获取榜单字典
export function getChartDict() {
return request({
method: 'GET',
url: `/api/union/summary/chartDict`,
})
return request({
method: "GET",
url: `/api/union/summary/chartDict`
});
}
// 根据字典信息,获取年份信息
export function getYearDict(id) {
return request({
method: "GET",
url: `/api/union/summary/chartYear/${id}`
});
}
// 中美博弈概况:中美科技实力对比
export function getCompare(id) {
return request({
method: 'GET',
url: `/api/union/summary/compare/${id}`,
})
export function getCompare(id, year) {
return request({
method: "GET",
url: `/api/union/summary/compare/${id}/${year}`
});
}
// 中美博弈分析
export function getTechnologyGameAnalysis() {
return request({
method: 'GET',
url: `/api/rivalryIndexV2/TechnologyGameAnalysis`,
})
return request({
method: "GET",
url: `/api/rivalryIndexV2/TechnologyGameAnalysis`
});
}
//中美博弈概览V7:美国政府部门对华制裁最新动态
export function getGovernmentSanctionsDynamics() {
return request({
method: 'GET',
url: `/api/rivalryIndex/governmentSanctionsDynamics`,
})
}
\ No newline at end of file
return request({
method: "GET",
url: `/api/rivalryIndex/governmentSanctionsDynamics`
});
}
......@@ -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,
......
......@@ -13,15 +13,15 @@ import '@/assets/fonts/font.css'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
// import AreaTag from '@/components/base/AreaTag/index.vue'
// import LeftBtn from "@/components/base/PageBtn/LeftBtn.vue";
// import RightBtn from "@/components/base/PageBtn/RightBtn.vue";
// import OverviewMainBox from "@/components/base/BoxBackground/OverviewMainBox.vue";
// import OverviewNormalBox from "@/components/base/BoxBackground/OverviewNormalBox.vue";
// import AnalysisBox from '@/components/base/BoxBackground/AnalysisBox.vue'
// import NewsList from '@/components/base/NewsList/index.vue'
// import LeftBtn from "@/components/base/pageBtn/leftBtn.vue";
// import RightBtn from "@/components/base/pageBtn/rightBtn.vue";
// import OverviewMainBox from "@/components/base/boxBackground/overviewMainBox.vue";
// import OverviewNormalBox from "@/components/base/boxBackground/overviewNormalBox.vue";
// import AnalysisBox from '@/components/base/boxBackground/analysisBox.vue'
// import NewsList from '@/components/base/newsList/index.vue'
// import ModuleHeader from '@/components/base/ModuleHeader/index.vue'
// import RiskSignal from "@/components/base/RiskSignal/index.vue";
// import MessageBubble from "@/components/base/MessageBubble/index.vue";
// import RiskSignal from "@/components/base/riskSignal/index.vue";
// import MessageBubble from "@/components/base/messageBubble/index.vue";
// import SourceTabLsit from '@/components/base/SourceTabList/index.vue'
// import ActionButton from "@/components/base/ActionButton/index.vue"
......
......@@ -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
......@@ -27,8 +27,8 @@
<pre>
{{
`
import LeftBtn from '@/components/base/PageBtn/LeftBtn.vue'
import RightBtn from '@/components/base/PageBtn/RightBtn.vue'
import LeftBtn from '@/components/base/pageBtn/leftBtn.vue'
import RightBtn from '@/components/base/pageBtn/rightBtn.vue'
<LeftBtn />
<RightBtn />
......
// 使用 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");
}
}
}
......
......@@ -18,6 +18,15 @@
>
<el-option :value="value.id" :label="value.name" v-for="(value, index) in originList" :key="index" />
</el-select>
<el-select
class="select-item"
size="default"
style="margin-left: 15px; width: 200px; height: 32px"
v-model="year"
@change="handleGetCompare()"
>
<el-option :value="value" :label="value" v-for="(value, index) in yearList" :key="index" />
</el-select>
</div>
</div>
<div style="display: flex; height: 650px; width: 100%; padding-top: 12px">
......@@ -237,7 +246,7 @@ import Echarts from "@/components/Chart/index.vue";
import mockData from "./mock.json";
import radarChart from "./radarChart3.js";
import { getCompare, getChartDict, getTechnologyGameAnalysis } from "@/api/zmOverview/risk/index.js";
import { getCompare, getChartDict, getYearDict, getTechnologyGameAnalysis } from "@/api/zmOverview/risk/index.js";
import icon1 from "./icon/btn-icon-0.png";
import icon2 from "./icon/btn-icon-1.png";
import icon3 from "./icon/btn-icon-2.png";
......@@ -351,10 +360,27 @@ const handleGetChartDict = async () => {
console.error("获取数据来源error", error);
}
};
const yearList = ref([]);
const year = ref("");
//年份
const handleGetYearDict = async () => {
try {
const res = await getYearDict(origin.value);
console.log("年份", res);
if (res.code === 200 && res.data) {
yearList.value = res.data;
year.value = res.data[0];
}
} catch (error) {
console.error("获取年份error", error);
}
};
//中美科技实力对比
const handleGetCompare = async () => {
try {
const res = await getCompare(origin.value);
const res = await getCompare(origin.value, year.value);
console.log("中美科技实力对比", res);
if (res.code === 200 && res.data) {
tableData.value = res.data[0].children;
......@@ -392,6 +418,7 @@ const handlegetTechnologyGameAnalysis = async () => {
};
onMounted(async () => {
await handleGetChartDict();
await handleGetYearDict();
await handleGetCompare();
await handlegetTechnologyGameAnalysis();
// const dom = document.getElementById("char");
......@@ -566,6 +593,7 @@ const lineOption = ref({
},
yAxis: {
type: "value",
min: 77,
// name: "指数",
nameLocation: "top",
nameGap: 35,
......
......@@ -37,7 +37,7 @@
<fourSuppress></fourSuppress>
<!-- 中美博弈概况 -->
<commonTitle id="zm-overview" title="中美博弈概况" style="margin-top: 64px; margin-bottom: 36px"></commonTitle>
<gameProfile></gameProfile>
<gameProfile />
<div class="bottom-info">
<div class="info-item">
<div class="info-item-left">
......
......@@ -662,10 +662,10 @@ const handleGetBillsPerson = async () => {
sortFun: !memberSortFun.value
};
if (footerSelect1.value !== "全部委员会") params.committeeId = footerSelect1.value;
if (!activeYyList.value.includes("全部议院")) params.congressIds = activeYyList.value;
if (!activeDpList.value.includes("全部党派")) params.partyIds = activeDpList.value;
if (!activeAreaList.value.includes("全部领域")) params.domainIds = activeAreaList.value;
if (footerSelect1.value !== "全部委员会") params.committeeIds = footerSelect1.value;
if (!activeYyList.value.includes("全部议院")) params.congressIds = activeYyList.value.join(",");
if (!activeDpList.value.includes("全部党派")) params.partyIds = activeDpList.value.join(",");
if (!activeAreaList.value.includes("全部领域")) params.domainIds = activeAreaList.value.join(",");
const formatDateYm = dateStr => {
if (!dateStr) return "";
......
......@@ -13,7 +13,7 @@
<div class="committee-cards-filter">
<span class="committee-cards-desc">近期美国国会各委员会涉华提案数量汇总</span>
<el-radio-group v-model="committeeTimeRange" class="committee-time-switch" size="default">
<el-radio-button v-for="item in committeeTimeOptions" :key="item.value" :label="item.value">
<el-radio-button v-for="item in committeeTimeOptions" :key="item.value" :value="item.value">
<span class="committee-time-switch-inner">
<el-icon v-if="committeeTimeRange === item.value" class="committee-time-switch-icon">
<Calendar />
......@@ -146,7 +146,7 @@
<div v-else id="box5Chart" class="overview-chart"></div>
</div>
<div class="overview-tip-row">
<TipTab class="overview-tip" :text="'图表说明:xxxxx,数据来源:美国国会官网'" />
<TipTab class="overview-tip" :text="'涉华科技法案数量及通过率变化趋势,数据来源:美国国会官网'" />
<AiButton class="overview-tip-action" @mouseenter="handleShowAiPane('box5')" />
</div>
<div v-if="aiPaneVisible.box5" class="overview-ai-pane" @mouseleave="handleHideAiPane('box5')">
......@@ -171,7 +171,7 @@
<div v-else id="box9Chart" class="overview-chart"></div>
</div>
<div class="overview-tip-row">
<TipTab class="overview-tip" :text="'图表说明:xxxxx,数据来源:美国国会官网'" />
<TipTab class="overview-tip" :text="'涉华科技法案领域分布情况,数据来源:美国国会官网'" />
<AiButton class="overview-tip-action" @mouseenter="handleShowAiPane('box6')" />
</div>
<div v-if="aiPaneVisible.box6" class="overview-ai-pane" @mouseleave="handleHideAiPane('box6')">
......@@ -193,7 +193,7 @@
<div v-else id="box7Chart" class="overview-chart"></div>
</div>
<div class="overview-tip-row">
<TipTab class="overview-tip" :text="'图表说明:xxxxx,数据来源:美国国会官网'" />
<TipTab class="overview-tip" :text="'提出涉华科技法案委员会分布情况,数据来源:美国国会官网'" />
<AiButton class="overview-tip-action" @mouseenter="handleShowAiPane('box7')" />
</div>
<div v-if="aiPaneVisible.box7" class="overview-ai-pane" @mouseleave="handleHideAiPane('box7')">
......@@ -216,7 +216,7 @@
</template>
</div>
<div class="overview-tip-row">
<TipTab class="overview-tip" :text="'图表说明:xxxxx,数据来源:美国国会官网'" />
<TipTab class="overview-tip" :text="'涉华科技法案立法进展分布情况,数据来源:美国国会官网'" />
<AiButton class="overview-tip-action" @mouseenter="handleShowAiPane('box8')" />
</div>
<div v-if="aiPaneVisible.box8" class="overview-ai-pane" @mouseleave="handleHideAiPane('box8')">
......@@ -231,7 +231,7 @@
<WordCloundChart v-else class="overview-chart" width="100%" height="100%" :data="wordCloudData" />
</div>
<div class="overview-tip-row">
<TipTab class="overview-tip" :text="'图表说明:xxxxx,数据来源:美国国会官网'" />
<TipTab class="overview-tip" :text="'涉华科技法案关键条款词云,数据来源:美国国会官网'" />
<AiButton class="overview-tip-action" @mouseenter="handleShowAiPane('box9')" />
</div>
<div v-if="aiPaneVisible.box9" class="overview-ai-pane" @mouseleave="handleHideAiPane('box9')">
......@@ -271,7 +271,7 @@ import {
import { getPersonSummaryInfo } from "@/api/common/index";
import { getChartAnalysis } from "@/api/aiAnalysis/index";
import DivideHeader from "@/components/DivideHeader.vue";
import overviewMainBox from "@/components/base/BoxBackground/OverviewMainBox.vue";
import overviewMainBox from "@/components/base/boxBackground/overviewMainBox.vue";
import OverviewCard from "./OverviewCard.vue";
import ResourceLibrarySection from "./ResourceLibrarySection.vue";
import { useContainerScroll } from "@/hooks/useScrollShow";
......@@ -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: '议院委员会',
......@@ -910,12 +917,16 @@ const handleGetKeyTK = async () => {
const res = await getBillOverviewKeyTK();
console.log("关键条款", res);
if (res.code === 200 && res.data) {
wordCloudData.value = res.data.map(item => {
wordCloudData.value = res.data
.slice()
.sort((a, b) => (b.count ?? 0) - (a.count ?? 0))
.slice(0, 20)
.map(item => {
return {
name: item.clause,
value: item.count
};
});
});
}
} catch (error) {
console.error("获取关键条款error", error);
......@@ -993,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);
......@@ -1175,7 +1187,7 @@ const handleBox8Data = async () => {
const selectParam = {
moduleType: '国会法案',
key: '领域',
key: '所处阶段',
selectedDate: box8selectetedTime.value,
isInvolveCn: 1
}
......@@ -1208,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%',
......
......@@ -72,12 +72,12 @@ const mainHeaderBtnList = ref([
name: "影响分析",
path: "/billLayout/influence"
},
{
icon: icon4,
activeIcon: icon4Active,
name: "相关情况",
path: "/billLayout/relevantCircumstance"
}
// {
// icon: icon4,
// activeIcon: icon4Active,
// name: "相关情况",
// path: "/billLayout/relevantCircumstance"
// }
]);
const activeTitle = ref("法案概况");
......
......@@ -16,6 +16,12 @@ import { onMounted, ref, computed, watch } from "vue";
import { useRoute } from "vue-router";
import router from '@/router'
import SiderTabs from "@/components/base/SiderTabs/index.vue";
//分支pre自动部署测试:
//第一次测试
//第二次测试
//第三次测试
//第四次测试
//第五次测试
const route = useRoute();
......
......@@ -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,26 +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 () => {
console.log('[v0] Step3 mounted, fetching data...')
const data = await fetchPredictionAnalysis()
console.log('[v0] Data fetched:', data)
predictionData.value = data
console.log('[v0] predictionData set:', predictionData.value)
})
// 预测分析数据 - 从 props 获取,暂无真实接口
const predictionData = computed(() => props.data || null)
// 根据索引和progressLevel返回进度条格子的类名
function getOverallSegmentClass(index: number) {
......@@ -143,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;
......@@ -160,7 +160,6 @@ function handleRepredict() {
.header-section {
justify-content: space-between;
align-items: flex-start;
/* margin-bottom: 24px; */
}
.header-left {
......
......@@ -160,7 +160,8 @@ const curCompanyId = ref("");
const handleClickCompany = (val, index) => {
companyActiveIndex.value = (currentPage.value - 1) * pageSize.value + index;
if (val) {
curCompanyId.value = val.id;
// 右侧鱼骨图查询:当前优先使用 chainCompanyId,后续可平滑切换到 id
curCompanyId.value = val.chainCompanyId || val.id;
// handleGetCompanyDetail();
companyInfo.value.name = val.name;
companyInfo.value.status = val.status || companyInfo.value.status;
......@@ -308,10 +309,10 @@ const headerChartData = (row) => {
switch (contentType.value) {
case 1:
onDecreeRelatedChain(row.id)
onDecreeRelatedChain(row.chainCompanyId)
break;
case 2:
onDecreeRelatedEntitie(row.id)
onDecreeRelatedEntitie(row.chainCompanyId)
break;
}
};
......
......@@ -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;
......
......@@ -114,7 +114,7 @@
</template>
<script setup>
import RiskSignal from "@/components/base/RiskSignal/index.vue";
import RiskSignal from "@/components/base/riskSignal/index.vue";
import { ref, onMounted, computed } from "vue";
import router from "@/router";
import { getCoopRestrictionTrends, getCoopRestrictionSignals } from "@/api/coopRestriction/coopRestriction.js";
......
......@@ -5,7 +5,7 @@
<script setup>
import { ref, onMounted, nextTick } from 'vue'
import setChart from '@/utils/setChart';
import getBarChart from './barchart';
import getBarChart from './barChart.js';
const props = defineProps({
barChartData: {
......
......@@ -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
......@@ -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
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论