提交 7a10f6fc authored 作者: 朱政's avatar 朱政

Merge branch 'pre' into zz-dev

流水线 #226 已通过 于阶段
in 1 分 24 秒
# 美国对华科技政令分析报告(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 | 一般规定:本命令不削弱任何部门法定权力或管理与预算局局长职能;实施须符合法律并受拨款限制;不创设任何可强制执行的权利或利益;出版费用由能源部承担。 |
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
<div class="icon"> <div class="icon">
<img :src="item.icon" alt="" /> <img :src="item.icon" alt="" />
</div> </div>
<div class="title">{{ item.title }}</div> <div class="title" :class="{ 'active-title': item.active }">{{ item.title }}</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -157,57 +157,68 @@ const menuList = ref([ ...@@ -157,57 +157,68 @@ const menuList = ref([
{ {
title: "科技法案", title: "科技法案",
icon: Menu2, icon: Menu2,
path: "/billHome" path: "/billHome",
active: false
}, },
{ {
title: "科技政令", title: "科技政令",
icon: Menu3, icon: Menu3,
path: "/decree" path: "/decree",
active: false
}, },
{ {
title: "美国科技智库", title: "美国科技智库",
icon: Menu4, icon: Menu4,
path: "/thinkTank" path: "/thinkTank",
active: false
}, },
{ {
title: "出口管制", title: "出口管制",
icon: Menu5, icon: Menu5,
path: "/exportControl" path: "/exportControl",
active: false
}, },
{ {
title: "科研合作限制", title: "科研合作限制",
icon: Menu6, icon: Menu6,
path: "/cooperationRestrictions" path: "/cooperationRestrictions",
active: false
}, },
{ {
title: "投融资限制", title: "投融资限制",
icon: Menu7, icon: Menu7,
path: "/finance" path: "/finance",
active: false
}, },
{ {
title: "市场准入限制", title: "市场准入限制",
icon: Menu8, icon: Menu8,
path: "/marketAccessRestrictions" path: "/marketAccessRestrictions",
active: false
}, },
{ {
title: "规则限制", title: "规则限制",
icon: Menu9, icon: Menu9,
path: "/ruleRestrictions" path: "/ruleRestrictions",
active: false
}, },
{ {
title: "美国科技人物观点", title: "美国科技人物观点",
icon: Menu10, icon: Menu10,
path: "/technologyFigures" path: "/technologyFigures",
active: false
}, },
{ {
title: "美国主要创新主体动向", title: "美国主要创新主体动向",
icon: Menu11, icon: Menu11,
path: "/innovationSubject" path: "/innovationSubject",
active: false
}, },
{ {
title: "美国科研资助体系", title: "美国科研资助体系",
icon: Menu12, icon: Menu12,
path: "/scientificFunding" path: "/scientificFunding",
active: false
} }
]); ]);
...@@ -235,6 +246,7 @@ const handleToModule = (item, index) => { ...@@ -235,6 +246,7 @@ const handleToModule = (item, index) => {
window.sessionStorage.setItem('homeActiveTitleIndex', index) window.sessionStorage.setItem('homeActiveTitleIndex', index)
if (index === 1) { if (index === 1) {
homeActiveTitleIndex.value = index homeActiveTitleIndex.value = index
item.active = true
router.push({ router.push({
path: item.path path: item.path
}) })
...@@ -253,7 +265,7 @@ const handleToModule = (item, index) => { ...@@ -253,7 +265,7 @@ const handleToModule = (item, index) => {
}; };
const handleClickTitle = (item, index) => { const handleClickTitle = (item, index) => {
if (index === 0 || index === 3) { if (index === 0 || index === 3) {
window.sessionStorage.setItem('homeActiveTitleIndex', index) window.sessionStorage.setItem('homeActiveTitleIndex', index)
homeActiveTitleIndex.value = index homeActiveTitleIndex.value = index
...@@ -497,6 +509,11 @@ onUnmounted(() => { ...@@ -497,6 +509,11 @@ onUnmounted(() => {
letter-spacing: 0px; letter-spacing: 0px;
text-align: left; text-align: left;
} }
.active-title {
color: var(--color-main-active) !important;
font-size: 20px !important;
}
} }
} }
} }
...@@ -528,6 +545,8 @@ onUnmounted(() => { ...@@ -528,6 +545,8 @@ onUnmounted(() => {
display: flex; display: flex;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
.title { .title {
color: var(--color-main-active); color: var(--color-main-active);
...@@ -557,6 +576,11 @@ onUnmounted(() => { ...@@ -557,6 +576,11 @@ onUnmounted(() => {
letter-spacing: 0px; letter-spacing: 0px;
text-align: left; 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() { ...@@ -295,13 +295,16 @@ export function useMarkdownStream() {
// 预处理内容 // 预处理内容
// const processedContent = preprocessMarkdown(rawContent.value) // const processedContent = preprocessMarkdown(rawContent.value)
let content = rawContent.value || '' let content = rawContent.value || ''
// 将 ==n== 转换为按钮样式的 HTML // 将 ==n== 转换为按钮样式的 HTML
// 使用正向预读和反向预读确保只匹配被 == 包裹的数字 // 使用正向预读和反向预读确保只匹配被 == 包裹的数字
content = content.replace(/==(\d+)==/g, (match, p1) => { // content = content.replace(/==(\d+)、==/g, (match, p1) => {
return `<button class="clause-ref-btn" data-clause="${p1}">${p1}</button>` // 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) return md.render(content)
}) })
......
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import md from '@/assets/icons/aiBox/2.md?raw'
export const useWrittingAsstaintStore = defineStore('writtingAsstaint', { export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
state: () => ({ state: () => ({
...@@ -51,6 +52,38 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', { ...@@ -51,6 +52,38 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
containerRef:null 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: { getters: {
...@@ -60,7 +93,8 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', { ...@@ -60,7 +93,8 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
const now = new Date() const now = new Date()
const pad = n => n.toString().padStart(2, '0') 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())}` return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
} },
}, },
actions: { actions: {
...@@ -78,6 +112,7 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', { ...@@ -78,6 +112,7 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
this.searchData.containerRef = containerRef this.searchData.containerRef = containerRef
}, },
resetGenerateState() { resetGenerateState() {
this.isGenerating = false; this.isGenerating = false;
this.isShowProcess = false; this.isShowProcess = false;
this.isShowSteps = false; this.isShowSteps = false;
...@@ -90,6 +125,21 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', { ...@@ -90,6 +125,21 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
this.isShowOriginal = true; this.isShowOriginal = true;
this.abortController?.abort(); this.abortController?.abort();
this.abortController = null; 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() { backToInputAndClear() {
...@@ -128,6 +178,49 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', { ...@@ -128,6 +178,49 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
this._keepStepsViewOnError(); this._keepStepsViewOnError();
await this._showErrorDialog(message); 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) { async setRouteParams(query) {
...@@ -279,6 +372,8 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', { ...@@ -279,6 +372,8 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
this.abortController = new AbortController(); this.abortController = new AbortController();
this.processLog = ''; this.processLog = '';
// 进度初始化1
this.bottomProgressNum=1
try { try {
const formData = new FormData(); const formData = new FormData();
formData.append('pdf', selectedFile); formData.append('pdf', selectedFile);
...@@ -300,6 +395,7 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', { ...@@ -300,6 +395,7 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
} }
}, },
onmessage: async (event) => { onmessage: async (event) => {
console.log(event)
if (!event || !event.data || event.data.trim() === '') return; if (!event || !event.data || event.data.trim() === '') return;
let jsonData = null; let jsonData = null;
...@@ -309,18 +405,23 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', { ...@@ -309,18 +405,23 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
console.warn('SSE消息JSON解析失败', parseError, event.data); console.warn('SSE消息JSON解析失败', parseError, event.data);
return; return;
} }
switch (event.event) { switch (event.event) {
case 'progress': case 'progress':
// 仅更新执行步骤 // 仅更新执行步骤
if (jsonData.message) { if (jsonData.message) {
this.processLog += `${this.formattedTime}:${jsonData.message}\r\n`; this.processLog += `${this.formattedTime}:${jsonData.message}\r\n`;
this.processLogAlone=`${this.formattedTime}:${jsonData.message}\r\n`;
} }
this.bottomProgressNum+=1
break; break;
case 'metadata': case 'metadata':
if (jsonData && jsonData.payload) { if (jsonData && jsonData.payload) {
this.pdfMetadata = jsonData.payload; this.pdfMetadata = jsonData.payload;
} }
if(this.bottomProgressNum<90){
this.bottomProgressNum+=5
}
break; break;
case 'clause_translation': case 'clause_translation':
// 保存条款翻译消息并显示侧边栏 // 保存条款翻译消息并显示侧边栏
...@@ -328,16 +429,25 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', { ...@@ -328,16 +429,25 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
this.clauseTranslationMessages.push(jsonData); this.clauseTranslationMessages.push(jsonData);
this.isShowClauseTranslation = true; this.isShowClauseTranslation = true;
this.isShowSteps = true; // 翻译出现时,步骤侧边栏也显示 this.isShowSteps = true; // 翻译出现时,步骤侧边栏也显示
// 假进度
if(this.bottomProgressNum<90){
this.bottomProgressNum+=5
}
} }
break; break;
case 'result': case 'result':
if (jsonData && Object.keys(jsonData).length) { if (jsonData && Object.keys(jsonData).length) {
await this.fetchReportData({ this.bottomProgressNum=100 // 假进度完成
query: this.writtingTitle, this.resultWriteData=jsonData //给写报使用
desc: this.descText, this.tabList[1].active=true //放开思维导图
topic: this.curTempTitle, this.processLogAlone = `点击查看`;
result: jsonData // await this.fetchReportData({
}); // query: this.writtingTitle,
// desc: this.descText,
// topic: this.curTempTitle,
// result: jsonData
// });
} }
break; break;
case 'error': case 'error':
...@@ -357,6 +467,7 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', { ...@@ -357,6 +467,7 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
this.resetGenerateState(); this.resetGenerateState();
}, },
onclose: () => { onclose: () => {
this.bottomProgressNum=100 // 假进度完成
console.log('SSE连接正常关闭'); console.log('SSE连接正常关闭');
this.isGenerating = false; this.isGenerating = false;
} }
...@@ -374,9 +485,13 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', { ...@@ -374,9 +485,13 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
// ========== AI 生成报文 SSE(更新报文内容 + 执行步骤) ========== // ========== AI 生成报文 SSE(更新报文内容 + 执行步骤) ==========
async fetchReportData(params) { async fetchReportData(params) {
console.log(">")
if (this.abortController) this.abortController.abort(); if (this.abortController) this.abortController.abort();
this.abortController = new AbortController(); this.abortController = new AbortController();
this.processLog = ''; // this.processLog = '';
// this.processLogAlone=''
this.writeProgressNum=1
// 用于把 SSE 的分片内容先聚合,再按“句子/段落边界”一次性提交到 reportContent // 用于把 SSE 的分片内容先聚合,再按“句子/段落边界”一次性提交到 reportContent
// 这样可以还原老版“一句完整再显示”的渲染效果,避免分片逐条渲染 // 这样可以还原老版“一句完整再显示”的渲染效果,避免分片逐条渲染
...@@ -425,7 +540,6 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', { ...@@ -425,7 +540,6 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
lastFlushedIndex = Math.max(lastFlushedIndex, lineIdx + 1); lastFlushedIndex = Math.max(lastFlushedIndex, lineIdx + 1);
} }
}; };
try { try {
const { fetchEventSource } = await import('@microsoft/fetch-event-source'); const { fetchEventSource } = await import('@microsoft/fetch-event-source');
await fetchEventSource('/sseWrite/api/v1/workflow/invoke', { await fetchEventSource('/sseWrite/api/v1/workflow/invoke', {
...@@ -436,6 +550,7 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', { ...@@ -436,6 +550,7 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
openWhenHidden: true, openWhenHidden: true,
onopen: (res) => { onopen: (res) => {
console.log('流式回答开始', res); console.log('流式回答开始', res);
}, },
onmessage: (res) => { onmessage: (res) => {
if (!res.data) return; if (!res.data) return;
...@@ -448,19 +563,27 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', { ...@@ -448,19 +563,27 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
} }
const str = msgData.data || ''; const str = msgData.data || '';
if (msgData.event_type === 'stream_agent_out') { if (msgData.event_type === 'stream_agent_out') {
if (str !== '[DONE]') { if (str !== '[DONE]') {
// SSE 分片先进入 buffer(仅用于报文内容) // SSE 分片先进入 buffer(仅用于报文内容)
streamBuffer += str; streamBuffer += str;
updateFlushIndexByBoundary(); updateFlushIndexByBoundary();
flushToReport(false); flushToReport(false);
console.log(streamBuffer,456)
console.log(msgData,'data')
} else { } else {
// 结束时把剩余内容强制 flush // 结束时把剩余内容强制 flush
flushToReport(true); flushToReport(true);
this.isGenerating = false; this.isGenerating = false;
this.isShowSteps = false; // 报文生成结束后关闭步骤侧边栏 this.isShowSteps = false; // 报文生成结束后关闭步骤侧边栏
ElMessage.success('报文生成结束'); ElMessage.success('报文生成结束');
this.writeProgressNum=100 //写报假进度
this.processLogAlone= '点击查看';
// 这里不再调用 resetGenerateState,因为可能需要保留翻译内容 // 这里不再调用 resetGenerateState,因为可能需要保留翻译内容
} }
} else if (msgData.event_type === 'workflow_complete') { } else if (msgData.event_type === 'workflow_complete') {
...@@ -469,6 +592,10 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', { ...@@ -469,6 +592,10 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
this.isGenerating = false; this.isGenerating = false;
this.isShowSteps = false; // 报文生成结束后关闭步骤侧边栏 this.isShowSteps = false; // 报文生成结束后关闭步骤侧边栏
ElMessage.success('报文生成结束'); ElMessage.success('报文生成结束');
this.tabList[2].active=true //写报生成之后放开写报按钮
this.writeProgressNum=100 //写报假进度
this.processLogAlone= '点击查看';
} else if ((msgData.event_type || '').toLowerCase().includes('error')) { } else if ((msgData.event_type || '').toLowerCase().includes('error')) {
// 优先从 data.error 获取详细错误描述 // 优先从 data.error 获取详细错误描述
const errorMsg = msgData.data?.error || str || '生成失败'; const errorMsg = msgData.data?.error || str || '生成失败';
...@@ -478,6 +605,10 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', { ...@@ -478,6 +605,10 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
// 这样可以避免 SSE 分片导致的“步骤破碎”(一条步骤被拆成多条显示) // 这样可以避免 SSE 分片导致的“步骤破碎”(一条步骤被拆成多条显示)
if (str) { if (str) {
this.processLog += str; this.processLog += str;
this.processLogAlone+= str;
if(this.writeProgressNum<=90){
this.writeProgressNum+=0.05
}
} }
this.curAgentTool = msgData.tool || '无'; this.curAgentTool = msgData.tool || '无';
} }
...@@ -499,15 +630,17 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', { ...@@ -499,15 +630,17 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
// ========== 业务入口 ========== // ========== 业务入口 ==========
async generateReport() { async generateReport() {
// 路由参数优先 this.bottomProgressNum=1
this.isGenerating = true;
this.isShowProcess = true;
if (Object.keys(this.routeQuery).length !== 0) { if (Object.keys(this.routeQuery).length !== 0) {
// 路由参数优先
const { fileId } = this.routeQuery; const { fileId } = this.routeQuery;
// 外部跳转:根据 topic 决定调用哪种数据获取接口,再触发生成 // 外部跳转:根据 topic 决定调用哪种数据获取接口,再触发生成
if (fileId) { if (fileId) {
this.isGenerating = true;
this.isShowProcess = true;
if (this.curTempTitle === '法案') { if (this.curTempTitle === '法案') {
await this.fetchBillReportData(fileId); await this.fetchBillReportData(fileId);
} else if (this.curTempTitle === '清单') { } else if (this.curTempTitle === '清单') {
...@@ -521,6 +654,7 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', { ...@@ -521,6 +654,7 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
} }
// 兼容:若仅有 routeQuery 但无 fileId,则直接走原生成接口 // 兼容:若仅有 routeQuery 但无 fileId,则直接走原生成接口
const params = { const params = {
query: this.writtingTitle, query: this.writtingTitle,
desc: this.descText, desc: this.descText,
...@@ -529,6 +663,7 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', { ...@@ -529,6 +663,7 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
}; };
await this.fetchReportData(params); await this.fetchReportData(params);
} else { } else {
console.log(10101010101010)
// 政令模板需要先解析PDF // 政令模板需要先解析PDF
if (this.curTempTitle === '政令') { if (this.curTempTitle === '政令') {
if (this.uploadFileList.length === 0) { if (this.uploadFileList.length === 0) {
...@@ -540,6 +675,9 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', { ...@@ -540,6 +675,9 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
} }
await this.fetchPdfData(rawFile); await this.fetchPdfData(rawFile);
} else { } else {
// 路由参数优先
this.isGenerating = true;
this.isShowProcess = true;
const params = { const params = {
query: this.writtingTitle, query: this.writtingTitle,
desc: this.descText, desc: this.descText,
...@@ -570,4 +708,4 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', { ...@@ -570,4 +708,4 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
this.resetGenerateState(); 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图表 // 绘制echarts图表
import getMonthRange from './getMonthRange' import getMonthRange from './getMonthRange'
import getQuarterRange from './getQuarterRange';
import * as echarts from 'echarts' import * as echarts from 'echarts'
import 'echarts-wordcloud'; import 'echarts-wordcloud';
import router from '@/router/index' import router from '@/router/index'
...@@ -77,6 +78,26 @@ const setChart = (option, chartId, allowClick, selectParam) => { ...@@ -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 @@ ...@@ -38,13 +38,9 @@
</div> </div>
</template> </template>
<template v-else> <template v-else>
<div <div class="left-box-bottom-item"
class="left-box-bottom-item" :class="{ leftBoxBottomItemActive: activeTitle === item.name }" v-for="item in tabs"
:class="{ leftBoxBottomItemActive: activeTitle === item.name }" :key="item.path" @click="emit('tab-click', item)">
v-for="item in tabs"
:key="item.path"
@click="emit('tab-click', item)"
>
<div class="icon"> <div class="icon">
<img v-if="activeTitle === item.name" :src="item.activeIcon" alt="" /> <img v-if="activeTitle === item.name" :src="item.activeIcon" alt="" />
<img v-else :src="item.icon" alt="" /> <img v-else :src="item.icon" alt="" />
...@@ -88,7 +84,13 @@ ...@@ -88,7 +84,13 @@
</div> </div>
</template> </template>
<template v-else> <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"> <div class="icon">
<img :src="btnIconAnalysis" alt="" /> <img :src="btnIconAnalysis" alt="" />
</div> </div>
...@@ -103,7 +105,8 @@ ...@@ -103,7 +105,8 @@
<script setup> <script setup>
import { computed } from "vue"; 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({ const props = defineProps({
billInfo: { billInfo: {
...@@ -347,6 +350,29 @@ const emit = defineEmits(["tab-click", "open-analysis"]); ...@@ -347,6 +350,29 @@ const emit = defineEmits(["tab-click", "open-analysis"]);
justify-content: flex-end; justify-content: flex-end;
gap: 8px; 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 { .btn3 {
cursor: pointer; cursor: pointer;
width: 120px; width: 120px;
...@@ -358,16 +384,6 @@ const emit = defineEmits(["tab-click", "open-analysis"]); ...@@ -358,16 +384,6 @@ const emit = defineEmits(["tab-click", "open-analysis"]);
align-items: center; align-items: center;
gap: 8px; gap: 8px;
.icon {
width: 16px;
height: 16px;
img {
width: 100%;
height: 100%;
}
}
.text { .text {
height: 24px; height: 24px;
color: rgba(255, 255, 255, 1); color: rgba(255, 255, 255, 1);
...@@ -387,4 +403,3 @@ const emit = defineEmits(["tab-click", "open-analysis"]); ...@@ -387,4 +403,3 @@ const emit = defineEmits(["tab-click", "open-analysis"]);
} }
} }
</style> </style>
...@@ -2,16 +2,9 @@ ...@@ -2,16 +2,9 @@
<div class="layout-container"> <div class="layout-container">
<!-- 导航菜单 --> <!-- 导航菜单 -->
<div class="layout-main"> <div class="layout-main">
<BillHeader <BillHeader :billInfo="billInfoGlobal" :defaultLogo="USALogo" :tabs="mainHeaderBtnList"
:billInfo="billInfoGlobal" :activeTitle="activeTitle" :showTabs="showHeaderTabs" :showActions="showHeaderActions"
:defaultLogo="USALogo" @tab-click="handleClickMainHeaderBtn" @open-analysis="handleAnalysisClick" />
:tabs="mainHeaderBtnList"
:activeTitle="activeTitle"
:showTabs="showHeaderTabs"
:showActions="showHeaderActions"
@tab-click="handleClickMainHeaderBtn"
@open-analysis="handleAnalysisClick"
/>
<div class="layout-main-center"> <div class="layout-main-center">
<router-view /> <router-view />
...@@ -115,12 +108,24 @@ const handleClickMainHeaderBtn = item => { ...@@ -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({ router.push({
path: "/writtingAsstaint", path: "/writtingAsstaint",
query: { query: {
topic: "法案", topic: "法案",
fileId: route.query.billId fileId: String(billId)
} }
}); });
}; };
...@@ -149,11 +154,13 @@ watch( ...@@ -149,11 +154,13 @@ watch(
// height: 1016px; // height: 1016px;
background: rgba(249, 250, 252, 1); background: rgba(249, 250, 252, 1);
position: relative; position: relative;
// margin: 0 auto; // margin: 0 auto;
.layout-main { .layout-main {
width: 100%; width: 100%;
height: calc(100vh - 72px); height: 100vh;
overflow-y: auto; overflow-y: auto;
.layout-main-center { .layout-main-center {
// height: calc(100% - 137px); // height: calc(100% - 137px);
width: 1600px; width: 1600px;
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
<div class="left" :style="{ width: (maxLineWidth + 250) + 'px' }"> <div class="left" :style="{ width: (maxLineWidth + 250) + 'px' }">
<div class="top"> <div class="top">
<div class="top-line" :style="{ width: lineWidth }"> <div class="top-line" :style="{ width: lineWidth }">
<div class="top-line1"></div> <div class="top-line1" ref="topLineEndRef"></div>
</div> </div>
<div class="start"> <div class="start">
<div class="icon"> <div class="icon">
...@@ -14,42 +14,52 @@ ...@@ -14,42 +14,52 @@
<div class="name">{{ "参议院" }}</div> <div class="name">{{ "参议院" }}</div>
</div> </div>
<div class="content-box" :style="senateBoxStyle"> <div class="content-box" :style="senateBoxStyle">
<div class="item-box" v-for="(item, index) in senateList" :key="item.id" <div
style="width: 280px; flex-shrink: 0;"> class="item-box"
<div class="item-box-dot"> v-for="slot in senateSlots"
<img src="./assets/images/top-line-dot.png" alt="" /> :key="slot.key"
</div> style="width: 280px; flex-shrink: 0;"
<div class="item-content"> >
<div class="item-header"> <template v-if="slot.item">
<div class="item-title" :title="item.actionTitle"> <div class="item-box-dot">
{{ item.actionTitle }} <span v-if="item.versionId">({{ item.versionId }})</span> <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>
<div class="item-header-icon" @click="handleClickDetail(true, item, $event)"> <div class="item-info" v-if="slot.item.agreeVote !== null || slot.item.disagreeVote !== null">
<img src="./assets/images/item-header-icon.png" alt="" /> {{ formatVoteText(slot.item) }}
</div> </div>
</div> <div class="item-main" v-if="slot.item.fynrList && slot.item.fynrList.length">
<div class="item-info" v-if="item.agreeVote !== null || item.disagreeVote !== null"> <div
{{ (item.agreeVote || 0) + "赞成:" + (item.disagreeVote || 0) + "反对" }} class="item-main-item"
</div> v-for="(sub, subIndex) in slot.item.fynrList"
<div class="item-main" v-if="item.fynrList && item.fynrList.length"> :key="`${slot.item.id}-${subIndex}-${sub}`"
<div class="item-main-item" v-for="(sub, subIndex) in item.fynrList" :key="subIndex"> >
<div class="icon"></div> <div class="icon"></div>
<CommonPrompt :content="sub"> <CommonPrompt :content="sub">
<div class="text">{{ sub }}</div> <div class="text">{{ sub }}</div>
</CommonPrompt> </CommonPrompt>
</div>
</div> </div>
</div> </div>
</div> <div class="item-time">
<div class="item-time"> {{ slot.item.actionDate }}
{{ item.actionDate }} </div>
</div> </template>
</div> </div>
</div> </div>
</div> </div>
<div class="bottom"> <div class="bottom">
<div class="bottom-line" :style="{ width: lineWidth }"> <div class="bottom-line" :style="{ width: lineWidth }">
<div class="bottom-line1"></div> <div class="bottom-line1" ref="bottomLineEndRef"></div>
</div> </div>
<div class="start"> <div class="start">
<div class="name">{{ "众议院" }}</div> <div class="name">{{ "众议院" }}</div>
...@@ -59,39 +69,49 @@ ...@@ -59,39 +69,49 @@
</div> </div>
</div> </div>
<div class="content-box" :style="houseBoxStyle"> <div class="content-box" :style="houseBoxStyle">
<div class="item-box" v-for="(item, index) in houseList" :key="item.id" <div
style="width: 280px; flex-shrink: 0;"> class="item-box"
<div class="item-time"> v-for="slot in houseSlots"
{{ item.actionDate }} :key="slot.key"
</div> style="width: 280px; flex-shrink: 0;"
<div class="item-box-dot"> >
<img src="./assets/images/bottom-line-dot.png" alt="" /> <template v-if="slot.item">
</div> <div class="item-time">
<div class="item-content"> {{ slot.item.actionDate }}
<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> </div>
<div class="item-info" v-if="item.agreeVote !== null || item.disagreeVote !== null"> <div class="item-box-dot">
{{ (item.agreeVote || 0) + "赞成:" + (item.disagreeVote || 0) + "反对" }} <img src="./assets/images/bottom-line-dot.png" alt="" />
</div> </div>
<div class="item-main" v-if="item.fynrList && item.fynrList.length"> <div class="item-content">
<div class="item-main-item" v-for="(sub, subIndex) in item.fynrList" :key="subIndex"> <div class="item-header">
<div class="icon"></div> <div class="item-title" :title="slot.item.actionTitle">
<CommonPrompt :content="sub"> {{ slot.item.actionTitle }} <span v-if="slot.item.versionId">({{ slot.item.versionId }})</span>
<div class="text">{{ sub }}</div> </div>
</CommonPrompt> <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> </div>
</div> </template>
</div> </div>
</div> </div>
</div> </div>
<div class="right" :style="{ left: rightPos }"> <div class="right" :style="{ left: rightPos, top: rightTop }">
<div class="junction-dot"> <div class="junction-dot">
<div class="inner-dot"></div> <div class="inner-dot"></div>
</div> </div>
...@@ -111,7 +131,7 @@ ...@@ -111,7 +131,7 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, computed } from "vue"; import { ref, onMounted, computed, nextTick } from "vue";
import { getBillDyqkSummary } from "@/api/bill"; import { getBillDyqkSummary } from "@/api/bill";
import CommonPrompt from "../../commonPrompt/index.vue"; import CommonPrompt from "../../commonPrompt/index.vue";
import ProcessOverviewDetailDialog from "../../ProcessOverviewDetailDialog.vue"; import ProcessOverviewDetailDialog from "../../ProcessOverviewDetailDialog.vue";
...@@ -138,30 +158,81 @@ const getBillDyqkSummaryList = async () => { ...@@ -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(() => { 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(() => { const timelineSlots = computed(() => {
return actionList.value return sortedTimeline.value.slice(0, mergeIndexExclusive.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(() => {
const houseList = computed(() => { return timelineSlots.value.map((step) => ({
return actionList.value key: step.id,
.filter(item => item.orgName === "众议院" && (!presidentAction.value || item.id !== presidentAction.value.id)) item: step.orgName === ORG_SENATE ? step : null
.sort((a, b) => new Date(a.actionDate) - new Date(b.actionDate)); }));
}); });
// 计算最大线条宽度数值 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 maxLineWidth = computed(() => {
const senateWidth = 254 + senateList.value.length * 280; const senateWidth = 254 + timelineCount.value * TIMELINE_ITEM_WIDTH_PX;
const houseWidth = 150 + houseList.value.length * 280; const houseWidth = 150 + timelineCount.value * TIMELINE_ITEM_WIDTH_PX;
return Math.max(1100, senateWidth, houseWidth); return Math.max(senateWidth, houseWidth);
}); });
// 绑定给线条的样式 // 绑定给线条的样式
...@@ -173,7 +244,7 @@ const lineWidth = computed(() => { ...@@ -173,7 +244,7 @@ const lineWidth = computed(() => {
const senateBoxStyle = computed(() => { const senateBoxStyle = computed(() => {
return { return {
width: (maxLineWidth.value + 110 - 254) + 'px', width: (maxLineWidth.value + 110 - 254) + 'px',
justifyContent: 'space-between' justifyContent: 'flex-start'
}; };
}); });
...@@ -181,7 +252,7 @@ const senateBoxStyle = computed(() => { ...@@ -181,7 +252,7 @@ const senateBoxStyle = computed(() => {
const houseBoxStyle = computed(() => { const houseBoxStyle = computed(() => {
return { return {
width: (maxLineWidth.value + 110 - 150) + 'px', width: (maxLineWidth.value + 110 - 150) + 'px',
justifyContent: 'space-between' justifyContent: 'flex-start'
}; };
}); });
...@@ -190,6 +261,10 @@ const rightPos = computed(() => { ...@@ -190,6 +261,10 @@ const rightPos = computed(() => {
return (maxLineWidth.value + 90) + 'px'; return (maxLineWidth.value + 90) + 'px';
}); });
const topLineEndRef = ref(null);
const bottomLineEndRef = ref(null);
const rightTop = ref('370px');
const isShowDetailDialog = ref(false); const isShowDetailDialog = ref(false);
const currentDetailItem = ref({}); const currentDetailItem = ref({});
const dialogPos = ref({ left: '0px', top: '0px' }); const dialogPos = ref({ left: '0px', top: '0px' });
...@@ -224,9 +299,43 @@ const handleClickDetail = (isShow, item = {}, event = null) => { ...@@ -224,9 +299,43 @@ const handleClickDetail = (isShow, item = {}, event = null) => {
}; };
// 挂载阶段调用 // 挂载阶段调用
onMounted(() => { onMounted(async () => {
getBillDyqkSummaryList(); 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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
......
...@@ -151,7 +151,7 @@ ...@@ -151,7 +151,7 @@
</template> </template>
<script setup> <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 ChartContainer from '../../components/ChartContainer/index.vue'
import ChartHeader from '../../components/ChartHeader/index.vue' import ChartHeader from '../../components/ChartHeader/index.vue'
import ActiveTag from '../../components/ActiveTag/index.vue' import ActiveTag from '../../components/ActiveTag/index.vue'
...@@ -171,6 +171,10 @@ import getDateRange from '@/utils/getDateRange' ...@@ -171,6 +171,10 @@ import getDateRange from '@/utils/getDateRange'
const route = useRoute(); const route = useRoute();
const timer1 = ref(null)
const timer2 = ref(null)
const timer3 = ref(null)
// 图表/数据 // 图表/数据
const isShowChart = ref(false) const isShowChart = ref(false)
// 点击切换数据/图表 // 点击切换数据/图表
...@@ -180,7 +184,7 @@ const handleSwitchChartData = () => { ...@@ -180,7 +184,7 @@ const handleSwitchChartData = () => {
const curDemensionItem = staticsDemensionList.value.filter(item => { const curDemensionItem = staticsDemensionList.value.filter(item => {
return item.name === curDemension.value return item.name === curDemension.value
})[0] })[0]
setTimeout(() => { timer1.value = setTimeout(() => {
activeChart.value = curDemensionItem.chartTypeList[0] activeChart.value = curDemensionItem.chartTypeList[0]
curChartData.value = curDemensionItem.data curChartData.value = curDemensionItem.data
}) })
...@@ -267,7 +271,7 @@ const handleClickDemensionItem = (val) => { ...@@ -267,7 +271,7 @@ const handleClickDemensionItem = (val) => {
}) })
val.active = true val.active = true
curDemension.value = val.name curDemension.value = val.name
setTimeout(() => { timer2.value = setTimeout(() => {
activeChart.value = val.chartTypeList[0] activeChart.value = val.chartTypeList[0]
curChartData.value = val.data curChartData.value = val.data
}) })
...@@ -702,8 +706,8 @@ const statusList = ref([ ...@@ -702,8 +706,8 @@ const statusList = ref([
id: '参议院通过' id: '参议院通过'
}, },
{ {
name: '双院通过', name: '分歧已解决',
id: '双院通过' id: '分歧已解决'
}, },
]) ])
...@@ -778,8 +782,8 @@ const selectedCount = computed(() => selectedMap.value.size) ...@@ -778,8 +782,8 @@ const selectedCount = computed(() => selectedMap.value.size)
// 获取表格数据(示例) // 获取表格数据(示例)
const fetchTableData = async () => { const fetchTableData = async () => {
isSelectedAll.value = false // isSelectedAll.value = false
selectedMap.value.clear() // selectedMap.value.clear()
// 调用接口获取数据... // 调用接口获取数据...
const params = { const params = {
page: currentPage.value, page: currentPage.value,
...@@ -793,8 +797,8 @@ const fetchTableData = async () => { ...@@ -793,8 +797,8 @@ const fetchTableData = async () => {
originChamber: selectedCongress.value === '全部议院' ? null : selectedCongress.value, originChamber: selectedCongress.value === '全部议院' ? null : selectedCongress.value,
originDepart: selectedOrg.value === '全部委员会' ? null : selectedOrg.value, originDepart: selectedOrg.value === '全部委员会' ? null : selectedOrg.value,
sponsorPersonName: selectedmember.value === '全部议员' ? null : selectedmember.value, sponsorPersonName: selectedmember.value === '全部议员' ? null : selectedmember.value,
status: selectedStatus.value === '通过' ? 1 : 0, status: selectedStatus.value === '全部阶段' ? null : selectedStatus.value,
isInvolveCn: isInvolveCn ? 'Y' : 'N', isInvolveCn: isInvolveCn.value ? 'Y' : 'N',
sort: isSort.value ? 0 : 1 // 0 先按分数降序 后按时间降序 1 先按分数降序,再按时间升序 sort: isSort.value ? 0 : 1 // 0 先按分数降序 后按时间降序 1 先按分数降序,再按时间升序
} }
try { try {
...@@ -836,7 +840,7 @@ const fetchTableData = async () => { ...@@ -836,7 +840,7 @@ const fetchTableData = async () => {
return item.name === curDemension.value return item.name === curDemension.value
})[0] })[0]
setTimeout(() => { timer3.value = setTimeout(() => {
activeChart.value = curDemensionItem.chartTypeList[0] activeChart.value = curDemensionItem.chartTypeList[0]
curChartData.value = curDemensionItem.data curChartData.value = curDemensionItem.data
}) })
...@@ -877,7 +881,7 @@ const fetchAllData = async () => { ...@@ -877,7 +881,7 @@ const fetchAllData = async () => {
originDepart: selectedOrg.value === '全部委员会' ? null : selectedOrg.value, originDepart: selectedOrg.value === '全部委员会' ? null : selectedOrg.value,
sponsorPersonName: selectedmember.value === '全部议员' ? null : selectedmember.value, sponsorPersonName: selectedmember.value === '全部议员' ? null : selectedmember.value,
status: selectedStatus.value === '通过' ? 1 : 0, status: selectedStatus.value === '通过' ? 1 : 0,
isInvolveCn: isInvolveCn ? 'Y' : 'N', isInvolveCn: isInvolveCn.value ? 'Y' : 'N',
sort: isSort.value ? 0 : 1 // 0 先按分数降序 后按时间降序 1 先按分数降序,再按时间升序 sort: isSort.value ? 0 : 1 // 0 先按分数降序 后按时间降序 1 先按分数降序,再按时间升序
} }
try { try {
...@@ -1116,14 +1120,21 @@ const handleExport = () => { ...@@ -1116,14 +1120,21 @@ const handleExport = () => {
onMounted(async () => { onMounted(async () => {
handleGetOrgList() handleGetOrgList()
handleGetMemberList() handleGetMemberList()
initParam() initParam()
// 初始化 // 初始化
await fetchTableData() await fetchTableData()
})
onBeforeUnmount(() => {
if (timer1.value) {
clearTimeout(timer1.value)
}
if (timer2.value) {
clearTimeout(timer2.value)
}
if (timer3.value) {
clearTimeout(timer3.value)
}
}) })
</script> </script>
...@@ -1239,10 +1250,11 @@ onMounted(async () => { ...@@ -1239,10 +1250,11 @@ onMounted(async () => {
.data-main-box { .data-main-box {
width: 1568px; width: 1568px;
height: 810px; min-height: 810px;
border-radius: 10px; border-radius: 10px;
background: var(--bg-white-100); background: var(--bg-white-100);
margin: 0 auto; margin: 0 auto;
margin-bottom: 20px;
overflow: hidden; overflow: hidden;
.data-main-box-header { .data-main-box-header {
...@@ -1274,8 +1286,7 @@ onMounted(async () => { ...@@ -1274,8 +1286,7 @@ onMounted(async () => {
.data-main-box-main { .data-main-box-main {
width: 1520px; width: 1520px;
// height: 633px; min-height: 680px;
height: 680px;
border-radius: 10px; border-radius: 10px;
border: 1px solid var(--bg-black-5); border: 1px solid var(--bg-black-5);
margin: 0 auto; margin: 0 auto;
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
<slot name="chart-box"></slot> <slot name="chart-box"></slot>
</div> </div>
<div class="tip-box"> <div class="tip-box">
<TipTab /> <TipTab text="数据来源:美国国会官网" />
</div> </div>
</div> </div>
</div> </div>
...@@ -176,8 +176,7 @@ const chartItemList = computed(() => { ...@@ -176,8 +176,7 @@ const chartItemList = computed(() => {
.tip-box { .tip-box {
height: 54px; height: 54px;
box-sizing: border-box; box-sizing: border-box;
padding-top: 10px; padding: 15px 600px;
// background: orange;
} }
} }
} }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<div class="select-wrapper" :class="{ 'select-wrapper-custom': selectValue === '自定义' }"> <div class="select-wrapper" :class="{ 'select-wrapper-custom': selectValue === '自定义' }">
<div class="select-left text-tip-1">{{ selectTitle + ':' }}</div> <div class="select-left text-tip-1">{{ selectTitle + ':' }}</div>
<div class="select-right" :class="{ 'select-right-custom': selectValue === '自定义' }"> <div class="select-right" :class="{ 'select-right-custom': selectValue === '自定义' }">
<el-select v-model="selectValue" :placeholder="placeholderName" style="width: 240px"> <el-select v-model="selectValue" :placeholder="placeholderName" filterable style="width: 240px">
<!-- <el-option label="全部领域" value="全部领域" /> --> <!-- <el-option label="全部领域" value="全部领域" /> -->
<el-option v-for="item in selectList" :key="item.id" :label="item.name" :value="item.id" /> <el-option v-for="item in selectList" :key="item.id" :label="item.name" :value="item.id" />
</el-select> </el-select>
......
...@@ -105,7 +105,7 @@ ...@@ -105,7 +105,7 @@
<div class="icon"> <div class="icon">
<img src="../assets/icons/download.svg" alt=""> <img src="../assets/icons/download.svg" alt="">
</div> </div>
<div class="text text-tip-1" @click="handleExport">{{ '导出' }}</div> <div class="text text-tip-1" @click="handleExport">{{ '导出' }}</div>
</div> </div>
<div class="header-right-item2 item2"> <div class="header-right-item2 item2">
<el-select v-model="curOperation" placeholder="批量操作" style="width: 120px"> <el-select v-model="curOperation" placeholder="批量操作" style="width: 120px">
...@@ -128,7 +128,7 @@ ...@@ -128,7 +128,7 @@
<el-table ref="tableRef" :data="tableData" row-key="id" @selection-change="handleSelectionChange" <el-table ref="tableRef" :data="tableData" row-key="id" @selection-change="handleSelectionChange"
@select="handleSelect" @select-all="handleSelectAll" style="width: 100%" :row-style="{ height: '52px' }"> @select="handleSelect" @select-all="handleSelectAll" style="width: 100%" :row-style="{ height: '52px' }">
<el-table-column type="selection" width="40" /> <el-table-column type="selection" width="40" />
<el-table-column label="法案名称" width="455"> <el-table-column label="政令名称" width="720">
<template #default="scope"> <template #default="scope">
<span class="title-item text-compact-bold" @click="handleClickToDetail(scope.row)">{{ scope.row.title <span class="title-item text-compact-bold" @click="handleClickToDetail(scope.row)">{{ scope.row.title
}}</span> }}</span>
...@@ -137,15 +137,23 @@ ...@@ -137,15 +137,23 @@
<el-table-column label="发布时间" width="120" class-name="date-column"> <el-table-column label="发布时间" width="120" class-name="date-column">
<template #default="scope">{{ scope.row.date }}</template> <template #default="scope">{{ scope.row.date }}</template>
</el-table-column> </el-table-column>
<el-table-column label="提案人" width="480"> <el-table-column label="发布机构" width="180">
<template #default="scope"> <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> }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column property="affiliation" label="所属党派" width="120" /> <el-table-column label="涉及领域" width="350" class-name="date-column">
<el-table-column property="originDepart" label="提出委员会" width="180" /> <template #default="scope">
<el-table-column property="status" label="所处阶段" width="120" /> <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> </el-table>
</div> </div>
</div> </div>
...@@ -175,6 +183,8 @@ import { search } from '@/api/comprehensiveSearch' ...@@ -175,6 +183,8 @@ import { search } from '@/api/comprehensiveSearch'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import getDateRange from '@/utils/getDateRange' import getDateRange from '@/utils/getDateRange'
import { getDepartmentList } from "@/api/decree/home";
const route = useRoute(); const route = useRoute();
// 图表/数据 // 图表/数据
...@@ -363,7 +373,7 @@ const activeTagList = computed(() => { ...@@ -363,7 +373,7 @@ const activeTagList = computed(() => {
} }
if (isInvolveTechnology.value) { if (isInvolveTechnology.value) {
const involveStr = '政令相关' const involveStr = '科技相关'
arr.push( arr.push(
{ {
tag: '科技相关', tag: '科技相关',
...@@ -593,18 +603,6 @@ const insList = ref([ ...@@ -593,18 +603,6 @@ const insList = ref([
name: '全部机构', name: '全部机构',
id: '全部机构' id: '全部机构'
}, },
{
name: '机构1',
id: '机构1'
},
{
name: '机构2',
id: '机构2'
},
{
name: '其他',
id: '其他'
},
]) ])
const selectedIns = ref('全部机构') const selectedIns = ref('全部机构')
const insPlaceHolder = ref('请选择发布机构') const insPlaceHolder = ref('请选择发布机构')
...@@ -612,6 +610,30 @@ const handleSelectIns = value => { ...@@ -612,6 +610,30 @@ const handleSelectIns = value => {
selectedIns.value = 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([ const decreeTypeList = ref([
{ {
...@@ -642,6 +664,7 @@ const isInvolveTechnology = ref(false) ...@@ -642,6 +664,7 @@ const isInvolveTechnology = ref(false)
const handleClear = () => { const handleClear = () => {
selectedArea.value = '全部领域' selectedArea.value = '全部领域'
selectedDate.value = '' selectedDate.value = ''
customTime.value = []
selectedIns.value = '全部机构' selectedIns.value = '全部机构'
selectedDecreeType.value = '全部类型' selectedDecreeType.value = '全部类型'
isInvolveCn.value = false isInvolveCn.value = false
...@@ -701,12 +724,12 @@ const fetchTableData = async () => { ...@@ -701,12 +724,12 @@ const fetchTableData = async () => {
keyword: '', keyword: '',
type: 2, // type 1= 法案 2= 政令 3 =智库 4=智库报告 5=实体清单【制裁记录】 6= 人物 7= 机构 8=新闻 9= 社媒 type: 2, // type 1= 法案 2= 政令 3 =智库 4=智库报告 5=实体清单【制裁记录】 6= 人物 7= 机构 8=新闻 9= 社媒
domains: selectedArea.value === '全部领域' ? null : [selectedArea.value], domains: selectedArea.value === '全部领域' ? null : [selectedArea.value],
proposedDateStart: customTime.value[0], proposedDateStart: customTime.value[0]?customTime.value[0]:null,
proposedDateEnd: customTime.value[1], proposedDateEnd: customTime.value[1]?customTime.value[1]:null,
affiliation: selectedIns.value === '全部机构' ? null : selectedIns.value, organizationName: selectedIns.value === '全部机构' ? null : selectedIns.value,
originChamber: selectedDecreeType.value === '全部类型' ? null : selectedDecreeType.value, decreeType: selectedDecreeType.value === '全部类型' ? null : selectedDecreeType.value,
sleStatus: isInvolveCn ? 'Y' : 'N', isInvolveCn: isInvolveCn.value ? 'Y' : 'N',
aaaa: isInvolveTechnology? 'Y' : 'N', isTechRelated: isInvolveTechnology.value ? 'Y' : 'N',
sort: isSort.value ? 0 : 1 // 0 先按分数降序 后按时间降序 1 先按分数降序,再按时间升序 sort: isSort.value ? 0 : 1 // 0 先按分数降序 后按时间降序 1 先按分数降序,再按时间升序
} }
try { try {
...@@ -731,7 +754,7 @@ const fetchTableData = async () => { ...@@ -731,7 +754,7 @@ const fetchTableData = async () => {
name: key, name: key,
value: Number(value) 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, name: key,
value: Number(value) value: Number(value)
})) }))
...@@ -773,11 +796,11 @@ const fetchAllData = async () => { ...@@ -773,11 +796,11 @@ const fetchAllData = async () => {
domains: selectedArea.value === '全部领域' ? null : [selectedArea.value], domains: selectedArea.value === '全部领域' ? null : [selectedArea.value],
proposedDateStart: customTime.value[0], proposedDateStart: customTime.value[0],
proposedDateEnd: customTime.value[1], proposedDateEnd: customTime.value[1],
affiliation: selectedIns.value === '全部机构' ? null : selectedIns.value, organizationName: selectedIns.value === '全部机构' ? null : selectedIns.value,
originChamber: selectedDecreeType.value === '全部类型' ? null : selectedDecreeType.value, decreeType: selectedDecreeType.value === '全部类型' ? null : selectedDecreeType.value,
sleStatus: isInvolveCn ? 'Y' : 'N', isInvolveCn: isInvolveCn.value ? 'Y' : 'N',
aaaa: isInvolveTechnology? 'Y' : 'N', isTechRelated: isInvolveTechnology.value ? 'Y' : 'N',
sort: isSort.value ? 0 : 1 // 0 先按分数降序 后按时间降序 1 先按分数降序,再按时间升序 sort: isSort.value ? 0 : 1
} }
try { try {
const res = await search(params) const res = await search(params)
...@@ -933,6 +956,9 @@ const initParam = () => { ...@@ -933,6 +956,9 @@ const initParam = () => {
selectedDate.value = '自定义' selectedDate.value = '自定义'
customTime.value = JSON.parse(route.query.selectedDate) customTime.value = JSON.parse(route.query.selectedDate)
} }
selectedIns.value = route.query.orgnizationName? route.query.orgnizationName : '全部机构'
isInvolveCn.value = route.query.isInvolveCn ? true : false isInvolveCn.value = route.query.isInvolveCn ? true : false
isInvolveTechnology.value = route.query.isInvolveTechnology ? true : false isInvolveTechnology.value = route.query.isInvolveTechnology ? true : false
...@@ -946,7 +972,7 @@ const initParam = () => { ...@@ -946,7 +972,7 @@ const initParam = () => {
} else { } else {
const savedQuery = JSON.parse(sessionStorage.getItem('decreeRouteQuery') || '{}'); const savedQuery = JSON.parse(sessionStorage.getItem('decreeRouteQuery') || '{}');
selectedArea.value = savedQuery.domains ? savedQuery.domains : '全部领域' 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 = '自定义' selectedDate.value = '自定义'
customTime.value = JSON.parse(savedQuery.selectedDate) customTime.value = JSON.parse(savedQuery.selectedDate)
} }
...@@ -959,16 +985,16 @@ const initParam = () => { ...@@ -959,16 +985,16 @@ const initParam = () => {
} }
// 跳转法案详情 // 跳转政令详情
const handleClickToDetail = (curBill) => { const handleClickToDetail = (curDecree) => {
console.log('curBill', curBill); console.log('curDecree', curDecree);
window.sessionStorage.setItem("billId", curBill.id); window.sessionStorage.setItem("billId", curDecree.id);
window.sessionStorage.setItem("curTabName", curBill.title); window.sessionStorage.setItem("curTabName", curDecree.title);
const route = router.resolve({ const route = router.resolve({
path: "/billLayout", path: "/decreeLayout",
query: { query: {
billId: curBill.id billId: curDecree.id
} }
}); });
window.open(route.href, "_blank"); window.open(route.href, "_blank");
...@@ -989,13 +1015,16 @@ const handlePerClick = item => { ...@@ -989,13 +1015,16 @@ const handlePerClick = item => {
// 导出 // 导出
const handleExport = () => { const handleExport = () => {
if(!selectedCount.value) {
ElMessage.warning('请选择至少一项数据!')
return
}
console.log(selectedMap.value); console.log(selectedMap.value);
const arr = Array.from(selectedMap.value); const arr = Array.from(selectedMap.value);
const jsonStr = JSON.stringify(arr, null, 2); const jsonStr = JSON.stringify(arr, null, 2);
const blob = new Blob([jsonStr], { type: 'application/json' }); const blob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const link = document.createElement('a'); const link = document.createElement('a');
link.href = url; link.href = url;
link.download = 'export.json'; link.download = 'export.json';
...@@ -1005,7 +1034,7 @@ const handleExport = () => { ...@@ -1005,7 +1034,7 @@ const handleExport = () => {
}; };
onMounted(async () => { onMounted(async () => {
handleGetDeparmentList()
initParam() initParam()
// 初始化 // 初始化
...@@ -1127,10 +1156,11 @@ onMounted(async () => { ...@@ -1127,10 +1156,11 @@ onMounted(async () => {
.data-main-box { .data-main-box {
width: 1568px; width: 1568px;
height: 810px; min-height: 810px;
border-radius: 10px; border-radius: 10px;
background: var(--bg-white-100); background: var(--bg-white-100);
margin: 0 auto; margin: 0 auto;
margin-bottom: 20px;
overflow: hidden; overflow: hidden;
.data-main-box-header { .data-main-box-header {
...@@ -1163,7 +1193,7 @@ onMounted(async () => { ...@@ -1163,7 +1193,7 @@ onMounted(async () => {
.data-main-box-main { .data-main-box-main {
width: 1520px; width: 1520px;
// height: 633px; // height: 633px;
height: 680px; min-height: 680px;
border-radius: 10px; border-radius: 10px;
border: 1px solid var(--bg-black-5); border: 1px solid var(--bg-black-5);
margin: 0 auto; margin: 0 auto;
...@@ -1257,6 +1287,14 @@ onMounted(async () => { ...@@ -1257,6 +1287,14 @@ onMounted(async () => {
.date-column { .date-column {
background-color: #ecf5ff; background-color: #ecf5ff;
.tag-box {
display: flex;
flex-wrap: wrap;
gap: 8px;
width: 340px;
}
} }
.date-column .cell { .date-column .cell {
...@@ -1284,6 +1322,8 @@ onMounted(async () => { ...@@ -1284,6 +1322,8 @@ onMounted(async () => {
} }
} }
:deep(.el-table__header-wrapper) { :deep(.el-table__header-wrapper) {
// background-color: #f5f7fa; // background-color: #f5f7fa;
height: 48px; height: 48px;
......
...@@ -72,7 +72,7 @@ ...@@ -72,7 +72,7 @@
</template> </template>
<script setup> <script setup>
import { computed, onMounted, ref } from 'vue' import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import Icon1 from './assets/icons/sider-icon1.svg' import Icon1 from './assets/icons/sider-icon1.svg'
import Icon2 from './assets/icons/sider-icon2.svg' import Icon2 from './assets/icons/sider-icon2.svg'
import Icon3 from './assets/icons/sider-icon3.svg' import Icon3 from './assets/icons/sider-icon3.svg'
...@@ -398,6 +398,8 @@ const handleClickTab = (tab) => { ...@@ -398,6 +398,8 @@ const handleClickTab = (tab) => {
}) })
} }
const timer = ref(null)
// 关闭当前标签页 // 关闭当前标签页
const handleCloseCurTab = (tab, index) => { const handleCloseCurTab = (tab, index) => {
...@@ -416,7 +418,7 @@ const handleCloseCurTab = (tab, index) => { ...@@ -416,7 +418,7 @@ const handleCloseCurTab = (tab, index) => {
}) })
if (index === openedTabList.value.length - 1) { if (index === openedTabList.value.length - 1) {
tagsViewStore.delView(tab) tagsViewStore.delView(tab)
setTimeout(() => { timer.value = setTimeout(() => {
tagsViewStore.visitedViews[tagsViewStore.visitedViews.length - 1].active = true tagsViewStore.visitedViews[tagsViewStore.visitedViews.length - 1].active = true
activeTab = tagsViewStore.visitedViews[tagsViewStore.visitedViews.length - 1] activeTab = tagsViewStore.visitedViews[tagsViewStore.visitedViews.length - 1]
router.push({ router.push({
...@@ -593,6 +595,11 @@ onMounted(() => { ...@@ -593,6 +595,11 @@ onMounted(() => {
}) })
onBeforeUnmount(() => {
if(timer.value) {
clearTimeout(timer.value)
}
})
</script> </script>
......
...@@ -814,7 +814,25 @@ const handleBox5 = async () => { ...@@ -814,7 +814,25 @@ const handleBox5 = async () => {
let chart1 = getBarChart(chart1Data.value.dataX, chart1Data.value.dataY); let chart1 = getBarChart(chart1Data.value.dataX, chart1Data.value.dataY);
chart1.yAxis.name = "数量"; chart1.yAxis.name = "数量";
chart1.yAxis.nameTextStyle = { align: 'right' } 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 () => { ...@@ -861,8 +879,19 @@ const handleGetDecreeArea = async () => {
}; };
const handleBox6 = async () => { const handleBox6 = async () => {
await handleGetDecreeArea(); 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); let chart2 = getPieChart(chart2Data.value);
setChart(chart2, "chart2"); setChart(chart2, "chart2", true, selectParam);
}; };
const handleBox6YearChange = () => { const handleBox6YearChange = () => {
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
<router-view /> <router-view />
</div> </div>
</div> </div>
<div class="right-btn" @click="handleClickToolBox"> <div class="right-btn" v-if="isShowToolBox" @click="handleClickToolBox">
<div class="item"> <div class="item">
<div class="icon"> <div class="icon">
<img src="@/assets/icons/overview/domain.png" alt="" /> <img src="@/assets/icons/overview/domain.png" alt="" />
...@@ -22,20 +22,7 @@ ...@@ -22,20 +22,7 @@
</div> </div>
</div> </div>
<div class="tool-box"> <div class="tool-box" v-if="isShowToolBox">
<!-- <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> -->
<el-tooltip content="智能写报" placement="left" :offset="10"> <el-tooltip content="智能写报" placement="left" :offset="10">
<div class="tool-item" @click="handleOpenPage('znxb')"> <div class="tool-item" @click="handleOpenPage('znxb')">
<img src="@/assets/icons/tool-item-icon1.png" alt="" /> <img src="@/assets/icons/tool-item-icon1.png" alt="" />
...@@ -96,11 +83,13 @@ import { ElMessage } from "element-plus"; ...@@ -96,11 +83,13 @@ import { ElMessage } from "element-plus";
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const isShowHeader = computed(() => { const isShowToolBox = computed(() => {
const isShow = route.meta.isShowHeader const isDataLibrary = route.fullPath.includes("dataLibrary");
return isShow? true : false const isWrittingAsstaint = route.path === "/writtingAsstaint";
return !isDataLibrary && !isWrittingAsstaint;
}) })
const isShowHeader = computed(() => !!route.meta.isShowHeader);
const isShowAiBox = ref(false); const isShowAiBox = ref(false);
...@@ -816,4 +805,7 @@ body { ...@@ -816,4 +805,7 @@ body {
cursor: not-allowed; cursor: not-allowed;
pointer-events: none; pointer-events: none;
} }
</style> </style>
<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> <template>
<div class="left-box-wrapper"> <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 }" <div class="left-box" :class="{ 'has-back-btn': store.isGenerating }"
v-if="!store.isShowClauseTranslation && !store.isShowSteps"> v-if="!store.isShowClauseTranslation && !store.isShowSteps">
<div class="left-box-input"> <div class="left-box-input">
...@@ -11,15 +11,15 @@ ...@@ -11,15 +11,15 @@
<div class="header">报文主题</div> <div class="header">报文主题</div>
<div class="title-box"> <div class="title-box">
<div class="title">主题名称</div> <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" /> class="title-input" placeholder="输入主题名称,如:大而美法案" v-model="store.writtingTitle" />
</div> </div>
<div class="description-box"> <!-- <div class="description-box">
<div class="title">主题描述</div> <div class="title">主题描述</div>
<el-input :disabled="store.isDisableTemplate" class="description-input" type="textarea" <el-input :disabled="store.isDisableTemplate" class="description-input" type="textarea"
style="width: 476px" :rows="8" placeholder="输入报文主题描述,如:从科技领域方面分析大而美法案通过后对中国可能产生的影响" style="width: 476px" :rows="8" placeholder="输入报文主题描述,如:从科技领域方面分析大而美法案通过后对中国可能产生的影响"
v-model="store.descText" /> v-model="store.descText" />
</div> </div> -->
</div> </div>
<!-- 报文模板 --> <!-- 报文模板 -->
...@@ -88,24 +88,24 @@ ...@@ -88,24 +88,24 @@
</div> </div>
<!-- 提交区域 --> <!-- 提交区域 -->
<div class="submit-area"> <!-- <div class="submit-area">
<div class="tips"> <div class="tips">
<div class="tips-icon"> <div class="tips-icon">
<img src="../assets/images/tips-icon.png" alt="" /> <img src="../assets/images/tips-icon.png" alt="" />
</div> </div>
<div class="tips-text">内容由AI生成,无法确保真实准确,仅供参考</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"> <div class="submit-icon">
<img src="../assets/images/ai.png" alt="" /> <img src="../assets/images/ai.png" alt="" />
</div> </div>
<div class="submit-text">生成报文</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"> <div class="footer-left">
{{ store.isGenerating ? "报文生成中..." : "报文已生成" }} {{ store.isGenerating ? "报文生成中..." : "报文已生成" }}
</div> </div>
...@@ -114,11 +114,11 @@ ...@@ -114,11 +114,11 @@
<div class="text">停止</div> <div class="text">停止</div>
</div> </div>
</div> </div>
</div> </div> end -->
</div> </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="left-box-input">
<div class="process-box"> <div class="process-box">
<div class="process-main-box"> <div class="process-main-box">
...@@ -137,17 +137,17 @@ ...@@ -137,17 +137,17 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div> -->
<div class="submit-area"> <!-- <div class="submit-area">
<div class="tips"> <div class="tips">
<div class="tips-icon"> <div class="tips-icon">
<img src="../assets/images/tips-icon.png" alt="" /> <img src="../assets/images/tips-icon.png" alt="" />
</div> </div>
<div class="tips-text">内容由AI生成,无法确保真实准确,仅供参考</div> <div class="tips-text">内容由AI生成,无法确保真实准确,仅供参考</div>
</div> </div> -->
<!-- 生成中状态 --> <!-- 生成中状态 -->
<div class="process-footer-box"> <!-- <div class="process-footer-box">
<div class="footer-left"> <div class="footer-left">
{{ store.isGenerating ? "报文生成中..." : "报文已生成" }} {{ store.isGenerating ? "报文生成中..." : "报文已生成" }}
</div> </div>
...@@ -155,25 +155,23 @@ ...@@ -155,25 +155,23 @@
<div class="icon"></div> <div class="icon"></div>
<div class="text">停止</div> <div class="text">停止</div>
</div> </div>
</div> </div> -->
</div> <!-- </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="left-box translation-box" :class="{ 'has-back-btn': store.isGenerating }"
v-if="store.isShowClauseTranslation">
<div class="translation-main-box"> <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 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="card-header">
<div class="chinese-name">{{ store.pdfMetadata.name }}</div> <div class="chinese-name">{{ store.pdfMetadata.name }}</div>
<div class="type-tag">{{ store.pdfMetadata.signing_date }}</div> <div class="type-tag">{{ store.pdfMetadata.signing_date }}</div>
</div> </div>
<div class="english-name">{{ store.pdfMetadata.order_title }}</div> <div class="english-name">{{ store.pdfMetadata.order_title }}</div>
</div> </div> -->
<div class="translation-header-new"> <div class="translation-header-new">
<div class="header-left">共{{ store.clauseTranslationMessages.length }}章节</div> <div class="header-left">共{{ store.clauseTranslationMessages.length }}章节</div>
<div class="header-right"> <div class="header-right">
...@@ -200,7 +198,7 @@ ...@@ -200,7 +198,7 @@
</div> </div>
</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 class="arrow" :class="{ 'is-active': store.isShowSteps }"></div>
</div> </div>
</div> </div>
...@@ -225,6 +223,10 @@ const emit = defineEmits(["generate"]); ...@@ -225,6 +223,10 @@ const emit = defineEmits(["generate"]);
const triggerGenerate = () => { const triggerGenerate = () => {
emit("generate"); emit("generate");
}; };
const isShowSteps=()=>{
store.isShowSteps = !store.isShowSteps
store.highlightClauseId=''
}
// 数字转中文序号 // 数字转中文序号
const getChineseNumber = (num) => { const getChineseNumber = (num) => {
...@@ -245,18 +247,30 @@ const { renderedProcess, updateProcess, clearContent } = useStream(); ...@@ -245,18 +247,30 @@ const { renderedProcess, updateProcess, clearContent } = useStream();
watch( watch(
() => store.highlightClauseId, () => store.highlightClauseId,
async (newId) => { async (newId) => {
if (!newId || !translationContentRef.value) return; console.log('789')
await nextTick(); await nextTick();
if (!newId || !translationContentRef.value) return;
const container = translationContentRef.value; 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; if (!item) return;
// 你已经拿到的 外层大标签
const element =itemHtml
// 你要找的文字
const targetText = result
const location=findTextInElement(element, targetText);
const containerRect = container.getBoundingClientRect(); const containerRect = container.getBoundingClientRect();
const itemRect = item.getBoundingClientRect(); const itemRect = item.getBoundingClientRect();
const delta = itemRect.top - containerRect.top; const delta = itemRect.top - containerRect.top;
const targetTop = container.scrollTop + delta; const targetTop = container.scrollTop + delta;
// 让高亮条款的“标题区域”贴到容器顶部 // 让高亮条款的“标题区域”贴到容器顶部
container.scrollTo({ container.scrollTo({
top: Math.max(0, targetTop), top: Math.max(0, targetTop),
...@@ -264,6 +278,74 @@ watch( ...@@ -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 变化,更新步骤内容并滚动 // 监听 store.processLog 变化,更新步骤内容并滚动
watch( watch(
...@@ -308,7 +390,7 @@ defineExpose({ ...@@ -308,7 +390,7 @@ defineExpose({
width: 521px; width: 521px;
height: 100%; height: 100%;
padding-top: 22px; padding-top: 22px;
padding-bottom: 29px; padding-bottom: 10px;
box-sizing: border-box; box-sizing: border-box;
border-right: 1px solid rgba(234, 236, 238, 1); border-right: 1px solid rgba(234, 236, 238, 1);
border-top: 1px solid rgba(234, 236, 238, 1); border-top: 1px solid rgba(234, 236, 238, 1);
...@@ -1026,4 +1108,4 @@ defineExpose({ ...@@ -1026,4 +1108,4 @@ defineExpose({
} }
} }
} }
</style> </style>
\ No newline at end of file
...@@ -41,7 +41,9 @@ const handleGlobalClick = (e) => { ...@@ -41,7 +41,9 @@ const handleGlobalClick = (e) => {
const clauseId = btn.getAttribute('data-clause'); const clauseId = btn.getAttribute('data-clause');
if (clauseId) { if (clauseId) {
store.highlightClauseId = clauseId; store.highlightClauseId = clauseId;
store.isShowSteps = true;
// 翻译栏一直显示,所以这里只需要确保它在视图内 // 翻译栏一直显示,所以这里只需要确保它在视图内
console.log(store.highlightClauseId )
} }
} }
}; };
...@@ -88,6 +90,7 @@ watch( ...@@ -88,6 +90,7 @@ watch(
}, },
{ immediate: true } { immediate: true }
); );
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
...@@ -103,7 +106,7 @@ watch( ...@@ -103,7 +106,7 @@ watch(
} }
.content-box { .content-box {
width: 1069px; width: 100%;
height: 100%; height: 100%;
overflow-y: auto; overflow-y: auto;
padding: 20px 80px; padding: 20px 80px;
...@@ -111,8 +114,8 @@ watch( ...@@ -111,8 +114,8 @@ watch(
font-size: 16px; font-size: 16px;
box-sizing: border-box; box-sizing: border-box;
border: 1px solid rgba(234, 236, 238, 1); // border: 1px solid rgba(234, 236, 238, 1);
border-radius: 10px; // border-radius: 10px;
background: rgba(255, 255, 255, 1); background: rgba(255, 255, 255, 1);
margin: 17px auto 0 auto; margin: 17px auto 0 auto;
...@@ -138,9 +141,10 @@ watch( ...@@ -138,9 +141,10 @@ watch(
cursor: pointer; cursor: pointer;
vertical-align: middle; vertical-align: middle;
transition: background-color 0.2s; transition: background-color 0.2s;
border-radius: 50%;
background: #818181;
&:hover { &:hover {
background-color: #044da5; background-color: #818181;
} }
&:active { &: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> <template>
<div class="writting-wrapper"> <div class="writting-wrapper">
<!-- 头部区域 --> <!-- 头部区域 -->
<div class="writting-header"> <!-- <div class="writting-header">
<div class="tab-box"> <div class="tab-box">
<div class="tab" :class="{ tabActive: item.active }" v-for="(item, index) in store.tabList" <div class="tab" :class="{ tabActive: item.active }" v-for="(item, index) in store.tabList"
:key="index"> :key="index">
...@@ -10,12 +10,6 @@ ...@@ -10,12 +10,6 @@
</div> </div>
<div class="edit-box"></div> <div class="edit-box"></div>
<div class="btn-box"> <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="btn" @click="store.toggleEditMode">
<div class="icon"> <div class="icon">
<img v-if="store.isEditMode" src="./assets/images/preview-icon.png" alt="" /> <img v-if="store.isEditMode" src="./assets/images/preview-icon.png" alt="" />
...@@ -30,22 +24,38 @@ ...@@ -30,22 +24,38 @@
<div class="text text1">保存</div> <div class="text text1">保存</div>
</div> </div>
</div> </div>
</div> </div> -->
<IntelligenceLeftTabBar></IntelligenceLeftTabBar>
<!-- 主体区域:子组件 --> <!-- 主体区域:子组件 -->
<div class="writting-main"> <div style="width: 100%;">
<!-- 左侧子组件:绑定ref --> <WrittingHeader @onExport="onExport"></WrittingHeader>
<writtingleftBox ref="leftBoxRef" @generate="handleGenerate" /> <div class="writting-main">
<!-- 右侧子组件:绑定ref --> <!-- 左侧子组件:绑定ref -->
<writtingMainBox v-show="!!store.reportContent" ref="mainBoxRef" :report-content="store.reportContent" /> <!-- <writtingleftBox ref="leftBoxRef" @generate="handleGenerate" /> -->
<!-- 无数据时显示占位图 --> <WrittingLeftBox ref="leftBoxRef" />
<div v-show="!store.reportContent" class="main-placeholder"> <!-- 翻译 -->
<img src="./assets/images/container-image.png" alt="无数据占位图" /> <WrittingTranslate v-if="store.isShowClauseTranslation&&store.headerTabType=='translate'"></WrittingTranslate>
<div class="placeholder-text"> <!-- 思维导图 " -->
<div v-if="store.isGenerating">智能体写报任务执行中...</div> <WrittingMind v-else-if="store.isShowClauseTranslation&&store.headerTabType=='mind' " ref="mindRef"></WrittingMind>
<div v-else>上传文件后点击“生成报文”开始写报...</div> <!-- 写报 -->
<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> </div>
<!-- 右侧子组件:绑定ref -->
<!-- <writtingMainBox v-show="!!store.reportContent" ref="mainBoxRef" :report-content="store.reportContent" /> -->
</div> </div>
<WrittingBottom @generate="handleGenerate" @write="handleWrite"></WrittingBottom>
</div> </div>
</div> </div>
</template> </template>
...@@ -55,14 +65,27 @@ import { onMounted, onUnmounted, ref, nextTick } from "vue"; ...@@ -55,14 +65,27 @@ import { onMounted, onUnmounted, ref, nextTick } from "vue";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { useWrittingAsstaintStore } from "@/stores/writtingAsstaintStore"; import { useWrittingAsstaintStore } from "@/stores/writtingAsstaintStore";
import writtingleftBox from "./components/WrittingLeftBox.vue"; import WrittingLeftBox from "./components/WrittingLeftBox.vue";
import writtingMainBox from "./components/WrittingMainBox.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(); const route = useRoute();
// 获取Pinia Store实例 // 获取Pinia Store实例
const leftBoxRef = ref(null); // 左侧子组件ref const leftBoxRef = ref(null); // 左侧子组件ref
const mainBoxRef = ref(null); // 右侧子组件ref const mindRef=ref(null) //思维导图ref
const onExport = () => {
mindRef.value.exportGraph()
}
const store = useWrittingAsstaintStore(); const store = useWrittingAsstaintStore();
// 2. 核心:触发生成流程 // 2. 核心:触发生成流程
...@@ -83,6 +106,19 @@ const handleGenerate = async () => { ...@@ -83,6 +106,19 @@ const handleGenerate = async () => {
console.error("生成报文失败:", error); 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 () => { onMounted(async () => {
...@@ -100,7 +136,7 @@ onUnmounted(() => { ...@@ -100,7 +136,7 @@ onUnmounted(() => {
.writting-wrapper { .writting-wrapper {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: flex;
.writting-header { .writting-header {
height: 60px; height: 60px;
box-sizing: border-box; box-sizing: border-box;
...@@ -191,7 +227,7 @@ onUnmounted(() => { ...@@ -191,7 +227,7 @@ onUnmounted(() => {
.writting-main { .writting-main {
display: flex; display: flex;
height: calc(100% - 60px); height: calc(100% - 126px);
position: relative; position: relative;
.main-placeholder { .main-placeholder {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论