提交 9a51cfe7 authored 作者: yanpeng's avatar yanpeng

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

流水线 #241 已通过 于阶段
in 1 分 37 秒
# 美国对华科技政令分析报告(2025-21665)
## 1.政令概览
**美国通过总统行政命令全面整合国家科技资源,以应对全球关键技术领域的竞争格局变化,重点推动人工智能、半导体、量子信息、先进制造等八大战略方向的协同突破。** 该政令依托国家科学与技术政策备忘录2号的授权,构建统一的联邦级科学与安全平台,整合超级计算能力、多源科学数据与智能代理系统,旨在重塑关键技术研发体系与基础设施优势。尽管政令涵盖多个前沿科技领域并设置七项具体条款,其中六项直接关联科技研发,但未明确指向任何特定国家或实体,体现出以能力构建为核心、以系统性优势为路径的政策导向。政令于2025年11月24日签署,28日正式生效,标志着美国在科技安全与创新协同机制上的重大制度升级。
### 基本情况
**政令名称**为“启动创世纪任务”,**政令编号****14363**,由**白宫**颁布,于**2025-11-24**由总统签署,并于**2025-11-28**正式颁布生效。
### 政令背景
中国在人工智能、半导体、量子信息科学和先进制造等领域持续加速技术突破,引发美国对全球科技主导权的担忧。
### 政令简介
《启动创世纪任务》通过构建统一的美国科学与安全平台,整合联邦超级计算资源、多领域科学数据集与AI代理系统,推动人工智能在先进制造、生物技术、关键材料、核能、量子信息及半导体等战略领域的颠覆性突破,同时建立严格的安全管控与国际合作框架,强化美国在关键科技赛道的全球主导地位,尤其聚焦对华科技竞争中的基础设施与研发优势重构。
### 法律依据
**国家科学与技术备忘录2**,即**国家科学与技术政策备忘录2号(2025年9月23日)**,作为本项政令的直接法律依据,明确了美国在科技领域对华政策的行政指导框架,确立了相关管制措施的授权基础与执行路径。
### 数据概览
**条款总数****7条**,其中**科技条款总数****6条****涉华科技条款总数****0条**。政令涉及领域共涵盖**8个**,分别为**人工智能****先进制造****集成电路****核****生物科技****新材料****量子科技****新能源**
**风险研判**
**该政令通过技术整合与资源集中,可能加剧全球科技体系的碎片化趋势。** 美国将超级计算与AI代理系统深度绑定国家战略产业,形成封闭式创新闭环,易引发盟友阵营的技术依赖与非盟友国家的反制性技术脱钩,长期可能催生多极化技术标准体系,增加全球供应链的协调成本与不确定性。
**战略目标解读**
**政令旨在重构美国在前沿科技领域的系统性优势,而非单纯遏制单一国家。** 通过整合八大高技术领域资源,建立跨部门协同的国家级科研平台,其核心是提升研发效率与技术转化速度,确保在下一代计算、材料与能源等关键赛道中保持领先,形成以基础设施为支点的长期竞争壁垒。
**对华影响**
**中国虽未被直接点名,但政令的实施将间接强化对华技术围堵的底层架构。** 美国通过强化自身在AI、量子与半导体等领域的研发能力,加速技术代差扩大,可能压缩中国获取高端设备与算法工具的外部空间,迫使中国在基础科研与自主生态建设上投入更大资源以对冲外部压力。
## 2.政令深度分析
### 2.1 条款分析
**科技政策体系以人工智能为核心引擎,构建了覆盖先进制造、集成电路、核能等六大关键领域的国家级协同研发框架,通过统一的“美国科学与安全平台”整合联邦计算资源、AI模型与安全数据基础设施,形成以白宫科技政策办公室统筹、能源部主导执行、多部门协同推进的层级化治理结构,27项核心任务聚焦平台建设、资源标准化与跨机构协作,时间轴明确锚定在30至270天内分阶段落地,而生物科技、新材料、量子科技与新能源等新兴领域虽被纳入国家科技挑战清单并要求动态更新,但其政策实施仍依附于主平台机制,整体呈现以AI为中枢、多领域梯次跟进、执行高度制度化与集中化的战略布局特征。**
#### 2.1.1 科技条款领域分布
人工智能领域以5项条款居首,显著领先于其他领域;先进制造与核领域并列第二,均为3项,显示其在科技政策中的稳定地位;集成电路与新材料领域各占2项,处于中等活跃水平;生物科技、量子科技与新能源领域均为1项,表明其在当前条款分布中仍属新兴或低频关注方向,整体呈现头部集中、尾部分散的分布特征。
![](./out/img/300d35e7-3c44-41cf-8ec7-ba1636ce8587/bar_20260325_154830_11b7d6cb.png)
结论:科技条款分布高度集中于人工智能,其余领域呈现梯队式分化,新兴技术领域整体关注度仍显不足。
#### 2.1.2 科技条款内容
#### 人工智能
人工智能驱动的“Genesis Mission”国家重大计划旨在通过构建统一的“美国科学与安全平台”,整合联邦计算资源、多领域AI基础模型、安全数据集与自动化实验工具,加速科学发现与技术创新;该计划由总统科技助理统筹协调跨部门协作,能源部长负责具体实施与资源整合,强制推行严格的数据安全、访问授权与网络安全标准,并配套建立联邦奖学金、跨机构资助机制与标准化合作框架,同时明确知识产权与商业化政策,全面推动AI在科学研发中的深度应用,确保国家安全、公共利益与创新活力的协同提升。
人工智能领域举措以多部门协同为核心,形成以白宫科学和技术政策办公室、能源部及国家科学技术委员会为主导的治理架构。高层机构通过任务驱动机制向下分解具体行动,27项执行节点集中于平台建设、资源整合、安全合规与跨机构协作,显示政策执行高度聚焦于系统性基础设施构建与标准化流程确立,呈现从战略规划到操作落地的清晰传导路径。
![](./out/img/300d35e7-3c44-41cf-8ec7-ba1636ce8587/mind_map_20260325_154842_3809c030.png)
结论:该举措通过集中联邦资源与跨机构协同,构建以AI驱动科学突破为核心的国家平台,凸显美国在关键科技领域强化系统性布局与公私协同的战略意图。
#### 先进制造
先进制造领域通过建立并运营美国科学与安全平台,整合联邦高性能计算资源、AI建模框架与安全数据环境,构建覆盖全科学领域的领域基础模型与AI赋能的预测、模拟与设计优化工具,支持自主与AI增强的实验与制造流程;平台聚焦高影响力制造场景,确保符合国家安全与网络安全标准,系统性整合能源部国家实验室、学术机构及私营部门的计算、数据与设施资源,并在限定时间内完成初始能力部署与挑战清单扩展;通过年度报告机制持续追踪平台运行、资源整合、用户参与及科研突破,推动先进制造等国家科技挑战的加速突破与技术转化。
先进制造领域举措由白宫管理和预算办公室、科学和技术政策办公室、国家科学技术委员会参与机构、能源部及国务院共同推动,形成以能源部为核心执行主体的多层级协同架构。各机构在90至270天内密集部署平台建设、资源评估、数据整合与能力审查等关键任务,同步建立国家科技挑战清单机制与年度更新流程,体现政策推进的阶段性、系统性与跨部门联动特征。
![](./out/img/300d35e7-3c44-41cf-8ec7-ba1636ce8587/mind_map_20260325_154853_0937faf9.png)
结论:该举措通过构建统一计算与实验平台,系统整合联邦资源与跨机构协作机制,以明确时间轴和责任分工推动先进制造领域国家科技挑战的规模化攻关。
#### 集成电路
集成电路领域被明确列为美国科学与安全平台的优先研发方向之一,该平台通过整合联邦高性能计算资源、AI驱动的建模与仿真工具、领域专用基础模型及安全数据集,系统性支持半导体与微电子领域的前沿研发;平台将在法案生效后60天内识别至少20项国家重要技术挑战,并在90至270天内完成资源盘点、数据资产标准化、网络安全框架构建及AI增强实验设施评估,旨在加速芯片设计优化、制造自动化与新材料开发,同时确保所有操作符合国家安全与供应链安全标准,推动美国在集成电路领域的技术自主与全球竞争力。
集成电路领域举措由国家科学技术委员会、能源部、国务院及白宫科学和技术政策办公室协同推动,形成以平台建设为核心的多层级任务体系。能源部主导技术平台构建与资源评估,国务院负责国家科技挑战清单的识别与更新,白宫科学和技术政策办公室协调跨机构整合,国家科学技术委员会统筹执行。各机构在90至270天内分阶段推进计算资源整合、数据标准化、设施能力审查与初始能力验证,时间轴呈现清晰的阶段性递进特征。
![](./out/img/300d35e7-3c44-41cf-8ec7-ba1636ce8587/mind_map_20260325_154904_6074e105.png)
结论:该举措通过跨部门协同与明确时间锚点,构建了以国家科技挑战为导向、平台为枢纽的集成电路研发协同体系。
#### 核
核:建立并运营一个国家级“美国科学与安全平台”,整合能源部国家实验室及联邦与行业伙伴的高性能计算、AI建模、数据资源与智能实验设施,重点支撑核裂变与核聚变等国家科技挑战,通过安全可控的AI驱动模型训练、模拟优化、合成数据生成与自动化研发工具,加速前沿核能科研与工程突破;平台需在法定时限内完成资源识别、数据资产标准化、网络安全框架构建与设施审查,并建立年度报告机制,确保平台运行符合国家安全标准,推动跨机构协同研发,提升美国在核能领域的科技竞争力与自主创新能力。
核领域举措由五大联邦机构协同推动,其中能源部主导平台建设与技术评估,涉及计算资源整合、数据资产标准化、AI驱动实验能力提升等11项具体任务,时间跨度从90天至年度周期,形成递进式实施路径。国务院负责国家科技挑战清单的初始识别与年度更新,白宫科学和技术政策办公室协同国家科学技术委员会协调任务扩展与跨机构对齐,体现多层级、分阶段、任务导向的治理结构。
![](./out/img/300d35e7-3c44-41cf-8ec7-ba1636ce8587/mind_map_20260325_154914_854d35fa.png)
结论:该举措以能源部为核心执行主体,通过联邦机构协同与时间轴驱动的任务分解,构建了以AI和高性能计算为支撑、安全合规为前提的国家核科技研发协同体系。
#### 生物科技
生物科技被明确列为国家科学技术挑战的六大优先领域之一,纳入国家使命框架下的核心支持范围;相关机构须依据由总统科学技术顾问委员会与国家科学技术委员会协同制定并年度更新的扩展清单开展研发活动==4、(c)在完成第(b)款所述扩展清单后,参与本使命的各机构应利用该平台推进与扩展清单中所确定的国家科技挑战相一致的研究与开发工作,须符合适用法律及其各自使命,并受可用拨款限制==,该清单需反映技术进展、新兴国家需求及政府研发优先事项的一致性,涵盖至少20项具有国家重要性的生物技术挑战,并整合各成员机构提案,其实施受法律约束与拨款限制,确保在《国家科学技术备忘录2》定义的优先方向下系统推进生物科技领域的突破性创新。
生物科技领域举措以国家科技挑战为核心,形成由国务院与白宫科学和技术政策办公室主导的多层次协同机制。国务院负责初始清单制定,白宫机构负责协调扩展,国家科学技术委员会参与机构推动研发落地,年度更新机制确保挑战清单与研发优先事项动态对齐。各层级职责明确,流程闭环,体现政策执行的系统性与持续性。
![](./out/img/300d35e7-3c44-41cf-8ec7-ba1636ce8587/mind_map_20260325_154925_99bec68c.png)
结论:国家科技挑战的制定与实施形成以行政主导、跨部门协同、动态更新为特征的闭环治理体系,强化生物科技领域战略资源的集中配置与长期对齐。
#### 新材料
新材料领域通过识别至少20项国家优先科学技术挑战,系统推动先进制造与关键材料等核心方向的研发突破;由APST与NSTC协同扩展并动态更新挑战清单,确保与国家需求和新兴技术趋势同步;要求联邦机构依托平台推进相关研发,每年评估国家实验室与研究设施的资源整合、用户参与(含学生培训)、科研成果(如突破性论文与原型技术)、公私合作与技术转化成效,并据此提出权限或跨机构支持建议,构建全链条、可衡量、持续优化的新材料创新生态系统。
新材料领域举措由五大核心机构协同推动,形成以国务院、白宫科学和技术政策办公室、国家科学技术委员会、能源部及管理和预算办公室为关键节点的多层级联动体系。二级节点聚焦清单制定、协调扩展、研发推进与年度更新等任务,三级节点明确时间节点与责任主体,体现政策执行的流程化与周期性特征,整体呈现从顶层设计到落地实施的递进式治理结构。
![](./out/img/300d35e7-3c44-41cf-8ec7-ba1636ce8587/mind_map_20260325_154936_50d08430.png)
结论:国家科技挑战的系统性管理通过跨机构协同与定期审查机制,实现从清单识别到研发整合的闭环运行,强化了新材料领域战略优先事项的动态响应能力。
#### 量子科技
量子科技被明确列为国家科学技术挑战的优先领域之一,美国政府将识别至少20项具有国家重要性的关键科技挑战==4、(a)在本命令颁布之日起60天内,部长应识别并提交一份详细清单至总统科学技术顾问委员会(APST),列出至少20项具有国家重要性、部长评估可通过本使命加以解决的科学技术挑战,这些挑战须涵盖2025年9月23日《国家科学技术备忘录2号》所确定的优先领域,包括:(i)先进制造==,其中量子信息科学为核心方向==4、(v)量子信息科学==;总统科学技术顾问委员会与国家科学技术委员会将在30天内联合制定扩展清单==4、(b)在提交第(a)款所述清单后30天内,总统科学技术顾问委员会(APST)应审查该拟议清单,并与国家科学技术委员会(NSTC)的参与机构合作,协调制定一份扩展清单,作为本使命拟应对的首批国家科技挑战,该清单应包括通过NSTC由各参与机构提出的其他挑战,但须受可用拨款限制==,作为国家使命驱动研发的基准框架,相关机构须据此推进对齐研究,并由部长会同委员会每年审查更新清单==4、(d)此后每年,部长应与总统科学技术顾问委员会(APST)和国家科学技术委员会(NSTC)协商,审查并更新挑战清单,以反映已取得的进展、新兴的国家需求以及与本政府研究与开发优先事项的一致性==,动态响应技术进展与国家战略需求,构建系统化、持续演进的量子科技国家研发体系。
量子科技领域举措呈现多层级协同治理结构,国家科学技术委员会、国务院与白宫科学和技术政策办公室构成核心决策节点,分别主导清单制定、审查与跨机构协调。国务院负责初始挑战清单的识别与年度更新,白宫机构主导协同扩展与战略对齐,各参与方通过制度化流程推动研发活动与国家优先事项一致,体现政策执行的系统性与动态响应特征。
![](./out/img/300d35e7-3c44-41cf-8ec7-ba1636ce8587/mind_map_20260325_154946_b4e086c9.png)
结论:量子科技发展由顶层机构协同驱动,形成以清单管理为核心、动态更新为机制、跨部门联动为特征的国家科技治理模式。
#### 新能源
新能源领域要求能源部秘书在本命令生效后一年内及之后每年向总统提交年度报告,全面汇报新能源相关平台的运行状态与能力,重点包括国家实验室及联邦研究伙伴在计算资源、数据基础设施和研究设施方面的共享进展;用户参与情况,特别是学生研究人员的参与与培训活动;研究工作进展与可衡量的科学突破、论文发表和原型技术成果;公私合作伙伴关系的范围、合作项目及技术转化与商业化成效;并提出为实现新能源使命目标所需的新授权或跨机构支持建议,以推动新能源技术创新与系统性发展。
新能源领域举措由三大核心机构协同推动,能源部、白宫管理和预算办公室、白宫科学和技术政策办公室均作为关键节点接入顶层决策框架。所有关联路径均指向单一执行指令,表明政策执行高度集中,信息流与责任链呈单向收敛结构,体现跨部门协同的统一性与指令性特征。
![](./out/img/300d35e7-3c44-41cf-8ec7-ba1636ce8587/mind_map_20260325_154957_17ad9ce2.png)
结论:新能源政策执行高度依赖三大机构协同,且所有责任最终汇聚于单一年度报告机制,凸显集中化决策与标准化监控的治理模式。
**该科技战略体系通过集中资源构建统一国家平台,以人工智能为引擎、先进制造与核能为支柱,形成高度结构化、任务导向的跨部门协同治理模式,其本质是通过制度化清单管理、强制性时间轴与标准化安全框架,系统性压缩关键技术突破周期并强化对全球科技主导权的掌控;风险层面,该架构将科研活动深度绑定国家安全议程,可能加剧技术脱钩与研发封闭化,尤其在芯片、量子与生物领域设置排他性准入机制,实质构建了以美国为中心的科技同盟壁垒;战略目标上,其不仅追求技术领先,更意图通过平台化整合重塑全球创新生态的规则制定权,迫使他国在技术标准、数据流动与供应链协作上被动适应;对华影响则体现为多维压制:一方面通过出口管制与实体清单限制关键设备与人才流动,另一方面以“安全合规”为名构建技术隔离区,使中国在AI训练数据、高性能算力、先进材料与核技术等核心环节面临更严苛的外部约束,同时其年度评估与清单动态更新机制,意味着对华技术竞争已从单点封锁转向长期、系统、可迭代的制度性围堵。**
## 3.风险研判
### 风险研判
该政令通过构建封闭式国家科技平台,将高性能计算、人工智能代理与敏感科学数据深度绑定,形成以国家安全为名的制度性技术壁垒。其核心风险在于推动全球科技体系向“阵营化”演进:一方面,美国通过强制数据访问标准、供应链安全审查与知识产权管控,将盟友纳入其技术生态,加剧全球研发资源的碎片化;另一方面,非盟友国家面临技术准入被系统性排除的风险,可能触发多边技术脱钩、标准互不兼容与创新生态割裂,显著提升全球科技协作的制度性成本与不确定性。平台所依赖的动态挑战清单机制,使技术围堵具备持续迭代能力,长期可能固化美国在关键领域的技术代差优势。
### 对华影响
尽管政令未直接点名中国,但其资源集中机制与安全准入框架构成对华技术压制的底层架构。中国在人工智能训练数据、高性能算力获取、先进材料研发与核能仿真工具等方面将面临更严苛的外部约束。美国通过平台整合国家实验室、私营企业与高校资源,形成闭环式创新体系,压缩中国获取高端算法模型、实验设施与协同研发渠道的外部空间。同时,年度挑战清单更新机制与公私合作准入标准,使技术隔离从单点管制升级为制度性、可扩展的长期围堵,迫使中国在基础科研、自主算力体系与国产材料替代上投入更大资源以对冲系统性压力。
### 政策建议
应加快构建自主可控的国家级科研协同基础设施,推动科学数据资源的标准化整合与安全共享机制建设。强化基础研究与前沿技术的系统性布局,建立跨部门、跨领域的研发任务清单动态响应机制。完善科技人才跨境流动与国际合作的法律保障框架,鼓励本土企业与科研机构通过非传统路径参与国际科研协作。推动形成符合自身发展需求的技术标准体系与安全评估规范,提升在关键领域自主定义技术路径与规则的能力。建立科技竞争风险预警与政策弹性调整机制,增强对国际技术规则演变的预判与应对韧性。
## 4.总结
美国通过总统行政命令14363启动“创世纪任务”,依托国家科学与技术政策备忘录2号,构建统一的“美国科学与安全平台”,以人工智能为引擎,整合联邦超级计算、AI模型、多领域数据集与实验设施,系统推进人工智能、先进制造、集成电路、核能、生物科技、新材料、量子科技与新能源八大战略方向的协同突破。政令虽未明确指向特定国家,但其通过制度化清单管理、强制性时间轴与安全合规框架,构建了高度集中、闭环运行的国家科技研发体系,旨在重塑全球科技主导权。该体系以基础设施为支点、以安全为名构建技术壁垒,其深层意图在于长期巩固美国在下一代关键技术领域的系统性优势,间接加剧全球科技体系的分裂风险,并对中国形成多维度、制度性、可迭代的技术围堵压力。
## 5.附录
| 条款编号 | 条款内容摘要 |
|----------|--------------|
| 1 | 宗旨:正式启动“创世计划”,作为国家级努力,整合联邦科学数据、AI模型、国家实验室、大学与企业资源,构建AI驱动的科学发现平台,以加速科学突破、强化国家安全、提升能源主导力与技术领导力,类比“曼哈顿计划”的历史意义。 |
| 2 | 创世任务设立:由能源部长负责在能源部内实施,总统科技助理提供总体领导,通过国家科学与技术委员会协调跨部门协作。 |
| 3 | 美国科学与安全平台运行:能源部长须建立并运营平台,整合高性能计算资源、AI建模框架、预测与仿真工具、领域基础模型、安全数据集(含专有、联邦、开放与合成数据)、AI增强实验与制造工具;确保符合国家安全、分类、隐私、知识产权与网络安全标准;90日内识别联邦计算资源,120日内完成相关部署。 |
| 4 | 识别国家科技挑战:60日内识别至少20项涵盖先进制造、生物技术、关键材料、核裂变与聚变、量子信息科学、半导体与微电子的国家科技挑战;30日内由总统科技顾问委员会与国家科学与技术委员会联合制定扩展清单;各机构据此推进研发;此后每年更新清单,反映进展与优先事项变化。 |
| 5 | 机构间协调与外部合作:总统科技助理通过国家科学与技术委员会协调机构,避免重复、促进互操作、整合数据与基础设施;发起跨机构资助或竞赛激励私营部门;设立面向AI科学应用的奖学金、实习与学徒计划,安排至国家实验室;建立机制促进与外部合作伙伴(含企业)通过合作协议、用户设施等方式协作,保护联邦资产并最大化公共利益。 |
| 6 | 评估与报告:能源部长每年通过总统科技助理与管理与预算办公室向总统提交报告,内容包括平台运行状况、计算与数据基础设施共享进展、用户参与(含学生培训)、科研成果(论文、原型)、公私合作范围与技术转化成效,以及实现目标所需的权限或跨机构支持建议。 |
| 7 | 一般规定:本命令不削弱任何部门法定权力或管理与预算局局长职能;实施须符合法律并受拨款限制;不创设任何可强制执行的权利或利益;出版费用由能源部承担。 |
......@@ -46,6 +46,7 @@
img {
width: 100%;
height: 100%;
display: block;
}
}
......
......@@ -37,7 +37,7 @@
<div class="icon">
<img :src="item.icon" alt="" />
</div>
<div class="title">{{ item.title }}</div>
<div class="title" :class="{ 'active-title': item.active }">{{ item.title }}</div>
</div>
</div>
</div>
......@@ -157,57 +157,68 @@ const menuList = ref([
{
title: "科技法案",
icon: Menu2,
path: "/billHome"
path: "/billHome",
active: false
},
{
title: "科技政令",
icon: Menu3,
path: "/decree"
path: "/decree",
active: false
},
{
title: "美国科技智库",
icon: Menu4,
path: "/thinkTank"
path: "/thinkTank",
active: false
},
{
title: "出口管制",
icon: Menu5,
path: "/exportControl"
path: "/exportControl",
active: false
},
{
title: "科研合作限制",
icon: Menu6,
path: "/cooperationRestrictions"
path: "/cooperationRestrictions",
active: false
},
{
title: "投融资限制",
icon: Menu7,
path: "/finance"
path: "/finance",
active: false
},
{
title: "市场准入限制",
icon: Menu8,
path: "/marketAccessRestrictions"
path: "/marketAccessRestrictions",
active: false
},
{
title: "规则限制",
icon: Menu9,
path: "/ruleRestrictions"
path: "/ruleRestrictions",
active: false
},
{
title: "美国科技人物观点",
icon: Menu10,
path: "/technologyFigures"
path: "/technologyFigures",
active: false
},
{
title: "美国主要创新主体动向",
icon: Menu11,
path: "/innovationSubject"
path: "/innovationSubject",
active: false
},
{
title: "美国科研资助体系",
icon: Menu12,
path: "/scientificFunding"
path: "/scientificFunding",
active: false
}
]);
......@@ -235,6 +246,7 @@ const handleToModule = (item, index) => {
window.sessionStorage.setItem('homeActiveTitleIndex', index)
if (index === 1) {
homeActiveTitleIndex.value = index
item.active = true
router.push({
path: item.path
})
......@@ -253,7 +265,7 @@ const handleToModule = (item, index) => {
};
const handleClickTitle = (item, index) => {
if (index === 0 || index === 3) {
window.sessionStorage.setItem('homeActiveTitleIndex', index)
homeActiveTitleIndex.value = index
......@@ -497,6 +509,11 @@ onUnmounted(() => {
letter-spacing: 0px;
text-align: left;
}
.active-title {
color: var(--color-main-active) !important;
font-size: 20px !important;
}
}
}
}
......@@ -528,6 +545,8 @@ onUnmounted(() => {
display: flex;
cursor: pointer;
&:hover {
.title {
color: var(--color-main-active);
......@@ -557,6 +576,11 @@ onUnmounted(() => {
letter-spacing: 0px;
text-align: left;
}
.active-title {
color: var(--color-main-active) !important;
font-size: 20px !important;
}
}
}
}
......
<template>
<div class="intelligenceLeftTabBar">
<div class="navBox" :class="{navBoxShow:isNavMenuShow}">
<div class="navList" v-for="(item,index) in navList " :key="index" :class="{on:navPath==item.path}" @click="onNavListClick(item.path)">
<div class="icon" :style="{background:`url(${item.img})no-repeat`,backgroundSize:'24px 24px',backgroundPosition:'17px 17px'}"></div>
<span class="text-tip-1" style="white-space: nowrap; ">{{ item.name }}</span>
</div>
</div>
<img class="show" src="@/assets/icons/muenShow.png" :style="isNavMenuShow?'transform: scaleX(1)':''" alt="" @click="()=>{isNavMenuShow=!isNavMenuShow}">
</div>
</template>
<script setup>
import muen1 from '@/assets/icons/tool-item-icon1.png'
import muen2 from '@/assets/icons/tool-item-icon2.png'
import muen3 from '@/assets/icons/tool-item-icon3.png'
import muen4 from '@/assets/icons/tool-item-icon4.png'
import { onMounted, onUnmounted, ref, nextTick } from "vue";
import { useRoute } from "vue-router";
import { ElMessage } from "element-plus";
import { useWrittingAsstaintStore } from "@/stores/writtingAsstaintStore";
const isNavMenuShow=ref(false)
const navList=ref([
{
img:muen1,
path:'/writtingAsstaint',
name:'智能写报'
},
{
img:muen2,
path:'/writtingAsstaint1',
name:'智能翻译'
},
{
img:muen3,
path:'/writtingAsstaint2',
name:'智能查询'
},
{
img:muen4,
path:'/writtingAsstaint3',
name:'智能对话'
},
])
const navPath=ref()
const route=useRoute()
if(route.path){
navPath.value=route.path
}
const onNavListClick=(path)=>{
if(path=='/writtingAsstaint'){
navPath.value=path
}else{
ElMessage.error('正在开发中')
}
}
</script>
<style lang="scss" scoped>
.intelligenceLeftTabBar{
padding: 5px 0;
border-right: 1px solid rgb(234, 236, 238);
position: relative;
.navBox{
height: 100%;
width: 65px;
transition: all 0.3s;
padding: 0 3px;
.navList{
display: flex;
align-items: center;
cursor: pointer;
border-radius: 8px;
overflow: hidden;
height: 60px;
.icon{
width: 60px;
height: 60px;
flex-shrink: 0;
border-radius: 10px;
margin-right: 15px;
}
}
.on{
background-color: var(--color-primary-10);
color: var(--color-primary-100);
font-weight: Bold;
}
}
.navBoxShow{
width: 200px;
transition: all 0.3s;
}
.show{
position: absolute;
width: 24px;
height: 24px;
right: 21px;
bottom: 21px;
cursor: pointer;
transform: scaleX(-1)
}
}
</style>
\ No newline at end of file
......@@ -295,13 +295,16 @@ export function useMarkdownStream() {
// 预处理内容
// const processedContent = preprocessMarkdown(rawContent.value)
let content = rawContent.value || ''
// 将 ==n== 转换为按钮样式的 HTML
// 使用正向预读和反向预读确保只匹配被 == 包裹的数字
content = content.replace(/==(\d+)==/g, (match, p1) => {
return `<button class="clause-ref-btn" data-clause="${p1}">${p1}</button>`
})
// content = content.replace(/==(\d+)、==/g, (match, p1) => {
// return `<button class="clause-ref-btn" data-clause="${p1}">${p1}</button>`
// })
console.log(content,11223)
content = content.replace(/==\s*(\d+)、.*?==/g, (match, p1) => {
return `<button class="clause-ref-btn" data-clause="${match.replace(/==/g, '') }">${p1}</button>`;
});
return md.render(content)
})
......
......@@ -2,6 +2,8 @@
const thinkTank = () => import('@/views/thinkTank/index.vue')
const ThinkTankDetail = () => import('@/views/thinkTank/ThinkTankDetail/index.vue')
const ReportDetail = () => import('@/views/thinkTank/ReportDetail/index.vue')
const SurveyProjectView = () => import('@/views/thinkTank/SurveyProjectView/index.vue')
const CongressHearingView = () => import('@/views/thinkTank/CongressHearingView/index.vue')
const ReportOriginal = () => import('@/views/thinkTank/reportOriginal/index.vue')
const allThinkTank= () => import('@/views/thinkTank/allThinkTank/index.vue')
const MultiThinkTankViewAnalysis= () => import('@/views/thinkTank/MultiThinkTankViewAnalysis/index.vue')
......@@ -40,6 +42,16 @@ const thinktankRoutes = [
name: "ReportOriginal",
component: ReportOriginal,
},
{
path: "/thinkTank/SurveyProjectView/:id",
name: "SurveyProjectView",
component: SurveyProjectView,
},
{
path: "/thinkTank/CongressHearingView/:id",
name: "CongressHearingView",
component: CongressHearingView,
},
{
path: "/thinkTank/allThinkTank",
......
import { defineStore } from 'pinia'
import { ElMessage, ElMessageBox } from 'element-plus'
import md from '@/assets/icons/aiBox/2.md?raw'
export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
state: () => ({
......@@ -51,6 +52,38 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
containerRef:null
},
// ==================================
// 显示全部进度
isProcessLog:false,
// ====头
isSsearchFor:false,
//list
tabList:[
{
type:'translate',
name:'翻译',
active: true
},
{
type:'mind',
name:'思维导图',
active: false
},
{
type:'message',
name:'写报',
active: false
}
],
headerTabType:'translate',
// 底部
bottomProgressNum:0, //文档解析 假进度
resultWriteData:null, //文档分析结束之后 写报使用
// 写报
isWriteStart:false,// 写报进行中
processLogAlone:'', //写报步骤
writeProgressNum:0, //写报 假进度
}),
getters: {
......@@ -60,7 +93,8 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
const now = new Date()
const pad = n => n.toString().padStart(2, '0')
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
}
},
},
actions: {
......@@ -78,6 +112,7 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
this.searchData.containerRef = containerRef
},
resetGenerateState() {
this.isGenerating = false;
this.isShowProcess = false;
this.isShowSteps = false;
......@@ -90,6 +125,21 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
this.isShowOriginal = true;
this.abortController?.abort();
this.abortController = null;
this.isProcessLog=false
this.headerTabType='translate',
// 底部
this.bottomProgressNum=0, //文档解析 假进度
this.resultWriteData=null, //文档分析结束之后 写报使用
this.processLogAlone='', //写报步骤
// 写报
this.isWriteStart=false,// 写报进行中
// this.processWriteLog='', //写报步骤
this.writeProgressNum=0, //写报 假进度
this.tabList[1].active=false
this.tabList[2].active=false
this.reportContent='' //写报内容
},
backToInputAndClear() {
......@@ -128,6 +178,49 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
this._keepStepsViewOnError();
await this._showErrorDialog(message);
},
//header tab切换
async handleHeaderTab(type){
this.headerTabType=type
},
// 关闭搜索栏
handleIsSsearchFor(){
this.isSsearchFor=!this.isSsearchFor
},
// 智能写报
async generateWrite(){
this.isWriteStart=true
// const obj={}
// await this.fetchReportData(obj);
// this.isShowClauseTranslation=true
// this.headerTabType='message'
// this.reportContent=md
// this.tabList[2].active=true //写报生成之后放开写报按钮
// this.bottomProgressNum=100
// this.writeProgressNum=100 //写报假进度
// this.headerTabType='message'
await this.fetchReportData({
query: this.writtingTitle,
desc: this.descText,
topic: this.curTempTitle,
result: this.resultWriteData
});
},
// 停止写报
writeGenerateState(){
// this.bottomProgressNum=100
this.headerTabType='translate'
this.writeProgressNum=0
this.processLogAlone=''
this.tabList[2].active=false
this.abortController.abort()
},
// ========== 路由参数处理 ==========
async setRouteParams(query) {
......@@ -279,6 +372,8 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
this.abortController = new AbortController();
this.processLog = '';
// 进度初始化1
this.bottomProgressNum=1
try {
const formData = new FormData();
formData.append('pdf', selectedFile);
......@@ -300,6 +395,7 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
}
},
onmessage: async (event) => {
console.log(event)
if (!event || !event.data || event.data.trim() === '') return;
let jsonData = null;
......@@ -309,18 +405,23 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
console.warn('SSE消息JSON解析失败', parseError, event.data);
return;
}
switch (event.event) {
case 'progress':
// 仅更新执行步骤
if (jsonData.message) {
this.processLog += `${this.formattedTime}:${jsonData.message}\r\n`;
this.processLogAlone=`${this.formattedTime}:${jsonData.message}\r\n`;
}
this.bottomProgressNum+=1
break;
case 'metadata':
if (jsonData && jsonData.payload) {
this.pdfMetadata = jsonData.payload;
}
if(this.bottomProgressNum<90){
this.bottomProgressNum+=5
}
break;
case 'clause_translation':
// 保存条款翻译消息并显示侧边栏
......@@ -328,16 +429,25 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
this.clauseTranslationMessages.push(jsonData);
this.isShowClauseTranslation = true;
this.isShowSteps = true; // 翻译出现时,步骤侧边栏也显示
// 假进度
if(this.bottomProgressNum<90){
this.bottomProgressNum+=5
}
}
break;
case 'result':
if (jsonData && Object.keys(jsonData).length) {
await this.fetchReportData({
query: this.writtingTitle,
desc: this.descText,
topic: this.curTempTitle,
result: jsonData
});
this.bottomProgressNum=100 // 假进度完成
this.resultWriteData=jsonData //给写报使用
this.tabList[1].active=true //放开思维导图
this.processLogAlone = `点击查看`;
// await this.fetchReportData({
// query: this.writtingTitle,
// desc: this.descText,
// topic: this.curTempTitle,
// result: jsonData
// });
}
break;
case 'error':
......@@ -357,6 +467,7 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
this.resetGenerateState();
},
onclose: () => {
this.bottomProgressNum=100 // 假进度完成
console.log('SSE连接正常关闭');
this.isGenerating = false;
}
......@@ -374,9 +485,13 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
// ========== AI 生成报文 SSE(更新报文内容 + 执行步骤) ==========
async fetchReportData(params) {
console.log(">")
if (this.abortController) this.abortController.abort();
this.abortController = new AbortController();
this.processLog = '';
// this.processLog = '';
// this.processLogAlone=''
this.writeProgressNum=1
// 用于把 SSE 的分片内容先聚合,再按“句子/段落边界”一次性提交到 reportContent
// 这样可以还原老版“一句完整再显示”的渲染效果,避免分片逐条渲染
......@@ -425,7 +540,6 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
lastFlushedIndex = Math.max(lastFlushedIndex, lineIdx + 1);
}
};
try {
const { fetchEventSource } = await import('@microsoft/fetch-event-source');
await fetchEventSource('/sseWrite/api/v1/workflow/invoke', {
......@@ -436,6 +550,7 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
openWhenHidden: true,
onopen: (res) => {
console.log('流式回答开始', res);
},
onmessage: (res) => {
if (!res.data) return;
......@@ -448,19 +563,27 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
}
const str = msgData.data || '';
if (msgData.event_type === 'stream_agent_out') {
if (str !== '[DONE]') {
// SSE 分片先进入 buffer(仅用于报文内容)
streamBuffer += str;
updateFlushIndexByBoundary();
flushToReport(false);
console.log(streamBuffer,456)
console.log(msgData,'data')
} else {
// 结束时把剩余内容强制 flush
flushToReport(true);
this.isGenerating = false;
this.isShowSteps = false; // 报文生成结束后关闭步骤侧边栏
ElMessage.success('报文生成结束');
this.writeProgressNum=100 //写报假进度
this.processLogAlone= '点击查看';
// 这里不再调用 resetGenerateState,因为可能需要保留翻译内容
}
} else if (msgData.event_type === 'workflow_complete') {
......@@ -469,6 +592,10 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
this.isGenerating = false;
this.isShowSteps = false; // 报文生成结束后关闭步骤侧边栏
ElMessage.success('报文生成结束');
this.tabList[2].active=true //写报生成之后放开写报按钮
this.writeProgressNum=100 //写报假进度
this.processLogAlone= '点击查看';
} else if ((msgData.event_type || '').toLowerCase().includes('error')) {
// 优先从 data.error 获取详细错误描述
const errorMsg = msgData.data?.error || str || '生成失败';
......@@ -478,6 +605,10 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
// 这样可以避免 SSE 分片导致的“步骤破碎”(一条步骤被拆成多条显示)
if (str) {
this.processLog += str;
this.processLogAlone+= str;
if(this.writeProgressNum<=90){
this.writeProgressNum+=0.05
}
}
this.curAgentTool = msgData.tool || '无';
}
......@@ -499,15 +630,17 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
// ========== 业务入口 ==========
async generateReport() {
// 路由参数优先
this.isGenerating = true;
this.isShowProcess = true;
this.bottomProgressNum=1
if (Object.keys(this.routeQuery).length !== 0) {
// 路由参数优先
const { fileId } = this.routeQuery;
// 外部跳转:根据 topic 决定调用哪种数据获取接口,再触发生成
if (fileId) {
this.isGenerating = true;
this.isShowProcess = true;
if (this.curTempTitle === '法案') {
await this.fetchBillReportData(fileId);
} else if (this.curTempTitle === '清单') {
......@@ -521,6 +654,7 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
}
// 兼容:若仅有 routeQuery 但无 fileId,则直接走原生成接口
const params = {
query: this.writtingTitle,
desc: this.descText,
......@@ -529,6 +663,7 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
};
await this.fetchReportData(params);
} else {
console.log(10101010101010)
// 政令模板需要先解析PDF
if (this.curTempTitle === '政令') {
if (this.uploadFileList.length === 0) {
......@@ -540,6 +675,9 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
}
await this.fetchPdfData(rawFile);
} else {
// 路由参数优先
this.isGenerating = true;
this.isShowProcess = true;
const params = {
query: this.writtingTitle,
desc: this.descText,
......@@ -570,4 +708,4 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
this.resetGenerateState();
}
}
});
\ No newline at end of file
});
const getQuarterRange = (quatarNum) => {
const quarters = {
1: ['2025-01-01', '2025-03-31'],
2: ['2025-04-01', '2025-06-30'],
3: ['2025-07-01', '2025-09-30'],
4: ['2025-10-01', '2025-12-31']
};
return quarters[quatarNum];
}
export default getQuarterRange
\ No newline at end of file
// 绘制echarts图表
import getMonthRange from './getMonthRange'
import getQuarterRange from './getQuarterRange';
import * as echarts from 'echarts'
import 'echarts-wordcloud';
import router from '@/router/index'
......@@ -77,6 +78,26 @@ const setChart = (option, chartId, allowClick, selectParam) => {
}
}
break
case '政令':
if (params.componentType === 'series' && params.seriesType === 'pie') {
selectParam.domains = params.name
const route = router.resolve({
path: "/dataLibrary/dataDecree",
query: selectParam
});
window.open(route.href, "_blank");
} else if (params.componentType === 'series' && params.seriesType === 'bar') {
const quatarNum = Number(params.name[params.name.length - 1])
selectParam.selectedDate = JSON.stringify(getQuarterRange(quatarNum))
const route = router.resolve({
path: "/dataLibrary/dataDecree",
query: selectParam
});
window.open(route.href, "_blank");
}
}
......
<svg viewBox="0 0 12 13" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="12.000000" height="13.000000" fill="none" customFrame="#000000">
<path id="矢量 1651" d="M8.05031 2.15239e-07C8.33835 2.69049e-07 8.61901 0.0965686 8.85205 0.275865C9.08508 0.455161 9.25853 0.70798 9.34755 0.998087C9.43656 1.28819 9.43656 1.6007 9.34755 1.8908C9.25853 2.18091 9.08508 2.43373 8.85205 2.61303C8.61901 2.79232 8.33835 2.88889 8.05031 2.88889L3.9583 2.88889C3.69558 2.88873 3.43848 2.80821 3.21793 2.65703C2.99737 2.50585 2.82274 2.29043 2.71502 2.03667C2.35984 2.08328 2.0252 2.23849 1.752 2.48332C1.4788 2.72814 1.27889 3.05197 1.17673 3.41519C1.07456 3.77842 1.07456 4.1653 1.17673 4.52853C1.27889 4.89176 1.4788 5.21558 1.752 5.46041C2.0252 5.70523 2.35984 5.86044 2.71502 5.90706C2.82284 5.65343 2.99751 5.43816 3.21806 5.28711C3.43861 5.13606 3.69565 5.05566 3.9583 5.05556L8.05031 5.05556C8.31755 5.05563 8.57888 5.13883 8.80185 5.29483C9.02483 5.45084 9.19961 5.67276 9.3045 5.93306C9.88944 5.98317 10.4467 6.21802 10.9042 6.60722C11.3618 6.99642 11.6985 7.52206 11.8708 8.11613C12.0431 8.7102 12.0431 9.34536 11.8708 9.93943C11.6985 10.5335 11.3618 11.0591 10.9042 11.4483C10.4467 11.8375 9.88944 12.0724 9.3045 12.1225C9.19961 12.3828 9.02483 12.6047 8.80185 12.7607C8.57888 12.9167 8.31755 12.9999 8.05031 13L3.9583 13C3.71881 13.0001 3.48352 12.9334 3.27608 12.8067C3.06865 12.6799 2.89638 12.4976 2.77661 12.2779C2.65684 12.0583 2.59378 11.8092 2.59378 11.5556C2.59378 11.3019 2.65684 11.0528 2.77661 10.8332C2.89638 10.6136 3.06865 10.4312 3.27608 10.3044C3.48352 10.1777 3.71881 10.111 3.9583 10.1111L8.05031 10.1111C8.60409 10.1111 9.07944 10.4607 9.29291 10.9626C9.64884 10.9171 9.98445 10.7624 10.2585 10.5177C10.5326 10.273 10.7333 9.94881 10.8358 9.58501C10.9384 9.22121 10.9384 8.83362 10.8358 8.46982C10.7333 8.10603 10.5326 7.78186 10.2585 7.53714C9.98445 7.29241 9.64884 7.13778 9.29291 7.09222C9.18531 7.34593 9.0108 7.56132 8.79036 7.71251C8.56992 7.86369 8.31294 7.94423 8.05031 7.94444L3.9583 7.94444C3.69087 7.94445 3.42933 7.86121 3.20622 7.70506C2.9831 7.54892 2.80826 7.32676 2.70342 7.06622C2.11732 7.01784 1.55849 6.78394 1.09952 6.39491C0.640554 6.00587 0.302655 5.47968 0.129709 4.88468C-0.0432364 4.28967 -0.0432363 3.65334 0.129709 3.05833C0.302654 2.46332 0.640554 1.93713 1.09952 1.54809C1.55849 1.15906 2.11732 0.925163 2.70342 0.876778C2.80847 0.616508 2.9834 0.394654 3.2065 0.238777C3.4296 0.0829001 3.69102 -0.000130273 3.9583 2.15239e-07L8.05031 2.15239e-07Z" fill="rgb(95,101,108)" fill-rule="nonzero" />
</svg>
<svg viewBox="0 0 12 13.3145" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="12.000000" height="13.314453" fill="none" customFrame="#000000">
<path id="矢量 52" d="M2.47123 9.11862L8.566 3.02386C8.82631 2.76355 8.82631 2.3415 8.566 2.08119C8.30569 1.82088 7.88364 1.82088 7.62333 2.08119L1.52857 8.17595C1.40356 8.30096 1.33333 8.4705 1.33333 8.64729C1.33333 9.01542 1.63176 9.31385 1.9999 9.31385C2.17668 9.31385 2.34623 9.24363 2.47123 9.11862ZM2.82867 10.6472L1 10.6472C0.447715 10.6472 0 10.1995 0 9.64719L0 8.23273C0 7.96752 0.105357 7.71316 0.292894 7.52563L7.62333 0.195193C7.74833 0.0702133 7.91787 0 8.09467 0C8.27147 0 8.441 0.0702133 8.566 0.195193L10.452 2.08119C10.577 2.20621 10.6472 2.37575 10.6472 2.55253C10.6472 2.7293 10.577 2.89884 10.452 3.02386L2.82867 10.6472L2.82867 10.6472ZM0.666666 11.9805L11.3333 11.9805C11.7015 11.9805 12 12.279 12 12.6472C12 13.0154 11.7015 13.3139 11.3333 13.3139L0.666667 13.3139C0.298477 13.3139 0 13.0154 0 12.6472C0 12.279 0.298477 11.9805 0.666666 11.9805Z" fill="rgb(255,255,255)" fill-rule="evenodd" />
</svg>
......@@ -38,13 +38,9 @@
</div>
</template>
<template v-else>
<div
class="left-box-bottom-item"
:class="{ leftBoxBottomItemActive: activeTitle === item.name }"
v-for="item in tabs"
:key="item.path"
@click="emit('tab-click', item)"
>
<div class="left-box-bottom-item"
:class="{ leftBoxBottomItemActive: activeTitle === item.name }" v-for="item in tabs"
:key="item.path" @click="emit('tab-click', item)">
<div class="icon">
<img v-if="activeTitle === item.name" :src="item.activeIcon" alt="" />
<img v-else :src="item.icon" alt="" />
......@@ -88,7 +84,13 @@
</div>
</template>
<template v-else>
<div class="btn3" @click="emit('open-analysis')">
<div class="btn2" @click="emit('open-analysis', 'forsee')">
<div class="icon">
<img :src="btnIconForsee" alt="" />
</div>
<div class="text">{{ "进展预测" }}</div>
</div>
<div class="btn3" @click="emit('open-analysis', 'analysis')">
<div class="icon">
<img :src="btnIconAnalysis" alt="" />
</div>
......@@ -103,7 +105,8 @@
<script setup>
import { computed } from "vue";
import btnIconAnalysis from "@/views/thinkTank/ReportDetail/images/btn-icon3.png";
import btnIconAnalysis from "@/views/bill/billLayout/assets/icons/writting-icon.svg";
import btnIconForsee from "@/views/bill/billLayout/assets/icons/forsee-icon.svg";
const props = defineProps({
billInfo: {
......@@ -347,6 +350,29 @@ const emit = defineEmits(["tab-click", "open-analysis"]);
justify-content: flex-end;
gap: 8px;
.icon {
width: 16px;
height: 16px;
img {
width: 100%;
height: 100%;
}
}
.btn2 {
cursor: pointer;
width: 120px;
height: 36px;
border-radius: 6px;
background: var(--bg-white-100);
border: 1px solid var(--bg-black-10);
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
}
.btn3 {
cursor: pointer;
width: 120px;
......@@ -358,16 +384,6 @@ const emit = defineEmits(["tab-click", "open-analysis"]);
align-items: center;
gap: 8px;
.icon {
width: 16px;
height: 16px;
img {
width: 100%;
height: 100%;
}
}
.text {
height: 24px;
color: rgba(255, 255, 255, 1);
......@@ -387,4 +403,3 @@ const emit = defineEmits(["tab-click", "open-analysis"]);
}
}
</style>
......@@ -2,16 +2,9 @@
<div class="layout-container">
<!-- 导航菜单 -->
<div class="layout-main">
<BillHeader
:billInfo="billInfoGlobal"
:defaultLogo="USALogo"
:tabs="mainHeaderBtnList"
:activeTitle="activeTitle"
:showTabs="showHeaderTabs"
:showActions="showHeaderActions"
@tab-click="handleClickMainHeaderBtn"
@open-analysis="handleAnalysisClick"
/>
<BillHeader :billInfo="billInfoGlobal" :defaultLogo="USALogo" :tabs="mainHeaderBtnList"
:activeTitle="activeTitle" :showTabs="showHeaderTabs" :showActions="showHeaderActions"
@tab-click="handleClickMainHeaderBtn" @open-analysis="handleAnalysisClick" />
<div class="layout-main-center">
<router-view />
......@@ -115,12 +108,24 @@ const handleClickMainHeaderBtn = item => {
});
};
const handleAnalysisClick = () => {
const handleAnalysisClick = analysisType => {
const billId = route.query.billId;
if (!billId) return;
// 进展预测 -> 法案简介页(法案进展)
if (analysisType === "forsee") {
router.push({
path: `/billLayout/ProgressForecast/${billId}`,
});
return;
}
// 分析报告 -> 写作助手
router.push({
path: "/writtingAsstaint",
query: {
topic: "法案",
fileId: route.query.billId
fileId: String(billId)
}
});
};
......@@ -149,11 +154,13 @@ watch(
// height: 1016px;
background: rgba(249, 250, 252, 1);
position: relative;
// margin: 0 auto;
.layout-main {
width: 100%;
height: calc(100vh - 72px);
height: 100vh;
overflow-y: auto;
.layout-main-center {
// height: calc(100% - 137px);
width: 1600px;
......
......@@ -5,7 +5,7 @@
<div class="left" :style="{ width: (maxLineWidth + 250) + 'px' }">
<div class="top">
<div class="top-line" :style="{ width: lineWidth }">
<div class="top-line1"></div>
<div class="top-line1" ref="topLineEndRef"></div>
</div>
<div class="start">
<div class="icon">
......@@ -14,42 +14,52 @@
<div class="name">{{ "参议院" }}</div>
</div>
<div class="content-box" :style="senateBoxStyle">
<div class="item-box" v-for="(item, index) in senateList" :key="item.id"
style="width: 280px; flex-shrink: 0;">
<div class="item-box-dot">
<img src="./assets/images/top-line-dot.png" alt="" />
</div>
<div class="item-content">
<div class="item-header">
<div class="item-title" :title="item.actionTitle">
{{ item.actionTitle }} <span v-if="item.versionId">({{ item.versionId }})</span>
<div
class="item-box"
v-for="slot in senateSlots"
:key="slot.key"
style="width: 280px; flex-shrink: 0;"
>
<template v-if="slot.item">
<div class="item-box-dot">
<img src="./assets/images/top-line-dot.png" alt="" />
</div>
<div class="item-content">
<div class="item-header">
<div class="item-title" :title="slot.item.actionTitle">
{{ slot.item.actionTitle }} <span v-if="slot.item.versionId">({{ slot.item.versionId }})</span>
</div>
<div class="item-header-icon" @click="handleClickDetail(true, slot.item, $event)">
<img src="./assets/images/item-header-icon.png" alt="" />
</div>
</div>
<div class="item-header-icon" @click="handleClickDetail(true, item, $event)">
<img src="./assets/images/item-header-icon.png" alt="" />
<div class="item-info" v-if="slot.item.agreeVote !== null || slot.item.disagreeVote !== null">
{{ formatVoteText(slot.item) }}
</div>
</div>
<div class="item-info" v-if="item.agreeVote !== null || item.disagreeVote !== null">
{{ (item.agreeVote || 0) + "赞成:" + (item.disagreeVote || 0) + "反对" }}
</div>
<div class="item-main" v-if="item.fynrList && item.fynrList.length">
<div class="item-main-item" v-for="(sub, subIndex) in item.fynrList" :key="subIndex">
<div class="icon"></div>
<CommonPrompt :content="sub">
<div class="text">{{ sub }}</div>
</CommonPrompt>
<div class="item-main" v-if="slot.item.fynrList && slot.item.fynrList.length">
<div
class="item-main-item"
v-for="(sub, subIndex) in slot.item.fynrList"
:key="`${slot.item.id}-${subIndex}-${sub}`"
>
<div class="icon"></div>
<CommonPrompt :content="sub">
<div class="text">{{ sub }}</div>
</CommonPrompt>
</div>
</div>
</div>
</div>
<div class="item-time">
{{ item.actionDate }}
</div>
<div class="item-time">
{{ slot.item.actionDate }}
</div>
</template>
</div>
</div>
</div>
<div class="bottom">
<div class="bottom-line" :style="{ width: lineWidth }">
<div class="bottom-line1"></div>
<div class="bottom-line1" ref="bottomLineEndRef"></div>
</div>
<div class="start">
<div class="name">{{ "众议院" }}</div>
......@@ -59,39 +69,49 @@
</div>
</div>
<div class="content-box" :style="houseBoxStyle">
<div class="item-box" v-for="(item, index) in houseList" :key="item.id"
style="width: 280px; flex-shrink: 0;">
<div class="item-time">
{{ item.actionDate }}
</div>
<div class="item-box-dot">
<img src="./assets/images/bottom-line-dot.png" alt="" />
</div>
<div class="item-content">
<div class="item-header">
<div class="item-title" :title="item.actionTitle">
{{ item.actionTitle }} <span v-if="item.versionId">({{ item.versionId }})</span>
</div>
<div class="item-header-icon" @click="handleClickDetail(true, item, $event)">
<img src="./assets/images/item-header-icon.png" alt="" />
</div>
<div
class="item-box"
v-for="slot in houseSlots"
:key="slot.key"
style="width: 280px; flex-shrink: 0;"
>
<template v-if="slot.item">
<div class="item-time">
{{ slot.item.actionDate }}
</div>
<div class="item-info" v-if="item.agreeVote !== null || item.disagreeVote !== null">
{{ (item.agreeVote || 0) + "赞成:" + (item.disagreeVote || 0) + "反对" }}
<div class="item-box-dot">
<img src="./assets/images/bottom-line-dot.png" alt="" />
</div>
<div class="item-main" v-if="item.fynrList && item.fynrList.length">
<div class="item-main-item" v-for="(sub, subIndex) in item.fynrList" :key="subIndex">
<div class="icon"></div>
<CommonPrompt :content="sub">
<div class="text">{{ sub }}</div>
</CommonPrompt>
<div class="item-content">
<div class="item-header">
<div class="item-title" :title="slot.item.actionTitle">
{{ slot.item.actionTitle }} <span v-if="slot.item.versionId">({{ slot.item.versionId }})</span>
</div>
<div class="item-header-icon" @click="handleClickDetail(true, slot.item, $event)">
<img src="./assets/images/item-header-icon.png" alt="" />
</div>
</div>
<div class="item-info" v-if="slot.item.agreeVote !== null || slot.item.disagreeVote !== null">
{{ formatVoteText(slot.item) }}
</div>
<div class="item-main" v-if="slot.item.fynrList && slot.item.fynrList.length">
<div
class="item-main-item"
v-for="(sub, subIndex) in slot.item.fynrList"
:key="`${slot.item.id}-${subIndex}-${sub}`"
>
<div class="icon"></div>
<CommonPrompt :content="sub">
<div class="text">{{ sub }}</div>
</CommonPrompt>
</div>
</div>
</div>
</div>
</template>
</div>
</div>
</div>
<div class="right" :style="{ left: rightPos }">
<div class="right" :style="{ left: rightPos, top: rightTop }">
<div class="junction-dot">
<div class="inner-dot"></div>
</div>
......@@ -111,7 +131,7 @@
</template>
<script setup>
import { ref, onMounted, computed } from "vue";
import { ref, onMounted, computed, nextTick } from "vue";
import { getBillDyqkSummary } from "@/api/bill";
import CommonPrompt from "../../commonPrompt/index.vue";
import ProcessOverviewDetailDialog from "../../ProcessOverviewDetailDialog.vue";
......@@ -138,30 +158,81 @@ const getBillDyqkSummaryList = async () => {
}
};
// 总统签署节点
const ORG_SENATE = "参议院";
const ORG_HOUSE = "众议院";
const PRESIDENT_KEYWORD = "呈递给总统";
const TIMELINE_ITEM_WIDTH_PX = 280;
const getTime = (actionDate) => {
const t = new Date(actionDate).getTime();
return Number.isFinite(t) ? t : 0;
};
const formatVoteText = (item) => {
if (!item) return "";
const agree = item.agreeVote ?? 0;
const disagree = item.disagreeVote ?? 0;
return `${agree}赞成:${disagree}反对`;
};
// 总统交汇节点(用于确定两条时间线的汇合位置)
const presidentAction = computed(() => {
return actionList.value.find(item => item.actionTitle && item.actionTitle.includes("呈递给总统")) || null;
return (
actionList.value.find(
(item) => item.actionTitle && item.actionTitle.includes(PRESIDENT_KEYWORD)
) || null
);
});
// 全局时间排序后的“时间步”
const sortedTimeline = computed(() => {
return [...actionList.value].sort((a, b) => {
const tA = getTime(a.actionDate);
const tB = getTime(b.actionDate);
if (tA !== tB) return tA - tB;
// 时间相同的情况下用 id 保证稳定排序,避免节点在不同渲染中漂移
const idA = String(a.id ?? "");
const idB = String(b.id ?? "");
return idA.localeCompare(idB);
});
});
// 交汇点(总统节点)在全局时间线里的位置:slice endIndexExclusive 用来排除总统节点本身
const mergeIndexExclusive = computed(() => {
if (!sortedTimeline.value.length) return 0;
if (!presidentAction.value) return sortedTimeline.value.length;
const idx = sortedTimeline.value.findIndex((item) => item.id === presidentAction.value.id);
return idx >= 0 ? idx : sortedTimeline.value.length;
});
// 参议院列表
const senateList = computed(() => {
return actionList.value
.filter(item => item.orgName === "参议院" && (!presidentAction.value || item.id !== presidentAction.value.id))
.sort((a, b) => new Date(a.actionDate) - new Date(b.actionDate));
// 两条时间线共享同一组时间步(每个时间步只展示属于该阵营的事件;其他阵营用空占位对齐)
const timelineSlots = computed(() => {
return sortedTimeline.value.slice(0, mergeIndexExclusive.value);
});
// 众议院列表
const houseList = computed(() => {
return actionList.value
.filter(item => item.orgName === "众议院" && (!presidentAction.value || item.id !== presidentAction.value.id))
.sort((a, b) => new Date(a.actionDate) - new Date(b.actionDate));
const senateSlots = computed(() => {
return timelineSlots.value.map((step) => ({
key: step.id,
item: step.orgName === ORG_SENATE ? step : null
}));
});
// 计算最大线条宽度数值
const houseSlots = computed(() => {
return timelineSlots.value.map((step) => ({
key: step.id,
item: step.orgName === ORG_HOUSE ? step : null
}));
});
const timelineCount = computed(() => timelineSlots.value.length);
// 计算最大线条宽度数值(两条线共享时间步长度)
const maxLineWidth = computed(() => {
const senateWidth = 254 + senateList.value.length * 280;
const houseWidth = 150 + houseList.value.length * 280;
return Math.max(1100, senateWidth, houseWidth);
const senateWidth = 254 + timelineCount.value * TIMELINE_ITEM_WIDTH_PX;
const houseWidth = 150 + timelineCount.value * TIMELINE_ITEM_WIDTH_PX;
return Math.max(senateWidth, houseWidth);
});
// 绑定给线条的样式
......@@ -173,7 +244,7 @@ const lineWidth = computed(() => {
const senateBoxStyle = computed(() => {
return {
width: (maxLineWidth.value + 110 - 254) + 'px',
justifyContent: 'space-between'
justifyContent: 'flex-start'
};
});
......@@ -181,7 +252,7 @@ const senateBoxStyle = computed(() => {
const houseBoxStyle = computed(() => {
return {
width: (maxLineWidth.value + 110 - 150) + 'px',
justifyContent: 'space-between'
justifyContent: 'flex-start'
};
});
......@@ -190,6 +261,10 @@ const rightPos = computed(() => {
return (maxLineWidth.value + 90) + 'px';
});
const topLineEndRef = ref(null);
const bottomLineEndRef = ref(null);
const rightTop = ref('370px');
const isShowDetailDialog = ref(false);
const currentDetailItem = ref({});
const dialogPos = ref({ left: '0px', top: '0px' });
......@@ -224,9 +299,43 @@ const handleClickDetail = (isShow, item = {}, event = null) => {
};
// 挂载阶段调用
onMounted(() => {
getBillDyqkSummaryList();
onMounted(async () => {
await getBillDyqkSummaryList();
await nextTick();
updateRightTop();
});
const updateRightTop = () => {
// 交汇点需要精确对齐上下两条斜线端点在页面中的 y 坐标
// rightTop 是相对 .process-overview-wrap 的 absolute top
const wrap = document.querySelector('.process-overview-wrap');
if (!wrap) return;
const topLineEndEl = topLineEndRef.value;
const bottomLineEndEl = bottomLineEndRef.value;
if (!topLineEndEl || !bottomLineEndEl) return;
const wrapRect = wrap.getBoundingClientRect();
const topRect = topLineEndEl.getBoundingClientRect();
const bottomRect = bottomLineEndEl.getBoundingClientRect();
// 根据 CSS 的旋转原点:
// - top-line1: rotate(45deg) 且 transform-origin: 0 0,因此左侧“尖端”约等于 rect.top
// - bottom-line1: rotate(-45deg) 且 transform-origin: 0 100%,因此左侧“尖端”约等于 rect.bottom
// 但我们对齐的是“斜线中心线”,而不是外包矩形边缘。
// 该斜线块在样式里高度为 8px,所以中心线偏移 4px
const LINE_THICKNESS_PX = 8;
const topLineCenterY = topRect.top + LINE_THICKNESS_PX / 2;
const bottomLineCenterY = bottomRect.bottom - LINE_THICKNESS_PX / 2;
const desiredCenterY = (topLineCenterY + bottomLineCenterY) / 2;
// .right 里 junction-dot 高度为 24px,flex 会让 right-line 与其垂直居中
const junctionCenterOffsetY = 12;
// 经验补偿:整体向上平移约 49px,使视觉交汇点精确落在两条斜线交点
const VISUAL_OFFSET_Y = -49;
rightTop.value = (desiredCenterY - wrapRect.top - junctionCenterOffsetY + VISUAL_OFFSET_Y) + 'px';
};
</script>
<style lang="scss" scoped>
......
......@@ -25,8 +25,8 @@
</div>
</div>
</div> -->
<NewsList :newsList="leftList" @item-click="handleToNewsDetail" @more-click="handleToMoreNews" img="image"
title="title" content="content" from="from" />
<NewsList :newsList="leftList" @item-click="item => gotoNewsDetail(item.id)" @more-click="handleToMoreNews"
img="image" title="title" content="content" from="from" />
<MessageBubble :messageList="rightList" imageUrl="personImage" @more-click="handleToSocialDetail"
@person-click="handleToSocialDetail" name="name" content="content" source="orgName" image-url="image" />
<!-- <div class="right">
......@@ -60,6 +60,8 @@ import title03 from './assets/title03.png'
import title01bg from './assets/title01bg.png'
import title02bg from './assets/title02bg.png'
import title03bg from './assets/title03bg.png'
import { useGotoNewsDetail } from '@/router/modules/news';
const gotoNewsDetail = useGotoNewsDetail()
// 合作限制-查询社交媒体接口
const getCoopRestrictionSocialData = async () => {
......
......@@ -12,50 +12,52 @@
<el-carousel ref="carouselRef" height="412px" direction="horizontal" :autoplay="true" :interval="5000"
arrow="never" indicator-position="none" @change="handleCarouselChange">
<el-carousel-item v-for="(item, index) in coopRestrictionTrends" :key="item.ID || index">
<div class="carousel-item-content">
<div class="left-center">
<img :src="item.IMAGEURL || defaultImg" alt="" />
<div class="left-center-main">
<div class="left-center-main-title">{{ item.LIMITNAME || "暂无动态" }}</div>
<div class="left-center-main-ul">
<ul>
<li>
<span class="ul-title">数据来源:</span>
<span class="ul-content">{{ item.ORGNAME || "未知" }}</span>
</li>
<li>
<span class="ul-title">合作限制类型:</span>
<span class="ul-content">{{ item.LIMITTYPE || "未知" }}</span>
</li>
<li>
<span class="ul-title">发布日期:</span>
<span class="ul-content">{{ item.LIMITDATE || "未知" }}</span>
</li>
<li>
<span class="ul-title">涉及领域:</span>
<div class="ul-tags" v-if="item.AREA">
<span v-for="(field, fIndex) in typeof item.AREA === 'string'
? item.AREA.split(',')
: item.AREA" :key="fIndex" class="ul-pie" :class="'cl' + ((fIndex % 3) + 1)">
{{ field }}
</span>
</div>
<span v-else class="ul-content">未知</span>
</li>
</ul>
</div>
<div class="left-center">
<img :src="item.IMAGEURL || defaultImg" alt="" />
<div class="left-center-main">
<div class="left-center-main-title">{{ item.LIMITNAME || "暂无动态" }}</div>
<div class="left-center-main-ul">
<ul>
<li>
<span class="ul-title">数据来源:</span>
<span class="ul-content">{{ item.ORGNAME || "未知" }}</span>
</li>
<li>
<span class="ul-title">合作限制类型:</span>
<span class="ul-content">{{ item.LIMITTYPE || "未知" }}</span>
</li>
<li>
<span class="ul-title">发布日期:</span>
<span class="ul-content">{{ item.LIMITDATE || "未知" }}</span>
</li>
<li>
<span class="ul-title">涉及领域:</span>
<div class="ul-tags" v-if="item.AREA">
<span v-for="(field, fIndex) in typeof item.AREA === 'string'
? item.AREA.split(',')
: item.AREA" :key="fIndex" class="ul-pie" :class="'cl' + ((fIndex % 3) + 1)">
{{ field }}
</span>
</div>
<span v-else class="ul-content">未知</span>
</li>
</ul>
</div>
<!-- <div class="left-center-title">{{ item.LIMITTYPE }}</div> -->
</div>
<div class="left-bottom">
<ul>
<li class="left-bottom-li">内容摘要:</li>
</ul>
<div class="left-bottom-content">
{{ item.INTRODUCTION || "暂无内容摘要" }}
</div>
<div class="left-center-type" v-if="item.type">{{ item.type }}</div>
<!-- <div class="left-center-title">{{ item.LIMITTYPE }}</div> -->
</div>
<div class="left-bottom">
<ul>
<li class="left-bottom-li">内容摘要:</li>
</ul>
<div class="left-bottom-content">
{{ item.INTRODUCTION || "暂无内容摘要" }}
</div>
</div>
</el-carousel-item>
<!-- 无数据时的占位展示 -->
......@@ -108,6 +110,7 @@
查看更多
</div>
</div> -->
<RiskSignal :list="riskSignals" @more-click="handleToMoreRiskSignal" postDate="time" name="content"
riskLevel="title" @item-click="handleClickToDetail" />
</div>
......@@ -233,7 +236,7 @@ onMounted(() => {
.left {
width: 1064px;
height: 460px;
height: 450px;
margin-right: 16px;
border-radius: 10px;
background-color: #fff;
......@@ -330,6 +333,7 @@ onMounted(() => {
.left-center-main {
width: 439px;
height: 175px;
position: relative;
.left-center-main-title {
margin-left: 19px;
......@@ -420,6 +424,29 @@ onMounted(() => {
}
}
}
}
.left-center-type {
position: absolute;
top: 0;
right: 0;
height: 32px;
font-family: "Source Han Sans CN";
font-weight: 700;
font-size: 18px;
line-height: 24px;
letter-spacing: 0px;
text-align: left;
color: rgb(5, 95, 194);
background-color: rgb(231, 243, 255);
align-items: center;
border-radius: 4px;
padding-left: 8px;
padding-right: 8px;
padding-top: 3px;
padding-bottom: 5px;
}
.left-center-title {
......@@ -439,7 +466,7 @@ onMounted(() => {
}
.left-bottom {
margin: 17px 0 0 62px;
margin: 17px 0 0 59px;
ul {
list-style-position: inside;
......
<template>
<div class="datasub">
<div class="left">
<div class="left-title">
<img src="./assets/icon01.png" alt="" />
<div class="tit">各类型合作限制政策对比</div>
<el-select v-model="value" placeholder="Select" class="select" @change="getCoopRestrictionCompareData">
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
<div class="left-main">
<div class="left-main-echarts" ref="leftChartRef"></div>
<div class="datasub">
<div class="left">
<div class="left-title">
<img src="./assets/icon01.png" alt="" />
<div class="tit">各类型合作限制政策对比</div>
<el-select v-model="value" placeholder="Select" class="select" @change="getCoopRestrictionCompareData">
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
</div>
<div class="right">
<div class="right-title">
<img src="./assets/icon02.png" alt="" />
<div class="tit">各领域规则分布情况</div>
<div class="left-main" :class="{ 'left-main--empty': !hasLeftChartData }">
<template v-if="!hasLeftChartData">
<el-empty class="datasub-left-empty" description="暂无数据" :image-size="100" />
</template>
<template v-else>
<div class="left-chart-row">
<div ref="leftChartRef" class="left-main-echarts"></div>
</div>
<div class="source">
<TipTab :text="COOP_LEFT_TIP_TEXT" />
</div>
<div class="chart-box">
<div v-if="!isShowAiLeft" class="btn-box" @mouseenter="handleSwitchAiLeft(true)">
<AiButton />
</div>
<div v-if="isShowAiLeft" class="content-box" @mouseleave="handleSwitchAiLeft(false)">
<AiPane :aiContent="aiContentLeft" />
</div>
</div>
</template>
</div>
</div>
<div class="right">
<div class="right-title">
<img src="./assets/icon02.png" alt="" />
<div class="tit">各领域规则分布情况</div>
<el-select v-model="value1" placeholder="Select" class="select" @change="getCoopRestrictionDomainData">
<el-option v-for="item in options1" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
<div class="right-main">
<div class="right-main-echarts" ref="rightChartRef"></div>
<el-option v-for="item in options1" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
<div class="right-main" :class="{ 'right-main--empty': !hasRightChartData }">
<template v-if="!hasRightChartData">
<el-empty class="datasub-right-empty" description="暂无数据" :image-size="100" />
</template>
<template v-else>
<div class="right-chart-row">
<div ref="rightChartRef" class="right-main-echarts"></div>
</div>
<div class="source">
<TipTab :text="COOP_RIGHT_TIP_TEXT" />
</div>
<div class="chart-box">
<div v-if="!isShowAiRight" class="btn-box" @mouseenter="handleSwitchAiRight(true)">
<AiButton />
</div>
<div v-if="isShowAiRight" class="content-box" @mouseleave="handleSwitchAiRight(false)">
<AiPane :aiContent="aiContentRight" />
</div>
</div>
</template>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, watch } from "vue";
import { ref, onMounted, onBeforeUnmount, watch, nextTick, computed } from "vue";
import * as echarts from "echarts";
import { getCoopRestrictionCompare, getCoopRestrictionDomain } from "@/api/coopRestriction/coopRestriction";
import { getChartAnalysis } from "@/api/aiAnalysis/index";
import TipTab from "@/views/thinkTank/TipTab/index.vue";
import AiButton from "@/components/base/Ai/AiButton/index.vue";
import AiPane from "@/components/base/Ai/AiPane/index.vue";
const COOP_LEFT_TIP_TEXT = "各类型合作限制政策对比,数据来源:美对华科技合作限制信息平台";
const COOP_RIGHT_TIP_TEXT = "各领域规则分布情况,数据来源:美对华科技合作限制信息平台";
const value = ref(10);
const value1 = ref("2025");
const options = [
{ value: 1, label: "近一年" },
{ value: 2, label: "近两年" },
{ value: 3, label: "近三年" },
{ value: 4, label: "近四年" },
{ value: 5, label: "近五年" },
{ value: 10, label: "近十年" },
{ value: 15, label: "近十五年" },
{ value: 20, label: "近二十年" }
];
const options1 = [
{ value: "2026", label: "2026年" },
{ value: "2025", label: "2025年" },
{ value: "2024", label: "2024年" },
{ value: "2023", label: "2023年" },
{ value: "2022", label: "2022年" },
{ value: "2021", label: "2021年" },
{ value: "2020", label: "2020年" },
{ value: "2019", label: "2019年" },
{ value: "2018", label: "2018年" },
{ value: "2017", label: "2017年" },
{ value: "2016", label: "2016年" },
{ value: "2015", label: "2015年" },
{ value: "2014", label: "2014年" },
{ value: "2013", label: "2013年" },
{ value: "2012", label: "2012年" },
{ value: "2011", label: "2011年" },
{ value: "2010", label: "2010年" }
];
// 合作限制-各领域规则分布情况接口
const coopRestrictionDomain = ref([]);
const getCoopRestrictionDomainData = async () => {
try {
const res = await getCoopRestrictionDomain({
year: value1.value
});
if (res && res.code === 200) {
coopRestrictionDomain.value = res.data || [];
}
} catch (error) {
console.error("获取合作限制各领域规则分布情况数据失败:", error);
}
try {
const res = await getCoopRestrictionDomain({
year: value1.value
});
if (res && res.code === 200) {
coopRestrictionDomain.value = res.data || [];
} else {
// 接口返回 500/非 200:清空,避免继续展示上一次的图表数据
coopRestrictionDomain.value = [];
aiContentRight.value = "";
}
} catch (error) {
console.error("获取合作限制各领域规则分布情况数据失败:", error);
// 请求失败同样清空,避免展示旧图表/旧 AI
coopRestrictionDomain.value = [];
aiContentRight.value = "";
}
};
// 合作限制-各类型合作限制政策对比接口
const coopRestrictionCompare = ref([]);
const getCoopRestrictionCompareData = async () => {
try {
const res = await getCoopRestrictionCompare({
years: value.value
});
if (res && res.code === 200) {
coopRestrictionCompare.value = res.data || [];
}
} catch (error) {
console.error("获取合作限制各类型合作限制政策对比数据失败:", error);
}
try {
const res = await getCoopRestrictionCompare({
years: value.value
});
if (res && res.code === 200) {
coopRestrictionCompare.value = res.data || [];
} else {
// 接口返回 500/非 200:清空,避免继续展示上一次的图表数据
coopRestrictionCompare.value = [];
aiContentLeft.value = "";
}
} catch (error) {
console.error("获取合作限制各类型合作限制政策对比数据失败:", error);
// 请求失败同样清空,避免展示旧图表/旧 AI
coopRestrictionCompare.value = [];
aiContentLeft.value = "";
}
};
watch(() => coopRestrictionCompare.value, () => {
initLeftChart();
}, { deep: true });
watch(() => coopRestrictionDomain.value, () => {
initRightChart();
}, { deep: true });
const hasLeftChartData = computed(() => {
const list = coopRestrictionCompare.value;
return Array.isArray(list) && list.length > 0;
});
const value = ref(10);
const value1 = ref("2026");
const options = [
{ value: 1, label: "近一年" },
{ value: 2, label: "近两年" },
{ value: 3, label: "近三年" },
{ value: 4, label: "近四年" },
{ value: 5, label: "近五年" },
{ value: 10, label: "近十年" },
{ value: 15, label: "近十五年" },
{ value: 20, label: "近二十年" }
];
const options1 = [
{
value: "2026",
label: "2026年"
},
{
value: "2025",
label: "2025年"
},
{
value: "2024",
label: "2024年"
},
{
value: "2023",
label: "2023年"
},
{
value: "2022",
label: "2022年"
},
{
value: "2021",
label: "2021年"
},
{
value: "2020",
label: "2020年"
},
{
value: "2019",
label: "2019年"
},
{
value: "2018",
label: "2018年"
},
{
value: "2017",
label: "2017年"
},
{
value: "2016",
label: "2016年"
},
{
value: "2015",
label: "2015年"
},
{
value: "2014",
label: "2014年"
},
{
value: "2013",
label: "2013年"
},
{
value: "2012",
label: "2012年"
},
{
value: "2011",
label: "2011年"
},
{
value: "2010",
label: "2010年"
}
];
const hasRightChartData = computed(() => {
const list = coopRestrictionDomain.value;
return Array.isArray(list) && list.length > 0;
});
const leftChartRef = ref(null);
let leftChart;
const rightChartRef = ref(null);
let rightChart;
const isShowAiLeft = ref(true);
const aiContentLeft = ref("");
const isLeftInterpretLoading = ref(false);
const isShowAiRight = ref(true);
const aiContentRight = ref("");
const isRightInterpretLoading = ref(false);
const handleSwitchAiLeft = (val) => {
isShowAiLeft.value = val;
if (val) {
fetchLeftInterpretation();
}
};
const handleSwitchAiRight = (val) => {
isShowAiRight.value = val;
if (val) {
fetchRightInterpretation();
}
};
/** 兼容 getChartAnalysis 返回对象:从 data[0] 提取「解读」文本 */
const getInterpretationTextFromChartResponse = (res) => {
const list = res?.data;
const first = Array.isArray(list) ? list[0] : null;
return (
first?.["解读"] ||
first?.["interpretation"] ||
first?.["analysis"] ||
first?.["content"] ||
""
);
};
const appendAiInterpretationChunk = (targetRef, chunk, loadingText = "解读生成中…") => {
if (!chunk) {
return;
}
const current = String(targetRef.value || "");
const base = current === loadingText ? "" : current;
targetRef.value = base + String(chunk);
};
/** 折线图解读入参(与智库概览数量变化趋势一致结构) */
const buildLeftChartPayload = () => {
const rawData = coopRestrictionCompare.value || [];
if (!rawData.length) {
return null;
}
const yearsSet = new Set();
const typesSet = new Set();
rawData.forEach((item) => {
yearsSet.add(item.LIMITDATE);
typesSet.add(item.TYPENAME);
});
const finalYears = Array.from(yearsSet).sort();
const types = Array.from(typesSet);
if (!finalYears.length || !types.length) {
return null;
}
const data = finalYears.map((year) => {
const point = { period: String(year) };
types.forEach((type) => {
const found = rawData.find((item) => item.TYPENAME === type && item.LIMITDATE === year);
point[type] = found ? found.TYPECOUNT : 0;
});
return point;
});
return {
type: "折线图",
name: "各类型合作限制政策对比",
data
};
};
const fetchLeftInterpretation = async () => {
const payload = buildLeftChartPayload();
if (!payload) {
aiContentLeft.value = "暂无图表数据";
return;
}
const hasValidContent =
aiContentLeft.value &&
aiContentLeft.value !== "解读生成中…" &&
aiContentLeft.value !== "解读加载失败" &&
aiContentLeft.value !== "暂无图表数据";
if (hasValidContent || isLeftInterpretLoading.value) {
return;
}
isLeftInterpretLoading.value = true;
aiContentLeft.value = "解读生成中…";
try {
const res = await getChartAnalysis(
{ text: JSON.stringify(payload) },
{
onChunk: (chunk) => {
// 与智库概览「数量变化趋势」一致:按 chunk 增量拼接展示
appendAiInterpretationChunk(aiContentLeft, chunk);
}
}
);
const text = getInterpretationTextFromChartResponse(res);
// 与智库概览一致:优先用最终「解读」收口;否则保留已拼接内容
aiContentLeft.value = text || aiContentLeft.value || "未返回有效解读内容";
} catch (error) {
console.error("合作限制政策对比图表解读请求失败", error);
aiContentLeft.value = "解读加载失败";
} finally {
isLeftInterpretLoading.value = false;
}
};
/** 雷达图解读入参 */
const buildRightChartPayload = () => {
const rawData = coopRestrictionDomain.value || [];
if (!rawData.length) {
return null;
}
const domainsSet = new Set();
const typesSet = new Set();
rawData.forEach((item) => {
domainsSet.add(item.AREA);
typesSet.add(item.COOPERTYPE);
});
const domains = Array.from(domainsSet);
const types = Array.from(typesSet);
if (!domains.length || !types.length) {
return null;
}
const series = types.map((type) => ({
name: type,
values: domains.map((domain) => {
const found = rawData.find((item) => item.COOPERTYPE === type && item.AREA === domain);
return {
domain,
value: found ? found.COOPERTYPECOUNT : 0
};
})
}));
return {
type: "雷达图",
name: "各领域规则分布情况",
year: value1.value,
data: series
};
};
const fetchRightInterpretation = async () => {
const payload = buildRightChartPayload();
if (!payload) {
aiContentRight.value = "";
return;
}
const hasValidContent =
aiContentRight.value &&
aiContentRight.value !== "解读生成中…" &&
aiContentRight.value !== "解读加载失败" &&
aiContentRight.value !== "暂无图表数据";
if (hasValidContent || isRightInterpretLoading.value) {
return;
}
isRightInterpretLoading.value = true;
aiContentRight.value = "解读生成中…";
try {
const res = await getChartAnalysis(
{ text: JSON.stringify(payload) },
{
onChunk: (chunk) => {
appendAiInterpretationChunk(aiContentRight, chunk);
}
}
);
const text = getInterpretationTextFromChartResponse(res);
aiContentRight.value = text || aiContentRight.value || "未返回有效解读内容";
} catch (error) {
console.error("合作限制领域分布图表解读请求失败", error);
aiContentRight.value = "解读加载失败";
} finally {
isRightInterpretLoading.value = false;
}
};
const initLeftChart = () => {
if (!leftChartRef.value) return;
if (leftChart) leftChart.dispose();
if (!hasLeftChartData.value) {
if (leftChart) {
leftChart.dispose();
leftChart = null;
}
return;
}
if (!leftChartRef.value) {
return;
}
if (leftChart) {
leftChart.dispose();
}
leftChart = echarts.init(leftChartRef.value);
// 处理动态数据
const rawData = coopRestrictionCompare.value;
const yearsSet = new Set();
const typesSet = new Set();
rawData.forEach(item => {
rawData.forEach((item) => {
yearsSet.add(item.LIMITDATE);
typesSet.add(item.TYPENAME);
});
const years = Array.from(yearsSet).sort();
const types = Array.from(typesSet);
// 如果没有数据,给一些默认年份展示
const finalYears = years.length > 0 ? years : ["2020", "2021", "2022", "2023", "2024", "2025"];
// 定义颜色配置,确保线条色与阴影色对应
const finalYears = years.length > 0 ? years : [];
if (!finalYears.length || !types.length) {
leftChart.dispose();
leftChart = null;
return;
}
const colorMap = [
{ line: "#2f7ae5", start: "rgba(47, 122, 229, 0.3)", end: "rgba(47, 122, 229, 0.05)" },
{ line: "#29c0b1", start: "rgba(41, 192, 177, 0.3)", end: "rgba(41, 192, 177, 0.05)" },
{ line: "#e45f5f", start: "rgba(228, 95, 95, 0.3)", end: "rgba(228, 95, 95, 0.05)" },
{ line: "#7b5de6", start: "rgba(123, 93, 230, 0.3)", end: "rgba(123, 93, 230, 0.05)" }
];
const series = types.map((type, index) => {
const data = finalYears.map(year => {
const found = rawData.find(item => item.TYPENAME === type && item.LIMITDATE === year);
return found ? found.TYPECOUNT : 0;
const data = finalYears.map((year) => {
const found = rawData.find((item) => item.TYPENAME === type && item.LIMITDATE === year);
const v = found ? found.TYPECOUNT : 0;
// 确保是整数,避免 ECharts 自动推断出小数坐标/刻度
return Math.round(Number(v) || 0);
});
// 根据索引获取颜色配置,超出范围则循环使用
const colorConfig = colorMap[index % colorMap.length];
return {
name: type,
type: "line",
smooth: false,
data: data,
smooth: true,
data,
symbol: "circle",
symbolSize: 6,
// 点应覆盖在折线之上,避免视觉上“穿过”节点圆
z: 3,
lineStyle: {
color: colorConfig.line,
width: 2
},
itemStyle: {
color: colorConfig.line
color: "#fff",
borderColor: colorConfig.line,
borderWidth: 2
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: colorConfig.start },
{ offset: 1, color: colorConfig.end }
])
}
},
axisLine: {
lineStyle: {
color: 'rgba(231, 241, 255, 1)'
}
},
};
});
const legendData = types.map((type, index) => ({
name: type,
itemStyle: {
// 图例需要实心圆:独立于 series 的空心点样式
color: colorMap[index % colorMap.length].line,
borderColor: colorMap[index % colorMap.length].line,
borderWidth: 0
}
}));
const maxYValue = Math.max(
0,
...series.flatMap((s) => (Array.isArray(s.data) ? s.data : []))
);
const option = {
color: colorMap.map(c => c.line),
color: colorMap.map((c) => c.line),
grid: { left: 40, right: 24, top: 46, bottom: 36 },
tooltip: { trigger: "axis", axisPointer: { type: "line" } },
legend: {
top: 8,
icon: "circle",
itemWidth: 12,
itemHeight: 12,
itemGap: 24,
textStyle: {
color: "rgb(95, 101, 108)",
fontSize: 16,
fontFamily: "Microsoft YaHei",
fontWeight: 400,
lineHeight: 24
},
data: types
legend: {
top: 8,
icon: "circle",
itemWidth: 12,
itemHeight: 12,
itemGap: 24,
textStyle: {
color: "rgb(95, 101, 108)",
fontSize: 16,
fontFamily: "Microsoft YaHei",
fontWeight: 400,
lineHeight: 24
},
data: legendData
},
xAxis: {
type: "category",
boundaryGap: false,
data: finalYears,
axisLine: { show: false },
axisTick: { show: false },
axisLabel: {
color: "rgba(132, 136, 142, 1)",
fontSize: 14,
lineHeight: 22,
fontFamily: "Microsoft YaHei",
fontWeight: 400
}
xAxis: {
type: "category",
boundaryGap: false,
data: finalYears,
axisLine: {
show: true,
lineStyle: {
color: "rgba(231, 241, 255, 1)"
}
},
axisTick: {
show: true,
// 让刻度和 label 对齐,显示短直线
alignWithLabel: true,
length: 6,
length2: 0,
lineStyle: {
color: "rgba(231, 241, 255, 1)",
type: "solid"
}
},
axisLabel: {
color: "rgba(132, 136, 142, 1)",
fontSize: 14,
lineHeight: 22,
fontFamily: "Microsoft YaHei",
fontWeight: 400
}
},
yAxis: {
type: "value",
min: 0,
splitLine: { show: true, lineStyle: { color: "#e6e6e6", type: "dashed" } },
axisLine: { show: false },
axisTick: { show: false },
axisLabel: {
color: "rgba(132, 136, 142, 1)",
fontSize: 14,
fontFamily: "Microsoft YaHei",
fontWeight: 400,
lineHeight: 22
}
yAxis: {
type: "value",
min: 0,
// 强制整数刻度:0/1/2/3...
interval: 1,
max: Math.ceil(maxYValue),
name: "数量",
nameLocation: "end",
nameGap: 20,
nameRotate: 0,
nameTextStyle: {
color: "rgba(170, 173, 177, 1)",
fontFamily: "Microsoft YaHei",
fontWeight: 400,
fontSize: 14,
lineHeight: 22,
letterSpacing: 0,
align: "right",
verticalAlign: "bottom"
},
axisLabel: {
color: "rgba(132, 136, 142, 1)",
fontSize: 14,
fontFamily: "Microsoft YaHei",
fontWeight: 400,
lineHeight: 22,
formatter: (val) => String(Math.round(Number(val) || 0))
},
splitLine: {
show: true,
lineStyle: { color: "rgba(231, 241, 255, 1)", type: "dashed", width: 1 }
},
axisLine: { show: false },
axisTick: { show: false }
},
series: series
series
};
leftChart.setOption(option);
window.addEventListener("resize", handleResize);
};
const handleResize = () => { if (leftChart) leftChart.resize(); if (rightChart) rightChart.resize(); };
const handleResize = () => {
if (leftChart) {
leftChart.resize();
}
if (rightChart) {
rightChart.resize();
}
};
const initRightChart = () => {
if (!rightChartRef.value) return;
if (rightChart) rightChart.dispose();
if (!hasRightChartData.value) {
if (rightChart) {
rightChart.dispose();
rightChart = null;
}
return;
}
if (!rightChartRef.value) {
return;
}
if (rightChart) {
rightChart.dispose();
}
rightChart = echarts.init(rightChartRef.value);
const rawData = coopRestrictionDomain.value;
// 数据为空处理
if (!rawData || rawData.length === 0) {
rightChart.setOption({
graphic: {
type: 'text',
left: 'center',
top: 'middle',
style: {
text: '暂无数据',
fill: '#999',
fontSize: 16
}
}
});
return;
}
// 1. 动态提取所有领域(雷达图的维度)
const domainsSet = new Set();
const typesSet = new Set();
rawData.forEach(item => {
rawData.forEach((item) => {
domainsSet.add(item.AREA);
typesSet.add(item.COOPERTYPE);
});
......@@ -310,18 +572,16 @@ const initRightChart = () => {
const domains = Array.from(domainsSet);
const types = Array.from(typesSet);
// 2. 构造指示器(Indicator),自动计算 Max 值
const indicators = domains.map(domain => {
const domainData = rawData.filter(item => item.AREA === domain);
const maxVal = Math.max(...domainData.map(d => d.COOPERTYPECOUNT), 5); // 最小给个5
return { name: domain, max: Math.ceil(maxVal * 1.2) }; // 留 20% 的余量
const indicators = domains.map((domain) => {
const domainData = rawData.filter((item) => item.AREA === domain);
const maxVal = Math.max(...domainData.map((d) => d.COOPERTYPECOUNT), 5);
return { name: domain, max: Math.ceil(maxVal * 1.2) };
});
// 3. 构造 Series 数据
const colorMap = ["#2f7ae5", "#29c0b1", "#e45f5f", "#7b5de6"];
const seriesData = types.map((type, index) => {
const dataValues = domains.map(domain => {
const found = rawData.find(item => item.COOPERTYPE === type && item.AREA === domain);
const dataValues = domains.map((domain) => {
const found = rawData.find((item) => item.COOPERTYPE === type && item.AREA === domain);
return found ? found.COOPERTYPECOUNT : 0;
});
......@@ -329,18 +589,19 @@ const initRightChart = () => {
name: type,
value: dataValues,
itemStyle: { color: colorMap[index % colorMap.length] },
areaStyle: { color: colorMap[index % colorMap.length], opacity: 0.1 }
// 不要填充多边形:让雷达图“圆里面是空的”
// areaStyle 不设置(或设为 0)可避免穿透同心圆的填充效果
};
});
const option = {
color: colorMap,
legend: {
top: 8,
icon: "circle",
itemWidth: 12,
itemHeight: 12,
itemGap: 24,
legend: {
top: 8,
icon: "circle",
itemWidth: 12,
itemHeight: 12,
itemGap: 24,
textStyle: {
color: "rgb(95, 101, 108)",
fontSize: 16,
......@@ -353,11 +614,7 @@ const initRightChart = () => {
radar: {
center: ["50%", "55%"],
radius: "65%",
indicator: indicators.length > 0 ? indicators : [
{ name: "暂无数据", max: 100 },
{ name: "暂无数据", max: 100 },
{ name: "暂无数据", max: 100 }
],
indicator: indicators,
axisName: {
color: "rgba(132, 136, 142, 1)",
fontSize: 14,
......@@ -366,66 +623,110 @@ const initRightChart = () => {
splitLine: { lineStyle: { color: ["#e6e6e6"] } },
splitArea: { show: false }
},
series: [{
type: "radar",
data: seriesData
}]
series: [
{
type: "radar",
data: seriesData,
// 移除雷达图端点圆点(既不显示实心圆,也不显示空心圆)
symbol: "none",
symbolSize: 0,
showSymbol: false
}
]
};
rightChart.setOption(option);
window.addEventListener("resize", handleResize);
};
onMounted(() => {
// 合作限制-各类型合作限制政策对比接口
getCoopRestrictionCompareData();
// 合作限制-各领域规则分布情况接口
getCoopRestrictionDomainData();
initLeftChart();
initRightChart();
watch(
coopRestrictionCompare,
async () => {
aiContentLeft.value = "";
await nextTick();
initLeftChart();
if (isShowAiLeft.value && hasLeftChartData.value) {
fetchLeftInterpretation();
}
},
{ deep: true, immediate: true }
);
watch(
coopRestrictionDomain,
async () => {
aiContentRight.value = "";
await nextTick();
initRightChart();
if (isShowAiRight.value && hasRightChartData.value) {
fetchRightInterpretation();
}
},
{ deep: true, immediate: true }
);
onMounted(async () => {
window.addEventListener("resize", handleResize);
await getCoopRestrictionCompareData();
await getCoopRestrictionDomainData();
});
onBeforeUnmount(() => {
window.removeEventListener("resize", handleResize);
if (leftChart) {
leftChart.dispose();
leftChart = null;
}
if (rightChart) {
rightChart.dispose();
rightChart = null;
}
});
onBeforeUnmount(() => { window.removeEventListener("resize", handleResize); if (leftChart) { leftChart.dispose(); leftChart = null; } if (rightChart) { rightChart.dispose(); rightChart = null; } });
</script>
<style lang="scss" scoped>
* {
margin: 0;
padding: 0;
margin: 0;
padding: 0;
}
.datasub {
width: 1600px;
height: 460px;
display: flex;
justify-content: space-between;
.left {
width: 1063px;
height: 460px;
margin-right: 16px;
border-radius: 10px;
// border: 1px solid rgb(234, 236, 238);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background-color: #fff;
.left-title {
width: 1063px;
height: 48px;
border-bottom: 1px solid rgb(234, 236, 238);
position: relative;
img {
width: 19px;
height: 19px;
position: absolute;
top: 16px;
left: 21px;
}
.tit {
margin-left: 60px;
height: 48px;
padding: 11px 0;
font-size: 20px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 26px;
color: rgb(5, 95, 194);
}
width: 1600px;
height: 460px;
display: flex;
justify-content: space-between;
.left {
width: 1063px;
height: 460px;
margin-right: 16px;
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background-color: #fff;
.left-title {
width: 1063px;
height: 48px;
border-bottom: 1px solid rgb(234, 236, 238);
position: relative;
img {
width: 19px;
height: 19px;
position: absolute;
top: 16px;
left: 21px;
}
.tit {
margin-left: 60px;
height: 48px;
padding: 11px 0;
font-size: 20px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 26px;
color: rgb(5, 95, 194);
}
.select {
width: 150px;
height: 28px;
......@@ -434,46 +735,104 @@ onBeforeUnmount(() => { window.removeEventListener("resize", handleResize); if (
top: 11px;
right: 31px;
}
}
}
.left-main {
width: 1063px;
height: 412px;
padding: 15px 23px 25px 38px;
box-sizing: border-box;
position: relative;
padding: 24px 24px 65px 24px;
&.left-main--empty {
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
:deep(.el-empty__image) {
margin-bottom: 0;
}
}
.datasub-left-empty {
padding: 0;
margin: 0;
}
.left-chart-row {
display: flex;
flex-direction: row;
align-items: flex-start;
width: 100%;
box-sizing: border-box;
}
.left-main-echarts {
width: 1002px;
height: 372px;
width: 1015px;
height: 323px;
}
.source {
position: absolute;
bottom: 21px;
left: 24px;
height: 22px;
display: flex;
}
.chart-box {
position: absolute;
right: 0;
bottom: 18px;
.btn-box {
width: 74px;
height: 28px;
}
.content-box {
width: 1063px;
position: absolute;
right: 0;
bottom: -18px;
}
}
}
}
.right {
width: 521px;
height: 460px;
border-radius: 10px;
// border: 1px solid rgb(234, 236, 238);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background-color: #fff;
.right-title {
width: 521px;
height: 48px;
border-bottom: 1px solid rgb(234, 236, 238);
position: relative;
img {
width: 19px;
height: 19px;
position: absolute;
top: 16px;
left: 21px;
}
.tit {
margin-left: 60px;
height: 48px;
padding: 11px 0;
font-size: 20px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 26px;
color: rgb(5, 95, 194);
}
}
.right {
width: 521px;
height: 460px;
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background-color: #fff;
.right-title {
width: 521px;
height: 48px;
border-bottom: 1px solid rgb(234, 236, 238);
position: relative;
img {
width: 19px;
height: 19px;
position: absolute;
top: 16px;
left: 21px;
}
.tit {
margin-left: 60px;
height: 48px;
padding: 11px 0;
font-size: 20px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 26px;
color: rgb(5, 95, 194);
}
.select {
width: 150px;
height: 28px;
......@@ -482,16 +841,68 @@ onBeforeUnmount(() => { window.removeEventListener("resize", handleResize); if (
top: 11px;
right: 31px;
}
}
}
.right-main {
width: 521px;
height: 421px;
padding: 15px 50px 25px 50px;
height: 412px;
box-sizing: border-box;
padding: 24px 24px 64px 24px;
position: relative;
&.right-main--empty {
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
:deep(.el-empty__image) {
margin-bottom: 0;
}
}
.datasub-right-empty {
padding: 0;
margin: 0;
}
.right-chart-row {
display: flex;
width: 100%;
box-sizing: border-box;
}
.right-main-echarts {
width: 420px;
height: 372px;
width: 473px;
height: 324px;
}
.source {
position: absolute;
bottom: 21px;
left: 24px;
height: 22px;
display: flex;
}
.chart-box {
position: absolute;
right: 0;
bottom: 18px;
.btn-box {
width: 74px;
height: 28px;
}
.content-box {
width: 520px;
position: absolute;
right: 0;
bottom: -18px;
}
}
}
}
}
}
</style>
<template>
<div class="reslib-page" ref="reslibContainer">
<div class="nav">
<div
v-for="item in navList"
:key="item.id"
class="nav-item"
:class="{ active: item.id === activeItem }"
@click="activeItem = item.id"
>
<div v-for="item in navList" :key="item.id" class="nav-item" :class="{ active: item.id === activeItem }"
@click="activeItem = item.id">
{{ item.name }}
</div>
</div>
<el-select v-model="value" placeholder="排序方式" class="select">
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
<el-select v-model="sortModel" placeholder="发布时间" class="select" :teleported="true" placement="bottom-start"
:popper-options="sortPopperOptions" @change="handleSortChange">
<template #prefix>
<img v-if="sortModel !== true" src="@/views/thinkTank/ThinkTankDetail/thinkDynamics/images/image down.png"
class="select-prefix-img" alt="" @click.stop="toggleSortPrefix" />
<img v-else src="@/views/thinkTank/ThinkTankDetail/thinkDynamics/images/image up.png" class="select-prefix-img"
alt="" @click.stop="toggleSortPrefix" />
</template>
<el-option :key="true" label="正序" :value="true" />
<el-option :key="false" label="倒序" :value="false" />
</el-select>
<div class="main">
<div class="left">
<div class="left-ti1"></div>
<div class="left-ti2"></div>
<div class="left-title">数据来源</div>
<div class="left-title ">科技领域</div>
<div class="left-content">
<div v-for="item in dataList" :key="item.id" class="left-item">
<input
type="checkbox"
:value="String(item.id)"
v-model="selectedSources"
/>{{ item.name }}
<div v-for="item in dataList2" :key="item.id" class="left-item">
<input type="checkbox" :value="String(item.id)" v-model="selectedDomains" />{{ item.name }}
</div>
</div>
<div class="left-title cl1">涉及领域</div>
<div class="left-ti2"></div>
<div class="left-title">数据来源</div>
<div class="left-content">
<div v-for="item in dataList2" :key="item.id" class="left-item">
<input
type="checkbox"
:value="String(item.id)"
v-model="selectedDomains"
/>{{ item.name }}
<div v-for="item in dataList" :key="item.id" class="left-item">
<input type="checkbox" :value="String(item.id)" v-model="selectedSources" />{{ item.name }}
</div>
</div>
</div>
......@@ -47,30 +44,26 @@
<div class="right-main">
<div class="main-content">
<div v-for="item in mainDataList" :key="item.id" class="main-item">
<div class="date">{{ item.date }}</div>
<div class="date">{{ formatDateCn(item.date) }}</div>
<img :src="item.img" alt="" class="img" />
<div class="box">
<div class="title" @click="handleClick(item)">{{ item.title }}</div>
<div class="content" @click="handleClick(item)">{{ item.content }}</div>
<div class="domain">
<div v-for="(domain, i) in item.domain" :key="i" class="domain-item">{{ domain }}</div>
<AreaTag v-for="(domain, i) in item.domain" :key="i" " :tagName="domain">
</AreaTag>
</div>
<div class="type" :class="getTypeClass(item.type)">
{{ item.type }}
</div>
</div>
</div>
</div>
<div class="page">
<div class="count">{{ total }}</div>
<el-pagination
v-model:current-page="currentPage"
:page-size="pageSize"
:total="total"
layout="prev, pager, next"
background
@current-change="handlePageChange"
/>
</div>
<div class="page">
<div class="count">共 {{ total }} 项调查</div>
<el-pagination v-model:current-page="currentPage" :page-size="pageSize" :total="total"
layout="prev, pager, next" background @current-change="handlePageChange" />
</div>
</div>
</div>
......@@ -79,12 +72,23 @@
</template>
<script setup>
import { ref, onMounted, watch } from "vue";
import { ref, onMounted, watch, computed } from "vue";
import { useRouter } from "vue-router";
import { getCoopRestrictionList } from "@/api/coopRestriction/coopRestriction";
import defaultImg from "../../assets/images/default-icon2.png";
const formatDateCn = (dateStr) => {
const s = String(dateStr || "").trim();
// 兼容 YYYY-MM-DD / YYYY/MM/DD
const m = s.match(/^(\d{4})[-/](\d{1,2})[-/](\d{1,2})/);
if (!m) return s;
const y = m[1];
const month = String(Number(m[2]) || "");
const day = String(Number(m[3]) || "");
return `${y}\n${month}${day}日`;
};
// 合作限制-获取合作限制列表接口
const getMainDataList = async () => {
const params = {
......@@ -102,12 +106,11 @@ const getMainDataList = async () => {
if (activeItem.value !== "0") {
params.type = activeItem.value;
}
if (value.value) {
params.sortOrder = value.value;
}
// 与智库概览一致:null 作为占位但默认按倒序;true=正序,false=倒序
params.sortOrder = sort.value === true ? "asc" : "desc";
try {
console.log('----params getMainDataList', params)
try {
console.log('----params getMainDataList', params)
const res = await getCoopRestrictionList(params);
if (res && res.code === 200) {
mainDataList.value = (res.data.content || []).map(item => ({
......@@ -153,18 +156,43 @@ const navList = ref([
]);
const activeItem = ref("0");
const value = ref("");
/** null:占位「发布时间」且默认倒序;true 正序;false 倒序(显式),与智库概览一致 */
const sort = ref(null);
const options = [
{
value: "asc",
label: "正序"
const sortPopperOptions = {
modifiers: [
{ name: "preventOverflow", options: { mainAxis: false, altAxis: false } },
{ name: "flip", enabled: false }
]
};
const sortModel = computed({
get() {
return sort.value;
},
{
value: "desc",
label: "倒序"
set(v) {
sort.value = v;
}
});
const handleSortChange = () => {
// 改变排序后从第一页开始
if (currentPage.value === 1) {
getMainDataList();
} else {
currentPage.value = 1;
}
};
const toggleSortPrefix = () => {
sort.value = sort.value === true ? false : true;
// 切换排序后从第一页开始
if (currentPage.value === 1) {
getMainDataList();
} else {
currentPage.value = 1;
}
];
};
const dataList = ref([
{
id: "0",
......@@ -276,7 +304,7 @@ onMounted(() => {
});
watch(
[activeItem, selectedSources, selectedDomains, value],
[activeItem, selectedSources, selectedDomains, sort],
(newVal, oldVal) => {
const [newActive, newSources, newDomains] = newVal;
const [, oldSources, oldDomains] = oldVal;
......@@ -284,7 +312,7 @@ watch(
if (newSources.includes("0") && newSources.length > 1) {
if (oldSources.includes("0")) {
selectedSources.value = newSources.filter(i => i !== "0");
return;
return;
} else {
selectedSources.value = ["0"];
return;
......@@ -326,12 +354,13 @@ watch(currentPage, () => {
margin: 0;
padding: 0;
}
.reslib-page {
width: 1600px;
min-height: 1565px;
height: auto;
position: relative;
padding-bottom: 50px;
.nav {
width: 808px;
height: 42px;
......@@ -339,6 +368,7 @@ watch(currentPage, () => {
align-items: center;
justify-content: space-between;
margin-bottom: 34px;
.nav-item {
cursor: pointer;
padding: 8px 20px;
......@@ -348,6 +378,7 @@ watch(currentPage, () => {
line-height: 26px;
color: rgb(59, 65, 75);
}
.active {
background-color: rgb(5, 95, 194);
border-radius: 21px;
......@@ -355,25 +386,44 @@ watch(currentPage, () => {
font-weight: 700;
}
}
.select {
width: 128px;
position: absolute;
top: 7px;
right: 0px;
}
.select-prefix-img {
width: 7px;
height: 14px;
margin-right: 6px;
cursor: pointer;
}
/* 下拉项内边距:与智库模块 el-select 视觉一致 */
:deep(.el-select-dropdown__item) {
padding: 0 20px;
}
.main {
width: 1600px;
height: auto;
min-height: 1489px;
display: flex;
margin-bottom: 35px;
.left {
width: 300px;
height: 760px;
width: 360px;
height: 432px;
padding-bottom: 24px;
box-sizing: border-box;
border: 1px solid rgba(234, 236, 238, 1);
margin-right: 16px;
border-radius: 10px;
background-color: #fff;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
box-shadow: 0px 0px 20px 0px rgba(94, 95, 95, 0.1);
position: relative;
.left-ti1 {
width: 8px;
height: 16px;
......@@ -381,9 +431,10 @@ watch(currentPage, () => {
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
position: absolute;
top: 17px;
top: 20px;
left: 0px;
}
.left-ti2 {
width: 8px;
height: 16px;
......@@ -391,47 +442,62 @@ watch(currentPage, () => {
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
position: absolute;
top: 207px;
top: 320px;
left: 0px;
}
.left-title {
margin-left: 25px;
color: rgb(5, 95, 194);
margin-top: 16px;
color: var(--color-main-active);
font-size: 16px;
font-weight: 700;
font-family: "Microsoft YaHei";
font-family: "Source Han Sans CN";
line-height: 24px;
margin-top: 13px;
letter-spacing: 1px;
height: 24px;
}
.left-content {
// width: 109px;
// height: 132px;
margin-left: 25px;
margin-top: 13px;
margin-left: 24px;
margin-top: 12px;
display: grid;
grid-template-columns: repeat(2, 160px);
gap: 8px 4px;
.left-item {
// width: 89px;
height: 30px;
margin-bottom: 4px;
width: 160px;
height: 24px;
margin: 0;
display: flex;
align-items: center;
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
font-family: "Source Han Sans CN";
line-height: 24px;
color: rgb(95, 101, 108);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
input[type="checkbox"] {
-webkit-appearance: none;
appearance: none;
width: 14px;
height: 14px;
margin-right: 8px;
flex: 0 0 auto;
border: 1px solid rgb(200, 204, 210);
border-radius: 4px;
background-color: #fff;
vertical-align: middle;
}
input[type="checkbox"]:checked {
background-color: rgb(5, 95, 194);
border-color: rgb(5, 95, 194);
}
input[type="checkbox"]:checked::after {
content: "";
display: block;
......@@ -445,22 +511,23 @@ watch(currentPage, () => {
}
}
}
.cl1 {
margin-top: 21px;
}
}
.right {
width: 1284px;
height: auto;
min-height: 1489px;
width: 1224px;
border-radius: 10px;
background-color: #fff;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
.right-title {
width: 1284px;
width: 1224px;
height: 48px;
border-bottom: 1px solid rgb(235, 238, 242);
position: relative;
img {
width: 22px;
height: 19px;
......@@ -468,6 +535,7 @@ watch(currentPage, () => {
top: 15px;
left: 20px;
}
div {
width: 120px;
height: 48px;
......@@ -480,69 +548,89 @@ watch(currentPage, () => {
padding: 11px 0;
}
}
.right-main {
width: 1284px;
height: auto;
min-height: 1441px;
padding: 22px 43px 80px 20px;
width: 1224px;
padding: 12px 0px 80px 0px;
position: relative;
.main-content {
width: 1221px;
height: auto;
min-height: 1345px;
width: 1224px;
padding-left: 28px;
border-bottom: 1px solid rgb(235, 238, 242);
.main-item {
width: 1221px;
height: auto;
min-height: 116px;
margin-bottom: 24px;
width: 1167px;
position: relative;
display: flex;
&::after {
content: "";
position: absolute;
top: 37px;
bottom: -37px;
left: 91px;
top: 37px;
bottom: -37px;
left: 108px;
width: 2px;
background-color: rgb(230, 231, 232);
z-index: 1;
}
&:last-child::after {
display: none;
content: "";
position: absolute;
top: 37px;
bottom: -37px;
left: 108px;
width: 2px;
background-color: rgb(230, 231, 232);
z-index: 1;
height: calc(100% - 37px);
}
.date {
flex-shrink: 0;
width: 62px;
width: 80px;
font-size: 16px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(5, 95, 194);
text-align: right;
margin-top: 13px;
margin-top: 6px;
white-space: pre-line;
}
.img {
flex-shrink: 0;
width: 24px;
height: 24px;
margin: 13px 21px 0 18px;
margin: 14px 16px 0 16px;
position: relative;
z-index: 100;
}
.box {
flex: 1;
min-height: 91px;
position: relative;
padding-top: 10px;
padding-top: 14px;
.title {
font-size: 20px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 26px;
color: rgb(59, 65, 75);
margin-bottom: 8px;
margin-bottom: 9px;
cursor: pointer;
}
.content {
font-size: 16px;
font-weight: 400;
......@@ -552,6 +640,7 @@ watch(currentPage, () => {
margin-bottom: 9px;
cursor: pointer;
}
.type {
padding: 2px 8px;
border-radius: 20px;
......@@ -563,29 +652,37 @@ watch(currentPage, () => {
top: 10px;
right: 0px;
}
.type1 {
background-color: rgba(255, 149, 77, 0.1);
color: rgb(255, 149, 77);
}
.type2 {
background-color: rgba(206, 79, 81, 0.1);
color: rgb(206, 79, 81);
}
.type3 {
background-color: rgba(5, 95, 194, 0.1);
color: rgb(5, 95, 194);
}
.type4 {
background-color: rgba(103, 194, 58, 0.1);
color: rgb(103, 194, 58);
}
.type-default {
background-color: rgba(144, 147, 153, 0.1);
color: rgb(144, 147, 153);
}
.domain {
margin-bottom: 15px;
margin-bottom: 12px;
display: flex;
gap: 8px;
.domain-item {
padding: 2px 8px;
border-radius: 4px;
......@@ -601,8 +698,9 @@ watch(currentPage, () => {
}
}
}
.page {
width: 1221px;
width: 1159px;
height: 40px;
display: flex;
align-items: center;
......@@ -611,6 +709,7 @@ watch(currentPage, () => {
bottom: 20px;
left: 20px;
padding-left: 11px;
.count {
font-size: 16px;
font-weight: 400;
......@@ -618,10 +717,12 @@ watch(currentPage, () => {
line-height: 24px;
color: rgb(59, 65, 75);
}
:deep(.el-pagination) {
display: flex;
align-items: center;
}
:deep(.el-pagination.is-background .el-pager li) {
min-width: 32px;
height: 32px;
......@@ -635,12 +736,14 @@ watch(currentPage, () => {
font-weight: 400;
font-family: "Microsoft YaHei";
}
:deep(.el-pagination.is-background .el-pager li.is-active) {
background-color: #fff;
color: rgba(22, 119, 255, 1);
border-color: rgba(22, 119, 255, 1);
// box-shadow: 0 0 0 2px rgba(47, 122, 229, 0.15) inset;
}
:deep(.el-pagination.is-background .el-pager li.is-ellipsis) {
border: none;
background-color: transparent;
......@@ -648,6 +751,7 @@ watch(currentPage, () => {
min-width: 16px;
margin: 0 6px;
}
:deep(.el-pagination.is-background .btn-prev),
:deep(.el-pagination.is-background .btn-next) {
min-width: 32px;
......@@ -660,6 +764,7 @@ watch(currentPage, () => {
font-family: "Microsoft YaHei";
margin: 0 6px;
}
:deep(.el-pagination.is-background .btn-prev.is-disabled),
:deep(.el-pagination.is-background .btn-next.is-disabled) {
color: rgba(95, 101, 108, 0.45);
......
......@@ -4,9 +4,9 @@
<div class="nav-main">
<img :src="coopData?.IMAGEURL || defaultImg" alt="" />
<div class="content">
<div class="cl1">{{coopData?.LIMITNAMEZH}}</div>
<div class="cl2">{{coopData?.LIMITNAME}}</div>
<div class="cl3">{{coopData?.LIMITDATE}} · {{ coopData?.LIMITORGNAME }}</div>
<div class="cl1">{{ coopData?.LIMITNAMEZH }}</div>
<div class="cl2">{{ coopData?.LIMITNAME }}</div>
<div class="cl3">{{ coopData?.LIMITDATE }} · {{ coopData?.LIMITORGNAME }}</div>
</div>
<div class="btn">
<button class="btn1"><img src="./assets/icon01.png" alt="" />查看原文</button>
......@@ -15,45 +15,42 @@
</div>
</div>
</div>
<div class="title" v-if="coopData?.Relation?.[0]" @click="handleClick">
<span class="title-one">当前合作限制数据已关联至{{coopData.Relation[0]?.TYPE}}</span>
<span class="title-two">{{coopData.Relation[0]?.RELATIONNAME}}{{coopData.Relation[0]?.RELATIONDATE}}</span>
<div class="title" v-if="coopData.Relation?.[0]" @click="handleClick">
<span class="title-one">当前合作限制数据已关联至{{ coopData.Relation[0]?.TYPE }}</span>
<span class="title-two">{{ coopData.Relation[0]?.RELATIONNAME }}{{ coopData.Relation[0]?.RELATIONDATE }}</span>
<img src="./assets/right.png" alt="" />
</div>
<div class="main">
<div class="left">
<!-- 制裁概况 -->
<div class="left-top">
<img class="img1" src="./assets/bluetitle.png" alt="" />
<div class="left-top-title">制裁概况</div>
<img class="img2" src="./assets/下载按钮.png" alt="" />
<img class="img3" src="./assets/收藏按钮.png" alt="" />
<div class="left-top-content">
<span
>{{ coopData?.INTRODUCTION }}</span
>
</div>
<div class="left-top-bottom">
<div><span class="tit">限制时间:</span><span class="tit1">{{ coopData?.LIMITDATE }}</span></div>
<div @click="handleClickOnLimitOrg" style="cursor: pointer;"><span class="tit">限制机构:</span><span class="tit1 tit2">{{ coopData?.LIMITORGNAME }} ></span></div>
<div><span class="tit">限制手段:</span><span class="tit1">{{ coopData?.LIMITMEANS }}</span></div>
<div><span class="tit">限制类型:</span><span class="tit1 tit3">{{ coopData?.LIMITTYPE }}</span></div>
<div><span class="tit">限制领域:</span><span class="tit1">{{ coopData?.area }}</span></div>
</div>
<AnalysisBox title="制裁概况" :showAllBtn="true">
<div class="box1-main">
<div class="left-top-content">
<span>{{ coopData?.INTRODUCTION }}</span>
</div>
<div class="left-top-bottom">
<div><span class="tit1">限制时间:</span><span class="tit2">{{ coopData?.LIMITDATE }}</span></div>
<div @click="handleClickOnLimitOrg" style="cursor: pointer;"><span class="tit1">限制机构:</span><span
class=" tit3">{{ coopData?.LIMITORGNAME }} ></span></div>
<div><span class="tit1">限制手段:</span><span class="tit2">{{ coopData?.LIMITMEANS }}</span></div>
<div><span class="tit1">限制类型:</span><span class="tit4">{{ coopData?.LIMITTYPE }}</span></div>
<div><span class="tit1">限制领域:</span><span class="tit2">{{ coopData?.area }}</span></div>
</div>
</div>
</AnalysisBox>
</div>
<!-- 相关实体 -->
<div class="left-bottom">
<img class="img1" src="./assets/bluetitle.png" alt="" />
<div class="left-bottom-title">相关实体</div>
<img class="img2" src="./assets/下载按钮.png" alt="" />
<img class="img3" src="./assets/收藏按钮.png" alt="" />
<div class="left-bottom-main">
<div v-for="item in coopRelatedData" :key="item.id" class="main-box" @click="handleClickOnEntity(item)">
<img :src="item.img || defaultCom" alt="" />
<div class="name">{{ item.ENTITYNAME }}</div>
<div class="type">{{ item.type }}</div>
<AnalysisBox title="相关实体" :showAllBtn="true">
<div class="left-bottom-main">
<div v-for="item in coopRelatedData" :key="item.id" class="main-box" @click="handleClickOnEntity(item)">
<img :src="item.img || defaultCom" alt="" />
<div class="name">{{ item.ENTITYNAME }}</div>
<div class="type">{{ item.type }}</div>
</div>
</div>
</div>
</AnalysisBox>
</div>
</div>
<div class="right">
......@@ -75,8 +72,8 @@
<div class="right-bottom">
<img class="img1" src="./assets/bluetitle.png" alt="" />
<div class="right-bottom-title">限制条款</div>
<div class="btn cl1" :class="{'active': active2 === '涉华背景'}" @click="active2 = '涉华背景'">涉华背景</div>
<div class="btn cl2" :class="{'active': active2 === '全部背景'}" @click="active2 = '全部背景'">全部背景</div>
<div class="btn cl1" :class="{ 'active': active2 === '涉华条款' }" @click="active2 = '涉华条款'">涉华条款</div>
<div class="btn cl2" :class="{ 'active': active2 === '全部条款' }" @click="active2 = '全部条款'">全部条款</div>
<div class="right-bottom-content">
<div v-for="(item, index) in filteredClauseList" :key="index" class="clause-item">
<div class="clause-item-title">
......@@ -122,14 +119,16 @@ const getlimitClauseData = async () => {
limitId: route.query.id
});
if (res && res.code === 200) {
limitClauseData.value = res.data || [];
// 兼容后端返回:数组 / 单对象 / 包一层 data
const raw = res?.data?.data ?? res?.data ?? [];
limitClauseData.value = Array.isArray(raw) ? raw : raw ? [raw] : [];
} else {
limitClauseData.value = [];
}
} catch (error) {
console.error("获取合作限制限制条款数据失败:", error);
limitClauseData.value = [];
}
}
};
......@@ -150,7 +149,7 @@ const getbackgroundData = async () => {
} catch (error) {
console.error("获取合作限制背景分析数据失败:", error);
backgroundList.value = [];
}
}
};
......@@ -171,7 +170,7 @@ const getcoopRelatedData = async () => {
} catch (error) {
console.error("获取合作限制相关实体数据失败:", error);
coopRelatedData.value = {};
}
}
};
// 点击跳转关联实体详情
const handleClickOnEntity = (item) => {
......@@ -198,7 +197,7 @@ const getCoopRestrictionIntroductionData = async () => {
} catch (error) {
console.error("获取合作限制简介数据失败:", error);
coopData.value = {};
}
}
};
// 点击跳转关联文件详情
......@@ -257,10 +256,11 @@ const filteredBackgroundList = computed(() => {
}
});
const active2 = ref("涉华背景");
// 限制条款筛选:涉华条款/全部条款
const active2 = ref("涉华条款");
const chineseNumbers = ["一", "二", "三", "四", "五", "六", "七", "八", "九", "十"];
const filteredClauseList = computed(() => {
if (active2.value === "全部背景") {
if (active2.value === "全部条款") {
return limitClauseData.value;
} else {
return limitClauseData.value.filter(item => item.ISCN === "Y");
......@@ -376,12 +376,14 @@ const dataList3 = ref([
margin: 0;
padding: 0;
}
.cooperation-restrictions-detail {
width: 100%;
height: 100%;
background: rgba(243, 243, 244, 1);
overflow: auto;
padding-bottom: 50px;
.nav {
width: 100%;
height: 120px;
......@@ -390,52 +392,60 @@ const dataList3 = ref([
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
position: sticky;
top: 0;
z-index: 99999999;
z-index: 9;
.nav-main {
width: 1600px;
height: 81px;
margin: 0 auto;
display: flex;
align-items: center;
img {
width: 72px;
height: 72px;
margin-right: 16px;
}
.content {
width: 758px;
height: 81px;
margin-right: 378px;
.cl1 {
font-size: 24px;
font-weight: 700;
line-height: 32px;
font-family: "Microsoft YaHei";
font-family: "Source Han Sans CN";
color: rgb(59, 65, 75);
margin-bottom: 1px;
}
.cl2 {
height: 24px;
font-size: 16px;
font-weight: 400;
line-height: 24px;
font-family: "Microsoft YaHei";
font-family: "Source Han Sans CN";
color: rgb(59, 65, 75);
margin-bottom: 1px;
}
.cl3 {
font-size: 16px;
font-weight: 400;
line-height: 24px;
font-family: "Microsoft YaHei";
font-family: "Source Han Sans CN";
color: rgb(95, 101, 108);
}
}
.btn {
width: 376px;
height: 36px;
display: flex;
justify-content: right;
.btn1 {
border-radius: 6px;
border: 1px solid rgb(230, 231, 232);
......@@ -447,17 +457,20 @@ const dataList3 = ref([
align-items: center;
justify-content: center;
cursor: pointer;
img {
width: 16px;
height: 16px;
margin-right: 8px;
}
font-size: 16px;
font-weight: 400;
line-height: 22px;
font-family: "Microsoft YaHei";
color: rgb(95, 101, 108);
}
.active {
background-color: rgb(5, 95, 194);
color: #fff;
......@@ -465,6 +478,7 @@ const dataList3 = ref([
}
}
}
.title {
width: 1600px;
height: 50px;
......@@ -477,22 +491,29 @@ const dataList3 = ref([
align-items: center;
position: relative;
cursor: pointer;
.title-one {
margin-left: 23px;
font-size: 16px;
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
line-height: 24px;
font-family: "Microsoft YaHei";
letter-spacing: 0px;
text-align: justify;
color: rgb(59, 65, 75);
}
.title-two {
font-size: 16px;
font-family: "Source Han Sans CN";
font-weight: 700;
font-size: 16px;
line-height: 24px;
font-family: "Microsoft YaHei";
letter-spacing: 0px;
text-align: justify;
color: rgb(5, 95, 194);
cursor: pointer;
}
img {
width: 24px;
height: 24px;
......@@ -502,15 +523,19 @@ const dataList3 = ref([
cursor: pointer;
}
}
.main {
width: 1600px;
height: 1373px;
margin: 0 auto;
display: flex;
margin-top: 16px;
.left {
width: 520px;
height: 1012px;
margin-right: 17px;
.left-top {
margin-bottom: 16px;
width: 520px;
......@@ -521,194 +546,175 @@ const dataList3 = ref([
position: relative;
box-sizing: border-box;
overflow: hidden;
padding-bottom: 33px;
.left-top-title {
font-size: 20px;
font-weight: 700;
line-height: 26px;
font-family: "Microsoft YaHei";
color: rgb(5, 95, 194);
position: absolute;
top: 14px;
left: 22px;
}
.img1 {
width: 8px;
height: 20px;
position: absolute;
left: 0px;
top: 18px;
}
.img2 {
width: 28px;
height: 28px;
position: absolute;
top: 14px;
right: 44px;
cursor: pointer;
}
.img3 {
width: 28px;
height: 28px;
position: absolute;
top: 14px;
right: 12px;
cursor: pointer;
}
.left-top-content {
width: 470px;
margin-top: 58px;
margin-left: 26px;
border-radius: 4px;
border: 1px solid rgba(231, 243, 255, 1);
background-color: rgba(246, 250, 255, 1);
.box1-main {
display: flex;
justify-content: center;
align-items: center;
padding: 16px 24px;
span {
font-size: 16px;
font-weight: 700;
line-height: 30px;
font-family: "Microsoft YaHei";
color: rgb(5, 95, 194);
}
}
.left-top-bottom {
width: 460px;
height: 184px;
margin-top: 19px;
margin-left: 26px;
div {
height: 24px;
margin-bottom: 16px;
.tit {
display: inline-block;
width: 120px;
flex-direction: column;
height: 100%;
padding-bottom: 33px;
.left-top-content {
width: 470px;
margin-top: 4px;
margin-left: 26px;
border-radius: 4px;
border: 1px solid rgba(231, 243, 255, 1);
background: linear-gradient(to bottom, rgba(231, 243, 255, 1), rgba(231, 243, 255, 0));
display: flex;
justify-content: center;
align-items: center;
padding: 16px 24px;
span {
font-size: 16px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(59, 65, 75);
}
.tit1 {
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 30px;
color: rgb(59, 65, 75);
}
.tit2 {
font-family: "Microsoft YaHei";
color: rgb(5, 95, 194);
cursor: pointer;
}
.tit3 {
display: inline-block;
border-radius: 4px;
background-color: rgba(231, 243, 255, 1);
color: rgb(5, 95, 194);
padding: 2px 4px;
}
.left-top-bottom {
width: 460px;
height: 184px;
margin-top: 19px;
margin-left: 26px;
div {
height: 24px;
margin-bottom: 16px;
.tit1 {
display: inline-block;
width: 120px;
font-size: 16px;
height: 26px;
font-weight: 700;
font-family: "Source Han Sans CN";
line-height: 24px;
color: rgb(59, 65, 75);
}
.tit2 {
font-size: 16px;
font-weight: 400;
font-family: "Source Han Sans CN";
line-height: 30px;
color: rgb(59, 65, 75);
}
.tit3 {
color: rgb(5, 95, 194);
font-family: "Source Han Sans CN";
line-height: 30px;
font-size: 16px;
font-weight: 400;
cursor: pointer;
}
.tit4 {
display: inline-block;
font-family: "Source Han Sans CN";
border-radius: 4px;
line-height: 24px !important;
background-color: rgb(231, 243, 255);
color: rgb(5, 95, 194);
padding: 2px 4px;
}
}
}
}
}
.left-bottom {
width: 520px;
height: 610px;
background-color: #fff;
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
position: relative;
.left-bottom-title {
font-size: 20px;
font-weight: 700;
line-height: 26px;
font-family: "Microsoft YaHei";
color: rgb(5, 95, 194);
position: absolute;
top: 14px;
left: 22px;
}
.img1 {
width: 8px;
height: 20px;
position: absolute;
left: 0px;
top: 18px;
}
.img2 {
width: 28px;
height: 28px;
position: absolute;
top: 14px;
right: 44px;
cursor: pointer;
}
.img3 {
width: 28px;
height: 28px;
position: absolute;
top: 14px;
right: 12px;
cursor: pointer;
}
.left-bottom-main {
width: 478px;
height: 528px;
position: absolute;
top: 60px;
left: 21px;
width: 520px;
padding: 6px 21px 22px 21px;
display: flex;
flex-direction: column;
gap: 12px;
.main-box {
width: 480px;
height: 48px;
border-radius: 50px;
border: 1px solid rgb(234, 236, 238);
margin-bottom: 12px;
position: relative;
display: flex;
padding-left: 16px;
padding-top: 12px;
padding-bottom: 12px;
background-color: rgb(247, 248, 249);
cursor: pointer;
img {
width: 24px;
height: 24px;
position: absolute;
top: 12px;
left: 16px;
}
.name {
position: absolute;
top: 12px;
left: 52px;
font-size: 16px;
font-family: "Source Han Sans CN";
font-weight: 700;
font-family: "Microsoft YaHei";
font-size: 16px;
line-height: 24px;
letter-spacing: 0;
text-align: left;
color: rgb(59, 65, 75);
margin-left: 12px;
}
.type {
position: absolute;
top: 12px;
right: 12px;
font-size: 16px;
font-family: "Source Han Sans CN";
font-weight: 400;
font-family: "Microsoft YaHei";
font-size: 16px;
line-height: 24px;
letter-spacing: 0px;
text-align: right;
color: rgb(95, 101, 108);
margin-left: auto;
margin-right: 18px;
}
}
}
}
/* 仅本页左侧两个 AnalysisBox:头部高度改为 54px(放在 .left 作用域,避免 SCSS 嵌套导致选择器失效) */
:deep(.left-top .analysis-box-wrapper .wrapper-header),
:deep(.left-bottom .analysis-box-wrapper .wrapper-header) {
height: 54px !important;
}
:deep(.left-top .analysis-box-wrapper .wrapper-header .header-title > div),
:deep(.left-bottom .analysis-box-wrapper .wrapper-header .header-title > div) {
line-height: 54px !important;
}
}
.right {
width: 1063px;
height: 1373px;
.right-top {
margin-bottom: 16px;
width: 1063px;
height: 476px;
background-color: #fff;
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
position: relative;
padding: 60px 19px 24px 22px;
.right-top-title {
font-size: 20px;
font-weight: 700;
......@@ -719,6 +725,7 @@ const dataList3 = ref([
top: 14px;
left: 22px;
}
.img1 {
width: 8px;
height: 20px;
......@@ -726,9 +733,11 @@ const dataList3 = ref([
left: 0px;
top: 18px;
}
.right-top-content {
width: 1022px;
height: 392px;
.right-top-item {
width: 1022px;
padding: 12px 0px;
......@@ -737,11 +746,13 @@ const dataList3 = ref([
align-items: center;
overflow: auto;
}
.right-top-item:nth-child(odd) {
background-color: rgb(247, 248, 249);
border-top: 1px solid rgb(234, 236, 238);
border-bottom: 1px solid rgb(234, 236, 238);
}
.right-top-item .id {
display: inline-block;
width: 24px;
......@@ -758,16 +769,20 @@ const dataList3 = ref([
top: 16px;
left: 24px;
}
.right-top-item .name {
display: inline-block;
width: 902px;
font-size: 16px;
font-family: "Source Han Sans CN";
font-weight: 400;
font-family: "Microsoft YaHei";
font-size: 16px;
line-height: 30px;
letter-spacing: 0px;
text-align: justify;
color: rgb(59, 65, 75);
margin-left: 64px;
}
.right-top-item img {
width: 16px;
height: 31px;
......@@ -777,6 +792,7 @@ const dataList3 = ref([
cursor: pointer;
}
}
.btn {
padding: 2px 8px;
font-size: 16px;
......@@ -789,29 +805,36 @@ const dataList3 = ref([
background-color: #fff;
cursor: pointer;
position: absolute;
z-index: 100;
}
.cl1 {
top: 14px;
right: 107px;
}
.cl2 {
top: 14px;
right: 19px;
}
.active {
color: rgb(5, 95, 194);
background-color: rgba(246, 250, 255, 1);
border-color: rgb(5, 95, 194);
}
}
.right-bottom {
width: 1063px;
height: 881px;
background-color: #fff;
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
position: relative;
padding: 60px 19px 0px 22px;
.right-bottom-title {
font-size: 20px;
font-weight: 700;
......@@ -822,6 +845,7 @@ const dataList3 = ref([
top: 14px;
left: 22px;
}
.img1 {
width: 8px;
height: 20px;
......@@ -829,6 +853,7 @@ const dataList3 = ref([
left: 0px;
top: 18px;
}
.btn {
padding: 2px 8px;
font-size: 16px;
......@@ -841,40 +866,45 @@ const dataList3 = ref([
background-color: #fff;
cursor: pointer;
position: absolute;
z-index: 100;
}
.cl1 {
top: 14px;
right: 107px;
}
.cl2 {
top: 14px;
right: 19px;
}
.active {
color: rgb(5, 95, 194);
background-color: rgba(246, 250, 255, 1);
border-color: rgb(5, 95, 194);
}
.right-bottom-content {
width: 1022px;
position: absolute;
top: 60px;
left: 22px;
margin-bottom: 24px;
overflow: auto;
.clause-item {
margin-bottom: 24px;
.clause-item-title {
width: 1022px;
height: 55px;
padding: 14px 56px 17px 24px;
padding: 14px 24px 17px 24px;
display: flex;
align-items: center;
position: relative;
justify-content: space-between;
background-color: rgb(247, 248, 249);
border-top: 1px solid rgb(234, 236, 238);
border-bottom: 1px solid rgb(234, 236, 238);
span {
font-size: 18px;
font-weight: 700;
......@@ -882,18 +912,20 @@ const dataList3 = ref([
line-height: 24px;
color: rgb(59, 65, 75);
}
img {
width: 16px;
height: 31px;
position: absolute;
right: 24px;
top: 14px;
cursor: pointer;
}
}
.clause-item-content {
width: 1022px;
padding: 12px 24px 12px 78px;
padding: 12px 24px 12px 54px;
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
......
......@@ -15,7 +15,8 @@
<div class="main-content" ref="homeMainRef" :class="{ 'scroll-main': isShow }">
<div class="home-top-bg"></div>
<!-- 搜索栏部分 -->
<SearchContainer v-if="homeMainRef" placeholder="搜索合作限制" :containerRef="homeMainRef" areaName="" />
<SearchContainer v-if="homeMainRef" :countInfo="cooperationCountInfo" placeholder="搜索合作限制"
:containerRef="homeMainRef" areaName="" />
<!-- 最新动态 -->
<div class="newdata" id="position1">
......@@ -57,9 +58,36 @@ import newData from "./components/dataNew/index.vue";
import askPage from "./components/askPage/index.vue";
import dataSub from "./components/dataSub/index.vue";
import resLib from "./components/resLib/index.vue";
import { getCoopRestrictionStatistics } from "@/api/coopRestriction/coopRestriction.js";
import { useContainerScroll } from "@/hooks/useScrollShow";
const cooperationCountInfo = ref([]);
// const
const getCooperationCountInfo = async () => {
try {
const res = await getCoopRestrictionStatistics();
if (res && res.code === 200) {
// console.log('----getStatCountInfo', res.data)
cooperationCountInfo.value = [
{
name: "相关法案",
count: res.data.billCounts
},
{
name: "相关政令",
count: res.data.aocounts
},
{
name: "相关政府公告",
count: res.data.ggcounts
},
];
}
} catch (error) {
console.error("获取首页统计接口失败:", error);
}
};
// 搜索框
const input = ref("");
const homeMainRef = ref(null);
......@@ -71,7 +99,9 @@ const router = useRouter();
const handleSearch = () => {
console.log("搜索内容:", input.value);
};
onMounted(() => {
getCooperationCountInfo();
});
// 锚点跳转
const handleToPosi = id => {
const element = document.getElementById(id);
......@@ -106,6 +136,7 @@ const handleToPosi = id => {
.coop-page {
width: 100%;
height: 100%;
display: flex;
// .breadcrumb {
// width: 100%;
// height: 64px;
......@@ -138,6 +169,12 @@ const handleToPosi = id => {
top: -64px;
}
:deep(.search-container .search-center) {
width: 440px;
gap: 40px;
justify-content: center;
}
.search {
width: 960px;
height: 168px;
......@@ -467,8 +504,6 @@ const handleToPosi = id => {
}
}
.scroll-main {
height: calc(100% - 144px) !important;
}
}
</style>
......@@ -151,7 +151,7 @@
</template>
<script setup>
import { ref, computed, watch, onMounted, nextTick } from 'vue'
import { ref, computed, watch, onMounted, nextTick, onBeforeUnmount } from 'vue'
import ChartContainer from '../../components/ChartContainer/index.vue'
import ChartHeader from '../../components/ChartHeader/index.vue'
import ActiveTag from '../../components/ActiveTag/index.vue'
......@@ -171,6 +171,10 @@ import getDateRange from '@/utils/getDateRange'
const route = useRoute();
const timer1 = ref(null)
const timer2 = ref(null)
const timer3 = ref(null)
// 图表/数据
const isShowChart = ref(false)
// 点击切换数据/图表
......@@ -180,7 +184,7 @@ const handleSwitchChartData = () => {
const curDemensionItem = staticsDemensionList.value.filter(item => {
return item.name === curDemension.value
})[0]
setTimeout(() => {
timer1.value = setTimeout(() => {
activeChart.value = curDemensionItem.chartTypeList[0]
curChartData.value = curDemensionItem.data
})
......@@ -267,7 +271,7 @@ const handleClickDemensionItem = (val) => {
})
val.active = true
curDemension.value = val.name
setTimeout(() => {
timer2.value = setTimeout(() => {
activeChart.value = val.chartTypeList[0]
curChartData.value = val.data
})
......@@ -702,8 +706,8 @@ const statusList = ref([
id: '参议院通过'
},
{
name: '双院通过',
id: '双院通过'
name: '分歧已解决',
id: '分歧已解决'
},
])
......@@ -778,8 +782,8 @@ const selectedCount = computed(() => selectedMap.value.size)
// 获取表格数据(示例)
const fetchTableData = async () => {
isSelectedAll.value = false
selectedMap.value.clear()
// isSelectedAll.value = false
// selectedMap.value.clear()
// 调用接口获取数据...
const params = {
page: currentPage.value,
......@@ -793,8 +797,8 @@ const fetchTableData = async () => {
originChamber: selectedCongress.value === '全部议院' ? null : selectedCongress.value,
originDepart: selectedOrg.value === '全部委员会' ? null : selectedOrg.value,
sponsorPersonName: selectedmember.value === '全部议员' ? null : selectedmember.value,
status: selectedStatus.value === '通过' ? 1 : 0,
isInvolveCn: isInvolveCn ? 'Y' : 'N',
status: selectedStatus.value === '全部阶段' ? null : selectedStatus.value,
isInvolveCn: isInvolveCn.value ? 'Y' : 'N',
sort: isSort.value ? 0 : 1 // 0 先按分数降序 后按时间降序 1 先按分数降序,再按时间升序
}
try {
......@@ -836,7 +840,7 @@ const fetchTableData = async () => {
return item.name === curDemension.value
})[0]
setTimeout(() => {
timer3.value = setTimeout(() => {
activeChart.value = curDemensionItem.chartTypeList[0]
curChartData.value = curDemensionItem.data
})
......@@ -877,7 +881,7 @@ const fetchAllData = async () => {
originDepart: selectedOrg.value === '全部委员会' ? null : selectedOrg.value,
sponsorPersonName: selectedmember.value === '全部议员' ? null : selectedmember.value,
status: selectedStatus.value === '通过' ? 1 : 0,
isInvolveCn: isInvolveCn ? 'Y' : 'N',
isInvolveCn: isInvolveCn.value ? 'Y' : 'N',
sort: isSort.value ? 0 : 1 // 0 先按分数降序 后按时间降序 1 先按分数降序,再按时间升序
}
try {
......@@ -1116,14 +1120,21 @@ const handleExport = () => {
onMounted(async () => {
handleGetOrgList()
handleGetMemberList()
initParam()
// 初始化
await fetchTableData()
})
onBeforeUnmount(() => {
if (timer1.value) {
clearTimeout(timer1.value)
}
if (timer2.value) {
clearTimeout(timer2.value)
}
if (timer3.value) {
clearTimeout(timer3.value)
}
})
</script>
......@@ -1239,10 +1250,11 @@ onMounted(async () => {
.data-main-box {
width: 1568px;
height: 810px;
min-height: 810px;
border-radius: 10px;
background: var(--bg-white-100);
margin: 0 auto;
margin-bottom: 20px;
overflow: hidden;
.data-main-box-header {
......@@ -1274,8 +1286,7 @@ onMounted(async () => {
.data-main-box-main {
width: 1520px;
// height: 633px;
height: 680px;
min-height: 680px;
border-radius: 10px;
border: 1px solid var(--bg-black-5);
margin: 0 auto;
......
......@@ -17,7 +17,7 @@
<slot name="chart-box"></slot>
</div>
<div class="tip-box">
<TipTab />
<TipTab text="数据来源:美国国会官网" />
</div>
</div>
</div>
......@@ -176,8 +176,7 @@ const chartItemList = computed(() => {
.tip-box {
height: 54px;
box-sizing: border-box;
padding-top: 10px;
// background: orange;
padding: 15px 600px;
}
}
}
......
......@@ -2,7 +2,7 @@
<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 === '自定义' }">
<el-select v-model="selectValue" :placeholder="placeholderName" style="width: 240px">
<el-select v-model="selectValue" :placeholder="placeholderName" filterable 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>
......
......@@ -105,7 +105,7 @@
<div class="icon">
<img src="../assets/icons/download.svg" alt="">
</div>
<div class="text text-tip-1" @click="handleExport">{{ '导出' }}</div>
<div class="text text-tip-1" @click="handleExport">{{ '导出' }}</div>
</div>
<div class="header-right-item2 item2">
<el-select v-model="curOperation" placeholder="批量操作" style="width: 120px">
......@@ -128,7 +128,7 @@
<el-table ref="tableRef" :data="tableData" row-key="id" @selection-change="handleSelectionChange"
@select="handleSelect" @select-all="handleSelectAll" style="width: 100%" :row-style="{ height: '52px' }">
<el-table-column type="selection" width="40" />
<el-table-column label="法案名称" width="455">
<el-table-column label="政令名称" width="720">
<template #default="scope">
<span class="title-item text-compact-bold" @click="handleClickToDetail(scope.row)">{{ scope.row.title
}}</span>
......@@ -137,15 +137,23 @@
<el-table-column label="发布时间" width="120" class-name="date-column">
<template #default="scope">{{ scope.row.date }}</template>
</el-table-column>
<el-table-column label="提案人" width="480">
<el-table-column label="发布机构" width="180">
<template #default="scope">
<span class="person-item text-compact" @click="handlePerClick(scope.row)">{{ scope.row.sponsorPersonName
<span class="person-item text-compact" @click="handlePerClick(scope.row)">{{ scope.row.organizationName
}}</span>
</template>
</el-table-column>
<el-table-column property="affiliation" label="所属党派" width="120" />
<el-table-column property="originDepart" label="提出委员会" width="180" />
<el-table-column property="status" label="所处阶段" width="120" />
<el-table-column label="涉及领域" width="350" class-name="date-column">
<template #default="scope">
<div class="tag-box">
<AreaTag v-for="tag, index in scope.row.domains" :key="index" :tagName="tag" />
</div>
</template>
</el-table-column>
<el-table-column label="政令类型" width="100">
<template #default="scope">{{ scope.row.typeStr }}</template>
</el-table-column>
</el-table>
</div>
</div>
......@@ -175,6 +183,8 @@ import { search } from '@/api/comprehensiveSearch'
import { ElMessage } from 'element-plus'
import getDateRange from '@/utils/getDateRange'
import { getDepartmentList } from "@/api/decree/home";
const route = useRoute();
// 图表/数据
......@@ -363,7 +373,7 @@ const activeTagList = computed(() => {
}
if (isInvolveTechnology.value) {
const involveStr = '政令相关'
const involveStr = '科技相关'
arr.push(
{
tag: '科技相关',
......@@ -593,18 +603,6 @@ const insList = ref([
name: '全部机构',
id: '全部机构'
},
{
name: '机构1',
id: '机构1'
},
{
name: '机构2',
id: '机构2'
},
{
name: '其他',
id: '其他'
},
])
const selectedIns = ref('全部机构')
const insPlaceHolder = ref('请选择发布机构')
......@@ -612,6 +610,30 @@ const handleSelectIns = value => {
selectedIns.value = value
}
const handleGetDeparmentList = async () => {
try {
// let { keyWord, pageNum, pageSize, day } = organizationInfo
const params = {
day: 365,
}
const res = await getDepartmentList(params);
console.log("机构列表", res);
if (res.code === 200) {
let arr = res.data.orgList.map(item => {
return {
name: item.orgName,
id: item.orgName
}
})
insList.value = [...insList.value, ...arr]
}
} catch (error) {
}
}
// 政令类型列表
const decreeTypeList = ref([
{
......@@ -642,6 +664,7 @@ const isInvolveTechnology = ref(false)
const handleClear = () => {
selectedArea.value = '全部领域'
selectedDate.value = ''
customTime.value = []
selectedIns.value = '全部机构'
selectedDecreeType.value = '全部类型'
isInvolveCn.value = false
......@@ -701,12 +724,12 @@ const fetchTableData = async () => {
keyword: '',
type: 2, // type 1= 法案 2= 政令 3 =智库 4=智库报告 5=实体清单【制裁记录】 6= 人物 7= 机构 8=新闻 9= 社媒
domains: selectedArea.value === '全部领域' ? null : [selectedArea.value],
proposedDateStart: customTime.value[0],
proposedDateEnd: customTime.value[1],
affiliation: selectedIns.value === '全部机构' ? null : selectedIns.value,
originChamber: selectedDecreeType.value === '全部类型' ? null : selectedDecreeType.value,
sleStatus: isInvolveCn ? 'Y' : 'N',
aaaa: isInvolveTechnology? 'Y' : 'N',
proposedDateStart: customTime.value[0]?customTime.value[0]:null,
proposedDateEnd: customTime.value[1]?customTime.value[1]:null,
organizationName: selectedIns.value === '全部机构' ? null : selectedIns.value,
decreeType: selectedDecreeType.value === '全部类型' ? null : selectedDecreeType.value,
isInvolveCn: isInvolveCn.value ? 'Y' : 'N',
isTechRelated: isInvolveTechnology.value ? 'Y' : 'N',
sort: isSort.value ? 0 : 1 // 0 先按分数降序 后按时间降序 1 先按分数降序,再按时间升序
}
try {
......@@ -731,7 +754,7 @@ const fetchTableData = async () => {
name: key,
value: Number(value)
}))
staticsDemensionList.value[2].data = Object.entries(res.data.aggregationsOriginChamber).map(([key, value]) => ({
staticsDemensionList.value[2].data = Object.entries(res.data.aggregationsorganizationName).map(([key, value]) => ({
name: key,
value: Number(value)
}))
......@@ -773,11 +796,11 @@ const fetchAllData = async () => {
domains: selectedArea.value === '全部领域' ? null : [selectedArea.value],
proposedDateStart: customTime.value[0],
proposedDateEnd: customTime.value[1],
affiliation: selectedIns.value === '全部机构' ? null : selectedIns.value,
originChamber: selectedDecreeType.value === '全部类型' ? null : selectedDecreeType.value,
sleStatus: isInvolveCn ? 'Y' : 'N',
aaaa: isInvolveTechnology? 'Y' : 'N',
sort: isSort.value ? 0 : 1 // 0 先按分数降序 后按时间降序 1 先按分数降序,再按时间升序
organizationName: selectedIns.value === '全部机构' ? null : selectedIns.value,
decreeType: selectedDecreeType.value === '全部类型' ? null : selectedDecreeType.value,
isInvolveCn: isInvolveCn.value ? 'Y' : 'N',
isTechRelated: isInvolveTechnology.value ? 'Y' : 'N',
sort: isSort.value ? 0 : 1
}
try {
const res = await search(params)
......@@ -933,6 +956,9 @@ const initParam = () => {
selectedDate.value = '自定义'
customTime.value = JSON.parse(route.query.selectedDate)
}
selectedIns.value = route.query.orgnizationName? route.query.orgnizationName : '全部机构'
isInvolveCn.value = route.query.isInvolveCn ? true : false
isInvolveTechnology.value = route.query.isInvolveTechnology ? true : false
......@@ -946,7 +972,7 @@ const initParam = () => {
} else {
const savedQuery = JSON.parse(sessionStorage.getItem('decreeRouteQuery') || '{}');
selectedArea.value = savedQuery.domains ? savedQuery.domains : '全部领域'
if (Array.isArray(JSON.parse(savedQuery.selectedDate)) && JSON.parse(savedQuery.selectedDate).length) {
if (savedQuery.selectedDate && Array.isArray(JSON.parse(savedQuery.selectedDate)) && JSON.parse(savedQuery.selectedDate).length) {
selectedDate.value = '自定义'
customTime.value = JSON.parse(savedQuery.selectedDate)
}
......@@ -959,16 +985,16 @@ const initParam = () => {
}
// 跳转法案详情
const handleClickToDetail = (curBill) => {
console.log('curBill', curBill);
// 跳转政令详情
const handleClickToDetail = (curDecree) => {
console.log('curDecree', curDecree);
window.sessionStorage.setItem("billId", curBill.id);
window.sessionStorage.setItem("curTabName", curBill.title);
window.sessionStorage.setItem("billId", curDecree.id);
window.sessionStorage.setItem("curTabName", curDecree.title);
const route = router.resolve({
path: "/billLayout",
path: "/decreeLayout",
query: {
billId: curBill.id
billId: curDecree.id
}
});
window.open(route.href, "_blank");
......@@ -989,13 +1015,16 @@ const handlePerClick = item => {
// 导出
const handleExport = () => {
if(!selectedCount.value) {
ElMessage.warning('请选择至少一项数据!')
return
}
console.log(selectedMap.value);
const arr = Array.from(selectedMap.value);
const jsonStr = JSON.stringify(arr, null, 2);
const blob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'export.json';
......@@ -1005,7 +1034,7 @@ const handleExport = () => {
};
onMounted(async () => {
handleGetDeparmentList()
initParam()
// 初始化
......@@ -1127,10 +1156,11 @@ onMounted(async () => {
.data-main-box {
width: 1568px;
height: 810px;
min-height: 810px;
border-radius: 10px;
background: var(--bg-white-100);
margin: 0 auto;
margin-bottom: 20px;
overflow: hidden;
.data-main-box-header {
......@@ -1163,7 +1193,7 @@ onMounted(async () => {
.data-main-box-main {
width: 1520px;
// height: 633px;
height: 680px;
min-height: 680px;
border-radius: 10px;
border: 1px solid var(--bg-black-5);
margin: 0 auto;
......@@ -1257,6 +1287,14 @@ onMounted(async () => {
.date-column {
background-color: #ecf5ff;
.tag-box {
display: flex;
flex-wrap: wrap;
gap: 8px;
width: 340px;
}
}
.date-column .cell {
......@@ -1284,6 +1322,8 @@ onMounted(async () => {
}
}
:deep(.el-table__header-wrapper) {
// background-color: #f5f7fa;
height: 48px;
......
......@@ -72,7 +72,7 @@
</template>
<script setup>
import { computed, onMounted, ref } from 'vue'
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import Icon1 from './assets/icons/sider-icon1.svg'
import Icon2 from './assets/icons/sider-icon2.svg'
import Icon3 from './assets/icons/sider-icon3.svg'
......@@ -398,6 +398,8 @@ const handleClickTab = (tab) => {
})
}
const timer = ref(null)
// 关闭当前标签页
const handleCloseCurTab = (tab, index) => {
......@@ -416,7 +418,7 @@ const handleCloseCurTab = (tab, index) => {
})
if (index === openedTabList.value.length - 1) {
tagsViewStore.delView(tab)
setTimeout(() => {
timer.value = setTimeout(() => {
tagsViewStore.visitedViews[tagsViewStore.visitedViews.length - 1].active = true
activeTab = tagsViewStore.visitedViews[tagsViewStore.visitedViews.length - 1]
router.push({
......@@ -593,6 +595,11 @@ onMounted(() => {
})
onBeforeUnmount(() => {
if(timer.value) {
clearTimeout(timer.value)
}
})
</script>
......
......@@ -814,7 +814,25 @@ const handleBox5 = async () => {
let chart1 = getBarChart(chart1Data.value.dataX, chart1Data.value.dataY);
chart1.yAxis.name = "数量";
chart1.yAxis.nameTextStyle = { align: 'right' }
setChart(chart1, "chart1");
let org = '全部机构'
if(box5Params.proposeName) {
org = keyOrganizationList.value.filter(item => {
return item.orgId === box5Params.proposeName
})[0].orgName
}
let domain = '全部领域'
if(box5Params.domainId) {
domain = areaList.value.filter(item => {
return item.id === box5Params.domainId
})[0].name
}
const selectParam = {
moduleType: '政令',
orgnizationName: org,
domains: domain,
selectDate: box5Params.year
}
setChart(chart1, "chart1", true, selectParam);
};
// 政令科技领域
......@@ -861,8 +879,19 @@ const handleGetDecreeArea = async () => {
};
const handleBox6 = async () => {
await handleGetDecreeArea();
let org = '全部机构'
if(box6Params.proposeName) {
org = keyOrganizationList.value.filter(item => {
return item.orgId === box6Params.proposeName
})[0].orgName
}
const selectParam = {
moduleType: '政令',
orgnizationName: org,
selectedDate: JSON.stringify([box6Params.year+'-01-01', box6Params.year+'-12-31'])
}
let chart2 = getPieChart(chart2Data.value);
setChart(chart2, "chart2");
setChart(chart2, "chart2", true, selectParam);
};
const handleBox6YearChange = () => {
......
......@@ -7,7 +7,7 @@
<router-view />
</div>
</div>
<div class="right-btn" @click="handleClickToolBox">
<div class="right-btn" v-if="isShowToolBox" @click="handleClickToolBox">
<div class="item">
<div class="icon">
<img src="@/assets/icons/overview/domain.png" alt="" />
......@@ -22,20 +22,7 @@
</div>
</div>
<div class="tool-box">
<!-- <div class="tool-item">
<img src="@/assets/icons/tool-item-icon1.png" alt="" />
</div>
<div class="tool-item">
<img src="@/assets/icons/tool-item-icon2.png" alt="" />
</div>
<div class="tool-item">
<img src="@/assets/icons/tool-item-icon3.png" alt="" />
</div>
<div class="tool-item">
<img src="@/assets/icons/tool-item-icon4.png" alt="" />
</div> -->
<div class="tool-box" v-if="isShowToolBox">
<el-tooltip content="智能写报" placement="left" :offset="10">
<div class="tool-item" @click="handleOpenPage('znxb')">
<img src="@/assets/icons/tool-item-icon1.png" alt="" />
......@@ -96,11 +83,13 @@ import { ElMessage } from "element-plus";
const router = useRouter();
const route = useRoute();
const isShowHeader = computed(() => {
const isShow = route.meta.isShowHeader
return isShow? true : false
const isShowToolBox = computed(() => {
const isDataLibrary = route.fullPath.includes("dataLibrary");
const isWrittingAsstaint = route.path === "/writtingAsstaint";
return !isDataLibrary && !isWrittingAsstaint;
})
const isShowHeader = computed(() => !!route.meta.isShowHeader);
const isShowAiBox = ref(false);
......@@ -816,4 +805,7 @@ body {
cursor: not-allowed;
pointer-events: none;
}
</style>
<template>
<div class="wrap">
<div class="scroll-inner">
<div class="header">
<div class="header-top">
<div class="header-top-left">
<img src="../assets/images/box1-logo.png" alt="" />
<div>
<div class="title">{{ thinkInfo.name }}</div>
<div class="en-title">
{{ thinkInfo.ename }}.{{ thinkInfo.times }}
</div>
<div class="tag-box">
<!-- <div class="tag-box" v-for="value,index in thinkInfo.tags" :key="index">
<div class="tag">{{ value.industryName }}</div>
</div> -->
<AreaTag v-for="(value, index) in thinkInfo.tags" :key="index" :tagName="value.industryName"></AreaTag>
</div>
</div>
</div>
<div class="header-top-right">
<div class="image-name-box">
<div class="image"><img :src="thinkInfo.thinkTankLogoUrl" alt="" /></div>
<div class="name">{{ thinkInfo.thinkTankName }}</div>
</div>
<div class="btn-box">
<!-- <div class="btn">
<div class="icon">
<img src="./images/btn-icon1.png" alt="" />
</div>
<div class="text">{{ "查看官网" }}</div>
</div> -->
<!-- <div class="btn">
<div class="icon">
<img src="./images/btn-icon2.png" alt="" />
</div>
<div class="text" @click="goToOfficialWebsite()">{{ "查看官网" }}</div>
</div> -->
<div class="btn">
<div class="icon">
<img src="./images/pdf-image.png" alt="" />
</div>
<div class="text" @click="toReport()">{{ "文档下载" }}</div>
</div>
<!-- <div class="btn" @click="handleDownloadDocument">
<div class="icon">
<img src="./images/btn-icon3.png" alt="" />
</div>
<div class="text">{{ "文档下载" }}</div>
</div> -->
<div class="btn btn1" @click="handleAnalysisClick">
<div class="icon">
<img src="./images/paper-image.png" alt="" />
</div>
<div class="text">{{ "查看原文" }}</div>
</div>
</div>
</div>
</div>
</div>
<div class="bottom-row">
<div class="left">
<div class="box1">
<!-- <div class="box-header">
<div class="header-left"></div>
<div class="title">内容摘要</div>
<div class="header-right">
<div class="icon">
<img src="@/assets/icons/box-header-icon2.png" alt="" />
</div>
<div class="icon">
<img src="@/assets/icons/box-header-icon3.png" alt="" />
</div>
</div>
</div>
<div class="box1-main">
{{ box1Data }}
</div> -->
<AnalysisBox title="基本信息" :showAllBtn="true">
<div class="box1-main">
<div class="text-box">
<div class="time">
<div class="time-title">发布时间:</div>
<div class="time-content">{{ publishTime }}</div>
</div>
<div class="topic">
<div class="topic-title">报告主题:</div>
<div class="topic-content">{{ reportTopic }}</div>
</div>
<div class="author">
<div class="author-title">报告作者:</div>
<div class="author-content">
<template v-if="Array.isArray(reportAuthors) && reportAuthors.length">
<span v-if="reportAuthors.length === 1">
{{ reportAuthors[0].name }}
</span>
<!-- 多个作者:显示第一个 + 等 -->
<span v-else>
{{ reportAuthors[0].name }}{{ reportAuthors.length }}
</span>
</template>
</div>
</div>
</div>
<div class="author-box">
<div class="author-item" v-for="(author, idx) in reportAuthors" :key="idx"
v-if="Array.isArray(reportAuthors) && reportAuthors.length"
@click="handleClickReportAuthor(author)">
<div class="image"><img :src="author.avatar ? author.avatar : DefaultIcon1" alt=""
@error="() => { if (author.avatar) author.avatar = null; }" /></div>
<div class="author-text">
<div class="author-name">{{ author.name }}</div>
</div>
</div>
</div>
</div>
</AnalysisBox>
</div>
<div class="box5">
<AnalysisBox title="关键词云" :showAllBtn="true">
<div class="box5-main">
<template v-if="!hasBox5ChartData">
<el-empty class="box5-el-empty" description="暂无数据" :image-size="100" />
</template>
<template v-else>
<div class="box5Chart">
<!-- 有数据后再挂载子组件:子组件仅在 onMounted 初始化,异步数据到达后需 v-if + key 强制重新挂载 -->
<WordCloudChart v-if="box5Data.length" :key="box5WordCloudKey" :data="box5Data" width="432px"
height="272px" />
</div>
<div class="box5-footer">
<TipTab :text="REPORT_ANALYSIS_TIP_BOX5" />
</div>
<div class="ai-wrap" @mouseenter="handleSwitchAiContentShowBox5(true)">
<AiButton />
</div>
<div class="ai-content" v-if="isShowAiContentBox5" @mouseleave="handleSwitchAiContentShowBox5(false)">
<AiPane :aiContent="aiContentBox5" />
</div>
</template>
</div>
</AnalysisBox>
</div>
</div>
<div class="right">
<div class="box3">
<AnalysisBox title="内容摘要" :showAllBtn="true">
<div class="box3-main">
<AiSummary>
<template #summary-content>
{{ box1Data }}
</template>
</AiSummary>
</div>
</AnalysisBox>
</div>
<div class="box4">
<AnalysisBox title="听证会内容" :showAllBtn="true">
<div class="search-box">
<el-input placeholder="搜索内容" v-model="searchOpinions" style="width: 180px"
@keyup.enter="handleSearchOpinions" />
<div class="icon">
<img src="../assets/images/Line_Search.png" alt="" @click="handleSearchOpinions" />
</div>
</div>
<div class="box4-main">
<div class="box4-main-main">
<div class="box4-item" v-for="(item, index) in filteredOpinions"
:key="item.id != null ? item.id : index">
<div class="top-row">
<div class="left">
{{ index + 1 }}
</div>
<div class="center">
<div class="title" v-html="highlightOpinionText(item.titleZh)"></div>
<div>
<img src="./images/image-open.png" alt="" class="center-image"
@click="handleOpenReportOriginal(item)" />
</div>
<div>
<img v-if="!isOpinionExpanded(item, index)" src="./images/image-down.png" alt=""
class="center-image" @click="toggleOpinion(item, index)" />
<img v-else src="./images/image-up.png" alt="" class="center-image"
@click="toggleOpinion(item, index)" />
</div>
</div>
</div>
<div v-if="isOpinionExpanded(item, index)" class="desc"
v-html="highlightOpinionText(item.contentZh)">
</div>
<!-- <div class="right"> -->
<!-- <div class="tag" v-for="(val, idx) in item.hylyList" :key="idx">
{{ val }}
</div>
<div class="tag" v-for="(val, idx) in item.serialNum" :key="idx">
{{ val }}
</div> -->
<!-- <AreaTag v-for="(val, idx) in item.hylyList" :key="idx" :tagName="val"></AreaTag>
</div> -->
<!-- <div class="more">
<img src="@/assets/icons/open.png" alt="" />
</div> -->
</div>
</div>
<div class="box4-main-footer">
<div class="info">共{{ opinionsTotal }}条听证会提问</div>
<div class="page-box">
<el-pagination :page-size="pageSize" background layout="prev, pager, next" :total="opinionsTotal"
@current-change="handleCurrentChange" :current-page="currentPage" />
</div>
</div>
</div>
</AnalysisBox>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import DefaultIcon1 from '@/assets/icons/default-icon1.png'
import WarningPane from "@/components/base/WarningPane/index.vue"
import WordCloudChart from "@/components/base/WordCloundChart/index.vue"
import SearchContainer from "@/components/SearchContainer.vue";
import { ref, onMounted, computed, defineProps } from "vue";
import { ElMessage } from "element-plus";
import {
getThinkTankReportAbstract,
getThinkTankReportContent,
getThinkTankReportIndustry,
getThinkTankReportIndustryCloud,
getThinkTankReportViewpoint
} from "@/api/thinkTank/overview";
import { getChartAnalysis } from "@/api/aiAnalysis/index";
import { useRouter } from "vue-router";
import "echarts-wordcloud";
import AiSummary from '@/components/base/Ai/AiSummary/index.vue'
import { getPersonSummaryInfo } from "@/api/common/index";
import AiButton from "@/components/base/Ai/AiButton/index.vue";
import AiPane from "@/components/base/Ai/AiPane/index.vue";
import TipTab from "@/views/thinkTank/TipTab/index.vue";
const router = useRouter();
const goToAllThinkTank = () => {
const thinkTankId = props?.thinkInfo?.thinkTankId || props?.thinkInfo?.id;
const route = router.resolve({
name: "MultiThinkTankViewAnalysis",
params: { id: thinkTankId }
});
window.open(route.href, "_blank");
};
const props = defineProps({
reportList: {
type: Object,
default: () => ({})
}
});
const thinkInfo = ref({
name: "探讨中国开发和管理的跨大陆电网的安全影响",
ename: "调查项目",
tags: [{ industryName: "深海" }, { industryName: "人工智能" }],
thinkTankName: "兰德科技智库",
thinkTankLogoUrl: "http://8.140.26.4:10010/kjb-files/images/org/land.webp",
times: "2024-05-28"
})
const REPORT_ANALYSIS_TIP_BOX5 =
"智库报告关键词云,数据来源:美国兰德公司官网";
// 刷新后默认展示「报告关键词云」AI 总结
const isShowAiContentBox5 = ref(true);
const aiContentBox5 = ref("");
const isBox5InterpretLoading = ref(false);
const handleSwitchAiContentShowBox5 = (val) => {
isShowAiContentBox5.value = val;
if (val) {
fetchBox5ChartInterpretation();
}
};
const searchOpinions = ref('');
const escapeHtml = (text) => {
return String(text ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
};
const escapeRegExp = (text) => {
return String(text ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
};
const highlightOpinionText = (text) => {
const safeText = escapeHtml(text);
const keyword = (searchOpinions.value || "").trim();
if (!keyword) return safeText;
const pattern = new RegExp(`(${escapeRegExp(keyword)})`, "gi");
return safeText.replace(pattern, `<span class="opinion-keyword-highlight">$1</span>`);
};
const handleSearchOpinions = () => {
currentPage.value = 1;
handleGetThinkTankReportViewpoint();
};
/** 可同时展开多条;用 id 区分项,避免翻页后索引与展开状态错位 */
const expandedOpinionKeys = ref(new Set());
const filteredOpinions = computed(() => majorOpinions.value);
const opinionsTotal = computed(() => total.value);
const getOpinionExpandKey = (item, index) => {
if (item != null && item.id != null && item.id !== "") {
return String(item.id);
}
return `idx-${index}`;
};
const isOpinionExpanded = (item, index) => {
return expandedOpinionKeys.value.has(getOpinionExpandKey(item, index));
};
const toggleOpinion = (item, index) => {
const key = getOpinionExpandKey(item, index);
const next = new Set(expandedOpinionKeys.value);
if (next.has(key)) {
next.delete(key);
} else {
next.add(key);
}
expandedOpinionKeys.value = next;
};
const publishTime = computed(() => {
const info = props.thinkInfo || {};
// 优先用 times,其次用 reportTime 的日期部分
if (info.times) return info.times;
if (info.reportTime && typeof info.reportTime === "string") {
return info.reportTime.split("T")[0];
}
return "";
});
const reportTopic = computed(() => {
const info = props.thinkInfo || {};
return info.summary;
});
const reportAuthors = computed(() => {
const info = props.thinkInfo || {};
if (Array.isArray(info.authors) && info.authors.length) {
return info.authors;
}
return [];
});
// 点击报告作者头像,跳转到人物主页
// 与核心研究人员逻辑一致:核心依赖 personId,本页面依赖作者的 id(作为 personId 传入)
const handleClickReportAuthor = async (author) => {
const personId = author?.id;
if (!personId) return;
const params = { personId };
const res = await getPersonSummaryInfo(params);
if (res.code !== 200 || !res.data) return;
window.sessionStorage.setItem("curTabName", author?.name || "");
const route = router.resolve({
path: "/characterPage",
query: {
personId
}
});
window.open(route.href, "_blank");
};
const riskSignal = computed(() => {
const info = props.thinkInfo || {};
return info.riskSignal;
});
// 内容摘要
const box1Data =
ref(`包括经济竞争在内的美中竞争自2017年以来一直在定义美国外交政策。这两个经济体是世界上第一和第二大国家经济体,并且深深交织在一起。改变关系,无论多么必要,可能是昂贵的。因此,美国面临着一项挑战,确保其经济在耦合的战略竞争条件下满足国家的需求。
为了应对这一挑战,兰德大学的研究人员对美中竞争进行了经济和制度分析,进行了参与式的远见练习,以了解确保美国经济健康的长期路径,并创建了两个经济竞争游戏,探索多个国家在相互交流的同时确保经济健康的动态...`);
//获取内容摘要
const handleGetThinkTankReportAbstract = async () => {
try {
const res = await getThinkTankReportAbstract(router.currentRoute._value.params.id);
console.log("内容摘要", res);
if (res.code === 200 && res.data) {
box1Data.value = res.data;
}
} catch (error) {
console.error("获取内容摘要error", error);
}
};
// 涉及科技领域
const areaList = ref([]);
const activeArea = ref(6);
const handleClickArea = area => {
activeArea.value = area;
handleGetThinkTankReportIndustryCloud();
};
const box2Data = ref([
// {
// name: "通用人工智能",
// value: 100
// },
// {
// name: "AI芯片",
// value: 66
// },
// {
// name: "计算能力又是",
// value: 72
// },
// {
// name: "基准测试",
// value: 88
// },
// {
// name: "出口管制",
// value: 78
// },
// {
// name: "军事AI",
// value: 85
// },
// {
// name: "生态系统",
// value: 88
// },
// {
// name: "模型能力",
// value: 89
// }
]);
// 报告关键词云
const box5Data = ref([]);
const hasBox5ChartData = computed(() => Array.isArray(box5Data.value) && box5Data.value.length > 0);
/** 词云子组件不 watch 数据,每次接口成功有数据时递增 key,强制重新挂载以触发 onMounted */
const box5WordCloudKey = ref(0);
//获取科技领域词云
const handleGetThinkTankReportIndustryCloud = async () => {
try {
const params = {
id: router.currentRoute._value.params.id
// industryId: activeArea.value
};
const res = await getThinkTankReportIndustryCloud(params);
console.log("科技领域词云", res);
if (res.code === 200 && res.data) {
const data = (res.data || []).map(item => ({
name: item.clause,
value: item.count
}));
// 该接口数据用于「报告关键词云」
box5Data.value = data;
if (data.length) {
box5WordCloudKey.value += 1;
}
// 刷新后默认展开 AI:数据就绪即触发解读
if (isShowAiContentBox5.value) {
fetchBox5ChartInterpretation();
}
} else {
box5Data.value = [];
}
} catch (error) {
console.error("获取科技领域词云error", error);
box5Data.value = [];
}
};
//涉及科技领域
const handleGetThinkTankReportIndustry = async () => {
try {
const res = await getThinkTankReportIndustry(router.currentRoute._value.params.id);
console.log("涉及科技领域", res);
if (res.code === 200 && res.data) {
areaList.value = res.data;
}
} catch (error) {
console.error("获取涉及科技领域error", error);
}
};
// 主要观点
const majorOpinions = ref([
{
id: 1,
title: "我是示例标题",
desc: "我是示例内容",
tagList: [
{
name: "关税",
status: 2
},
{
name: "跨境电商",
status: 1
}
]
},
{
id: 2,
title: "我是示例标题",
desc: "我是示例内容",
tagList: [
{
name: "私有经济",
status: 2
}
]
}
]);
//处理点击详情页事件
const handleOpenReportOriginal = item => {
const route = router.resolve({
name: "ReportOriginal",
params: {
id: router.currentRoute._value.params.id
},
query: {
currentPage: currentPage.value,
pageSize: pageSize.value,
opinionId: item?.id ?? "",
opinionContent: item?.content ?? ""
}
});
window.open(route.href, "_blank");
};
const tabActiveName = ref("报告分析");
const switchTab = name => {
tabActiveName.value = name;
};
// 处理页码改变事件
const currentPage = ref(1);
const pageSize = ref(10);
const total = ref(0);
const handleCurrentChange = page => {
currentPage.value = page;
handleGetThinkTankReportViewpoint();
};
// 获取报告核心论点(支持搜索)
const handleGetThinkTankReportViewpoint = async () => {
try {
const params = {
reportId: router.currentRoute._value.params.id,
currentPage: currentPage.value - 1,
pageSize: pageSize.value,
keyword: (searchOpinions.value || "").trim(),
orgIds: ""
};
const res = await getThinkTankReportViewpoint(params);
console.log("核心论点", res.data);
if (res.code === 200 && res.data) {
const nextOpinions = res.data.content || [];
majorOpinions.value = nextOpinions;
total.value = res.data.totalElements || 0;
// 默认:第一条展开,其余关闭
const nextExpandedKeys = new Set();
if (Array.isArray(nextOpinions) && nextOpinions.length > 0) {
nextExpandedKeys.add(getOpinionExpandKey(nextOpinions[0], 0));
}
expandedOpinionKeys.value = nextExpandedKeys;
}
} catch (error) {
console.error("获取主要观点error", error);
}
};
// 获取图表分析内容
const box3AnalysisContent = ref("");
const handleGetBox3AnalysisContent = async textJson => {
const params = {
text: textJson
};
const res = await getChartAnalysis(params);
console.log("图表解析内容", res);
};
const getInterpretationTextFromChartResponse = (res) => {
const list = res?.data;
const first = Array.isArray(list) ? list[0] : null;
return (
first?.["解读"] ||
first?.["interpretation"] ||
first?.["analysis"] ||
first?.["content"] ||
""
);
};
const appendAiInterpretationChunk = (targetRef, chunk, loadingText = "解读生成中…") => {
if (!chunk) {
return;
}
const current = String(targetRef.value || "");
const base = current === loadingText ? "" : current;
targetRef.value = base + String(chunk);
};
const fetchBox5ChartInterpretation = async () => {
const list = Array.isArray(box5Data.value) ? box5Data.value : [];
if (!list.length) {
aiContentBox5.value = "暂无图表数据";
return;
}
const hasValidContent =
aiContentBox5.value &&
aiContentBox5.value !== "解读生成中…" &&
aiContentBox5.value !== "解读加载失败" &&
aiContentBox5.value !== "暂无图表数据";
if (hasValidContent || isBox5InterpretLoading.value) {
return;
}
isBox5InterpretLoading.value = true;
aiContentBox5.value = "解读生成中…";
const chartPayload = {
type: "词云图",
name: "报告关键词云",
data: list.map((item) => ({
name: item.name,
value: item.value
}))
};
try {
const res = await getChartAnalysis(
{ text: JSON.stringify(chartPayload) },
{
onChunk: chunk => {
appendAiInterpretationChunk(aiContentBox5, chunk);
}
}
);
const text = getInterpretationTextFromChartResponse(res);
aiContentBox5.value = text || aiContentBox5.value || "未返回有效解读内容";
} catch (error) {
console.error("报告关键词云图表解读请求失败", error);
aiContentBox5.value = "解读加载失败";
} finally {
isBox5InterpretLoading.value = false;
}
};
onMounted(() => {
handleGetThinkTankReportAbstract();
handleGetThinkTankReportViewpoint();
handleGetThinkTankReportIndustry();
handleGetThinkTankReportIndustryCloud();
});
</script>
<style lang="scss" scoped>
.wrap {
position: absolute;
inset: 0;
box-sizing: border-box;
width: 100%;
overflow: hidden;
.scroll-inner {
box-sizing: border-box;
width: 100%;
height: 100%;
min-height: 0;
padding-bottom: 16px;
overflow-x: hidden;
overflow-y: auto;
scrollbar-gutter: stable;
scrollbar-width: thin;
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-thumb {
border-radius: 4px;
}
}
.header {
width: 100%;
height: 126px;
box-sizing: border-box;
border-bottom: 1px solid rgba(234, 236, 238, 1);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background: rgba(255, 255, 255, 1);
position: sticky;
top: 0;
z-index: 99999;
overflow: hidden;
.header-top {
margin: 0 auto;
margin-top: 20px;
width: 1600px;
display: flex;
justify-content: space-between;
.header-top-left {
display: flex;
img {
width: 72px;
height: 88px;
}
.title {
margin-left: 16px;
height: 26px;
color: rgb(59, 65, 75);
font-family: "Source Han Sans CN";
font-weight: 700;
font-size: 20px;
line-height: 26px;
letter-spacing: 0px;
text-align: left;
}
.en-title {
margin-top: 4px;
margin-left: 16px;
height: 24px;
color: rgb(95, 101, 108);
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
line-height: 24px;
letter-spacing: 0px;
text-align: left;
}
.tag-box {
margin-top: 12px;
display: flex;
gap: 8px;
margin-left: 16px;
}
}
.header-top-right {
display: flex;
flex-direction: column;
text-align: right;
align-items: flex-end;
.image-name-box {
width: 118px;
height: 24px;
gap: 6px;
text-align: right;
display: flex;
justify-content: flex-end;
.name {
height: 24px;
color: rgba(95, 101, 108, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 24px;
letter-spacing: 0px;
text-align: right;
}
.image {
width: 16px;
height: 16px;
margin-top: 5px;
img {
width: 100%;
height: 100%;
}
}
}
.btn-box {
display: flex;
gap: 12px;
margin-top: 34px;
.btn {
width: 120px;
height: 36px;
box-sizing: border-box;
border: 1px solid rgba(230, 231, 232, 1);
border-radius: 6px;
background: rgba(255, 255, 255, 1);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
.icon {
width: 16px;
height: 16px;
img {
width: 100%;
height: 100%;
}
}
.text {
width: 66px;
height: 22px;
color: rgba(95, 101, 108, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 22px;
letter-spacing: 0px;
text-align: center;
}
}
.btn1 {
border-radius: 6px;
background: var(--color-main-active);
.text {
color: rgba(255, 255, 255, 1);
}
}
}
.time {
height: 24px;
margin-top: 5px;
color: rgba(95, 101, 108, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 24px;
letter-spacing: 0px;
text-align: right;
}
}
}
}
.bottom-row {
flex-direction: row;
justify-content: center;
display: flex;
gap: 16px;
.left {
gap: 16px;
display: flex;
flex-direction: column;
margin-top: 16px;
.box1 {
width: 480px;
// border: 1px solid rgba(234, 236, 238, 1);
// border-radius: 10px;
// box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
// background: rgba(255, 255, 255, 1);
.box1-main {
width: 480px;
.text-box {
width: 437px;
margin-left: 22px;
margin-top: 8px;
gap: 12px;
display: flex;
flex-direction: column;
.time {
height: 24px;
display: flex;
gap: 4px;
.time-title {
width: 88px;
font-family: "Source Han Sans CN";
font-weight: 700;
font-size: 16px;
line-height: 24px;
letter-spacing: 1px;
text-align: left;
color: rgb(59, 65, 75);
}
.time-content {
width: 345px;
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
line-height: 24px;
letter-spacing: 0px;
text-align: left;
color: rgb(59, 65, 75);
}
}
.topic {
display: flex;
gap: 4px;
.topic-title {
width: 88px;
font-family: "Source Han Sans CN";
font-weight: 700;
font-size: 16px;
line-height: 24px;
letter-spacing: 1px;
text-align: left;
color: rgb(59, 65, 75);
}
.topic-content {
width: 345px;
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
line-height: 30px;
letter-spacing: 0px;
text-align: left;
color: rgb(59, 65, 75);
}
}
.author {
display: flex;
gap: 4px;
.author-title {
width: 88px;
font-family: "Source Han Sans CN";
font-weight: 700;
font-size: 16px;
line-height: 24px;
letter-spacing: 1px;
text-align: left;
color: rgb(59, 65, 75);
}
.author-content {
width: 345px;
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
line-height: 24px;
letter-spacing: 0px;
text-align: left;
color: rgb(59, 65, 75);
}
}
}
.author-box {
width: 437px;
height: auto;
margin-top: 34px;
margin-left: 18px;
display: grid;
grid-template-columns: 1fr 1fr;
/* 两列等宽 */
column-gap: 4px;
/* 左右间距(同一行) */
row-gap: 8px;
/* 上下间距(同一列) */
margin-bottom: 38px;
.author-item {
width: 213px;
height: 49px;
display: flex;
gap: 11px;
.image {
width: 42px;
height: 42px;
margin-top: 3px;
margin-left: 3px;
display: inline-block;
cursor: pointer;
img {
width: 100%;
height: 100%;
border-radius: 50%;
}
}
.author-text {
width: 154px;
height: 49px;
.author-name {
width: 154px;
height: 24px;
font-family: "Source Han Sans CN";
font-weight: 700;
font-size: 16px;
line-height: 24px;
letter-spacing: 0;
text-align: left;
color: rgb(59, 65, 75);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.author-position {
width: 154px;
height: 22px;
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 14px;
line-height: 22px;
letter-spacing: 0;
text-align: left;
color: rgb(95, 101, 108);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
}
}
}
.box5 {
width: 480px;
height: 415px;
.box5-main {
width: 480px;
height: 361px;
padding: 24px 24px 65px 24px;
display: flex;
flex-direction: column;
box-sizing: border-box;
overflow: hidden;
position: relative;
.box5Chart {
width: 100%;
height: 100%;
margin: 0 auto;
overflow: hidden;
}
.box5-footer {
display: flex;
justify-content: space-between;
align-items: center;
position: absolute;
bottom: 20px;
left: 32px;
}
.ai-wrap {
position: absolute;
bottom: 18px;
right: 0;
cursor: pointer;
}
.ai-content {
position: absolute;
bottom: 0;
right: 0;
min-width: 480px;
min-height: 156px;
}
}
}
.box2 {
width: 480px;
// border: 1px solid rgba(234, 236, 238, 1);
// border-radius: 10px;
// box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
// background: rgba(255, 255, 255, 1);
.box2-main {
width: 436px;
margin-top: 5px;
margin-left: 23px;
.box2-item {
height: 103px;
width: 100%;
border-bottom: 1px solid rgba(234, 236, 238, 1);
border-top: 1px solid rgba(234, 236, 238, 1);
.box2-item-content {
width: 100%;
height: 90px;
margin-top: 7px;
display: flex;
.left {
width: 56px;
height: 74px;
margin-top: 8px;
img {
width: 100%;
height: 100%;
}
}
.right-content {
margin-left: 13px;
width: 365px;
height: 76px;
margin-top: 7px;
.report-title {
height: 48px;
font-family: "Source Han Sans CN";
font-weight: 700;
font-size: 16px;
line-height: 24px;
letter-spacing: 0;
text-align: left;
/* 👇 下面是 两行文本超出省略 核心代码 */
display: -webkit-box;
-webkit-line-clamp: 2;
/* 限制显示 2 行 */
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
}
.report-footer {
margin-top: 4px;
height: 22px;
justify-content: space-between;
display: flex;
.report-time {
height: 22px;
width: 97px;
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 14px;
line-height: 22px;
letter-spacing: 0;
text-align: left;
color: rgb(95, 101, 108);
}
.report-footer-right {
height: 22px;
display: flex;
gap: 6px;
.footer-image {
width: 16px;
height: 16px;
margin-top: 3px;
img {
width: 100%;
height: 100%;
}
}
.think-name {
height: 22px;
}
}
}
}
}
}
}
.box2-btn {
margin-top: 16px;
margin-bottom: 21px;
margin-left: 23px;
width: 436px;
height: 36px;
background-color: rgb(5, 95, 194);
border-radius: 6px;
display: flex;
.btn-text {
color: rgb(255, 255, 255);
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
line-height: 22px;
letter-spacing: 0;
margin-left: 120px;
margin-top: 7px;
}
.btn-image {
width: 13px;
height: 8px;
margin-left: 8px;
display: inline-block;
margin-top: 14px;
img {
width: 100%;
height: 100%;
display: block;
}
}
}
}
}
.right {
margin-top: 16px;
gap: 16px;
display: flex;
flex-direction: column;
.box3 {
width: 1103px;
// border: 1px solid rgba(234, 236, 238, 1);
// border-radius: 10px;
// box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
// background: rgba(255, 255, 255, 1);
.box3-main {
width: 1058px;
margin-top: 3px;
margin-left: 22px;
padding-bottom: 22px;
:deep(.summary-main) {
margin-bottom: 25px;
font-family: "Source Han Sans CN";
font-weight: 400;
/* Regular 常规 */
font-size: 16px;
line-height: 30px;
letter-spacing: 0px;
text-align: justify;
color: rgb(59, 65, 75);
/* 两端对齐 */
}
.box3-top {
width: 1058px;
height: 48px;
background: linear-gradient(rgb(137, 193, 255, 0.1), rgb(255, 255, 255));
display: flex;
.top-title {
width: 1010px;
height: 32px;
margin-left: 24px;
margin-top: 16px;
.title-image {
width: 199px;
height: 32px;
img {
width: 100%;
height: 100%;
}
}
}
}
.box3-text {
width: 1006px;
margin-top: 24px;
margin-left: 26px;
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
line-height: 30px;
letter-spacing: 0px;
text-align: justify;
text-justify: inter-ideograph;
}
}
}
.box4 {
width: 1103px;
height: auto;
// border: 1px solid rgba(234, 236, 238, 1);
// border-radius: 10px;
// box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
// background: rgba(255, 255, 255, 1);
position: relative;
.search-box {
display: flex;
width: 180px;
height: 32px;
box-sizing: border-box;
border: 1px solid rgba(230, 231, 232, 1);
border-radius: 4px;
background: rgba(255, 255, 255, 1);
position: relative;
margin-top: 3px;
margin-bottom: 16px;
margin-left: 23px;
.icon {
width: 16px;
height: 16px;
cursor: pointer;
position: absolute;
right: 8px;
top: 8px;
display: flex;
justify-content: flex-end;
z-index: 10000;
img {
width: 100%;
height: 100%;
}
}
}
.box4-main {
width: 1057px;
height: auto;
margin: 0 auto;
.box4-main-main {
height: auto;
overflow: visible;
.box4-item {
width: 1057px;
box-sizing: border-box;
border-radius: 4px;
display: flex;
flex-direction: column;
position: relative;
border-bottom: 1px solid rgba(234, 236, 238, 1);
&:first-child {
border-top: 1px solid rgba(234, 236, 238, 1);
}
.top-row {
display: flex;
align-items: flex-start;
}
.left {
margin-top: 19px;
margin-left: 15px;
width: 24px;
height: 24px;
border-radius: 12px;
line-height: 24px;
text-align: center;
background: rgba(231, 243, 255, 1);
color: var(--color-main-active);
font-family: Microsoft YaHei;
font-size: 12px;
font-weight: 400;
letter-spacing: 0px;
}
.center {
min-height: 62px;
margin-left: 18px;
display: flex;
align-items: center;
// overflow: hidden;
// text-overflow: ellipsis;
// white-space: nowrap;
.title {
width: 918px;
// height: 55px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 700;
line-height: 30px;
letter-spacing: 0px;
text-align: left;
overflow: hidden;
// text-overflow: ellipsis;
// white-space: nowrap;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.center-image {
width: 16px;
height: 24px;
margin-top: 12px;
margin-left: 18px;
}
}
.right {
margin-top: 26px;
width: 180px;
height: 22px;
display: flex;
margin-top: 26px;
margin-left: 20px;
height: 22px;
display: flex;
gap: 4px;
.tag {
height: 22px;
padding: 0 8px;
box-sizing: border-box;
border: 1px solid rgba(217, 247, 190, 1);
border-radius: 4px;
background: rgba(246, 255, 237, 1);
color: rgba(82, 196, 26, 1);
font-family: Microsoft YaHei;
font-size: 14px;
font-weight: 400;
line-height: 20px;
}
}
.more {
width: 16px;
height: 16px;
position: absolute;
top: 28px;
right: 20px;
img {
width: 100%;
height: 100%;
}
}
.desc {
padding-top: 22px;
padding-bottom: 23px;
padding-left: 56px; // 24(left) + 13(center margin) + 一点间距
color: rgb(59, 65, 75);
font-family: "Source Han Sans CN";
font-weight: 400;
/* Regular 常规 */
font-size: 16px;
line-height: 30px;
letter-spacing: 0px;
text-align: justify;
/* 两端对齐 */
border-top: 1px solid rgba(234, 236, 238, 1);
}
.title :deep(.opinion-keyword-highlight),
.desc :deep(.opinion-keyword-highlight) {
background-color: #fff59d;
}
}
}
.box4-main-footer {
height: 80px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 30px 5px;
box-sizing: border-box;
overflow: hidden;
.info {
flex: 1 1 auto;
min-width: 0;
color: rgba(132, 136, 142, 1);
font-family: Microsoft YaHei;
font-size: 14px;
font-weight: 400;
line-height: 18px;
letter-spacing: 0px;
text-align: left;
}
.page-box {
/* 最大 300px:允许变小,但绝不变大 */
flex: 0 1 300px;
width: 100%;
max-width: 300px;
min-width: 0;
display: flex;
justify-content: flex-end;
overflow: hidden;
}
.page-box :deep(.el-pagination) {
max-width: 100%;
min-width: 0;
overflow: hidden;
}
}
}
.box4-footer {
position: absolute;
left: 22px;
bottom: 19px;
width: 1057px;
height: 64px;
box-sizing: border-box;
border: 1px solid rgba(231, 243, 255, 1);
border-radius: 4px;
background: rgba(246, 250, 255, 1);
display: flex;
align-items: center;
justify-content: center;
gap: 13px;
.footer-left {
width: 19px;
height: 20px;
img {
width: 100%;
height: 100%;
}
}
.footer-center {
width: 964px;
height: 48px;
color: var(--color-main-active);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 24px;
letter-spacing: 0px;
text-align: justify;
}
.footer-right {
width: 24px;
height: 24px;
img {
width: 100%;
height: 100%;
}
}
}
}
.box5 {
width: 1103px;
height: auto;
// border: 1px solid rgba(234, 236, 238, 1);
// border-radius: 10px;
// box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
// background: rgba(255, 255, 255, 1);
position: relative;
}
}
}
}
:deep(.analysis-box-wrapper .wrapper-header) {
height: 54px !important;
display: flex;
align-items: center;
.header-title>div {
line-height: 54px;
}
}
</style>
......@@ -474,7 +474,7 @@ const handleOpenReportOriginalFromSource = (sv) => {
const id = String(sv?.report_id ?? "").trim()
if (!id) return
const route = router.resolve({
name: "ReportOriginal",
name: "ReportDetail",
params: { id }
})
window.open(route.href, "_blank")
......
<template>
<div class="wrap">
<div class="scroll-inner">
<div class="header">
<div class="header-top">
<div class="header-top-left">
<img src="../assets/images/box1-logo.png" alt="" />
<div>
<div class="title">{{ thinkInfo.name }}</div>
<div class="en-title">
{{ thinkInfo.ename }}
</div>
</div>
</div>
<div class="header-top-right">
<div class="image-name-box">
<div class="image"><img src="../assets/images/box1-logo.png" alt="" /></div>
<div class="name">{{ thinkInfo.thinkTankName }}</div>
</div>
<div class="tag-box">
<AreaTag v-for="(value, index) in thinkInfo.tags" :key="index" :tagName="value.industryName"></AreaTag>
</div>
</div>
</div>
</div>
<div class="bottom-row">
<div class="left">
<div class="box1">
<AnalysisBox title="关键词云" :showAllBtn="true">
<div class="box1-main">
<template v-if="!hasBox5ChartData">
<el-empty class="box5-el-empty" description="暂无数据" :image-size="100" />
</template>
<template v-else>
<div class="box5Chart">
<!-- 有数据后再挂载子组件:子组件仅在 onMounted 初始化,异步数据到达后需 v-if + key 强制重新挂载 -->
<WordCloudChart v-if="box5Data.length" :key="box5WordCloudKey" :data="box5Data" width="432px"
height="272px" />
</div>
<div class="box1-footer">
<TipTab :text="REPORT_ANALYSIS_TIP_BOX5" />
</div>
<div class="ai-wrap" @mouseenter="handleSwitchAiContentShowBox5(true)">
<AiButton />
</div>
<div class="ai-content" v-if="isShowAiContentBox5" @mouseleave="handleSwitchAiContentShowBox5(false)">
<AiPane :aiContent="aiContentBox5" />
</div>
</template>
</div>
</AnalysisBox>
</div>
<div class="box2">
<!-- <div class="box-header">
<div class="header-left"></div>
<div class="title">涉及科技领域</div>
<div class="header-right">
<div class="icon">
<img src="@/assets/icons/box-header-icon2.png" alt="" />
</div>
<div class="icon">
<img src="@/assets/icons/box-header-icon3.png" alt="" />
</div>
</div>
</div>
<div class="box2-main">
<div class="box2-content" id="box2Chart"></div>
</div> -->
<AnalysisBox title="项目报告" :showAllBtn="true">
<div class="box2-main">
<div class="box2-item" v-for="(report, idx) in reportList" :key="idx">
<div class="box2-item-content">
<div class="left"><img :src="report.image" alt="" /></div>
<div class="right-content">
<div class="report-title">{{ report.name }}</div>
<div class="report-footer">
<div class="report-time">{{ report.postDate }}</div>
<div class="report-footer-right">
<div class="footer-image">
<img :src="report.thinktankLogo" alt="" />
</div>
<div class="think-name">{{ report.thinktankName }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</AnalysisBox>
</div>
</div>
<div class="right">
<div class="box3">
<AnalysisBox title="内容摘要" :showAllBtn="true">
<div class="box3-main">
<AiSummary>
<template #summary-content>
{{ box1Data }}
</template>
</AiSummary>
</div>
</AnalysisBox>
</div>
<div class="box4">
<AnalysisBox title="项目背景" :showAllBtn="true">
<div class="box4-main">
<div class="text">
{{
"可再生能源和清洁能源创新为摆脱对化石燃料的依赖提供了机会。然而,可再生能源的间歇性也对电网的实时供需平衡构成挑战。作为解决方案,中国提出开发全球能源互联(GEI)倡议,通过超高压输电线路和智能技术,直接将可再生能源生产者与全球消费者连接起来。北京认识到GEI的潜力,正站在GEI发展的前沿,领导关键推动技术的研究,输出中国技术和标准,支持海外发电和输电基础设施的发展。中国还在联合国和海湾合作委员会等国际组织推动GEI"
}}
</div>
</div>
</AnalysisBox>
</div>
<div class="box5">
<AnalysisBox title="项目团队" :showAllBtn="true">
<div class="box5-main">
<div class="box5-main-item-box">
<div class="item">
<div class="item-left">
<img src="../assets/images/rand-image.png" alt="" />
</div>
<div class="item-right">
<div class="item-name">{{ "纳迪娅·阿尔马萨尔基" }} </div>
<div class="item-position">{{ "副秘书" }}</div>
</div>
</div>
<div class="item">
<div class="item-left">
<img src="../assets/images/rand-image.png" alt="" />
</div>
<div class="item-right">
<div class="item-name">{{ "纳迪娅·阿尔马萨尔基" }} </div>
<div class="item-position">{{ "副秘书" }}</div>
</div>
</div>
<div class="item">
<div class="item-left">
<img src="../assets/images/rand-image.png" alt="" />
</div>
<div class="item-right">
<div class="item-name">{{ "纳迪娅·阿尔马萨尔基" }} </div>
<div class="item-position">{{ "副秘书" }}</div>
</div>
</div>
<div class="item">
<div class="item-left">
<img src="../assets/images/rand-image.png" alt="" />
</div>
<div class="item-right">
<div class="item-name">{{ "纳迪娅·阿尔马萨尔基" }} </div>
<div class="item-position">{{ "副秘书" }}</div>
</div>
</div>
<div class="item">
<div class="item-left">
<img src="../assets/images/rand-image.png" alt="" />
</div>
<div class="item-right">
<div class="item-name">{{ "纳迪娅·阿尔马萨尔基" }} </div>
<div class="item-position">{{ "副秘书" }}</div>
</div>
</div>
<div class="item">
<div class="item-left">
<img src="../assets/images/rand-image.png" alt="" />
</div>
<div class="item-right">
<div class="item-name">{{ "纳迪娅·阿尔马萨尔基" }} </div>
<div class="item-position">{{ "副秘书" }}</div>
</div>
</div>
</div>
</div>
</AnalysisBox>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import DefaultIcon1 from '@/assets/icons/default-icon1.png'
import WarningPane from "@/components/base/WarningPane/index.vue"
import WordCloudChart from "@/components/base/WordCloundChart/index.vue"
import SearchContainer from "@/components/SearchContainer.vue";
import { ref, onMounted, computed, defineProps } from "vue";
import { ElMessage } from "element-plus";
import {
getThinkTankReportAbstract,
getThinkTankReportContent,
getThinkTankReportIndustry,
getThinkTankReportIndustryCloud,
getThinkTankReportViewpoint
} from "@/api/thinkTank/overview";
import { getChartAnalysis } from "@/api/aiAnalysis/index";
import { useRouter } from "vue-router";
import "echarts-wordcloud";
import AiSummary from '@/components/base/Ai/AiSummary/index.vue'
import { getPersonSummaryInfo } from "@/api/common/index";
import AiButton from "@/components/base/Ai/AiButton/index.vue";
import AiPane from "@/components/base/Ai/AiPane/index.vue";
import TipTab from "@/views/thinkTank/TipTab/index.vue";
const router = useRouter();
const thinkInfo = ref({
name: "探讨中国开发和管理的跨大陆电网的安全影响",
ename: "调查项目",
tags: [{ industryName: "深海" }, { industryName: "人工智能" }],
thinkTankName: "兰德科技智库"
})
const REPORT_ANALYSIS_TIP_BOX5 =
"智库报告关键词云,数据来源:美国兰德公司官网";
// 刷新后默认展示「报告关键词云」AI 总结
const isShowAiContentBox5 = ref(true);
const aiContentBox5 = ref("");
const isBox5InterpretLoading = ref(false);
const handleSwitchAiContentShowBox5 = (val) => {
isShowAiContentBox5.value = val;
if (val) {
fetchBox5ChartInterpretation();
}
};
const searchOpinions = ref('');
const escapeHtml = (text) => {
return String(text ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
};
const escapeRegExp = (text) => {
return String(text ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
};
/** 可同时展开多条;用 id 区分项,避免翻页后索引与展开状态错位 */
const expandedOpinionKeys = ref(new Set());
const getOpinionExpandKey = (item, index) => {
if (item != null && item.id != null && item.id !== "") {
return String(item.id);
}
return `idx-${index}`;
};
// 内容摘要
const box1Data =
ref(`包括经济竞争在内的美中竞争自2017年以来一直在定义美国外交政策。这两个经济体是世界上第一和第二大国家经济体,并且深深交织在一起。改变关系,无论多么必要,可能是昂贵的。因此,美国面临着一项挑战,确保其经济在耦合的战略竞争条件下满足国家的需求。
为了应对这一挑战,兰德大学的研究人员对美中竞争进行了经济和制度分析,进行了参与式的远见练习,以了解确保美国经济健康的长期路径,并创建了两个经济竞争游戏,探索多个国家在相互交流的同时确保经济健康的动态...`);
//获取内容摘要
const handleGetThinkTankReportAbstract = async () => {
try {
const res = await getThinkTankReportAbstract(router.currentRoute._value.params.id);
console.log("内容摘要", res);
if (res.code === 200 && res.data) {
box1Data.value = res.data;
}
} catch (error) {
console.error("获取内容摘要error", error);
}
};
// 涉及科技领域
const areaList = ref([]);
const activeArea = ref(6);
const handleClickArea = area => {
activeArea.value = area;
handleGetThinkTankReportIndustryCloud();
};
const box2Data = ref([
// {
// name: "通用人工智能",
// value: 100
// },
// {
// name: "AI芯片",
// value: 66
// },
// {
// name: "计算能力又是",
// value: 72
// },
// {
// name: "基准测试",
// value: 88
// },
// {
// name: "出口管制",
// value: 78
// },
// {
// name: "军事AI",
// value: 85
// },
// {
// name: "生态系统",
// value: 88
// },
// {
// name: "模型能力",
// value: 89
// }
]);
// 报告关键词云
const box5Data = ref([]);
const hasBox5ChartData = computed(() => Array.isArray(box5Data.value) && box5Data.value.length > 0);
/** 词云子组件不 watch 数据,每次接口成功有数据时递增 key,强制重新挂载以触发 onMounted */
const box5WordCloudKey = ref(0);
//获取科技领域词云
const handleGetThinkTankReportIndustryCloud = async () => {
try {
const params = {
id: router.currentRoute._value.params.id
// industryId: activeArea.value
};
const res = await getThinkTankReportIndustryCloud(params);
console.log("科技领域词云", res);
if (res.code === 200 && res.data) {
const data = (res.data || []).map(item => ({
name: item.clause,
value: item.count
}));
// 该接口数据用于「报告关键词云」
box5Data.value = data;
if (data.length) {
box5WordCloudKey.value += 1;
}
// 刷新后默认展开 AI:数据就绪即触发解读
if (isShowAiContentBox5.value) {
fetchBox5ChartInterpretation();
}
} else {
box5Data.value = [];
}
} catch (error) {
console.error("获取科技领域词云error", error);
box5Data.value = [];
}
};
//涉及科技领域
const handleGetThinkTankReportIndustry = async () => {
try {
const res = await getThinkTankReportIndustry(router.currentRoute._value.params.id);
console.log("涉及科技领域", res);
if (res.code === 200 && res.data) {
areaList.value = res.data;
}
} catch (error) {
console.error("获取涉及科技领域error", error);
}
};
// 主要观点
const majorOpinions = ref([
{
id: 1,
title: "我是示例标题",
desc: "我是示例内容",
tagList: [
{
name: "关税",
status: 2
},
{
name: "跨境电商",
status: 1
}
]
},
{
id: 2,
title: "我是示例标题",
desc: "我是示例内容",
tagList: [
{
name: "私有经济",
status: 2
}
]
}
]);
//处理点击详情页事件
const handleOpenReportOriginal = item => {
const route = router.resolve({
name: "ReportOriginal",
params: {
id: router.currentRoute._value.params.id
},
query: {
currentPage: currentPage.value,
pageSize: pageSize.value,
opinionId: item?.id ?? "",
opinionContent: item?.content ?? ""
}
});
window.open(route.href, "_blank");
};
const tabActiveName = ref("报告分析");
const switchTab = name => {
tabActiveName.value = name;
};
// 处理页码改变事件
const currentPage = ref(1);
const pageSize = ref(10);
const total = ref(0);
const handleCurrentChange = page => {
currentPage.value = page;
handleGetThinkTankReportViewpoint();
};
// 获取报告核心论点(支持搜索)
const handleGetThinkTankReportViewpoint = async () => {
try {
const params = {
reportId: router.currentRoute._value.params.id,
currentPage: currentPage.value - 1,
pageSize: pageSize.value,
keyword: (searchOpinions.value || "").trim(),
orgIds: ""
};
const res = await getThinkTankReportViewpoint(params);
console.log("核心论点", res.data);
if (res.code === 200 && res.data) {
const nextOpinions = res.data.content || [];
majorOpinions.value = nextOpinions;
total.value = res.data.totalElements || 0;
// 默认:第一条展开,其余关闭
const nextExpandedKeys = new Set();
if (Array.isArray(nextOpinions) && nextOpinions.length > 0) {
nextExpandedKeys.add(getOpinionExpandKey(nextOpinions[0], 0));
}
expandedOpinionKeys.value = nextExpandedKeys;
}
} catch (error) {
console.error("获取主要观点error", error);
}
};
// 获取图表分析内容
const box3AnalysisContent = ref("");
const handleGetBox3AnalysisContent = async textJson => {
const params = {
text: textJson
};
const res = await getChartAnalysis(params);
console.log("图表解析内容", res);
};
const getInterpretationTextFromChartResponse = (res) => {
const list = res?.data;
const first = Array.isArray(list) ? list[0] : null;
return (
first?.["解读"] ||
first?.["interpretation"] ||
first?.["analysis"] ||
first?.["content"] ||
""
);
};
const appendAiInterpretationChunk = (targetRef, chunk, loadingText = "解读生成中…") => {
if (!chunk) {
return;
}
const current = String(targetRef.value || "");
const base = current === loadingText ? "" : current;
targetRef.value = base + String(chunk);
};
const fetchBox5ChartInterpretation = async () => {
const list = Array.isArray(box5Data.value) ? box5Data.value : [];
if (!list.length) {
aiContentBox5.value = "暂无图表数据";
return;
}
const hasValidContent =
aiContentBox5.value &&
aiContentBox5.value !== "解读生成中…" &&
aiContentBox5.value !== "解读加载失败" &&
aiContentBox5.value !== "暂无图表数据";
if (hasValidContent || isBox5InterpretLoading.value) {
return;
}
isBox5InterpretLoading.value = true;
aiContentBox5.value = "解读生成中…";
const chartPayload = {
type: "词云图",
name: "报告关键词云",
data: list.map((item) => ({
name: item.name,
value: item.value
}))
};
try {
const res = await getChartAnalysis(
{ text: JSON.stringify(chartPayload) },
{
onChunk: chunk => {
appendAiInterpretationChunk(aiContentBox5, chunk);
}
}
);
const text = getInterpretationTextFromChartResponse(res);
aiContentBox5.value = text || aiContentBox5.value || "未返回有效解读内容";
} catch (error) {
console.error("报告关键词云图表解读请求失败", error);
aiContentBox5.value = "解读加载失败";
} finally {
isBox5InterpretLoading.value = false;
}
};
onMounted(() => {
handleGetThinkTankReportAbstract();
handleGetThinkTankReportViewpoint();
handleGetThinkTankReportIndustry();
handleGetThinkTankReportIndustryCloud();
});
</script>
<style lang="scss" scoped>
.wrap {
position: absolute;
inset: 0;
box-sizing: border-box;
width: 100%;
overflow: hidden;
.scroll-inner {
box-sizing: border-box;
width: 100%;
height: 100%;
min-height: 0;
padding-bottom: 16px;
overflow-x: hidden;
overflow-y: auto;
scrollbar-gutter: stable;
scrollbar-width: thin;
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-thumb {
border-radius: 4px;
}
}
.header {
width: 100%;
min-height: 94px;
box-sizing: border-box;
border-bottom: 1px solid rgba(234, 236, 238, 1);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background: rgba(255, 255, 255, 1);
position: sticky;
top: 0;
z-index: 99999;
overflow: hidden;
.header-top {
margin: 0 auto;
margin-top: 20px;
width: 1600px;
display: flex;
justify-content: space-between;
.header-top-left {
display: flex;
img {
width: 54px;
height: 54px;
}
.title {
margin-left: 20px;
font-family: "Source Han Sans CN";
font-weight: 700;
font-size: 20px;
line-height: 26px;
letter-spacing: 0;
text-align: left;
color: rgb(59, 65, 75);
}
.en-title {
margin-left: 20px;
margin-top: 4px;
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
line-height: 24px;
letter-spacing: 0;
text-align: left;
color: rgb(95, 101, 108);
}
}
.header-top-right {
display: flex;
flex-direction: column;
text-align: right;
align-items: flex-end;
.image-name-box {
width: 118px;
height: 24px;
gap: 6px;
text-align: right;
display: flex;
justify-content: flex-end;
.name {
height: 24px;
color: rgba(95, 101, 108, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 24px;
letter-spacing: 0px;
text-align: right;
}
.image {
width: 16px;
height: 16px;
margin-top: 5px;
img {
width: 100%;
height: 100%;
}
}
}
.tag-box {
margin-top: 7px;
display: flex;
gap: 8px;
}
}
}
}
.bottom-row {
flex-direction: row;
justify-content: center;
display: flex;
gap: 16px;
margin-bottom: 70px;
.left {
gap: 16px;
display: flex;
flex-direction: column;
.box1 {
width: 480px;
height: 415px;
margin-top: 17px;
.box1-main {
width: 480px;
height: 361px;
padding: 24px 24px 65px 24px;
display: flex;
flex-direction: column;
box-sizing: border-box;
overflow: hidden;
position: relative;
.box5Chart {
width: 100%;
height: 100%;
margin: 0 auto;
overflow: hidden;
}
.box1-footer {
display: flex;
justify-content: space-between;
align-items: center;
position: absolute;
bottom: 20px;
left: 32px;
}
.ai-wrap {
position: absolute;
bottom: 18px;
right: 0;
cursor: pointer;
}
.ai-content {
position: absolute;
bottom: 0;
right: 0;
min-width: 480px;
min-height: 156px;
}
}
}
.box2 {
width: 480px;
// border: 1px solid rgba(234, 236, 238, 1);
// border-radius: 10px;
// box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
// background: rgba(255, 255, 255, 1);
.box2-main {
width: 436px;
margin-top: 5px;
margin-left: 23px;
.box2-item {
height: 103px;
width: 100%;
border-bottom: 1px solid rgba(234, 236, 238, 1);
border-top: 1px solid rgba(234, 236, 238, 1);
.box2-item-content {
width: 100%;
height: 90px;
margin-top: 7px;
display: flex;
.left {
width: 56px;
height: 74px;
margin-top: 8px;
img {
width: 100%;
height: 100%;
}
}
.right-content {
margin-left: 13px;
width: 365px;
height: 76px;
margin-top: 7px;
.report-title {
height: 48px;
font-family: "Source Han Sans CN";
font-weight: 700;
font-size: 16px;
line-height: 24px;
letter-spacing: 0;
text-align: left;
/* 👇 下面是 两行文本超出省略 核心代码 */
display: -webkit-box;
-webkit-line-clamp: 2;
/* 限制显示 2 行 */
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
}
.report-footer {
margin-top: 4px;
height: 22px;
justify-content: space-between;
display: flex;
.report-time {
height: 22px;
width: 97px;
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 14px;
line-height: 22px;
letter-spacing: 0;
text-align: left;
color: rgb(95, 101, 108);
}
.report-footer-right {
height: 22px;
display: flex;
gap: 6px;
.footer-image {
width: 16px;
height: 16px;
margin-top: 3px;
img {
width: 100%;
height: 100%;
}
}
.think-name {
height: 22px;
}
}
}
}
}
}
}
.box2-btn {
margin-top: 16px;
margin-bottom: 21px;
margin-left: 23px;
width: 436px;
height: 36px;
background-color: rgb(5, 95, 194);
border-radius: 6px;
display: flex;
.btn-text {
color: rgb(255, 255, 255);
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
line-height: 22px;
letter-spacing: 0;
margin-left: 120px;
margin-top: 7px;
}
.btn-image {
width: 13px;
height: 8px;
margin-left: 8px;
display: inline-block;
margin-top: 14px;
img {
width: 100%;
height: 100%;
display: block;
}
}
}
}
}
.right {
margin-top: 17px;
gap: 16px;
display: flex;
flex-direction: column;
.box3 {
width: 1103px;
// border: 1px solid rgba(234, 236, 238, 1);
// border-radius: 10px;
// box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
// background: rgba(255, 255, 255, 1);
.box3-main {
width: 1058px;
margin-top: 3px;
margin-left: 22px;
padding-bottom: 22px;
:deep(.summary-main) {
margin-bottom: 25px;
font-family: "Source Han Sans CN";
font-weight: 400;
/* Regular 常规 */
font-size: 16px;
line-height: 30px;
letter-spacing: 0px;
text-align: justify;
color: rgb(59, 65, 75);
/* 两端对齐 */
}
.box3-top {
width: 1058px;
height: 48px;
background: linear-gradient(rgb(137, 193, 255, 0.1), rgb(255, 255, 255));
display: flex;
.top-title {
width: 1010px;
height: 32px;
margin-left: 24px;
margin-top: 16px;
.title-image {
width: 199px;
height: 32px;
img {
width: 100%;
height: 100%;
}
}
}
}
.box3-text {
width: 1006px;
margin-top: 24px;
margin-left: 26px;
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
line-height: 30px;
letter-spacing: 0px;
text-align: justify;
text-justify: inter-ideograph;
}
}
}
.box4 {
width: 1103px;
height: auto;
// border: 1px solid rgba(234, 236, 238, 1);
// border-radius: 10px;
// box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
// background: rgba(255, 255, 255, 1);
position: relative;
.box4-main {
width: 1103px;
padding: 5px 24px 21px 28px;
.text {
font-family: "Source Han Sans CN";
font-weight: 400;
/* Regular */
font-size: 16px;
line-height: 30px;
letter-spacing: 0px;
text-align: left;
color: rgb(59, 65, 75);
}
}
}
.box5 {
width: 1103px;
height: auto;
position: relative;
.box5-main {
width: 1103px;
padding: 3px 18px 21px 20px;
.box5-main-item-box {
width: 1065px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px 0;
.item {
width: 532.5px;
height: 49px;
display: flex;
flex-direction: row;
.item-left {
width: 42px;
height: 42px;
border-radius: 50%;
margin-top: 3px;
margin-left: 3px;
img {
width: 100%;
height: 100%;
display: block;
border-radius: 50%;
/* 图片也要加,不然会漏出直角 */
object-fit: cover;
/* 防止图片变形 */
}
}
.item-right {
height: 49px;
margin-left: 11px;
display: flex;
flex-direction: column;
.item-name {
height: 24px;
margin-top: 1px;
font-family: "Source Han Sans CN";
font-weight: 700;
/* Bold 粗体 */
font-size: 16px;
line-height: 24px;
letter-spacing: 0px;
text-align: left;
color: rgb(59, 65, 75);
}
.item-position {
height: 22px;
font-family: "Source Han Sans CN";
font-weight: 400;
/* Regular */
font-size: 14px;
line-height: 22px;
letter-spacing: 0;
text-align: left;
color: rgb(95, 101, 108);
}
}
}
}
}
}
}
}
}
:deep(.analysis-box-wrapper .wrapper-header) {
height: 54px !important;
display: flex;
align-items: center;
.header-title>div {
line-height: 54px;
}
}
</style>
......@@ -1221,7 +1221,7 @@ function mapPolicyRowToView(row) {
const toDetail = item => {
window.sessionStorage.setItem("curTabName", item.contentZh ?? item.content ?? "");
const route = router.resolve({
name: "ReportOriginal",
name: "ReportDetail",
params: {
id: item.reportId
}
......
......@@ -7,18 +7,11 @@
<div class="title">{{ "科技领域" }}</div>
</div>
<div class="select-main">
<el-checkbox-group
class="checkbox-group"
:model-value="selectedAreaList"
@change="handleAreaGroupChange">
<el-checkbox-group class="checkbox-group" :model-value="selectedAreaList" @change="handleAreaGroupChange">
<el-checkbox class="filter-checkbox all-checkbox" :label="RESOURCE_FILTER_ALL_AREA">
{{ RESOURCE_FILTER_ALL_AREA }}
</el-checkbox>
<el-checkbox
v-for="research in areaList"
:key="research.id"
class="filter-checkbox"
:label="research.id">
<el-checkbox v-for="research in areaList" :key="research.id" class="filter-checkbox" :label="research.id">
{{ research.name }}
</el-checkbox>
</el-checkbox-group>
......@@ -31,18 +24,11 @@
<div class="title">{{ "发布时间" }}</div>
</div>
<div class="select-main">
<el-checkbox-group
class="checkbox-group"
:model-value="selectedPubTimeList"
@change="handleTimeGroupChange">
<el-checkbox-group class="checkbox-group" :model-value="selectedPubTimeList" @change="handleTimeGroupChange">
<el-checkbox class="filter-checkbox all-checkbox" :label="RESOURCE_FILTER_ALL_TIME">
{{ RESOURCE_FILTER_ALL_TIME }}
</el-checkbox>
<el-checkbox
v-for="time in pubTimeList"
:key="time.id"
class="filter-checkbox"
:label="time.id">
<el-checkbox v-for="time in pubTimeList" :key="time.id" class="filter-checkbox" :label="time.id">
{{ time.name }}
</el-checkbox>
</el-checkbox-group>
......
......@@ -120,7 +120,7 @@ const handleOpenReportOriginal = (item) => {
const reportId = item?.reportId || item?.report_id || item?.id
if (!reportId) return
const route = router.resolve({
name: "ReportOriginal",
name: "ReportDetail",
params: { id: String(reportId) }
})
window.open(route.href, "_blank")
......
......@@ -163,7 +163,35 @@ const getMultiLineChart = (data) => {
{
type: 'category',
boundaryGap: false,
data: title
data: title,
axisLabel: {
// 同时设置 color 与 textStyle,避免 ECharts/主题在 axisLine/axisTick 展示时覆盖
color: 'rgba(132, 136, 142, 1)',
textStyle: {
color: 'rgba(132, 136, 142, 1)',
fontSize: 14,
fontFamily: 'Source Han Sans CN',
fontWeight: 400
}
},
axisLine: {
show: true,
lineStyle: {
color: 'rgba(231, 241, 255, 1)' // 你要的红色
}
},
axisTick: {
show: true,
// 让刻度和 label 对齐,显示短直线
alignWithLabel: true,
length: 6,
length2: 0,
lineStyle: {
color: "rgba(231, 241, 255, 1)",
type: "solid"
}
},
}
],
yAxis: [
......
<template>
<div class="writtingBottom">
<!-- 文档停止解析 -->
<div class="parsed" v-if="store.bottomProgressNum>0&&store.bottomProgressNum!=100&&store.writeProgressNum<10">
<div class="analysis" @click="store.resetGenerateState">
<div class="icon"></div>
<span class="text-tip-2-bold">停止</span>
</div>
<div class="processLogBox" :class="{isProcessLog:store.isProcessLog}">
<div class="progress">
<div class="login">
<el-progress type="circle" :percentage="store.bottomProgressNum" :stroke-width="4" :width="24" :height="24" style="margin-right: 15px;" :show-text="false" color="rgb(5, 95, 194)"/>
<span class="text-tip-2-bold">文档翻译中</span>
</div>
<div class="text-tip-2" style="display: flex;align-items: center;">
<div class="processLogAlone" @click="progressAchieveClick">{{ storeProcessLogAlone }}</div>
<el-icon v-if="store.isProcessLog" @click="isProcessLogClick"><ArrowUpBold /></el-icon>
<el-icon v-else @click="isProcessLogClick"><ArrowDownBold /></el-icon>
</div>
</div>
<div class="processLogList" ref="processLogListRef">
<p>执行步骤:</p>
<div ref="processContainerRef" v-html="renderedProcess"></div>
</div>
</div>
</div>
<!-- 开始写报 -->
<div class="parsed" v-else-if="store.bottomProgressNum>=100">
<div class="analysis" v-if="store.writeProgressNum>0&&store.writeProgressNum!=100" @click="store.writeGenerateState">
<div class="icon"></div>
<span class="text-tip-2-bold">停止</span>
</div>
<div class="analysis" v-else-if="store.writeProgressNum>=100" @click="store.resetGenerateState">
<img src="@/assets/icons/aiBox/ai-logo-color.png" alt="">
<span class="text-tip-1">重新上传</span>
</div>
<div class="notAnalysis" v-else @click="onWriteClick()" >
<img src="@/assets/icons/aiBox/ai-logo.png" alt="">
<span class="text-tip-1">智能写报</span>
</div>
<div class="processLogBox" :class="{isProcessLog:store.isProcessLog}">
<div class="progress">
<!-- 如果store.writeProgressNum>=1 点击了写报 否则是思维导图已完成 -->
<div class="login" v-if="store.writeProgressNum>=1&&store.writeProgressNum<100">
<el-progress type="circle" :percentage="store.writeProgressNum" :stroke-width="4" :width="24" :height="24" style="margin-right: 15px;" :show-text="false" color="rgb(5, 95, 194)"/>
<span class="text-tip-2-bold">智能写报中</span>
</div>
<div class="login" v-else-if="store.writeProgressNum>=100||store.bottomProgressNum>=100">
<img class="steps" style="" src="@/assets/icons/aiBox/steps.png" alt="">
<span class="text-tip-2-bold" v-if="store.writeProgressNum>=100" > 智能写报完成</span>
<span class="text-tip-2-bold" v-else-if="store.bottomProgressNum>=100"> 思维导图已完成</span>
</div>
<div class="text-tip-2" style="display: flex;align-items: center;">
<div class="processLogAlone" @click="progressAchieveClick">{{ storeProcessLogAlone }}</div>
<el-icon v-if="store.isProcessLog" @click="isProcessLogClick"><ArrowUpBold /></el-icon>
<el-icon v-else @click="isProcessLogClick"><ArrowDownBold /></el-icon>
</div>
</div>
<div class="processLogList" ref="processLogListRef">
<p>执行步骤:</p>
<div ref="processContainerRef" v-html="renderedProcess"></div>
</div>
</div>
</div>
<!-- 开始解析文档 -->
<div class="parsed" v-else >
<div class="notAnalysis" @click="onAnalysisClick()">
<img src="@/assets/icons/aiBox/ai-logo.png" alt="">
<span class="text-tip-1">文档解析</span>
</div>
<div class="notProgress">
<img src="../assets/images/tips-icon.png" alt="">
<span class="text-tip-2">内容由AI生成,无法确保真实准确,仅供参考</span>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted, onUnmounted, ref, nextTick ,watch} from "vue";
import { useRoute } from "vue-router";
import { ElMessage } from "element-plus";
import { useWrittingAsstaintStore } from "@/stores/writtingAsstaintStore";
import { useStream } from "@/hooks/useStream";
// 子组件直接获取Pinia Store(核心优化)
const store = useWrittingAsstaintStore();
const emit = defineEmits(["generate","write"]);
const onAnalysisClick=()=>{
store.isShowSteps = !store.isShowSteps
emit("generate");
}
const onWriteClick=()=>{
store.isShowSteps = false
emit("write");
}
const { renderedProcess, updateProcess, clearContent } = useStream();
const processContainerRef = ref(null);
const processLogListRef=ref(null)
// 监听 store.processLog 变化,更新步骤内容并滚动
watch(
() => store.processLog,
async (newLog) => {
console.log(newLog,0)
if (newLog !== undefined && newLog !== null) {
await updateProcess(newLog, processContainerRef.value);
}
if(processLogListRef.value){
processLogListRef.value.scrollTo({
top:processLogListRef.value.scrollHeight,
behavior: 'smooth'
});
}
},
{ immediate: true }
);
defineExpose({
processContainerRef
});
const processLogAloneRef=ref(null)
const storeProcessLogAlone=ref()
watch(
() => store.processLogAlone,
async (newLog) => {
// console.log(newLog,1)
if (newLog !== undefined && newLog !== null) {
const lines = newLog.split('\n').filter(newLog => newLog.trim());
const lastLine = lines[lines.length - 1];
storeProcessLogAlone.value=lastLine
// await updateProcess(lastLine?lastLine:'', processLogAloneRef.value);
}
},
{ immediate: true }
);
const isProcessLogClick=()=>{
store.isProcessLog=!store.isProcessLog
nextTick(()=>{
processLogListRef.value.scrollTo({
top:processLogListRef.value.scrollHeight,
behavior: 'smooth'
});
})
}
// 进度完成点击切换
const progressAchieveClick= ()=>{
if(store.writeProgressNum>=100&&storeProcessLogAlone.value=='点击查看'){ //写报
console.log(store.headerTabType)
store.headerTabType='message'
}else if(store.bottomProgressNum>=100&&storeProcessLogAlone.value=='点击查看'){ //思维导图
console.log(store.headerTabType)
store.headerTabType='mind'
}
}
</script>
<style lang="scss" scoped>
.writtingBottom{
width: 100%;
background: #fff;
box-shadow: 0px 0px 3px 0px #d2d2d2;
position: relative;
padding: 0 22px;
.parsed{
display: flex;
justify-content: space-between;
// align-items: center;
height: 64px;
padding-top: 15px;
.notAnalysis{
display: flex;
justify-content: center;
align-items: center;
background-color: var(--color-primary-100);
border-radius: 4px;
width: 437px;
height: 36px;
color: #fff;
cursor: pointer;
img{
width: 21px;
height: 16px;
margin-right: 12px;
}
}
.notProgress{
background-color: #fff;
display: flex;
align-items: center;
justify-content: space-between;
color: rgba(132, 136, 142, 1);
img{
width: 16px;
height: 16px;
margin-right: 8px;
}
}
.analysis{
border: 1px solid var(--color-primary-100);
background-color: rgb(246, 250, 255);
color: var(--color-primary-100);
width: 437px;
height: 36px;
display: flex;
justify-content: center;
align-items: center;
margin-left: 22px;
cursor: pointer;
.icon{
width: 8px;
height: 8px;
border-radius: 2px;
background-color: var(--color-primary-100);
margin-right: 12px;
}
img{
width: 21px;
height: 16px;
margin-right: 12px;
}
}
.progress{
display: flex;
align-items: center;
justify-content: space-between;
color: var(--color-primary-100);
background-color: rgb(246, 250, 255);
border-radius: 50px;
height: 38px;
padding: 0 12px;
.login{
margin-right: 10px;
display: flex;
align-items: center;
.steps{
width: 16px;
height: 16px;
margin-right: 12px;
}
.text-tip-2-bold{
}
}
}
.processLogAlone{
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 300px;
margin-right: 15px;
cursor: pointer;
}
}
// 总步骤条
.processLogBox{
// background-color: rgba(246, 250, 255, 1);
border-radius: 5px;
transition: all 0.3s ease;
height: 300px;
transform: translateY(0);
.processLogList{
display: none;
padding: 0 40px;
font-size: 14px;
color: rgb(59, 65, 75);
line-height: 22px;
max-height: 240px;
overflow-y: auto;
width: 500px;
font-size: 16px;
}
}
.isProcessLog{
transform: translateY(-270px);
background: rgba(246, 250, 255, 1);
.progress{
background-color: rgba(246, 250, 255, 1);
}
.processLogList{
display: block;
}
}
}
</style>
\ No newline at end of file
<template>
<div class="headerBox">
<div class="tabBox" v-if="store.bottomProgressNum>0">
<div class="fileName">
<img src="@/assets/icons/pdf-icon.png" alt=" ">
<span class="text-tip-1-bold">{{ store.uploadFileList[0]?.name||'文件错误' }}</span>
</div>
<div class="tab">
<div class="tabList text-tip-1-bold" v-for="(item,index) in store.tabList" :key="index" :class="{'on':store.headerTabType==item.type}"
:style="!item.active?'color:#bfbfbf;cursor: no-drop;':''"
@click="onTabListClick(item.type,item.active)">{{ item.name }}</div>
</div>
<div class="switch" v-if="store.headerTabType=='translate'">
<el-switch v-model="store.isShowOriginal"/>
<div class="iconBOx">
<img src="@/assets/icons/translate-icon.png" alt="">
<span class="text-tip-1">显示原文</span>
</div>
<el-button @click="store.handleIsSsearchFor"><img style="width: 16px;" src="@/assets/icons/aiBox/search.png" alt=""> 查找</el-button>
</div>
<div v-else style="margin-right: 22px;">
<el-button @click="onExport">导出</el-button>
</div>
</div>
<div class="logo" v-else>
<img src="@/assets/icons/tool-item-icon1.png" alt="">
<span class="text-title-3-bold">智能写库</span>
</div>
</div>
</template>
<script setup>
import { onMounted, onUnmounted, ref, nextTick ,computed,watch} from "vue";
import { useRoute } from "vue-router";
import { ElMessage } from "element-plus";
import { useWrittingAsstaintStore } from "@/stores/writtingAsstaintStore";
const emit = defineEmits(["onExport"]);
// 子组件直接获取Pinia Store(核心优化)
const store = useWrittingAsstaintStore();
const onTabListClick= (type,active)=>{
if(!active) return
store.handleHeaderTab(type)
}
const onExport=()=>{
if(store.headerTabType=='mind'){
emit('onExport')
}else if(store.headerTabType=='message'){
store.exportContent
}
}
</script>
<style lang="scss" scoped>
.headerBox{
background-color: #fff;
border-bottom: 1px solid rgb(234, 236, 238);
.logo{
height: 60px;
display: flex;
align-items: center;
img{
width: 30px;
height: 30px;
margin-left: 28px;
margin-right: 20px;
}
}
.tabBox{
height: 60px;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
.fileName{
display: flex;
align-items: center;
img{
width: 24px;
height: 24px;
margin-left: 28px;
margin-right: 16px;
}
}
.tab{
width: 232px;
display: flex;
justify-content: space-between;
position: absolute;
left: 50%;
transform: translate(-50%);
.tabList{
cursor: pointer;
height: 59px;
line-height: 66px;
}
.on{
border-bottom: 4px solid var(--color-primary-100);
color:var(--color-primary-100) ;
transition: all 0.1s;
}
}
.btns{
margin-right: 23px;
}
}
.switch{
display: flex;
align-items: center;
margin-right: 23px;
.iconBOx{
display: flex;
align-items: center;
margin-right: 12px;
img{
width: 16px;
height: 16px;
margin:0 5px;
}
}
}
}
</style>
\ No newline at end of file
<template>
<div class="left-box-wrapper">
<div class="back" @click="store.resetGenerateState" v-if="store.isGenerating">&lt; 返回</div>
<!-- <div class="back" @click="store.resetGenerateState" v-if="store.isGenerating">&lt; 返回</div> -->
<div class="left-box" :class="{ 'has-back-btn': store.isGenerating }"
v-if="!store.isShowClauseTranslation && !store.isShowSteps">
<div class="left-box-input">
......@@ -11,15 +11,15 @@
<div class="header">报文主题</div>
<div class="title-box">
<div class="title">主题名称</div>
<el-input :disabled="store.isDisableTemplate" style="width: 476px; height: 32px"
<el-input :disabled="store.isDisableTemplate" style="width: 476px; height: 32px ;background: #f7f8f9;"
class="title-input" placeholder="输入主题名称,如:大而美法案" v-model="store.writtingTitle" />
</div>
<div class="description-box">
<!-- <div class="description-box">
<div class="title">主题描述</div>
<el-input :disabled="store.isDisableTemplate" class="description-input" type="textarea"
style="width: 476px" :rows="8" placeholder="输入报文主题描述,如:从科技领域方面分析大而美法案通过后对中国可能产生的影响"
v-model="store.descText" />
</div>
</div> -->
</div>
<!-- 报文模板 -->
......@@ -88,24 +88,24 @@
</div>
<!-- 提交区域 -->
<div class="submit-area">
<!-- <div class="submit-area">
<div class="tips">
<div class="tips-icon">
<img src="../assets/images/tips-icon.png" alt="" />
</div>
<div class="tips-text">内容由AI生成,无法确保真实准确,仅供参考</div>
</div>
</div> -->
<!-- 生成按钮 -->
<div class="submit-btn" @click="triggerGenerate" v-if="!store.isGenerating">
<!-- <div class="submit-btn" @click="triggerGenerate" v-if="!store.isGenerating">
<div class="submit-icon">
<img src="../assets/images/ai.png" alt="" />
</div>
<div class="submit-text">生成报文</div>
</div>
</div> -->
<!-- 生成中状态 -->
<div class="process-footer-box" v-else>
<!-- <div class="process-footer-box" v-else>
<div class="footer-left">
{{ store.isGenerating ? "报文生成中..." : "报文已生成" }}
</div>
......@@ -114,11 +114,11 @@
<div class="text">停止</div>
</div>
</div>
</div>
</div> end -->
</div>
<!-- 步骤侧边栏(拆分出来) -->
<div class="left-box process" :class="{ 'has-back-btn': store.isGenerating }" v-if="store.isShowSteps">
<!-- <div class="left-box process" :class="{ 'has-back-btn': store.isGenerating }" v-if="store.isShowSteps">
<div class="left-box-input">
<div class="process-box">
<div class="process-main-box">
......@@ -137,17 +137,17 @@
</div>
</div>
</div>
</div>
<div class="submit-area">
</div> -->
<!-- <div class="submit-area">
<div class="tips">
<div class="tips-icon">
<img src="../assets/images/tips-icon.png" alt="" />
</div>
<div class="tips-text">内容由AI生成,无法确保真实准确,仅供参考</div>
</div>
</div> -->
<!-- 生成中状态 -->
<div class="process-footer-box">
<!-- <div class="process-footer-box">
<div class="footer-left">
{{ store.isGenerating ? "报文生成中..." : "报文已生成" }}
</div>
......@@ -155,25 +155,23 @@
<div class="icon"></div>
<div class="text">停止</div>
</div>
</div>
</div>
</div>
<!-- 条款翻译侧边栏 -->
<div class="left-box translation-box" :class="{ 'has-back-btn': store.isGenerating }"
v-if="store.isShowClauseTranslation">
</div> -->
<!-- </div>
</div> -->
<!-- 条款翻译侧边栏 srot -->
<div class="left-box translation-box" :class="{ 'has-back-btn': store.isGenerating }" v-if="store.isShowSteps&&store.headerTabType=='message'">
<div class="translation-main-box">
<div class="translation-actions" v-if="!store.isGenerating">
<!-- <div class="translation-actions" v-if="!store.isGenerating">
<div class="back-input-btn" @click="store.backToInputAndClear">返回输入栏</div>
</div>
</div> -->
<!-- 政令标题卡片 -->
<div class="metadata-card" v-if="store.pdfMetadata">
<!-- <div class="metadata-card" v-if="store.pdfMetadata">
<div class="card-header">
<div class="chinese-name">{{ store.pdfMetadata.name }}</div>
<div class="type-tag">{{ store.pdfMetadata.signing_date }}</div>
</div>
<div class="english-name">{{ store.pdfMetadata.order_title }}</div>
</div>
</div> -->
<div class="translation-header-new">
<div class="header-left">共{{ store.clauseTranslationMessages.length }}章节</div>
<div class="header-right">
......@@ -200,7 +198,7 @@
</div>
</div>
<!-- 步骤侧边栏显隐按钮 -->
<div class="toggle-steps-btn" @click="store.isShowSteps = !store.isShowSteps">
<div class="toggle-steps-btn" @click=" isShowSteps ">
<div class="arrow" :class="{ 'is-active': store.isShowSteps }"></div>
</div>
</div>
......@@ -225,6 +223,10 @@ const emit = defineEmits(["generate"]);
const triggerGenerate = () => {
emit("generate");
};
const isShowSteps=()=>{
store.isShowSteps = !store.isShowSteps
store.highlightClauseId=''
}
// 数字转中文序号
const getChineseNumber = (num) => {
......@@ -245,18 +247,30 @@ const { renderedProcess, updateProcess, clearContent } = useStream();
watch(
() => store.highlightClauseId,
async (newId) => {
if (!newId || !translationContentRef.value) return;
console.log('789')
await nextTick();
if (!newId || !translationContentRef.value) return;
const container = translationContentRef.value;
const item = container.querySelector(`.translation-item[data-clause-number="${newId}"]`);
const result = newId.replace(/^\d+、/, '');
const item = container.querySelector(`.translation-item[data-clause-number="${newId.match(/^(\d+)/)[1]}"]`);
const itemHtml=item.querySelector(`.translated-text`);
if (!item) return;
// 你已经拿到的 外层大标签
const element =itemHtml
// 你要找的文字
const targetText = result
const location=findTextInElement(element, targetText);
const containerRect = container.getBoundingClientRect();
const itemRect = item.getBoundingClientRect();
const delta = itemRect.top - containerRect.top;
const targetTop = container.scrollTop + delta;
// 让高亮条款的“标题区域”贴到容器顶部
container.scrollTo({
top: Math.max(0, targetTop),
......@@ -264,6 +278,74 @@ watch(
});
}
);
// 👇 核心:在 element 内部找文字位置
// =========================================
const findTextInElement=(element, targetText )=> {
// 遍历标签内的所有内容
const nodes = element.childNodes;
let rect =''
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
// 只找纯文字
if (node.nodeType === 3) {
const originalText = node.textContent;
const cleanText=(str)=> {
return str
.replace(/(/g, '(')
.replace(/)/g, ')')
.replace(/,/g, ',')
.replace(/。/g, '.')
.replace(/:/g, ':')
.replace(/;/g, ';');
}
// 清理后的文字(无标点)
const nodeClean = cleanText(originalText);
const targetClean = cleanText(targetText);
// 用干净文字对比
if (nodeClean.includes(targetClean)) {
// 找到真实位置(用原始文本定位,不影响)
const index = originalText.indexOf(
originalText.includes(targetText) ? targetText : originalText
);
const range = document.createRange();
range.setStart(node, index);
range.setEnd(node, index + targetText.length);
// 拿到位置
rect = range.getBoundingClientRect();
const marks = element.querySelectorAll('mark');
marks.forEach(mark => {
// 把 mark 里的文字放回原位,删除标签
const parent = mark.parentNode;
while (mark.firstChild) {
parent.insertBefore(mark.firstChild, mark);
}
parent.removeChild(mark);
// 合并相邻文本节点(恢复页面原貌)
parent.normalize();
});
const mark = document.createElement("mark");
mark.style.backgroundColor = '#ffff00;';
mark.style.color = "#000"; // 文字颜色
range.surroundContents(mark);
break;
}
}
}
return rect
}
// 监听 store.processLog 变化,更新步骤内容并滚动
watch(
......@@ -308,7 +390,7 @@ defineExpose({
width: 521px;
height: 100%;
padding-top: 22px;
padding-bottom: 29px;
padding-bottom: 10px;
box-sizing: border-box;
border-right: 1px solid rgba(234, 236, 238, 1);
border-top: 1px solid rgba(234, 236, 238, 1);
......@@ -1026,4 +1108,4 @@ defineExpose({
}
}
}
</style>
\ No newline at end of file
</style>
......@@ -41,7 +41,9 @@ const handleGlobalClick = (e) => {
const clauseId = btn.getAttribute('data-clause');
if (clauseId) {
store.highlightClauseId = clauseId;
store.isShowSteps = true;
// 翻译栏一直显示,所以这里只需要确保它在视图内
console.log(store.highlightClauseId )
}
}
};
......@@ -88,6 +90,7 @@ watch(
},
{ immediate: true }
);
</script>
<style lang="scss" scoped>
......@@ -103,7 +106,7 @@ watch(
}
.content-box {
width: 1069px;
width: 100%;
height: 100%;
overflow-y: auto;
padding: 20px 80px;
......@@ -111,8 +114,8 @@ watch(
font-size: 16px;
box-sizing: border-box;
border: 1px solid rgba(234, 236, 238, 1);
border-radius: 10px;
// border: 1px solid rgba(234, 236, 238, 1);
// border-radius: 10px;
background: rgba(255, 255, 255, 1);
margin: 17px auto 0 auto;
......@@ -138,9 +141,10 @@ watch(
cursor: pointer;
vertical-align: middle;
transition: background-color 0.2s;
border-radius: 50%;
background: #818181;
&:hover {
background-color: #044da5;
background-color: #818181;
}
&:active {
......
<template>
<div style="width: 100%; display: flex;justify-content: center;align-items: stretch;">
<!-- 右侧子组件:绑定ref -->
<writtingMainBox v-show="!!store.reportContent" :report-content="store.reportContent" />
<div v-if="!store.reportContent" class="main-placeholder">
<img src="../assets/images/container-image.png" alt="无数据占位图" />
<div class="placeholder-text">
<div>智能体写报任务执行中...</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, watch, onMounted, onUnmounted } from "vue";
import { useWrittingAsstaintStore } from "@/stores/writtingAsstaintStore";
import writtingMainBox from "./WrittingMainBox.vue";
const store = useWrittingAsstaintStore();
const mainBoxRef = ref(null); // 右侧子组件ref
</script>
<style lang="scss" scoped>
.main-placeholder {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
background: #f7f8f9;
img {
max-width: 100%;
max-height: 80%;
object-fit: contain;
display: block;
}
.placeholder-text {
color: rgba(132, 136, 142, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 24px;
}
}
</style>
\ No newline at end of file
<template>
<div class="mind-map-container">
<div ref="containerRef" class="mind-map"></div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import G6 from '@antv/g6'
import { useWrittingAsstaintStore } from "@/stores/writtingAsstaintStore";
// 子组件直接获取Pinia Store(核心优化)
const store = useWrittingAsstaintStore();
// pushfenzhi
let mindMapData = {
id: 'root',
label: '名称',
children: [
]
}
let uniqueId = 1;
function convertMindMap(rawData) {
uniqueId = 1; // 每次重置
const tree = buildTreeRecursive(rawData.node, rawData.links);
return tree[0]; // 返回单个根节点
}
const raw = store.resultWriteData.政令深度分析.条款分析.科技条款内容[0].领域举措.data;
const treeData = convertMindMap(raw);
// 全局唯一自增计数器(最稳、最短、永不重复)
// 递归构建树 + 自动生成全局唯一数字ID
function buildTreeRecursive(nodes, links, parentId = null) {
const tree = [];
// 找到当前父节点的所有子节点
const childrenNodes = nodes.filter(node => {
if (parentId === null) {
return node.depth === 0;
}
return links.some(
link => link.source === parentId && link.target === node.id
);
});
// 遍历子节点,递归生成
for (const node of childrenNodes) {
const currentNode = {
id: `${uniqueId++}_`, // 🔥 纯数字自增,绝对唯一!
label: node.name,
children: buildTreeRecursive(nodes, links, node.id)
};
tree.push(currentNode);
}
return tree;
}
treeData.id= 'root',
console.log(treeData)
mindMapData=treeData
const containerRef = ref(null)
let graph = null
// 文字换行(不溢出)
function splitTextToLines(text, maxWidth, fontSize) {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
ctx.font = `${fontSize}px sans-serif`
const lines = []
let currentLine = ''
let width = 0
for (const char of text) {
const w = ctx.measureText(char).width
if (width + w > maxWidth) {
lines.push(currentLine)
currentLine = char
width = w
} else {
currentLine += char
width += w
}
}
if (currentLine) lines.push(currentLine)
const lineHeight = fontSize + 8
const totalHeight = lineHeight * lines.length + 24
return { lines, lineHeight, totalHeight }
}
onMounted(() => {
if (graph) graph.destroy()
const el = containerRef.value
if (!el) return
// 注册节点
G6.registerNode('custom-node', {
draw(cfg, group) {
const isRoot = cfg.id === 'root'
const isDept = cfg.id.startsWith('dept')
const MAX_WIDTH = isRoot ? 280 : isDept ? 240 : 400
const fontSize = isRoot ? 16 : isDept ? 15 : 14
const fontWeight = isRoot || isDept ? 'bold' : 'normal'
const color = 'rgba(5, 95, 194, 1)'
const padding = 16
const { lines, lineHeight, totalHeight } = splitTextToLines(cfg.label, MAX_WIDTH - padding * 2, fontSize)
const nodeW = MAX_WIDTH
const nodeH = totalHeight
// 节点背景
group.addShape('rect', {
attrs: {
x: -nodeW / 2,
y: -nodeH / 2,
width: nodeW,
height: nodeH,
fill: '#f8fcff',
stroke: color,
lineWidth: 1,
radius: 6
}
})
// 文字居中
lines.forEach((line, i) => {
group.addShape('text', {
attrs: {
text: line,
x: 0,
y: (i - (lines.length - 1) / 2) * lineHeight,
textAlign: 'center',
textBaseline: 'middle',
fontSize, fontWeight, fill: color
}
})
})
return group
},
getAnchorPoints: () => [[0, 0.5], [1, 0.5]]
}, 'single-node')
graph = new G6.TreeGraph({
container: el,
width: el.offsetWidth,
height: el.offsetHeight,
modes: {
default: ['drag-canvas', 'zoom-canvas', 'collapse-expand']
},
defaultNode: { type: 'custom-node' },
defaultEdge: {
type: 'cubic-horizontal',
style: { stroke: 'rgba(5, 95, 194, 0.4)', lineWidth: 1 }
},
layout: {
type: 'compactBox',
direction: 'LR',
getWidth: () => 400,
getHGap: () => 80,
getVGap: () => 50,
}
})
graph.data(mindMapData)
graph.render()
// 自适应显示
setTimeout(() => {
graph.fitCenter()
graph.zoomTo(0.65)
}, 200)
})
onUnmounted(() => graph?.destroy())
function exportGraph() {
if (!graph) return
graph.downloadFullImage('思维导图.png')
}
defineExpose({
exportGraph
});
</script>
<style scoped>
.mind-map-container {
width: 100%;
height: 100vh;
background: #f5f7fa;
}
.mind-map {
width: 92%;
height: 100%;
margin: 0 auto;
}
</style>
\ No newline at end of file
<template>
<div class="translation-content" ref="translationContentRef">
<!-- :class="{ active: store.highlightClauseId === item.payload?.clause_number }"
:data-clause-number="item.payload?.clause_number" -->
<!-- 查找 -->
<div class="searchFor" v-if="store.isSsearchFor">
<el-input v-model="keyword" style="width: 260px;" placeholder="查找原文内容" />
<div class="searchTextNum">
<span v-if="total==0">0</span>
<span v-else> {{ current + 1 }}</span>/{{ total }}
</div>
<div class="prev" @click="prev"><el-icon><ArrowUp /></el-icon></div>
<div class="next" @click="next"><el-icon><ArrowDown /></el-icon></div>
<div class="close" @click="closeClick"><el-icon><CloseBold /></el-icon></div>
</div>
<div class="content-box" ref="contentBox">
<div class="translation-item" v-for="(item, index) in renderList" :key="index">
<div class="item-body">
<div class="original-text" v-if="store.isShowOriginal">
<template v-for="(t, i) in item.fragments" :key="i">
<span :class="{ high: t.hit, current: t.hit && currentGlobalIndex === t.globalIndex}">
{{ t.text }}
</span>
</template>
</div>
<div class="translated-text">
<span class="clause-title">第{{ getChineseNumber(item.payload?.clause_number) }}节</span>{{ item.payload?.clause_content_zh }}
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted, onUnmounted, ref, nextTick ,watch,computed} from "vue";
import { useRoute } from "vue-router";
import { ElMessage } from "element-plus";
import { useWrittingAsstaintStore } from "@/stores/writtingAsstaintStore";
import { useStream } from "@/hooks/useStream";
// 子组件直接获取Pinia Store(核心优化)
const store = useWrittingAsstaintStore();
const translationContentRef = ref(null);
// 数字转中文序号
const getChineseNumber = (num) => {
const zh = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十'];
const n = parseInt(num);
if (n <= 10) return zh[n];
if (n < 20) return '十' + zh[n % 10];
if (n < 100) {
return zh[Math.floor(n / 10)] + '十' + (n % 10 === 0 ? '' : zh[n % 10]);
}
return num;
};
const closeClick=()=>{
store.handleIsSsearchFor()
current.value=0
total.value=0
keyword.value=''
}
const keyword = ref('')
const contentBox = ref(null)
const current = ref(0)
const total = ref(0)
// 渲染列表(不修改原数据,自动更新)
const renderList = computed(() => {
const key = keyword.value.trim()
return (store.clauseTranslationMessages || []).map(item => {
const section = item.payload?.clause_section || ''
const content = item.payload?.clause_content || ''
const fullText = section + ' ' + content
if (!key) {
return {
...item,
fragments: [{ text: fullText, hit: false, globalIndex: -1 }]
}
}
const parts = fullText.split(new RegExp(`(${key})`, 'g'))
const fragments = []
parts.forEach(t => {
fragments.push({
text: t,
hit: t === key,
globalIndex: -1
})
})
return { ...item, fragments }
})
})
// 全局匹配列表(只计算一次,修复蓝色全部选中BUG)
const globalMatchList = computed(() => {
const arr = []
renderList.value.forEach(item => {
item.fragments.forEach(f => {
if (f.hit) arr.push(f)
})
})
return arr
})
// 当前高亮的全局索引
const currentGlobalIndex = computed(() => {
if (!globalMatchList.value.length) return -1
return globalMatchList.value[current.value]?.globalIndex ?? -2
})
// 总数
watch(globalMatchList, (val) => {
total.value = val.length
current.value = 0
}, { immediate: true })
// 给每个命中项分配唯一 index
watch([renderList, globalMatchList], () => {
let idx = 0
const map = new Map()
globalMatchList.value.forEach(item => {
map.set(item, idx++)
})
renderList.value.forEach(item => {
item.fragments.forEach(f => {
if (f.hit) f.globalIndex = map.get(f)
})
})
})
function doSearch() {
current.value = 0
scrollTo(0)
}
function prev() {
if (!total.value) return
current.value = (current.value - 1 + total.value) % total.value
console.log(current.value )
scrollTo(current.value)
}
function next() {
if (!total.value) return
current.value = (current.value + 1) % total.value
scrollTo(current.value)
}
function scrollTo(idx) {
setTimeout(() => {
const all = contentBox.value?.querySelectorAll('.high')
if (all?.[idx]) {
contentBox.value.scrollTo({
top: all[idx].offsetTop - 100,
behavior: 'smooth'
})
}
}, 0)
}
</script>
<style lang="scss" scoped>
.content-box{
overflow-y: auto;
margin-top: 30px;
height: calc(100vh - 250px);
.translation-item{
margin-bottom: 24px;
.item-body{
display: flex;
justify-content: space-between;
width: 85%;
margin: 0 auto;
.original-text{
min-width: 48%;
line-height: 30px;
margin-right: 62px;
flex: 2;
}
.translated-text{
width: 100%;
line-height: 30px;
}
}
}
}
.searchFor{
display: flex;
align-items: center;
width: 430px;
height: 60px;
padding: 12px 0;
border-radius: 10px;
background-color: #fff;
border: 1px solid rgb(234, 236, 238);
position: fixed;
top: 120px;
right: 20px;
:deep(.el-input__wrapper){
background-color: #fff;
}
.searchTextNum{
width: 70px;
height: 100%;
border-right: 1px solid rgb(234, 236, 238);
line-height: 40px;
margin-right: 16px;
text-align: center;
}
.prev{
margin-right: 12px;
}
.next{
margin-right: 12px;
}
// position: absolute;
}
.high {
background: #ffeb3b;
}
.current {
background: #409eff !important;
color: #fff !important;
}
</style>
\ No newline at end of file
<template>
<div class="writting-wrapper">
<!-- 头部区域 -->
<div class="writting-header">
<!-- <div class="writting-header">
<div class="tab-box">
<div class="tab" :class="{ tabActive: item.active }" v-for="(item, index) in store.tabList"
:key="index">
......@@ -10,12 +10,6 @@
</div>
<div class="edit-box"></div>
<div class="btn-box">
<!-- <div class="btn" @click="store.exportContent">
<div class="icon">
<img src="./assets/images/export-icon.png" alt="" />
</div>
<div class="text">导出</div>
</div> -->
<div class="btn" @click="store.toggleEditMode">
<div class="icon">
<img v-if="store.isEditMode" src="./assets/images/preview-icon.png" alt="" />
......@@ -30,22 +24,38 @@
<div class="text text1">保存</div>
</div>
</div>
</div>
</div> -->
<IntelligenceLeftTabBar></IntelligenceLeftTabBar>
<!-- 主体区域:子组件 -->
<div class="writting-main">
<!-- 左侧子组件:绑定ref -->
<writtingleftBox ref="leftBoxRef" @generate="handleGenerate" />
<!-- 右侧子组件:绑定ref -->
<writtingMainBox v-show="!!store.reportContent" ref="mainBoxRef" :report-content="store.reportContent" />
<!-- 无数据时显示占位图 -->
<div v-show="!store.reportContent" class="main-placeholder">
<img src="./assets/images/container-image.png" alt="无数据占位图" />
<div class="placeholder-text">
<div v-if="store.isGenerating">智能体写报任务执行中...</div>
<div v-else>上传文件后点击“生成报文”开始写报...</div>
<div style="width: 100%;">
<WrittingHeader @onExport="onExport"></WrittingHeader>
<div class="writting-main">
<!-- 左侧子组件:绑定ref -->
<!-- <writtingleftBox ref="leftBoxRef" @generate="handleGenerate" /> -->
<WrittingLeftBox ref="leftBoxRef" />
<!-- 翻译 -->
<WrittingTranslate v-if="store.isShowClauseTranslation&&store.headerTabType=='translate'"></WrittingTranslate>
<!-- 思维导图 " -->
<WrittingMind v-else-if="store.isShowClauseTranslation&&store.headerTabType=='mind' " ref="mindRef"></WrittingMind>
<!-- 写报 -->
<WrittingMessage v-else-if="store.isShowClauseTranslation&&store.headerTabType=='message'"></WrittingMessage>
<!-- 无数据时显示占位图 -->
<div v-else class="main-placeholder">
<img src="./assets/images/container-image.png" alt="无数据占位图" />
<div class="placeholder-text">
<div v-if="store.isGenerating">智能体写报任务执行中...</div>
<div v-else>上传文件后点击“生成报文”开始写报...</div>
</div>
</div>
<!-- 右侧子组件:绑定ref -->
<!-- <writtingMainBox v-show="!!store.reportContent" ref="mainBoxRef" :report-content="store.reportContent" /> -->
</div>
<WrittingBottom @generate="handleGenerate" @write="handleWrite"></WrittingBottom>
</div>
</div>
</template>
......@@ -55,14 +65,27 @@ import { onMounted, onUnmounted, ref, nextTick } from "vue";
import { useRoute } from "vue-router";
import { ElMessage } from "element-plus";
import { useWrittingAsstaintStore } from "@/stores/writtingAsstaintStore";
import writtingleftBox from "./components/WrittingLeftBox.vue";
import writtingMainBox from "./components/WrittingMainBox.vue";
import WrittingLeftBox from "./components/WrittingLeftBox.vue";
import WrittingHeader from "./components/WrittingHeader.vue"; //头
import WrittingBottom from "./components/WrittingBottom.vue"; //底部
import WrittingTranslate from "./components/WrittingTranslate.vue"; //翻译
import WrittingMind from "./components/WrittingMind.vue"; //思维导图
import WrittingMessage from "./components/WrittingMessage.vue"; //写报
// 获取路由实例(组件内读取)
const route = useRoute();
// 获取Pinia Store实例
const leftBoxRef = ref(null); // 左侧子组件ref
const mainBoxRef = ref(null); // 右侧子组件ref
const mindRef=ref(null) //思维导图ref
const onExport = () => {
mindRef.value.exportGraph()
}
const store = useWrittingAsstaintStore();
// 2. 核心:触发生成流程
......@@ -83,6 +106,19 @@ const handleGenerate = async () => {
console.error("生成报文失败:", error);
}
};
const handleWrite=async ()=>{
try {
// // 等待DOM更新(确保子组件DOM已挂载)
store.tabList[2].active=true //写报生成之后放开写报按钮
store.headerTabType='message'
await nextTick();
await store.generateWrite()
} catch (error) {
ElMessage.error(error.message);
console.error("生成写报失败:", error);
}
}
// 生命周期
onMounted(async () => {
......@@ -100,7 +136,7 @@ onUnmounted(() => {
.writting-wrapper {
width: 100%;
height: 100%;
display: flex;
.writting-header {
height: 60px;
box-sizing: border-box;
......@@ -191,7 +227,7 @@ onUnmounted(() => {
.writting-main {
display: flex;
height: calc(100% - 60px);
height: calc(100% - 126px);
position: relative;
.main-placeholder {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论