提交 143ff2ac authored 作者: coderBryanFu's avatar coderBryanFu

feat:数据资源库更新

流水线 #247 已通过 于阶段
in 1 分 48 秒
......@@ -143,6 +143,7 @@ export function getChartAnalysis(data, options = {}) {
: typeof options?.onInterpretationDelta === "function"
? options.onInterpretationDelta
: null;
const externalSignal = options?.signal;
return new Promise((resolve, reject) => {
let buffer = "";
let latestInterpretation = "";
......@@ -150,16 +151,32 @@ export function getChartAnalysis(data, options = {}) {
let lastStreamedInterpretationLen = 0;
let settled = false;
const abortController = new AbortController();
const externalAbortHandler = () => {
abortController.abort();
};
if (externalSignal) {
if (externalSignal.aborted) {
abortController.abort();
} else {
externalSignal.addEventListener("abort", externalAbortHandler, { once: true });
}
}
const safeResolve = value => {
if (settled) return;
settled = true;
if (externalSignal) {
externalSignal.removeEventListener("abort", externalAbortHandler);
}
resolve(value);
};
const safeReject = err => {
if (settled) return;
settled = true;
if (externalSignal) {
externalSignal.removeEventListener("abort", externalAbortHandler);
}
reject(err);
};
......
......@@ -107,11 +107,12 @@ export function getBillPersonAnalyzeDy(params) {
* @param {id}
* @header token
*/
export function getBillContentId(params) {
export function getBillContentId(params, config = {}) {
return request({
method: 'GET',
url: `/api/billInfoBean/contentId/${params.id}`,
params,
signal: config.signal
})
}
......@@ -120,11 +121,12 @@ export function getBillContentId(params) {
* @param {billId,id,cRelated,currentPage,pageSize,domainNameList,measuresNameList,content}
* @header token
*/
export function getBillContentTk(params) {
export function getBillContentTk(params, config = {}) {
return request({
method: 'GET',
url: `/api/billInfoBean/content/tk/${params.billId}/${params.id}`,
params,
signal: config.signal
})
}
......@@ -133,11 +135,12 @@ export function getBillContentTk(params) {
* @param {billId,versionId,cRelated}
* @header token
*/
export function getBillContentXzfs(params) {
export function getBillContentXzfs(params, config = {}) {
return request({
method: 'GET',
url: `/api/billInfoBean/content/xzfs/${params.billId}/${params.versionId}`,
params,
signal: config.signal
})
}
......@@ -146,11 +149,12 @@ export function getBillContentXzfs(params) {
* @param {billId,versionId,cRelated}
* @header token
*/
export function getBillHyly(params) {
export function getBillHyly(params, config = {}) {
return request({
method: 'GET',
url: `/api/billInfoBean/content/hyly/${params.billId}/${params.versionId}`,
params,
signal: config.signal
})
}
......
......@@ -3,8 +3,14 @@ import request from "@/api/request.js";
// 涉华法案领域分布
/**
* @param {Object} params
* @param {string} params.year - 年份
* @param {string} [params.status] - 状态:提出法案/通过法案
* @param {string} params.year - 年份(2022-2026)
* @param {string} [params.status] - 法案状态:
* - 提案
* - 众议院通过
* - 参议院通过
* - 分歧已解决
* - 呈交总统
* - 完成立法
*/
export function getBillIndustry(params) {
return request({
......
# 美国对华科技政令分析报告(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 | 一般规定:本命令不削弱任何部门法定权力或管理与预算局局长职能;实施须符合法律并受拨款限制;不创设任何可强制执行的权利或利益;出版费用由能源部承担。 |
......@@ -82,6 +82,7 @@ const getGraphChart = (nodes, links, layoutType) => {
}
},
force: {
layoutAnimation: false, // 关闭初始化晃来晃去的动画
repulsion: 300,
gravity: 0,
edgeLength: 300
......
......@@ -17,7 +17,14 @@
</template>
<script setup>
import {ref} from 'vue'
import {onMounted, ref} from 'vue'
const props = defineProps({
activeTime: {
typeof: String,
default: '近一周'
}
})
const timeList = ref([
{
......@@ -34,6 +41,10 @@ const timeList = ref([
},
])
onMounted(() => {
timeList.value.forEach(item => { item.active = item.time === props.activeTime })
})
const handleTimeClick = (item, index) => {
timeList.value.forEach(time => {
time.active = false
......
<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,12 +295,15 @@ export function useMarkdownStream() {
// 预处理内容
// const processedContent = preprocessMarkdown(rawContent.value)
let content = rawContent.value || ''
// 将 ==n== 转换为按钮样式的 HTML
// 使用正向预读和反向预读确保只匹配被 == 包裹的数字
content = content.replace(/==(\d+)==/g, (match, p1) => {
return `<button class="clause-ref-btn" data-clause="${p1}">${p1}</button>`
})
// content = content.replace(/==(\d+)、==/g, (match, p1) => {
// return `<button class="clause-ref-btn" data-clause="${p1}">${p1}</button>`
// })
console.log(content,11223)
content = content.replace(/==\s*(\d+)、.*?==/g, (match, p1) => {
return `<button class="clause-ref-btn" data-clause="${match.replace(/==/g, '') }">${p1}</button>`;
});
return md.render(content)
})
......
......@@ -69,7 +69,8 @@ router.beforeEach((to, from, next) => {
if (to.meta.title) {
if (to.meta.dynamicTitle) {
console.log('to', to);
document.title = window.sessionStorage.getItem("curTabName") || to.meta.title;
const storageKey = to.meta.titleStorageKey || "curTabName";
document.title = window.sessionStorage.getItem(storageKey) || to.meta.title;
} else {
document.title = to.meta.title
......
......@@ -20,7 +20,8 @@ const cooperationRestrictionsRoutes = [
component: CooperationRestrictionsDetail,
meta: {
title: "合作限制详情",
dynamicTitle: true
dynamicTitle: true,
titleStorageKey: "cooperationRestrictionsTabName"
}
},
......
import { defineStore } from 'pinia'
import { ElMessage, ElMessageBox } from 'element-plus'
import md from '@/assets/icons/aiBox/2.md?raw'
export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
state: () => ({
......@@ -51,6 +52,38 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
containerRef:null
},
// ==================================
// 显示全部进度
isProcessLog:false,
// ====头
isSsearchFor:false,
//list
tabList:[
{
type:'translate',
name:'翻译',
active: true
},
{
type:'mind',
name:'思维导图',
active: false
},
{
type:'message',
name:'写报',
active: false
}
],
headerTabType:'translate',
// 底部
bottomProgressNum:0, //文档解析 假进度
resultWriteData:null, //文档分析结束之后 写报使用
// 写报
isWriteStart:false,// 写报进行中
processLogAlone:'', //写报步骤
writeProgressNum:0, //写报 假进度
}),
getters: {
......@@ -60,7 +93,8 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
const now = new Date()
const pad = n => n.toString().padStart(2, '0')
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
}
},
},
actions: {
......@@ -78,6 +112,7 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
this.searchData.containerRef = containerRef
},
resetGenerateState() {
this.isGenerating = false;
this.isShowProcess = false;
this.isShowSteps = false;
......@@ -90,6 +125,21 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
this.isShowOriginal = true;
this.abortController?.abort();
this.abortController = null;
this.isProcessLog=false
this.headerTabType='translate',
// 底部
this.bottomProgressNum=0, //文档解析 假进度
this.resultWriteData=null, //文档分析结束之后 写报使用
this.processLogAlone='', //写报步骤
// 写报
this.isWriteStart=false,// 写报进行中
// this.processWriteLog='', //写报步骤
this.writeProgressNum=0, //写报 假进度
this.tabList[1].active=false
this.tabList[2].active=false
this.reportContent='' //写报内容
},
backToInputAndClear() {
......@@ -128,6 +178,49 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
this._keepStepsViewOnError();
await this._showErrorDialog(message);
},
//header tab切换
async handleHeaderTab(type){
this.headerTabType=type
},
// 关闭搜索栏
handleIsSsearchFor(){
this.isSsearchFor=!this.isSsearchFor
},
// 智能写报
async generateWrite(){
this.isWriteStart=true
// const obj={}
// await this.fetchReportData(obj);
// this.isShowClauseTranslation=true
// this.headerTabType='message'
// this.reportContent=md
// this.tabList[2].active=true //写报生成之后放开写报按钮
// this.bottomProgressNum=100
// this.writeProgressNum=100 //写报假进度
// this.headerTabType='message'
await this.fetchReportData({
query: this.writtingTitle,
desc: this.descText,
topic: this.curTempTitle,
result: this.resultWriteData
});
},
// 停止写报
writeGenerateState(){
// this.bottomProgressNum=100
this.headerTabType='translate'
this.writeProgressNum=0
this.processLogAlone=''
this.tabList[2].active=false
this.abortController.abort()
},
// ========== 路由参数处理 ==========
async setRouteParams(query) {
......@@ -279,6 +372,8 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
this.abortController = new AbortController();
this.processLog = '';
// 进度初始化1
this.bottomProgressNum=1
try {
const formData = new FormData();
formData.append('pdf', selectedFile);
......@@ -300,6 +395,7 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
}
},
onmessage: async (event) => {
console.log(event)
if (!event || !event.data || event.data.trim() === '') return;
let jsonData = null;
......@@ -315,12 +411,17 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
// 仅更新执行步骤
if (jsonData.message) {
this.processLog += `${this.formattedTime}:${jsonData.message}\r\n`;
this.processLogAlone=`${this.formattedTime}:${jsonData.message}\r\n`;
}
this.bottomProgressNum+=1
break;
case 'metadata':
if (jsonData && jsonData.payload) {
this.pdfMetadata = jsonData.payload;
}
if(this.bottomProgressNum<90){
this.bottomProgressNum+=5
}
break;
case 'clause_translation':
// 保存条款翻译消息并显示侧边栏
......@@ -328,16 +429,25 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
this.clauseTranslationMessages.push(jsonData);
this.isShowClauseTranslation = true;
this.isShowSteps = true; // 翻译出现时,步骤侧边栏也显示
// 假进度
if(this.bottomProgressNum<90){
this.bottomProgressNum+=5
}
}
break;
case 'result':
if (jsonData && Object.keys(jsonData).length) {
await this.fetchReportData({
query: this.writtingTitle,
desc: this.descText,
topic: this.curTempTitle,
result: jsonData
});
this.bottomProgressNum=100 // 假进度完成
this.resultWriteData=jsonData //给写报使用
this.tabList[1].active=true //放开思维导图
this.processLogAlone = `点击查看`;
// await this.fetchReportData({
// query: this.writtingTitle,
// desc: this.descText,
// topic: this.curTempTitle,
// result: jsonData
// });
}
break;
case 'error':
......@@ -357,6 +467,7 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
this.resetGenerateState();
},
onclose: () => {
this.bottomProgressNum=100 // 假进度完成
console.log('SSE连接正常关闭');
this.isGenerating = false;
}
......@@ -374,9 +485,13 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
// ========== AI 生成报文 SSE(更新报文内容 + 执行步骤) ==========
async fetchReportData(params) {
console.log(">")
if (this.abortController) this.abortController.abort();
this.abortController = new AbortController();
this.processLog = '';
// this.processLog = '';
// this.processLogAlone=''
this.writeProgressNum=1
// 用于把 SSE 的分片内容先聚合,再按“句子/段落边界”一次性提交到 reportContent
// 这样可以还原老版“一句完整再显示”的渲染效果,避免分片逐条渲染
......@@ -425,7 +540,6 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
lastFlushedIndex = Math.max(lastFlushedIndex, lineIdx + 1);
}
};
try {
const { fetchEventSource } = await import('@microsoft/fetch-event-source');
await fetchEventSource('/sseWrite/api/v1/workflow/invoke', {
......@@ -436,6 +550,7 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
openWhenHidden: true,
onopen: (res) => {
console.log('流式回答开始', res);
},
onmessage: (res) => {
if (!res.data) return;
......@@ -449,18 +564,26 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
const str = msgData.data || '';
if (msgData.event_type === 'stream_agent_out') {
if (str !== '[DONE]') {
// SSE 分片先进入 buffer(仅用于报文内容)
streamBuffer += str;
updateFlushIndexByBoundary();
flushToReport(false);
console.log(streamBuffer,456)
console.log(msgData,'data')
} else {
// 结束时把剩余内容强制 flush
flushToReport(true);
this.isGenerating = false;
this.isShowSteps = false; // 报文生成结束后关闭步骤侧边栏
ElMessage.success('报文生成结束');
this.writeProgressNum=100 //写报假进度
this.processLogAlone= '点击查看';
// 这里不再调用 resetGenerateState,因为可能需要保留翻译内容
}
} else if (msgData.event_type === 'workflow_complete') {
......@@ -469,6 +592,10 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
this.isGenerating = false;
this.isShowSteps = false; // 报文生成结束后关闭步骤侧边栏
ElMessage.success('报文生成结束');
this.tabList[2].active=true //写报生成之后放开写报按钮
this.writeProgressNum=100 //写报假进度
this.processLogAlone= '点击查看';
} else if ((msgData.event_type || '').toLowerCase().includes('error')) {
// 优先从 data.error 获取详细错误描述
const errorMsg = msgData.data?.error || str || '生成失败';
......@@ -478,6 +605,10 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
// 这样可以避免 SSE 分片导致的“步骤破碎”(一条步骤被拆成多条显示)
if (str) {
this.processLog += str;
this.processLogAlone+= str;
if(this.writeProgressNum<=90){
this.writeProgressNum+=0.05
}
}
this.curAgentTool = msgData.tool || '无';
}
......@@ -499,15 +630,17 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
// ========== 业务入口 ==========
async generateReport() {
this.bottomProgressNum=1
if (Object.keys(this.routeQuery).length !== 0) {
// 路由参数优先
this.isGenerating = true;
this.isShowProcess = true;
if (Object.keys(this.routeQuery).length !== 0) {
const { fileId } = this.routeQuery;
// 外部跳转:根据 topic 决定调用哪种数据获取接口,再触发生成
if (fileId) {
this.isGenerating = true;
this.isShowProcess = true;
if (this.curTempTitle === '法案') {
await this.fetchBillReportData(fileId);
} else if (this.curTempTitle === '清单') {
......@@ -521,6 +654,7 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
}
// 兼容:若仅有 routeQuery 但无 fileId,则直接走原生成接口
const params = {
query: this.writtingTitle,
desc: this.descText,
......@@ -529,6 +663,7 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
};
await this.fetchReportData(params);
} else {
console.log(10101010101010)
// 政令模板需要先解析PDF
if (this.curTempTitle === '政令') {
if (this.uploadFileList.length === 0) {
......@@ -540,6 +675,9 @@ export const useWrittingAsstaintStore = defineStore('writtingAsstaint', {
}
await this.fetchPdfData(rawFile);
} else {
// 路由参数优先
this.isGenerating = true;
this.isShowProcess = true;
const params = {
query: this.writtingTitle,
desc: this.descText,
......
......@@ -144,7 +144,7 @@
</el-select>
</template>
<div class="overview-card-body box5-main">
<div class="overview-chart-wrap" :class="{ 'is-empty': !box5HasData }">
<div class="overview-chart-wrap" v-loading="chartLoading.box5" :class="{ 'is-empty': !box5HasData }">
<el-empty v-if="!box5HasData" description="暂无数据" :image-size="100" />
<div v-else id="box5Chart" class="overview-chart"></div>
</div>
......@@ -169,7 +169,7 @@
</el-select>
</template>
<div class="overview-card-body box6-main">
<div class="overview-chart-wrap" :class="{ 'is-empty': !box9HasData }">
<div class="overview-chart-wrap" v-loading="chartLoading.box6" :class="{ 'is-empty': !box9HasData }">
<el-empty v-if="!box9HasData" description="暂无数据" :image-size="100" />
<div v-else id="box9Chart" class="overview-chart"></div>
</div>
......@@ -191,7 +191,7 @@
</el-select>
</template>
<div class="overview-card-body box7-main">
<div class="overview-chart-wrap" :class="{ 'is-empty': !box7HasData }">
<div class="overview-chart-wrap" v-loading="chartLoading.box7" :class="{ 'is-empty': !box7HasData }">
<el-empty v-if="!box7HasData" description="暂无数据" :image-size="100" />
<div v-else id="box7Chart" class="overview-chart"></div>
</div>
......@@ -211,10 +211,10 @@
</el-select>
</template>
<div class="overview-card-body box8-main">
<div class="overview-chart-wrap" :class="{ 'is-empty': !box8HasData }">
<div class="overview-chart-wrap" v-loading="chartLoading.box8" :class="{ 'is-empty': !box8HasData }">
<el-empty v-if="!box8HasData" description="暂无数据" :image-size="100" />
<template v-else>
<div class="box8-desc">通过涉华法案{{ box8Summary }}</div>
<div class="box8-desc">完成立法涉华法案{{ box8Summary }}</div>
<div id="box8Chart" class="overview-chart box8-chart"></div>
</template>
</div>
......@@ -229,7 +229,7 @@
</OverviewCard>
<OverviewCard class="overview-card--single box9" title="关键条款词云" :icon="box7HeaderIcon">
<div class="overview-card-body box9-main">
<div class="overview-chart-wrap">
<div class="overview-chart-wrap" v-loading="chartLoading.box9">
<el-empty v-if="!wordCloudHasData" description="暂无数据" :image-size="100" />
<WordCloundChart v-else class="overview-chart" width="100%" height="100%" :data="wordCloudData" />
</div>
......@@ -512,6 +512,14 @@ const aiPaneLoading = ref({
box9: false
});
const chartLoading = ref({
box5: false,
box6: false,
box7: false,
box8: false,
box9: false
});
const gotoNewsDetail = useGotoNewsDetail();
const handleClickNewsDetail = news => {
const newsId = news?.newsId || news?.id;
......@@ -767,7 +775,7 @@ const box5Data = ref({
value: [145, 52, 84, 99, 71, 96, 128, 144, 140, 168, 188, 172]
},
{
name: "通过法案",
name: "完成立法",
value: [6, 3, 4, 6, 11, 5, 2, 14, 16, 27, 28, 44]
},
{
......@@ -779,13 +787,14 @@ const box5Data = ref({
value: []
},
{
name: "双院通过",
name: "解决分歧",
value: []
}
]
});
const box5HasData = ref(true);
const handleGetBillCount = async () => {
chartLoading.value = { ...chartLoading.value, box5: true };
try {
const params = {
dateDesc: box5ProposalTime.value
......@@ -806,7 +815,7 @@ const handleGetBillCount = async () => {
value: sortedData.map(item => item.proposedCount)
},
{
name: "通过法案",
name: "完成立法",
value: sortedData.map(item => item.passCount)
},
{
......@@ -818,7 +827,7 @@ const handleGetBillCount = async () => {
value: sortedData.map(item => item.senateCount)
},
{
name: "双院通过",
name: "解决分歧",
value: sortedData.map(item => item.hscount)
}
],
......@@ -831,10 +840,10 @@ const handleGetBillCount = async () => {
title: [],
data: [
{ name: "提出法案", value: [] },
{ name: "通过法案", value: [] },
{ name: "完成立法", value: [] },
{ name: "众议院通过", value: [] },
{ name: "参议院通过", value: [] },
{ name: "双院通过", value: [] }
{ name: "解决分歧", value: [] }
],
percent: []
};
......@@ -847,13 +856,15 @@ const handleGetBillCount = async () => {
title: [],
data: [
{ name: "提出法案", value: [] },
{ name: "通过法案", value: [] },
{ name: "完成立法", value: [] },
{ name: "众议院通过", value: [] },
{ name: "参议院通过", value: [] },
{ name: "双院通过", value: [] }
{ name: "解决分歧", value: [] }
],
percent: []
};
} finally {
chartLoading.value = { ...chartLoading.value, box5: false };
}
};
......@@ -895,6 +906,7 @@ const handleBox5Change = () => {
const box7HasData = ref(true);
const box7AiData = ref({ inner: [], outer: [] });
const handleBox7Data = async () => {
chartLoading.value = { ...chartLoading.value, box7: true };
try {
const res = await getBillPostOrg({ year: box7selectetedTime.value });
console.log("法案提出部门", res);
......@@ -949,6 +961,8 @@ const handleBox7Data = async () => {
console.error("获取法案提出部门数据失败", error);
box7HasData.value = false;
box7AiData.value = { inner: [], outer: [] };
} finally {
chartLoading.value = { ...chartLoading.value, box7: false };
}
};
......@@ -975,6 +989,7 @@ const handleToSocialDetail = item => {
const wordCloudData = ref([]);
const wordCloudHasData = computed(() => Array.isArray(wordCloudData.value) && wordCloudData.value.length > 0);
const handleGetKeyTK = async () => {
chartLoading.value = { ...chartLoading.value, box9: true };
try {
const res = await getBillOverviewKeyTK();
console.log("关键条款", res);
......@@ -992,6 +1007,8 @@ const handleGetKeyTK = async () => {
}
} catch (error) {
console.error("获取关键条款error", error);
} finally {
chartLoading.value = { ...chartLoading.value, box9: false };
}
};
const handleBox6 = async () => {
......@@ -1001,10 +1018,15 @@ const handleBox6 = async () => {
// 涉华领域分布
const box9ChartData = ref([]);
const box9selectetedTime = ref("2025");
const box9LegislativeStatus = ref("提出法案");
// 立法状态下拉:提出法案、众议院通过、参议院通过、解决分歧、完成立法
// v-model 存储的是接口需要的 status 值
const box9LegislativeStatus = ref("提案");
const box9LegislativeStatusList = ref([
{ label: "提出法案", value: "提出法案" },
{ label: "通过法案", value: "通过法案" }
{ label: "提出法案", value: "提案" },
{ label: "众议院通过", value: "众议院通过" },
{ label: "参议院通过", value: "参议院通过" },
{ label: "解决分歧", value: "分歧已解决" },
{ label: "完成立法", value: "完成立法" }
]);
const box9YearList = ref([
{
......@@ -1031,6 +1053,7 @@ const box9YearList = ref([
const box9HasData = ref(true);
let box9ChartInstance = null;
const getBox9Data = async () => {
chartLoading.value = { ...chartLoading.value, box6: true };
const params = {
year: box9selectetedTime.value,
status: box9LegislativeStatus.value
......@@ -1048,6 +1071,8 @@ const getBox9Data = async () => {
} catch (error) {
box9HasData.value = false;
box9ChartData.value = [];
} finally {
chartLoading.value = { ...chartLoading.value, box6: false };
}
};
const handleBox9Data = async () => {
......@@ -1064,6 +1089,10 @@ const handleBox9Data = async () => {
null,
{ showCount: false }
);
// 记录埋点时,将当前选中的立法状态映射为序号(0-4)
const selectedIndex = box9LegislativeStatusList.value.findIndex(
item => item.value === box9LegislativeStatus.value
);
const selectParam = {
moduleType: '国会法案',
key: 2,
......@@ -1091,14 +1120,13 @@ const box8StageList = ref([]);
const box8MockDataByYear = {
"2025": {
passCount: 19,
// 从上到下顺序:提出法案、众议院通过、参议院通过、解决分歧、完成立法
stages: [
{ name: "已提案", count: 24 },
{ name: "提出法案", count: 24 },
{ name: "众议院通过", count: 20 },
{ name: "众议院不通过", count: 25 },
{ name: "解决分歧", count: 23 },
{ name: "参议院通过", count: 26 },
{ name: "总统否决", count: 48 },
{ name: "立法", count: 19 }
{ name: "解决分歧", count: 23 },
{ name: "完成立法", count: 19 }
]
}
};
......@@ -1222,15 +1250,15 @@ const getBox8ChartOption = stageList => {
};
const handleBox8Data = async () => {
const stageOrder = ["提案", "众议院通过", "众议院不通过", "分歧已解决", "参议院通过", "总统否决或未签署", "完成立法"];
chartLoading.value = { ...chartLoading.value, box8: true };
// 进展分布显示顺序:提出法案(对应进度“提案”)、众议院通过、参议院通过、分歧已解决(解决分歧)、完成立法
const stageOrder = ["提案", "众议院通过", "参议院通过", "分歧已解决", "完成立法"];
const stageNameMap = {
提案: "已提案",
提案: "提出法案",
众议院通过: "众议院通过",
众议院不通过: "众议院不通过",
分歧已解决: "解决分歧",
参议院通过: "参议院通过",
"总统否决或未签署": "总统否决",
完成立法: "立法"
分歧已解决: "解决分歧",
完成立法: "完成立法"
};
try {
......@@ -1289,6 +1317,8 @@ const handleBox8Data = async () => {
box8StageList.value = [];
setChart({}, "box8Chart", true, selectParam);
}
} finally {
chartLoading.value = { ...chartLoading.value, box8: false };
}
};
......
......@@ -49,7 +49,8 @@ const getMultiLineChart = (dataX, proposedData, passData, houseData, senateData,
containLabel: true
},
legend: {
data: ['提出法案', '通过法案', '众议院通过', '参议院通过', '双院通过'],
// 图例顺序:提出法案、众议院通过、参议院通过、解决分歧、完成立法
data: ['提出法案', '众议院通过', '参议院通过', '解决分歧', '完成立法'],
show: true,
top: 10,
icon: 'circle',
......@@ -126,7 +127,8 @@ const getMultiLineChart = (dataX, proposedData, passData, houseData, senateData,
data: proposedData
},
{
name: '通过法案',
// 众议院通过
name: '众议院通过',
type: 'line',
smooth: true,
symbol: 'emptyCircle',
......@@ -137,10 +139,11 @@ const getMultiLineChart = (dataX, proposedData, passData, houseData, senateData,
itemStyle: {
color: lineColors[1]
},
data: passData
data: houseData
},
{
name: '众议院通过',
// 参议院通过
name: '参议院通过',
type: 'line',
smooth: true,
symbol: 'emptyCircle',
......@@ -151,10 +154,11 @@ const getMultiLineChart = (dataX, proposedData, passData, houseData, senateData,
itemStyle: {
color: lineColors[2]
},
data: houseData
data: senateData
},
{
name: '参议院通过',
// 解决分歧
name: '解决分歧',
type: 'line',
smooth: true,
symbol: 'emptyCircle',
......@@ -165,10 +169,11 @@ const getMultiLineChart = (dataX, proposedData, passData, houseData, senateData,
itemStyle: {
color: lineColors[3]
},
data: senateData
data: hsData
},
{
name: '双院通过',
// 完成立法
name: '完成立法',
type: 'line',
smooth: true,
symbol: 'emptyCircle',
......@@ -179,7 +184,7 @@ const getMultiLineChart = (dataX, proposedData, passData, houseData, senateData,
itemStyle: {
color: lineColors[4]
},
data: hsData
data: passData
}
]
}
......
<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>
......@@ -27,24 +27,9 @@
</div>
<div class="left-box-bottom" v-if="showTabs">
<template v-if="isLoading">
<div class="left-box-bottom-item is-skeleton" v-for="n in 4" :key="n">
<div class="icon">
<el-skeleton-item class="skeleton-tab-icon" variant="text" />
</div>
<div class="name">
<el-skeleton-item class="skeleton-tab-text" variant="text" />
</div>
</div>
</template>
<template v-else>
<div
class="left-box-bottom-item"
:class="{ leftBoxBottomItemActive: activeTitle === item.name }"
v-for="item in tabs"
:key="item.path"
@click="emit('tab-click', item)"
>
<div class="left-box-bottom-item"
:class="{ leftBoxBottomItemActive: activeTitle === item.name }" v-for="item in tabs"
:key="item.path" @click="emit('tab-click', item)">
<div class="icon">
<img v-if="activeTitle === item.name" :src="item.activeIcon" alt="" />
<img v-else :src="item.icon" alt="" />
......@@ -53,7 +38,6 @@
{{ item.name }}
</div>
</div>
</template>
</div>
</div>
......@@ -77,24 +61,18 @@
</div>
<div class="right-box-bottom" v-if="showActions">
<template v-if="isLoading">
<div class="btn3 is-skeleton">
<div class="btn2" @click="emit('open-analysis', 'forsee')">
<div class="icon">
<el-skeleton-item class="skeleton-action-icon" variant="text" />
</div>
<div class="text">
<el-skeleton-item class="skeleton-action-text" variant="text" />
<img :src="btnIconForsee" alt="" />
</div>
<div class="text">{{ "进展预测" }}</div>
</div>
</template>
<template v-else>
<div class="btn3" @click="emit('open-analysis')">
<div class="btn3" @click="emit('open-analysis', 'analysis')">
<div class="icon">
<img :src="btnIconAnalysis" alt="" />
</div>
<div class="text">{{ "分析报告" }}</div>
</div>
</template>
</div>
</div>
</div>
......@@ -103,7 +81,8 @@
<script setup>
import { computed } from "vue";
import btnIconAnalysis from "@/views/thinkTank/ReportDetail/images/btn-icon3.png";
import btnIconAnalysis from "@/views/bill/billLayout/assets/icons/writting-icon.svg";
import btnIconForsee from "@/views/bill/billLayout/assets/icons/forsee-icon.svg";
const props = defineProps({
billInfo: {
......@@ -347,6 +326,29 @@ const emit = defineEmits(["tab-click", "open-analysis"]);
justify-content: flex-end;
gap: 8px;
.icon {
width: 16px;
height: 16px;
img {
width: 100%;
height: 100%;
}
}
.btn2 {
cursor: pointer;
width: 120px;
height: 36px;
border-radius: 6px;
background: var(--bg-white-100);
border: 1px solid var(--bg-black-10);
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
}
.btn3 {
cursor: pointer;
width: 120px;
......@@ -358,16 +360,6 @@ const emit = defineEmits(["tab-click", "open-analysis"]);
align-items: center;
gap: 8px;
.icon {
width: 16px;
height: 16px;
img {
width: 100%;
height: 100%;
}
}
.text {
height: 24px;
color: rgba(255, 255, 255, 1);
......@@ -387,4 +379,3 @@ const emit = defineEmits(["tab-click", "open-analysis"]);
}
}
</style>
......@@ -2,16 +2,9 @@
<div class="layout-container">
<!-- 导航菜单 -->
<div class="layout-main">
<BillHeader
:billInfo="billInfoGlobal"
:defaultLogo="USALogo"
:tabs="mainHeaderBtnList"
:activeTitle="activeTitle"
:showTabs="showHeaderTabs"
:showActions="showHeaderActions"
@tab-click="handleClickMainHeaderBtn"
@open-analysis="handleAnalysisClick"
/>
<BillHeader :billInfo="billInfoGlobal" :defaultLogo="USALogo" :tabs="mainHeaderBtnList"
:activeTitle="activeTitle" :showTabs="showHeaderTabs" :showActions="showHeaderActions"
@tab-click="handleClickMainHeaderBtn" @open-analysis="handleAnalysisClick" />
<div class="layout-main-center">
<router-view />
......@@ -115,12 +108,24 @@ const handleClickMainHeaderBtn = item => {
});
};
const handleAnalysisClick = () => {
const handleAnalysisClick = analysisType => {
const billId = route.query.billId;
if (!billId) return;
// 进展预测 -> 法案简介页(法案进展)
if (analysisType === "forsee") {
router.push({
path: `/billLayout/ProgressForecast/${billId}`,
});
return;
}
// 分析报告 -> 写作助手
router.push({
path: "/writtingAsstaint",
query: {
topic: "法案",
fileId: route.query.billId
fileId: String(billId)
}
});
};
......@@ -149,11 +154,13 @@ watch(
// height: 1016px;
background: rgba(249, 250, 252, 1);
position: relative;
// margin: 0 auto;
.layout-main {
width: 100%;
height: calc(100vh - 72px);
height: 100vh;
overflow-y: auto;
.layout-main-center {
// height: calc(100% - 137px);
width: 1600px;
......
......@@ -5,7 +5,7 @@
<div class="left" :style="{ width: (maxLineWidth + 250) + 'px' }">
<div class="top">
<div class="top-line" :style="{ width: lineWidth }">
<div class="top-line1"></div>
<div class="top-line1" ref="topLineEndRef"></div>
</div>
<div class="start">
<div class="icon">
......@@ -14,25 +14,34 @@
<div class="name">{{ "参议院" }}</div>
</div>
<div class="content-box" :style="senateBoxStyle">
<div class="item-box" v-for="(item, index) in senateList" :key="item.id"
style="width: 280px; flex-shrink: 0;">
<div
class="item-box"
v-for="slot in senateSlots"
:key="slot.key"
style="width: 280px; flex-shrink: 0;"
>
<template v-if="slot.item">
<div class="item-box-dot">
<img src="./assets/images/top-line-dot.png" alt="" />
</div>
<div class="item-content">
<div class="item-header">
<div class="item-title" :title="item.actionTitle">
{{ item.actionTitle }} <span v-if="item.versionId">({{ item.versionId }})</span>
<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, item, $event)">
<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="item.agreeVote !== null || item.disagreeVote !== null">
{{ (item.agreeVote || 0) + "赞成:" + (item.disagreeVote || 0) + "反对" }}
<div class="item-info" v-if="slot.item.agreeVote !== null || slot.item.disagreeVote !== null">
{{ formatVoteText(slot.item) }}
</div>
<div class="item-main" v-if="item.fynrList && item.fynrList.length">
<div class="item-main-item" v-for="(sub, subIndex) in item.fynrList" :key="subIndex">
<div class="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>
......@@ -41,15 +50,16 @@
</div>
</div>
<div class="item-time">
{{ item.actionDate }}
{{ slot.item.actionDate }}
</div>
</template>
</div>
</div>
</div>
<div class="bottom">
<div class="bottom-line" :style="{ width: lineWidth }">
<div class="bottom-line1"></div>
<div class="bottom-line1" ref="bottomLineEndRef"></div>
</div>
<div class="start">
<div class="name">{{ "众议院" }}</div>
......@@ -59,28 +69,37 @@
</div>
</div>
<div class="content-box" :style="houseBoxStyle">
<div class="item-box" v-for="(item, index) in houseList" :key="item.id"
style="width: 280px; flex-shrink: 0;">
<div
class="item-box"
v-for="slot in houseSlots"
:key="slot.key"
style="width: 280px; flex-shrink: 0;"
>
<template v-if="slot.item">
<div class="item-time">
{{ item.actionDate }}
{{ slot.item.actionDate }}
</div>
<div class="item-box-dot">
<img src="./assets/images/bottom-line-dot.png" alt="" />
</div>
<div class="item-content">
<div class="item-header">
<div class="item-title" :title="item.actionTitle">
{{ item.actionTitle }} <span v-if="item.versionId">({{ item.versionId }})</span>
<div 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, item, $event)">
<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="item.agreeVote !== null || item.disagreeVote !== null">
{{ (item.agreeVote || 0) + "赞成:" + (item.disagreeVote || 0) + "反对" }}
<div class="item-info" v-if="slot.item.agreeVote !== null || slot.item.disagreeVote !== null">
{{ formatVoteText(slot.item) }}
</div>
<div class="item-main" v-if="item.fynrList && item.fynrList.length">
<div class="item-main-item" v-for="(sub, subIndex) in item.fynrList" :key="subIndex">
<div class="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>
......@@ -88,10 +107,11 @@
</div>
</div>
</div>
</template>
</div>
</div>
</div>
<div class="right" :style="{ left: rightPos }">
<div class="right" :style="{ left: rightPos, top: rightTop }">
<div class="junction-dot">
<div class="inner-dot"></div>
</div>
......@@ -111,7 +131,7 @@
</template>
<script setup>
import { ref, onMounted, computed } from "vue";
import { ref, onMounted, computed, nextTick } from "vue";
import { getBillDyqkSummary } from "@/api/bill";
import CommonPrompt from "../../commonPrompt/index.vue";
import ProcessOverviewDetailDialog from "../../ProcessOverviewDetailDialog.vue";
......@@ -138,30 +158,81 @@ const getBillDyqkSummaryList = async () => {
}
};
// 总统签署节点
const ORG_SENATE = "参议院";
const ORG_HOUSE = "众议院";
const PRESIDENT_KEYWORD = "呈递给总统";
const TIMELINE_ITEM_WIDTH_PX = 280;
const getTime = (actionDate) => {
const t = new Date(actionDate).getTime();
return Number.isFinite(t) ? t : 0;
};
const formatVoteText = (item) => {
if (!item) return "";
const agree = item.agreeVote ?? 0;
const disagree = item.disagreeVote ?? 0;
return `${agree}赞成:${disagree}反对`;
};
// 总统交汇节点(用于确定两条时间线的汇合位置)
const presidentAction = computed(() => {
return actionList.value.find(item => item.actionTitle && item.actionTitle.includes("呈递给总统")) || null;
return (
actionList.value.find(
(item) => item.actionTitle && item.actionTitle.includes(PRESIDENT_KEYWORD)
) || null
);
});
// 参议院列表
const senateList = computed(() => {
return actionList.value
.filter(item => item.orgName === "参议院" && (!presidentAction.value || item.id !== presidentAction.value.id))
.sort((a, b) => new Date(a.actionDate) - new Date(b.actionDate));
// 全局时间排序后的“时间步”
const 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);
});
});
// 众议院列表
const houseList = computed(() => {
return actionList.value
.filter(item => item.orgName === "众议院" && (!presidentAction.value || item.id !== presidentAction.value.id))
.sort((a, b) => new Date(a.actionDate) - new Date(b.actionDate));
// 交汇点(总统节点)在全局时间线里的位置: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 timelineSlots = computed(() => {
return sortedTimeline.value.slice(0, mergeIndexExclusive.value);
});
// 计算最大线条宽度数值
const senateSlots = computed(() => {
return timelineSlots.value.map((step) => ({
key: step.id,
item: step.orgName === ORG_SENATE ? step : null
}));
});
const houseSlots = computed(() => {
return timelineSlots.value.map((step) => ({
key: step.id,
item: step.orgName === ORG_HOUSE ? step : null
}));
});
const timelineCount = computed(() => timelineSlots.value.length);
// 计算最大线条宽度数值(两条线共享时间步长度)
const maxLineWidth = computed(() => {
const senateWidth = 254 + senateList.value.length * 280;
const houseWidth = 150 + houseList.value.length * 280;
return Math.max(1100, senateWidth, houseWidth);
const senateWidth = 254 + timelineCount.value * TIMELINE_ITEM_WIDTH_PX;
const houseWidth = 150 + timelineCount.value * TIMELINE_ITEM_WIDTH_PX;
return Math.max(senateWidth, houseWidth);
});
// 绑定给线条的样式
......@@ -173,7 +244,7 @@ const lineWidth = computed(() => {
const senateBoxStyle = computed(() => {
return {
width: (maxLineWidth.value + 110 - 254) + 'px',
justifyContent: 'space-between'
justifyContent: 'flex-start'
};
});
......@@ -181,7 +252,7 @@ const senateBoxStyle = computed(() => {
const houseBoxStyle = computed(() => {
return {
width: (maxLineWidth.value + 110 - 150) + 'px',
justifyContent: 'space-between'
justifyContent: 'flex-start'
};
});
......@@ -190,6 +261,10 @@ const rightPos = computed(() => {
return (maxLineWidth.value + 90) + 'px';
});
const topLineEndRef = ref(null);
const bottomLineEndRef = ref(null);
const rightTop = ref('370px');
const isShowDetailDialog = ref(false);
const currentDetailItem = ref({});
const dialogPos = ref({ left: '0px', top: '0px' });
......@@ -224,9 +299,43 @@ const handleClickDetail = (isShow, item = {}, event = null) => {
};
// 挂载阶段调用
onMounted(() => {
getBillDyqkSummaryList();
onMounted(async () => {
await getBillDyqkSummaryList();
await nextTick();
updateRightTop();
});
const updateRightTop = () => {
// 交汇点需要精确对齐上下两条斜线端点在页面中的 y 坐标
// rightTop 是相对 .process-overview-wrap 的 absolute top
const wrap = document.querySelector('.process-overview-wrap');
if (!wrap) return;
const topLineEndEl = topLineEndRef.value;
const bottomLineEndEl = bottomLineEndRef.value;
if (!topLineEndEl || !bottomLineEndEl) return;
const wrapRect = wrap.getBoundingClientRect();
const topRect = topLineEndEl.getBoundingClientRect();
const bottomRect = bottomLineEndEl.getBoundingClientRect();
// 根据 CSS 的旋转原点:
// - top-line1: rotate(45deg) 且 transform-origin: 0 0,因此左侧“尖端”约等于 rect.top
// - bottom-line1: rotate(-45deg) 且 transform-origin: 0 100%,因此左侧“尖端”约等于 rect.bottom
// 但我们对齐的是“斜线中心线”,而不是外包矩形边缘。
// 该斜线块在样式里高度为 8px,所以中心线偏移 4px
const LINE_THICKNESS_PX = 8;
const topLineCenterY = topRect.top + LINE_THICKNESS_PX / 2;
const bottomLineCenterY = bottomRect.bottom - LINE_THICKNESS_PX / 2;
const desiredCenterY = (topLineCenterY + bottomLineCenterY) / 2;
// .right 里 junction-dot 高度为 24px,flex 会让 right-line 与其垂直居中
const junctionCenterOffsetY = 12;
// 经验补偿:整体向上平移约 49px,使视觉交汇点精确落在两条斜线交点
const VISUAL_OFFSET_Y = -49;
rightTop.value = (desiredCenterY - wrapRect.top - junctionCenterOffsetY + VISUAL_OFFSET_Y) + 'px';
};
</script>
<style lang="scss" scoped>
......
......@@ -150,8 +150,8 @@
</template>
<script setup>
import { ref, onMounted, computed, watch } from "vue";
import { useRoute } from "vue-router";
import { ref, onMounted, onBeforeUnmount, computed, watch } from "vue";
import { useRoute, onBeforeRouteLeave } from "vue-router";
import * as echarts from "echarts";
import { Search } from "@element-plus/icons-vue";
import getPieChart from "./utils/piechart";
......@@ -165,6 +165,31 @@ import { extractTextEntity } from "@/api/intelligent/index";
const route = useRoute();
const pageAbortController = new AbortController();
const isRequestCanceled = error => {
return (
error?.code === "ERR_CANCELED" ||
error?.name === "CanceledError" ||
error?.name === "AbortError" ||
(typeof error?.message === "string" && /canceled|aborted/i.test(error.message))
);
};
const getPageSignal = () => pageAbortController.signal;
const stopCurrentPageRequests = () => {
pageAbortController.abort();
// 让旧请求回调全部失效,避免离开页面后回写状态
tkRequestToken.value += 1;
xzfsRequestToken.value += 1;
hylyRequestToken.value += 1;
entityRequestToken.value += 1;
termsLoading.value = false;
limitLoading.value = false;
domainLoading.value = false;
aiPaneLoading.value = { domain: false, limit: false };
};
const curBill = ref("");
const curBillId = ref(null);
......@@ -294,7 +319,7 @@ const ensureEntitiesForTerms = async terms => {
try {
const results = await Promise.all(
tasks.map(async item => {
const res = await extractTextEntity(item.text);
const res = await extractTextEntity(item.text, { signal: getPageSignal() });
const entities = normalizeEntities(res?.result ?? res?.data?.result ?? res?.data ?? res);
return { key: item.key, entities };
})
......@@ -434,6 +459,7 @@ const requestAiPaneContent = async key => {
const res = await getChartAnalysis(
{ text: JSON.stringify(payload) },
{
signal: getPageSignal(),
onChunk: chunk => {
const current = overviewAiContent.value[key];
const base = current === "智能总结生成中..." ? "" : current;
......@@ -455,6 +481,7 @@ const requestAiPaneContent = async key => {
}
aiPaneFetched.value = { ...aiPaneFetched.value, [key]: true };
} catch (error) {
if (isRequestCanceled(error)) return;
console.error("获取图表解读失败", error);
overviewAiContent.value = { ...overviewAiContent.value, [key]: "智能总结生成失败" };
} finally {
......@@ -518,7 +545,7 @@ const handleGetBillList = async () => {
id: route.query.billId
};
try {
const res = await getBillContentId(params);
const res = await getBillContentId(params, { signal: getPageSignal() });
console.log("法案id列表", res);
const rawList = Array.isArray(res?.data) ? res.data : [];
const seen = new Set();
......@@ -579,7 +606,7 @@ const handleGetBillContentTk = async cRelated => {
params.content = searchKeyword.value.trim();
}
try {
const res = await getBillContentTk(params);
const res = await getBillContentTk(params, { signal: getPageSignal() });
if (currentToken !== tkRequestToken.value) {
return;
}
......@@ -627,6 +654,9 @@ const handleGetBillContentTk = async cRelated => {
total.value = 0;
}
} catch (error) {
if (isRequestCanceled(error)) {
return;
}
if (currentToken !== tkRequestToken.value) {
return;
}
......@@ -652,7 +682,7 @@ const handleGetBillContentXzfs = async () => {
};
try {
const res = await getBillContentXzfs(params);
const res = await getBillContentXzfs(params, { signal: getPageSignal() });
if (currentToken !== xzfsRequestToken.value) {
return;
}
......@@ -679,6 +709,9 @@ const handleGetBillContentXzfs = async () => {
let chart1 = getPieChart(chart1Data.value, chart1ColorList.value);
setChart(chart1, "chart1");
} catch (error) {
if (isRequestCanceled(error)) {
return;
}
if (currentToken !== xzfsRequestToken.value) {
return;
}
......@@ -701,7 +734,7 @@ const handleGetBillHyly = async () => {
};
try {
const res = await getBillHyly(params);
const res = await getBillHyly(params, { signal: getPageSignal() });
if (currentToken !== hylyRequestToken.value) {
return;
}
......@@ -729,6 +762,9 @@ const handleGetBillHyly = async () => {
let chart2 = getPieChart(chart2Data.value, chart2ColorList.value);
setChart(chart2, "chart2");
} catch (error) {
if (isRequestCanceled(error)) {
return;
}
if (currentToken !== hylyRequestToken.value) {
return;
}
......@@ -745,6 +781,14 @@ onMounted(async () => {
await handleGetBillContentXzfs();
await handleGetBillHyly();
});
onBeforeRouteLeave(() => {
stopCurrentPageRequests();
});
onBeforeUnmount(() => {
stopCurrentPageRequests();
});
</script>
<style lang="scss" scoped>
......
......@@ -6,7 +6,7 @@
<div class="left-top">
<img src="./assets/icon01.png" alt="" />
<div class="left-top-title">合作限制动态</div>
<div class="more" @click="handleClickToDetail">查看详情 ></div>
<div class="more" @click="handleClickToDetail">{{ "查看详情 >" }}</div>
</div>
<el-carousel ref="carouselRef" height="412px" direction="horizontal" :autoplay="true" :interval="5000"
......@@ -46,7 +46,7 @@
</div>
</div>
<div class="left-center-type" v-if="item.type">{{ item.type }}</div>
<div class="left-center-type" v-if="item.limitMeans">{{ item.limitMeans }}</div>
<!-- <div class="left-center-title">{{ item.LIMITTYPE }}</div> -->
</div>
<div class="left-bottom">
......@@ -188,12 +188,12 @@ const riskSignals = ref([]);
// 点击查看详情
const handleClickToDetail = item => {
const activeItem = item && item.ID ? item : mainTrend.value;
const id = activeItem?.ID;
const id = activeItem?.ID || activeItem?.id || activeItem?.limitId;
if (!id) return;
window.sessionStorage.setItem("curTabName", activeItem?.LIMITNAME);
window.sessionStorage.setItem("cooperationRestrictionsTabName", activeItem?.LIMITNAME || "");
const curRoute = router.resolve({
path: "/cooperationRestrictions/detail",
name: "CooperationRestrictionsDetail",
query: { id: id }
});
window.open(curRoute.href, "_blank");
......@@ -201,9 +201,11 @@ const handleClickToDetail = item => {
// 点击风险信号详情
const handleToRiskDetail = (item) => {
const id = item?.cooperationId || item?.ID || item?.id || item?.limitId;
if (!id) return;
const curRoute = router.resolve({
path: "/cooperationRestrictions/detail",
query: { id: item.cooperationId },
name: "CooperationRestrictionsDetail",
query: { id },
});
window.open(curRoute.href, "_blank");
};
......@@ -318,7 +320,7 @@ onMounted(() => {
width: 967px;
height: 208px;
margin-top: 33px;
margin-left: 62px;
margin-left: 57px;
border-bottom: 1px solid rgb(234, 236, 238);
position: relative;
......@@ -326,6 +328,7 @@ onMounted(() => {
width: 148px;
height: 148px;
margin-right: 21px;
margin-left: 5px;
}
display: flex;
......
......@@ -4,7 +4,8 @@
<div class="left-title">
<img src="./assets/icon01.png" alt="" />
<div class="tit">各类型合作限制政策对比</div>
<el-select v-model="value" placeholder="Select" class="select" @change="getCoopRestrictionCompareData">
<el-select v-model="value" placeholder="Select" class="select" popper-class="coop-select-dropdown"
@change="getCoopRestrictionCompareData">
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
......@@ -34,7 +35,8 @@
<div class="right-title">
<img src="./assets/icon02.png" alt="" />
<div class="tit">各领域规则分布情况</div>
<el-select v-model="value1" placeholder="Select" class="select" @change="getCoopRestrictionDomainData">
<el-select v-model="value1" placeholder="Select" class="select" popper-class="coop-select-dropdown"
@change="getCoopRestrictionDomainData">
<el-option v-for="item in options1" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
......@@ -75,6 +77,22 @@ import AiPane from "@/components/base/Ai/AiPane/index.vue";
const COOP_LEFT_TIP_TEXT = "各类型合作限制政策对比,数据来源:美对华科技合作限制信息平台";
const COOP_RIGHT_TIP_TEXT = "各领域规则分布情况,数据来源:美对华科技合作限制信息平台";
// 临时展示 mock(不改样式):右侧“各领域规则分布情况”
// 用完把这个开关改回 false 即可恢复走接口
const USE_DOMAIN_MOCK = false;
const MOCK_COOP_RESTRICTION_DOMAIN = [
{ COOPERTYPE: "科研合作", COOPERTYPECOUNT: 1, AREA: "人工智能" },
{ COOPERTYPE: "科研合作", COOPERTYPECOUNT: 1, AREA: "生物科技" },
{ COOPERTYPE: "技术合作", COOPERTYPECOUNT: 2, AREA: "大数据" },
{ COOPERTYPE: "产学研合作", COOPERTYPECOUNT: 3, AREA: "新能源" },
{ COOPERTYPE: "项目合作", COOPERTYPECOUNT: 1, AREA: "智能制造" },
{ COOPERTYPE: "人才合作", COOPERTYPECOUNT: 2, AREA: "集成电路" },
{ COOPERTYPE: "科研合作", COOPERTYPECOUNT: 2, AREA: "大数据" },
{ COOPERTYPE: "科研合作", COOPERTYPECOUNT: 3, AREA: "新能源" },
{ COOPERTYPE: "科研合作", COOPERTYPECOUNT: 1, AREA: "智能制造" },
{ COOPERTYPE: "科研合作", COOPERTYPECOUNT: 2, AREA: "集成电路" }
];
const value = ref(10);
const value1 = ref("2025");
const options = [
......@@ -109,6 +127,10 @@ const options1 = [
const coopRestrictionDomain = ref([]);
const getCoopRestrictionDomainData = async () => {
if (USE_DOMAIN_MOCK) {
coopRestrictionDomain.value = MOCK_COOP_RESTRICTION_DOMAIN;
return;
}
try {
const res = await getCoopRestrictionDomain({
year: value1.value
......@@ -449,10 +471,18 @@ const initLeftChart = () => {
const option = {
color: colorMap.map((c) => c.line),
grid: { left: 40, right: 24, top: 46, bottom: 36 },
// 与智库概览「数量变化趋势」一致:预留图例空间,并用 containLabel 让轴文字不挤压绘图区
grid: {
top: "34%",
right: "3%",
bottom: "5%",
left: "2%",
containLabel: true
},
tooltip: { trigger: "axis", axisPointer: { type: "line" } },
legend: {
top: 8,
left: "center",
icon: "circle",
itemWidth: 12,
itemHeight: 12,
......@@ -572,6 +602,10 @@ const initRightChart = () => {
const domains = Array.from(domainsSet);
const types = Array.from(typesSet);
const legendSplitAt = Math.ceil(types.length / 2);
const legendFirstLine = types.slice(0, legendSplitAt);
const legendSecondLine = types.slice(legendSplitAt);
const indicators = domains.map((domain) => {
const domainData = rawData.filter((item) => item.AREA === domain);
const maxVal = Math.max(...domainData.map((d) => d.COOPERTYPECOUNT), 5);
......@@ -589,15 +623,21 @@ const initRightChart = () => {
name: type,
value: dataValues,
itemStyle: { color: colorMap[index % colorMap.length] },
// 不要填充多边形:让雷达图“圆里面是空的”
// areaStyle 不设置(或设为 0)可避免穿透同心圆的填充效果
// 雷达图围成区域填充:对应颜色 0.1 透明度
areaStyle: { color: colorMap[index % colorMap.length], opacity: 0.1 }
};
});
const option = {
color: colorMap,
legend: {
// 避免自动换行导致“第二行不居中”:拆成两行 legend,每行各自居中
legend: [
{
show: true,
type: "plain",
data: legendFirstLine,
top: 8,
left: "center",
icon: "circle",
itemWidth: 12,
itemHeight: 12,
......@@ -608,12 +648,31 @@ const initRightChart = () => {
fontFamily: "Microsoft YaHei",
fontWeight: 400,
lineHeight: 24
}
},
data: types
},
{
show: legendSecondLine.length > 0,
type: "plain",
data: legendSecondLine,
top: 32,
left: "center",
icon: "circle",
itemWidth: 12,
itemHeight: 12,
itemGap: 24,
textStyle: {
color: "rgb(95, 101, 108)",
fontSize: 16,
fontFamily: "Microsoft YaHei",
fontWeight: 400,
lineHeight: 24
}
}
],
radar: {
center: ["50%", "55%"],
radius: "65%",
// 对齐左侧折线图(grid top=34%)的“图例到图形”间距:下移雷达中心并略缩半径
center: ["50%", "62%"],
radius: "60%",
indicator: indicators,
axisName: {
color: "rgba(132, 136, 142, 1)",
......@@ -688,6 +747,11 @@ onBeforeUnmount(() => {
padding: 0;
}
/* 合作限制:下拉项内边距(teleport 到 body,用 :global 生效) */
:global(.coop-select-dropdown .el-select-dropdown__item) {
padding: 0 20px !important;
}
.datasub {
width: 1600px;
height: 460px;
......@@ -742,7 +806,7 @@ onBeforeUnmount(() => {
height: 412px;
box-sizing: border-box;
position: relative;
padding: 24px 24px 65px 24px;
padding: 0px 24px 64px 24px;
&.left-main--empty {
display: flex;
......@@ -770,7 +834,7 @@ onBeforeUnmount(() => {
.left-main-echarts {
width: 1015px;
height: 323px;
height: 348px;
}
.source {
......@@ -847,7 +911,7 @@ onBeforeUnmount(() => {
width: 521px;
height: 412px;
box-sizing: border-box;
padding: 24px 24px 64px 24px;
padding: 0px 24px 64px 24px;
position: relative;
&.right-main--empty {
......@@ -874,7 +938,7 @@ onBeforeUnmount(() => {
.right-main-echarts {
width: 473px;
height: 324px;
height: 348px;
}
.source {
......
......@@ -6,7 +6,8 @@
{{ item.name }}
</div>
</div>
<el-select v-model="sortModel" placeholder="发布时间" class="select" :teleported="true" placement="bottom-start"
<el-select v-model="sortModel" placeholder="发布时间" class="select" popper-class="coop-select-dropdown"
:teleported="true" placement="bottom-start"
:popper-options="sortPopperOptions" @change="handleSortChange">
<template #prefix>
<img v-if="sortModel !== true" src="@/views/thinkTank/ThinkTankDetail/thinkDynamics/images/image down.png"
......@@ -120,8 +121,8 @@ const getMainDataList = async () => {
date: item.limitDate,
domain: item.limitArea || [],
type: item.limitMeans,
// 使用默认图片
img: defaultImg
// 优先使用接口返回的机构 logo(limitOrgLogo),空则回退默认图
img: item.limitOrgLogo || defaultImg
}));
total.value = res.data.totalElements || 0;
} else {
......@@ -138,10 +139,16 @@ const getMainDataList = async () => {
const router = useRouter();
const handleClick = item => {
const id = item?.id || item?.limitId || item?.ID;
if (!id) return;
window.sessionStorage.setItem(
"cooperationRestrictionsTabName",
item?.limitName || item?.title || item?.name || ""
);
const routeData = router.resolve({
path: "/cooperationRestrictions/detail",
name: "CooperationRestrictionsDetail",
query: {
id: item.id
id
}
});
window.open(routeData.href, "_blank");
......@@ -355,6 +362,11 @@ watch(currentPage, () => {
padding: 0;
}
/* 合作限制:下拉项内边距(teleport 到 body,用 :global 生效) */
:global(.coop-select-dropdown .el-select-dropdown__item) {
padding: 0 20px !important;
}
.reslib-page {
width: 1600px;
......
......@@ -45,9 +45,9 @@
<AnalysisBox title="相关实体" :showAllBtn="true">
<div class="left-bottom-main">
<div v-for="item in coopRelatedData" :key="item.id" class="main-box" @click="handleClickOnEntity(item)">
<img :src="item.img || defaultCom" alt="" />
<img :src="item.img || defaultCom" alt="" class="img-left-item" />
<div class="name">{{ item.ENTITYNAME }}</div>
<div class="type">{{ item.type }}</div>
<div class="type">{{ item.position }}</div>
</div>
</div>
</AnalysisBox>
......@@ -80,8 +80,15 @@
<span>{{ chineseNumbers[index] }}{{ item.TITLE }} </span>
<img src="./assets/打开按钮.png" alt="">
</div>
<div class="clause-item-content">
{{ item.CONTENT }}
<!-- contentList:单条按原样式展示;多条则逐条展示并加 1.2.3. 前缀 -->
<div v-if="Array.isArray(item.contentList) && item.contentList.length > 1" class="clause-item-content-list">
<div v-for="(row, i) in item.contentList" :key="i" class="clause-item-content-row">
<span class="row-index">{{ i + 1 }}.</span>
<span class="row-text">{{ row.CONTENT }}</span>
</div>
</div>
<div v-else class="clause-item-content-row">
<span class="row-text">{{ item.contentList?.[0]?.CONTENT || "" }}</span>
</div>
</div>
</div>
......@@ -163,19 +170,34 @@ const getcoopRelatedData = async () => {
limitId: route.query.id
});
if (res && res.code === 200) {
coopRelatedData.value = res.data || {};
// 展示图片:优先后端返回的 imageUrl,其次用已有 img 字段,再兜底默认图
coopRelatedData.value = (Array.isArray(res.data) ? res.data : []).map((row) => ({
...row,
img: row?.imageUrl || row?.img || row?.IMAGEURL || row?.image || ""
}));
} else {
coopRelatedData.value = {};
coopRelatedData.value = [];
}
} catch (error) {
console.error("获取合作限制相关实体数据失败:", error);
coopRelatedData.value = {};
coopRelatedData.value = [];
}
};
// 点击跳转关联实体详情
const handleClickOnEntity = (item) => {
if (!item.ENTITYID) return;
const path = `/companyPages/${item.ENTITYID}`;
const entityType = item?.ENTITYTYPE;
// ENTITYTYPE: 'O' 机构/公司;'P' 人物
if (entityType === "P") {
const personId = item?.PERSONID || item?.ENTITYID || item?.id;
if (!personId) return;
const url = `http://localhost:3000/characterPage?type=2&personId=${encodeURIComponent(personId)}`;
window.open(url, "_blank");
return;
}
// 默认按公司/机构跳转(含 ENTITYTYPE === 'O' 或字段缺失)
const companyId = item?.ENTITYID || item?.id;
if (!companyId) return;
const path = `/companyPages/${companyId}`;
const { href } = router.resolve({ path });
window.open(href, "_blank");
};
......@@ -260,11 +282,21 @@ const filteredBackgroundList = computed(() => {
const active2 = ref("涉华条款");
const chineseNumbers = ["一", "二", "三", "四", "五", "六", "七", "八", "九", "十"];
const filteredClauseList = computed(() => {
const list = Array.isArray(limitClauseData.value) ? limitClauseData.value : [];
if (active2.value === "全部条款") {
return limitClauseData.value;
} else {
return limitClauseData.value.filter(item => item.ISCN === "Y");
}
// 展示全部条款及全部段落
return list.map((item) => ({
...item,
contentList: Array.isArray(item?.contentList) ? item.contentList : []
}));
}
// 涉华条款:仅展示 contentList 中 ISCN=Y 的段落;若过滤后为空则不展示该条款
return list
.map((item) => {
const contentList = (Array.isArray(item?.contentList) ? item.contentList : []).filter((row) => row?.ISCN === "Y");
return { ...item, contentList };
})
.filter((item) => Array.isArray(item?.contentList) && item.contentList.length > 0);
});
const dataList = ref([
......@@ -660,6 +692,12 @@ const dataList3 = ref([
img {
width: 24px;
height: 24px;
border-radius: 50%;
display: inline-block;
object-fit: cover;
object-position: center;
}
.name {
......@@ -923,7 +961,12 @@ const dataList3 = ref([
}
}
.clause-item-content {
.clause-item-content-list {
width: 1022px;
}
/* 每条段落的展示:padding / 描边 / 间距保持与旧版单条 CONTENT 一致 */
.clause-item-content-row {
width: 1022px;
padding: 12px 24px 12px 54px;
font-size: 16px;
......@@ -933,6 +976,17 @@ const dataList3 = ref([
color: rgb(59, 65, 75);
border-bottom: 1px solid rgb(234, 236, 238);
white-space: pre-line;
display: flex;
gap: 8px;
.row-index {
flex: 0 0 auto;
}
.row-text {
flex: 1;
min-width: 0;
}
}
}
}
......
......@@ -27,7 +27,7 @@
<img :src="tipsTcon" alt="">
</div>
<div class="date-text">近期美国各联邦政府机构发布涉华政令数量汇总</div>
<TimeTabPane @time-click="handleDateChange" />
<TimeTabPane @time-click="handleDateChange" activeTime="近一年" />
</div>
<div class="organization-list" ref="refOrganization" v-loading="organizationInfo.loading">
<div class="organization-item" v-for="(item, index) in organizationInfo.list" :key="index"
......@@ -77,7 +77,7 @@ const organizationInfo = reactive({
total: 0,
isSort: 1,
keyWord: "",
day: 7,
day: 365,
list: []
})
......
......@@ -34,7 +34,7 @@
<img :src="tipsTcon" alt="">
</div>
<div class="date-text">近期美国各联邦政府机构发布涉华政令数量汇总</div>
<TimeTabPane @time-click="onKeyOrganization" />
<TimeTabPane @time-click="onKeyOrganization" activeTime="近一年" />
</div>
<div class="home-main-header-item-box" v-if="keyOrganizationList.length">
<div class="organization-item" v-for="(item, index) in keyOrganizationList" :key="index" @click="handleToInstitution(item)">
......@@ -767,7 +767,7 @@ const handleGetDecreeYearOrder = async () => {
chart1Data.value.dataY = res.data.map(item => {
return item.count;
});
summarize1.value = await onChartInterpretation({type:"柱状图",name:"数量变化趋势",data:res.data})
onChartInterpretation({type:"柱状图",name:"数量变化趋势",data:res.data}, summarize1)
}
} catch (error) {
console.error("行政令发布频度error", error);
......@@ -775,15 +775,26 @@ const handleGetDecreeYearOrder = async () => {
box5Params.loading = false
};
// AI智能总结
const onChartInterpretation = async (text) => {
const onChartInterpretation = async (text, param) => {
param.value = "正在生成..."
// 👇 新增:超时 + 终止请求(只加这一段)
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000); // 10秒超时
try {
const response = await fetch('/aiAnalysis/chart_interpretation', {
method: 'POST',
headers: {
"X-API-Key": "aircasKEY19491001",
'Content-Type': 'application/json',
},
body: JSON.stringify({text}) // 把参数转为JSON字符串
body: JSON.stringify({text}),
signal: controller.signal // 👇 新增:绑定中断信号
});
clearTimeout(timeout); // 👇 新增:请求成功清除定时器
if (!response.ok) throw new Error(`HTTP 错误 ${response.status}`);
const reader = response.body.getReader();
const decoder = new TextDecoder();
......@@ -806,7 +817,10 @@ const onChartInterpretation = async (text) => {
}
}
}
return summarize
param.value = summarize
} catch (err) {
param.value = "系统异常,生成失败";
}
}
const handleBox5 = async () => {
......@@ -870,7 +884,7 @@ const handleGetDecreeArea = async () => {
value: item.count
};
});
summarize2.value = await onChartInterpretation({type:"环形图",name:"领域分布情况",data:res.data})
onChartInterpretation({type:"环形图",name:"领域分布情况",data:res.data}, summarize2)
}
} catch (error) {
console.error("政令科技领域error", error);
......@@ -1194,7 +1208,7 @@ const handleSearch = () => {
// 关键机构
const keyOrganizationList = ref([]);
const onKeyOrganization = async (event) => {
let day = 7
let day = 365
if (event?.time === '近一周') day = 7
if (event?.time === '近一月') day = 30
if (event?.time === '近一年') day = 365
......
......@@ -79,8 +79,9 @@
<div class="graph-box" v-if="contentType==1">
<ChartChain :listData="fishbone.list" :baseData="fishbone.base" />
</div>
<div class="graph-box" v-if="contentType==2 && graphInfo.nodes.length">
<GraphChart :nodes="graphInfo.nodes" :links="graphInfo.links" layoutType="force" />
<div class="graph-box" v-if="contentType==2">
<GraphChart v-if="graphInfo.nodes?.length" :nodes="graphInfo.nodes" :links="graphInfo.links" layoutType="force" />
<el-empty v-else style="padding: 60px 0" description="暂无数据" :image-size="100" />
</div>
</div>
</AnalysisBox>
......@@ -168,6 +169,7 @@ const onDecreeEntities = async (page=1) => {
const contentType = ref(1);
const headerContentType = (type) => {
contentType.value = type;
if (!entityInfo.total) return;
headerChartData(entityInfo.node)
};
const headerChartData = (row) => {
......
......@@ -84,15 +84,12 @@ const router = useRouter();
const route = useRoute();
const isShowToolBox = computed(() => {
const isShow = route.fullPath.includes('dataLibrary') ? false : true
return isShow
})
const isShowHeader = computed(() => {
const isShow = route.meta.isShowHeader
return isShow? true : false
const isDataLibrary = route.fullPath.includes("dataLibrary");
const isWrittingAsstaint = route.path === "/writtingAsstaint";
return !isDataLibrary && !isWrittingAsstaint;
})
const isShowHeader = computed(() => !!route.meta.isShowHeader);
const isShowAiBox = ref(false);
......@@ -808,4 +805,7 @@ body {
cursor: not-allowed;
pointer-events: none;
}
</style>
<template>
<div class="view-box">
<div class="icon-left">
<img src="@/assets/icons/box-footer-left-icon.png" alt="">
</div>
<div class="tips-content">{{ props.tips }}</div>
<div class="icon-right">
<img src="@/assets/icons/box-footer-right-icon.png" alt="">
</div>
</div>
</template>
<script setup lang="ts" name="AiTips">
const props = defineProps({
tips: {
type: String,
default: ''
}
});
</script>
<style scoped lang="scss">
.view-box {
width: 100%;
display: flex;
align-items: center;
padding: 7px 12px;
border: 1px solid rgba(231, 243, 255, 1);
border-radius: 4px;
background: rgba(246, 250, 255, 1);
.icon-left {
width: 20px;
height: 20px;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
.tips-content {
color: rgb(5, 95, 194);
font-size: 16px;
font-weight: 400;
line-height: 24px;
margin-left: 13px;
flex: 1;
}
.icon-right {
width: 24px;
height: 24px;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
}
</style>
\ No newline at end of file
<template>
<AnalysisBox :title="props.title" :showAllBtn="false" height="auto">
<div class="box-main">
<div v-for="(item, index) in props.listData" :key="index" class="box-item">
<div class="item-tag">行政令</div>
<div class="item-right">
<div class="item-head">
<div class="item-name one-line-ellipsis">{{ item.name }}</div>
<div class="item-time">{{ item.time }}</div>
</div>
<div class="item-text one-line-ellipsis">{{ item.text }}</div>
</div>
</div>
</div>
</AnalysisBox>
</template>
<script setup lang="ts" name="RelatedEvent">
const props = defineProps({
listData: {
type: Array as any,
default: () => ([])
},
title: {
type: String,
default: ""
}
})
</script>
<style scoped lang="scss">
.box-main {
padding: 0 16px 16px;
.box-item {
border-top: 1px solid var(--bg-black-5);
padding: 6px;
display: flex;
font-size: 16px;
font-family: Source Han Sans CN;
.item-tag {
width: 80px;
height: 28px;
line-height: 28px;
border-radius: 14px;
text-align: center;
margin-right: 16px;
margin-top: 7px;
color: var(--color-yellow-100);
background-color: var(--color-yellow-10);
}
.item-right {
width: 20px;
flex: auto;
line-height: 30px;
.item-head {
display: flex;
.item-name {
width: 20px;
flex: auto;
font-weight: bold;
color: var(--text-primary-80-color);
}
.item-time {
margin-left: 100px;
flex: none;
color: var(--text-primary-65-color);
}
}
.item-text {
color: var(--text-primary-65-color);
}
}
}
.box-item:last-child {
border-bottom: 1px solid var(--bg-black-5);
}
}
</style>
\ No newline at end of file
<template>
<AnalysisBox :title="title" :showAllBtn="false" height="auto">
<el-empty v-if="!props.listData?.length" description="暂无数据" :image-size="200" />
<div v-else class="box-main">
<div class="data-list">
<div class="data-item" v-for="(item, index) in props.listData" :key="index">
<div class="item-head">
<div class="item-name">{{ item.title }}</div>
<div class="button-box">
<div class="button-icon">
<img src="../assets/icons/open.png" alt="" />
</div>
<div class="button-text">跳转原文</div>
</div>
</div>
<div class="item-down">
<div class="item-text" v-for="(text, num) in item.data" :key="num">{{ text }}</div>
</div>
</div>
</div>
<AiTips :tips="tips"></AiTips>
</div>
</AnalysisBox>
</template>
<script setup lang="ts" name="SurveyConclusion">
import AiTips from "@/views/marketAccessRestrictions/com/AiTips.vue";
const props = defineProps({
listData: {
type: Array as any,
default: () => ([])
},
title: {
type: String,
default: ""
},
tips: {
type: String,
default: ""
}
})
</script>
<style scoped lang="scss">
.box-main {
padding: 0 22px 20px;
.data-list {
margin-bottom: 16px;
border-top: 1px solid rgba(234, 236, 238, 1);
.data-item {
.item-head {
padding: 0 20px;
height: 48px;
border-bottom: 1px solid rgba(234, 236, 238, 1);
background: rgba(247, 248, 249, 1);
display: flex;
align-items: center;
.item-name {
width: 20px;
flex: auto;
font-family: Source Han Sans CN;
font-size: 18px;
font-weight: bold;
line-height: 30px;
color: var(--text-primary-80-color);
}
.button-box {
display: flex;
align-items: center;
margin-left: 50px;
.button-icon {
width: 16px;
height: 16px;
font-size: 0;
margin-right: 4px;
img {
width: 100%;
height: 100%;
}
}
.button-text {
color: var(--color-primary-100);
font-family: Microsoft YaHei;
font-size: 12px;
font-weight: 400;
line-height: 12px;
}
}
}
.item-text {
letter-spacing: 1px;
padding: 12px 20px 12px 40px;
color: rgba(59, 65, 75, 1);
font-family: Source Han Sans CN;
font-size: 16px;
font-weight: 400;
line-height: 30px;
letter-spacing: 0px;
text-align: justify;
border-bottom: 1px solid rgba(234, 236, 238, 1);
}
}
}
}
</style>
\ No newline at end of file
<template>
<div class="view-box">
<el-empty v-if="!props.surveyList?.length" description="当前条件下暂无数据" :image-size="200" />
<div class="timeline-item" v-for="(item, index) in props.surveyList" :key="item.searchid" @click="onNavigateToDetail(item)">
<div class="timeline-date">
<div class="date-text">{{ item.searchdatezh.slice(0,4) }}</div>
<div class="date-text">{{ item.searchdatezh.slice(5) }}</div>
</div>
<div class="timeline-line-box">
<div class="timeline-icon">
<img v-if="item.sortimageurl" :src="item.sortimageurl" alt="" />
<div v-else class="default-dot"></div>
</div>
<div class="timeline-line" v-if="index !== props.surveyList.length - 1"></div>
</div>
<div class="timeline-content-card">
<div class="item-head">
<div :class="`item-tag tag-${item.sortcode}`">{{ item.sortcode }}</div>
<div class="item-name">{{ item.searchname }}</div>
<div class="item-state">
<span class="dot"></span> {{ item.casestatus }}
</div>
</div>
<div class="card-body">
{{ item.content }}
</div>
<div class="card-footer">
<div class="footer-left-tags">
<AreaTag v-for="(name, num) in item.searchArea" :key="num" :tagName="name"></AreaTag>
</div>
<div class="footer-right-flags">
<div class="flag-icon" v-for="(name, num) in item.countryImage" :key="num">
<img :src="name" alt="" />
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup name="SurveyHistory">
import router from "@/router";
const props = defineProps({
surveyList: {
type: Array,
default: () => ([]),
},
})
const onNavigateToDetail = item => {
window.sessionStorage.setItem("curTabName", item.searchname);
const curRoute = router.resolve({
path: "/marketSingleCaseLayout/overview",
query: { id: item.sortcode, searchId: item.searchid }
});
window.open(curRoute.href, "_blank");
};
</script>
<style scoped lang="scss">
.view-box {
min-height: 600px;
width: 100%;
padding: 18px 27px 0 24px;
border-top: 1px solid rgba(234, 236, 238, 1);
border-bottom: 1px solid rgba(234, 236, 238, 1);
}
.timeline-item {
display: flex;
cursor: pointer;
.timeline-date {
width: 80px;
text-align: right;
margin-right: 16px;
.date-text {
height: 24px;
font-size: 16px;
font-weight: 700;
color: var(--color-main-active);
line-height: 24px;
letter-spacing: 1px;
}
}
.timeline-line-box {
width: 40px;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
.timeline-icon {
width: 32px;
height: 32px;
z-index: 2;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 0;
img {
width: 100%;
height: 100%;
}
.default-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: orange;
}
}
.timeline-line {
width: 2px;
flex: 1;
background: #eaeeef;
margin: 4px 0;
}
}
.timeline-content-card {
width: 20px;
flex: auto;
padding: 2px 16px 0;
margin-bottom: 30px;
&:hover .item-head .item-name {
color: var(--color-main-active);
text-decoration: underline;
}
.item-head {
display: flex;
align-items: center;
margin-bottom: 10px;
.item-tag {
width: 48px;
height: 24px;
line-height: 24px;
text-align: center;
border-radius: 4px;
font-weight: bold;
font-size: 16px;
margin-right: 12px;
}
.tag-337 {
border: 1px solid #91caff;
background: #e6f4ff;
color: #055fc2;
}
.tag-232 {
border: 1px solid #b37feb;
background: #f9f0ff;
color: #722ed1;
}
.tag-301 {
border: 1px solid #ffd591;
background: #fff7e6;
color: #fa8c16;
}
.item-name {
font-size: 18px;
line-height: 18px;
font-weight: bold;
color: #3b414b;
width: 20px;
flex: auto;
}
.item-state {
font-size: 16px;
color: #84888e;
color: var(--color-main-active);
margin-left: 100px;
}
}
.card-body {
font-size: 16px;
color: #5f656c;
line-height: 30px;
margin-bottom: 8px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
overflow: hidden;
}
.card-footer {
display: flex;
gap: 8px;
align-items: center;
.footer-left-tags {
display: flex;
gap: 8px;
.area-tag {
padding: 2px 12px;
background: #e6f7ff;
border: 1px solid #91d5ff;
color: #1890ff;
border-radius: 4px;
font-size: 14px;
}
}
.footer-right-flags {
display: flex;
gap: 4px;
.flag-icon {
width: 12px;
height: 24px;
img {
border-radius: 50%;
border: 1px solid #eee;
width: 24px;
height: 100%;
object-fit: cover;
}
}
}
}
}
}
</style>
\ No newline at end of file
......@@ -8,42 +8,15 @@
placeholder="搜索市场准入限制调查" :containerRef="containerRef" areaName="" />
</div>
<div class="home-main-header-card-box">
<div class="home-main-header-card-box-box1 card"
@click="handleClickCardToDetail(sortInfo[1]?.sortCode, sortInfo[1]?.sortName)">
<div :class="getCardClass(item.sortCode)" v-for="item in sortInfo" :key="item.sortCode" @click="onNavigateToCase(item)">
<div class="header">
<div class="header-left">{{ sortInfo[1]?.sortName }}</div>
<div class="header-right">{{ `${sortInfo[1]?.sortCount}项` }}</div>
<div class="header-left">{{ item.sortName }}</div>
<div class="header-right">{{ `${item.sortCount}项` }}</div>
</div>
<div class="content-box">
<div class="item">{{ sortInfo[1]?.sortMeasure }}</div>
<div class="item">{{ item.sortMeasure }}</div>
<div class="item">
{{ sortInfo[1]?.sortDescription }}
</div>
</div>
</div>
<div class="home-main-header-card-box-box2 card"
@click="handleClickCardToDetail(sortInfo[2]?.sortCode, sortInfo[2]?.sortName)">
<div class="header">
<div class="header-left">{{ sortInfo[2]?.sortName }}</div>
<div class="header-right">{{ `${sortInfo[2]?.sortCount}项` }}</div>
</div>
<div class="content-box">
<div class="item">{{ sortInfo[2]?.sortMeasure }}</div>
<div class="item">
{{ sortInfo[2]?.sortDescription }}
</div>
</div>
</div>
<div class="home-main-header-card-box-box3 card"
@click="handleClickCardToDetail(sortInfo[0]?.sortCode, sortInfo[0]?.sortName)">
<div class="header">
<div class="header-left">{{ sortInfo[0]?.sortName }}</div>
<div class="header-right">{{ `${sortInfo[0]?.sortCount}项` }}</div>
</div>
<div class="content-box">
<div class="item">{{ sortInfo[0]?.sortMeasure }}</div>
<div class="item">
{{ sortInfo[0]?.sortDescription }}
{{ item.sortDescription }}
</div>
</div>
</div>
......@@ -55,7 +28,7 @@
<div class="box1">
<overviewMainBox title="调查进展" @toDetail="handleClickToDetail()">
<template #header-icon>
<img style="width: 100%; height: 100%" src="./assets/icons/icon1.svg" alt="" />
<img style="width: 100%; height: 100%" src="./assets/icons/icon_1599.png" alt="" />
</template>
<div class="box1-left" @click="handleSwithCurSurvey('left')">
<LeftBtn />
......@@ -128,8 +101,7 @@
</el-carousel>
</overviewMainBox>
</div>
<RiskSignal :list="box2Data" @more-click="handleToMoreRiskSignal" @item-click="handleClickCardToDetail"
postDate="signalTime" name="signalTitle" riskLevel="signalLevel" />
<RiskSignal :list="box2Data" @more-click="handleToMoreRiskSignal" @item-click="onNavigateToDetail" postDate="signalTime" name="signalTitle" riskLevel="signalLevel" />
</div>
<DivideHeader id="position2" class="divide-header" :titleText="'资讯要闻'"></DivideHeader>
<div class="center-center">
......@@ -326,53 +298,7 @@
</div>
<div class="title">{{ "市场准入限制调查" }}</div>
</div>
<div class="right-main">
<el-empty v-if="surveyInfoList.length === 0" description="当前条件下暂无数据" :image-size="200" />
<div v-else class="right-main-item" v-for="(item, index) in surveyInfoList" :key="index"
@click="handleClickCardToDetail(item.sortcode, item.sortName)">
<div class="item-left">
<div class="item-left-item"> {{ formatDate(item.searchdate, 'year') }}</div>
<div class="item-left-item"> {{ formatDate(item.searchdate, 'date') }}</div>
</div>
<div class="item-center">
<div class="icon">
<img :src='item.sortimageurl' alt="" />
</div>
<div class="line" v-if="index !== surveyInfoList.length - 1"></div>
</div>
<div class="item-right">
<div class="item-right-header">
<div class="tag"
:class="{ tag1: item.sortcode === '337', tag2: item.sortcode === '301', tag3: item.sortcode === '232' }">
{{ item.sortcode }}</div>
<div class="title">{{ item.searchname }}</div>
<div class="status" v-if="item.casestatus">
<div class="status-icon"></div>
<div class="status-text">{{ item.casestatus }}</div>
</div>
</div>
<div class="item-right-content" v-if="item.content">
{{ item.content }}
</div>
<div class="item-right-footer">
<!-- <div class="area-box" v-for="(val, idx) in item.searchArea" :key="idx">
<div class="area">{{ val }}</div>
</div> -->
<div class="area-box" v-if="item.searchArea.length">
<AreaTag v-for="(val, idx) in item.searchArea" :key="idx" :tagName="val"></AreaTag>
</div>
<div class="flag-box">
<div class="flag" v-for="(vall, idxx) in item.countryImage" :key="idxx">
<img :src="vall" alt="" />
</div>
</div>
</div>
</div>
</div>
</div>
<SurveyHistory :surveyList="surveyInfoList"></SurveyHistory>
<div class="right-footer">
<div class="footer-left">
{{ `共 ${totalDiscussNum} 项` }}
......@@ -399,6 +325,7 @@ import TipTab from "@/components/base/TipTab/index.vue"
import MessageBubble from "@/components/base/messageBubble/index.vue"
import NewsList from "@/components/base/newsList/index.vue";
import DivideHeader from "@/components/DivideHeader.vue";
import SurveyHistory from "@/views/marketAccessRestrictions/com/SurveyHistory.vue"
import setChart from "@/utils/setChart";
import router from "@/router";
......@@ -425,6 +352,9 @@ import {
} from "@/api/marketAccessRestrictions/index";
import { getRiskSignal, getNews, getRemarks } from "@/api/common/index";
import { ElMessage } from "element-plus";
import { useGotoNewsDetail } from '@/router/modules/news';
const getCardClass = (code) => ['card', `theme-${code}`]
const handleToPosi = id => {
const element = document.getElementById(id);
......@@ -471,8 +401,9 @@ const handleGetStatSort = async () => {
try {
const res = await getStatSort();
console.log("首页分类", res);
// sortInfo.value = res.data.sort((a, b) => a.sortCode - b.sortCode);
sortInfo.value = res.data;
} catch (error) { }
} catch (error) {}
};
// 调查进展
......@@ -487,25 +418,35 @@ const handleGetBox1Data = async () => {
} catch (error) { }
};
const carouselRef = ref(null);
// 点击查看详情
const handleClickToDetail = () => {
let activeIndex = 0;
if (carouselRef.value) {
activeIndex = carouselRef.value.activeIndex;
}
const id = box1DataList.value[activeIndex].SEARCHSORT;
handleClickCardToDetail(id, id + "调查概览")
let index = carouselRef.value?.activeIndex || 0;
let item = box1DataList.value[index];
if (item) onNavigateToDetail(item)
};
const handleClickCardToDetail = (id, name) => {
window.sessionStorage.setItem("curTabName", name);
// 跳转调查案件
const onNavigateToCase = (item) => {
window.sessionStorage.setItem("curTabName", item.sortName);
const route = router.resolve({
path: "/marketAccessLayout",
query: {
id: id
}
query: { id: item.sortCode }
});
window.open(route.href, "_blank");
};
// 跳转调查详情
const onNavigateToDetail = (item) => {
console.log(item)
let curTabName = item.searchname || item.signalTitle;
let id = item.sortcode || item.sortCode || item.searchsort;
let searchId = item.searchid || item.searchId;
window.sessionStorage.setItem("curTabName", curTabName);
const route = router.resolve({
path: "/marketSingleCaseLayout/overview",
query: { id, searchId },
});
window.open(route.href, "_blank");
};
......@@ -536,14 +477,9 @@ const handleGetBox2Data = async () => {
// 新闻资讯
const newsList = ref([]);
// 获取新闻资讯列表
const handleGetBox3Data = async () => {
const params = {
moduleId: "0104"
};
try {
const res = await getNews(params);
const res = await getNews({moduleId: "0104"});
console.log("新闻资讯", res);
if (res.code === 200 && res.data) {
newsList.value = res.data.map(item => {
......@@ -555,26 +491,17 @@ const handleGetBox3Data = async () => {
}
} catch (error) { }
};
// 点击新闻条目,跳转到新闻分析页
// 查看新闻资讯详情
const gotoNewsDetail = useGotoNewsDetail()
const handleToNewsAnalysis = news => {
const route = router.resolve({
path: "/newsAnalysis",
query: {
newsId: news.newsId
}
});
window.open(route.href, "_blank");
gotoNewsDetail(news.newsId)
};
// 社交媒体
const messageList = ref([]);
const handleGetBox4Data = async () => {
const params = {
moduleId: "0104"
};
try {
const res = await getRemarks(params);
const res = await getRemarks({moduleId: "0104"});
console.log("社交媒体", res);
if (res.code === 200 && res.data) {
messageList.value = res.data;
......@@ -1276,9 +1203,7 @@ const handleChangeCheckedCountry = (val) => {
handleGetSurveyList();
};
const surveyInfoList = ref([
]);
const surveyInfoList = ref([]);
const handleGetSurveyList = async () => {
......@@ -1464,14 +1389,6 @@ onMounted(async () => {
.card {
transition: transform 0.3s ease, box-shadow 0.3s ease;
&:hover {
transform: translateY(-3px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
}
.home-main-header-card-box-box1 {
width: 520px;
height: 142px;
background: #fff;
......@@ -1488,7 +1405,6 @@ onMounted(async () => {
top: 15px;
width: 4px;
height: 111px;
background: rgba(250, 140, 22, 1);
}
.header {
......@@ -1510,7 +1426,6 @@ onMounted(async () => {
margin-top: 28px;
margin-right: 35px;
height: 24px;
color: rgba(250, 140, 22, 1);
font-family: Source Han Sans CN;
font-size: 30px;
font-weight: 700;
......@@ -1535,143 +1450,40 @@ onMounted(async () => {
white-space: nowrap;
}
}
}
.home-main-header-card-box-box2 {
width: 520px;
height: 142px;
background: #fff;
border-radius: 10px;
box-shadow: 0px 0px 15px 0px rgba(60, 87, 126, 0.2);
position: relative;
cursor: pointer;
&:hover {
transform: translateY(-3px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
}
// &:hover {
// margin-top: -2px;
// }
.theme-301 {
&::before {
content: "";
position: absolute;
z-index: 99;
left: 0;
top: 15px;
width: 4px;
height: 111px;
background: rgba(114, 46, 209, 1);
background: rgba(250, 140, 22, 1);
}
.header {
height: 56px;
display: flex;
justify-content: space-between;
.header-left {
margin-left: 30px;
margin-top: 26px;
color: rgba(59, 65, 75, 1);
font-family: Source Han Sans CN;
font-size: 20px;
font-weight: 700;
line-height: 26px;
}
.header-right {
margin-top: 28px;
margin-right: 35px;
height: 24px;
color: rgba(114, 46, 209, 1);
font-family: Source Han Sans CN;
font-size: 30px;
font-weight: 700;
line-height: 24px;
color: rgba(250, 140, 22, 1);
}
}
.content-box {
margin-left: 30px;
.item {
width: 485px;
height: 24px;
color: rgba(95, 101, 108, 1);
font-family: Source Han Sans CN;
font-size: 16px;
font-weight: 400;
line-height: 24px;
margin-top: 8px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.theme-232 {
&::before {
background: rgba(114, 46, 209, 1);
}
.header {
.header-right {
color: rgba(114, 46, 209, 1);
}
}
.home-main-header-card-box-box3 {
width: 520px;
height: 142px;
background: #fff;
border-radius: 10px;
box-shadow: 0px 0px 15px 0px rgba(60, 87, 126, 0.2);
position: relative;
cursor: pointer;
// &:hover {
// margin-top: -3px;
// box-shadow: 0 0 5px 5px rgba(10, 87, 166, 0.1);
// }
}
.theme-337 {
&::before {
content: "";
position: absolute;
z-index: 99;
left: 0;
top: 15px;
width: 4px;
height: 111px;
background: var(--color-main-active);
}
.header {
height: 56px;
display: flex;
justify-content: space-between;
.header-left {
margin-left: 30px;
margin-top: 26px;
color: rgba(59, 65, 75, 1);
font-family: Source Han Sans CN;
font-size: 20px;
font-weight: 700;
line-height: 26px;
}
.header-right {
margin-top: 28px;
margin-right: 35px;
height: 24px;
color: var(--color-main-active);
font-family: Source Han Sans CN;
font-size: 30px;
font-weight: 700;
line-height: 24px;
}
}
.content-box {
margin-left: 30px;
.item {
width: 485px;
height: 24px;
color: rgba(95, 101, 108, 1);
font-family: Source Han Sans CN;
font-size: 16px;
font-weight: 400;
line-height: 24px;
margin-top: 8px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
......@@ -2470,6 +2282,7 @@ onMounted(async () => {
margin: 0 auto;
margin-top: 34px;
display: flex;
align-items: flex-start;
gap: 16px;
.left {
......@@ -2555,7 +2368,6 @@ onMounted(async () => {
.right-header {
height: 48px;
display: flex;
border-bottom: 1px solid rgba(234, 236, 238, 1);
.icon {
width: 24px;
......@@ -2584,225 +2396,6 @@ onMounted(async () => {
}
}
.right-main {
box-sizing: border-box;
padding: 18px 27px 0 24px;
height: calc(100% - 108px);
border-bottom: 1px solid rgba(230, 231, 232, 1);
.right-main-item {
height: 154px;
display: flex;
cursor: pointer;
height: max-content;
.item-left {
width: 100px;
height: 48px;
padding-right: 20px;
.item-left-item {
height: 24px;
color: var(--color-main-active);
font-family: Source Han Sans CN;
font-style: Bold;
font-size: 16px;
font-weight: 700;
line-height: 24px;
letter-spacing: 1px;
text-align: right;
}
}
.item-center {
width: 30px;
.icon {
width: 30px;
height: 30px;
border-radius: 15px;
img {
width: 100%;
height: 100%;
}
}
.line {
height: 100%;
margin-left: 14px;
width: 2px;
min-height: 64px;
max-height: 124px;
border-radius: 1.5px;
background: rgba(234, 236, 238, 1);
}
}
.item-right {
padding-left: 20px;
&:hover {
.item-right-header>.title {
color: var(--color-main-active);
text-decoration: underline;
}
}
.item-right-header {
height: 26px;
display: flex;
align-items: center;
.tag {
width: 48px;
height: 24px;
padding: 0 8px;
box-sizing: border-box;
font-family: Source Han Sans CN;
font-style: Bold;
font-size: 16px;
font-weight: 700;
line-height: 24px;
letter-spacing: 1px;
text-align: left;
border-radius: 4px;
}
.tag1 {
border: 1px solid rgba(145, 202, 255, 1);
background: rgba(230, 244, 255, 1);
color: rgba(5, 95, 194, 1);
}
.tag2 {
border: 1px solid rgba(255, 213, 145, 1);
background: rgba(255, 247, 230, 1);
color: rgba(250, 140, 22, 1);
}
.tag3 {
border: 1px solid rgba(211, 173, 247, 1);
background: rgba(249, 240, 255, 1);
color: rgba(114, 46, 209, 1);
}
.title {
margin-left: 9px;
width: 880px;
height: 26px;
color: rgba(59, 65, 75, 1);
font-family: Source Han Sans CN;
font-style: Bold;
font-size: 20px;
font-weight: 700;
line-height: 26px;
letter-spacing: 0px;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.status {
margin-left: 9px;
display: flex;
width: 76px;
height: 24px;
justify-content: flex-end;
gap: 5px;
align-items: center;
.status-icon {
width: 4px;
height: 4px;
border-radius: 2px;
background: var(--color-main-active);
}
.status-text {
color: var(--color-main-active);
font-family: Source Han Sans CN;
font-style: Regular;
font-size: 16px;
font-weight: 400;
line-height: 24px;
letter-spacing: 0px;
text-align: right;
}
}
}
.item-right-content {
margin-top: 9px;
width: 1022px;
height: 60px;
color: rgba(95, 101, 108, 1);
font-family: Source Han Sans CN;
font-style: Regular;
font-size: 16px;
font-weight: 400;
line-height: 30px;
letter-spacing: 0px;
text-align: justify;
}
.item-right-footer {
width: 1022px;
height: 24px;
margin-top: 9px;
display: flex;
gap: 8px;
margin-bottom: 10px;
.area-box {
display: flex;
gap: 8px;
.area {
height: 24px;
padding: 0 8px;
border-radius: 4px;
background: rgba(231, 243, 255, 1);
color: var(--color-main-active);
font-family: Source Han Sans CN;
font-style: Regular;
font-size: 14px;
font-weight: 400;
line-height: 24px;
letter-spacing: 0px;
text-align: left;
}
}
.flag-box {
display: flex;
position: relative;
.flag {
width: 24px;
height: 24px;
border-radius: 12px;
border: 2px solid #fff;
overflow: hidden;
position: absolute;
img {
width: 100%;
height: 100%;
}
}
:nth-child(1) {
left: 18px;
z-index: 99;
}
}
}
}
}
}
.right-footer {
box-sizing: border-box;
padding-top: 15px;
......
......@@ -3,16 +3,7 @@
<div class="wrapper-header">
<div class="header-filters">
<div class="search-box">
<el-input
v-model="searchText"
style="width: 240px; height: 32px"
placeholder="搜索调查案件"
@keyup.enter="handleSearch"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-input v-model="searchText" style="width: 360px; height: 32px" placeholder="搜索调查案件" @keyup.enter="handleSearch" :suffix-icon="Search"></el-input>
</div>
<div class="dropdown-filters">
<el-select v-model="filterStage" placeholder="全部阶段" class="filter-select" clearable>
......@@ -40,59 +31,31 @@
</div>
<div class="wrapper-main">
<div class="left">
<div class="left-box2">
<div class="left-box2-header">
<!-- 科技领域 -->
<div class="left-box">
<div class="left-header">
<div class="icon"></div>
<div class="title">{{ "科技领域" }}</div>
</div>
<div class="left-box2-main">
<div class="checkbox-group">
<el-checkbox
v-model="checkAllAreas"
:indeterminate="isIndeterminateAreas"
@change="handleCheckAllAreasChange"
class="filter-checkbox"
>
全部领域
</el-checkbox>
<div class="area-grid">
<el-checkbox
v-for="area in surveyAreaList"
:key="area.id"
v-model="checkedAreaList"
:label="area.id"
class="filter-checkbox"
@change="handleCheckedAreasChange"
>
<div class="left-main">
<el-checkbox-group class="checkbox-group" v-model="checkedAreaList" @change="handleCheckedAreasChange">
<el-checkbox class="filter-checkbox" label="全部领域"> 全部领域 </el-checkbox>
<el-checkbox v-for="area in surveyAreaList" :key="area.id" :label="area.id" class="filter-checkbox">
{{ area.name }}
</el-checkbox>
</el-checkbox-group>
</div>
</div>
</div>
</div>
<div class="left-box1">
<div class="left-box1-header">
<!-- 发布时间 -->
<div class="left-box">
<div class="left-header">
<div class="icon"></div>
<div class="title">{{ "发布时间" }}</div>
</div>
<div class="left-box1-main">
<div class="checkbox-group">
<el-checkbox
v-model="checkAllYears"
:indeterminate="isIndeterminateYears"
@change="handleCheckAllYearsChange"
class="filter-checkbox"
>
全部时间
</el-checkbox>
<el-checkbox
v-for="year in displayedYearList"
:key="year.id"
v-model="checkedSurveyYears"
:label="year.id"
class="filter-checkbox"
@change="handleCheckedYearsChange"
>
<div class="left-main">
<el-checkbox-group class="checkbox-group" v-model="checkedSurveyYears" @change="handleCheckedYearsChange">
<el-checkbox class="filter-checkbox" label="全部时间"> 全部时间 </el-checkbox>
<el-checkbox v-for="year in displayedYearList" :key="year.id" :label="year.id" class="filter-checkbox">
{{ year.name }}
</el-checkbox>
<div v-if="surveyYearList.length > 6" class="expand-btn" @click="isYearExpanded = !isYearExpanded">
......@@ -102,7 +65,7 @@
<ArrowDown v-else />
</el-icon>
</div>
</div>
</el-checkbox-group>
</div>
</div>
</div>
......@@ -113,44 +76,7 @@
</div>
<div class="title">{{ "232调查历程" }}</div>
</div>
<div class="right-main" v-loading="listLoading">
<div
class="timeline-item"
v-for="(item, index) in surveyInfoList"
:key="index"
@click="handleToSingleCase(item)"
>
<div class="timeline-date">
<div class="date-text">{{ item.timeZH || item.time }}</div>
</div>
<div class="timeline-line-box">
<div class="timeline-icon">
<img v-if="item.sortImageUrl" :src="item.sortImageUrl" alt="" />
<div v-else class="default-dot"></div>
</div>
<div class="timeline-line" v-if="index !== surveyInfoList.length - 1"></div>
</div>
<div class="timeline-content-card">
<div class="card-header">
<div class="tag-box">
<div class="tag-232">{{ item.tag }}</div>
<div class="title">{{ item.title }}</div>
</div>
<div class="status" :class="{ 'status-active': item.status === '调查中' }">
<span class="dot"></span> {{ item.status }}
</div>
</div>
<div class="card-body">
{{ item.content }}
</div>
<div class="card-footer">
<div class="area-tag" v-for="(area, idx) in item.areaList" :key="idx">
{{ area }}
</div>
</div>
</div>
</div>
</div>
<SurveyHistory v-loading="listLoading" :surveyList="surveyInfoList"></SurveyHistory>
<div class="right-footer">
<div class="footer-left">
{{ `共${totalDiscussNum}项调查` }}
......@@ -173,12 +99,9 @@
<script setup>
import { ref, onMounted, watch, computed } from "vue";
import router from "@/router";
import { useRoute } from "vue-router";
import { Search, ArrowDown, ArrowUp } from "@element-plus/icons-vue";
import { getSearchAllArea, getSearchAllYear, getSurveyList } from "@/api/marketAccessRestrictions";
const route = useRoute();
import SurveyHistory from "@/views/marketAccessRestrictions/com/SurveyHistory.vue"
// 顶部过滤项
const searchText = ref("");
......@@ -255,19 +178,7 @@ const handleFetchSurveyList = async () => {
};
const res = await getSurveyList(params);
if (res.code === 200 && res.data) {
surveyInfoList.value = res.data.content.map(item => {
return {
time: item.SEARCHDATE,
timeZH: item.SEARCHDATEZH,
title: item.SEARCHNAME,
content: item.SEARCHDESC || item.SEARCHNAME,
status: item.CASESTATUS || "调查中",
areaList: item.searchArea || [],
tag: item.SORTCODE || "232",
id: item.SEARCHID,
sortImageUrl: item.SORTIMAGEURL
};
});
surveyInfoList.value = res.data.content;
totalDiscussNum.value = res.data.totalElements || 0;
}
} catch (error) {
......@@ -287,17 +198,6 @@ const handleSearch = () => {
handleFetchSurveyList();
};
const handleToSingleCase = item => {
const curRoute = router.resolve({
path: "/marketSingleCaseLayout/overview",
query: {
id: "232",
searchId: item.id + ""
}
});
window.open(curRoute.href, "_blank");
};
// 监听过滤条件
watch([checkedSurveyYears, checkedAreaList, isSort, filterStage, filterParty, filterReason], () => {
if (isInitializing.value) return;
......@@ -343,7 +243,7 @@ onMounted(async () => {
.case-wrapper {
width: 1600px;
margin: 0 auto;
padding-top: 20px;
padding: 20px 0;
.wrapper-header {
height: 32px;
......@@ -357,6 +257,12 @@ onMounted(async () => {
gap: 16px;
align-items: center;
.search-box {
background-color: #fff;
border: 1px solid rgba(234, 236, 238, 1);
border-radius: 4px;
}
.dropdown-filters {
display: flex;
gap: 12px;
......@@ -396,10 +302,10 @@ onMounted(async () => {
.wrapper-main {
display: flex;
justify-content: space-between;
gap: 16px;
.left {
width: 300px;
width: 360px;
min-height: 560px;
height: fit-content;
padding-bottom: 20px;
......@@ -407,19 +313,20 @@ onMounted(async () => {
box-shadow: 0px 0px 15px 0px rgba(60, 87, 126, 0.2);
background: #fff;
.left-box1,
.left-box2 {
.left-box {
margin-top: 17px;
.left-box1-header,
.left-box2-header {
.left-header {
display: flex;
align-items: center;
.icon {
width: 8px;
height: 16px;
background: var(--color-main-active);
border-radius: 0 2px 2px 0;
}
.title {
margin-left: 17px;
color: var(--color-main-active);
......@@ -431,16 +338,8 @@ onMounted(async () => {
.checkbox-group {
padding: 10px 0 0 25px;
display: flex;
flex-direction: column;
.area-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px;
}
.filter-checkbox {
width: 130px;
margin-bottom: 8px;
height: 32px;
:deep(.el-checkbox__label) {
......@@ -448,6 +347,7 @@ onMounted(async () => {
color: #5f656c;
}
}
.expand-btn {
color: var(--color-main-active);
font-size: 14px;
......@@ -455,6 +355,7 @@ onMounted(async () => {
display: flex;
align-items: center;
margin-top: 4px;
.el-icon {
margin-left: 4px;
}
......@@ -463,7 +364,8 @@ onMounted(async () => {
}
.right {
width: 1284px;
flex: auto;
width: 20px;
min-height: 700px;
border-radius: 10px;
box-shadow: 0px 0px 15px 0px rgba(60, 87, 126, 0.2);
......@@ -474,7 +376,6 @@ onMounted(async () => {
height: 48px;
display: flex;
align-items: center;
border-bottom: 1px solid #eaeeef;
padding: 0 20px;
.icon {
width: 22px;
......@@ -490,136 +391,6 @@ onMounted(async () => {
}
}
.right-main {
padding: 30px 40px;
.timeline-item {
display: flex;
cursor: pointer;
&:hover {
.timeline-content-card {
background-color: #f5f7fa;
}
}
.timeline-date {
width: 100px;
text-align: right;
padding-right: 20px;
.date-text {
font-size: 16px;
font-weight: 700;
color: var(--color-main-active);
line-height: 24px;
}
}
.timeline-line-box {
width: 40px;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
.timeline-icon {
width: 32px;
height: 32px;
z-index: 2;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
img {
width: 100%;
height: 100%;
}
.default-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: orange;
}
}
.timeline-line {
width: 2px;
flex: 1;
background: #eaeeef;
margin: 4px 0;
}
}
.timeline-content-card {
flex: 1;
// border: 1px solid #eaeeef;
border-radius: 8px;
padding: 16px 20px;
margin-left: 10px;
margin-bottom: 24px;
transition: all 0.3s;
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
.tag-box {
display: flex;
align-items: center;
gap: 12px;
.tag-232 {
padding: 2px 10px;
border: 1px solid #b37feb;
background: #f9f0ff;
color: #722ed1;
border-radius: 4px;
font-weight: 700;
font-size: 16px;
}
.title {
font-size: 18px;
font-weight: 700;
color: #3b414b;
}
}
.status {
font-size: 16px;
color: #84888e;
.dot {
margin-right: 4px;
}
}
.status-active {
color: var(--color-main-active);
}
}
.card-body {
font-size: 16px;
color: #5f656c;
line-height: 1.6;
margin-bottom: 16px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
overflow: hidden;
}
.card-footer {
display: flex;
gap: 8px;
.area-tag {
padding: 2px 12px;
background: #e6f7ff;
border: 1px solid #91d5ff;
color: #1890ff;
border-radius: 4px;
font-size: 14px;
}
}
}
}
}
.right-footer {
display: flex;
justify-content: space-between;
......
......@@ -2,17 +2,7 @@
<div class="case-wrapper">
<div class="wrapper-header">
<div class="search-box">
<el-input
v-model="searchText"
style="width: 300px; height: 32px"
placeholder="搜索调查案件"
@keyup.enter="handleSearch"
>
<template #append
><el-button @click="handleSearch"
><el-icon><Search color="#84888E" /></el-icon></el-button
></template>
</el-input>
<el-input v-model="searchText" style="width: 360px; height: 32px" placeholder="搜索调查案件" @keyup.enter="handleSearch" :suffix-icon="Search"></el-input>
</div>
<div class="select-box">
<div class="paixu-btn" @click="handleSwithSort">
......@@ -30,75 +20,44 @@
</div>
<div class="wrapper-main">
<div class="left">
<div class="left-box2">
<div class="left-box2-header">
<!-- 科技领域 -->
<div class="left-box">
<div class="left-header">
<div class="icon"></div>
<div class="title">{{ "科技领域" }}</div>
</div>
<div class="left-box2-main">
<div class="checkbox-group">
<el-checkbox
v-model="checkAllAreas"
:indeterminate="isIndeterminateAreas"
@change="handleCheckAllAreasChange"
class="filter-checkbox"
>
全部领域
</el-checkbox>
<el-checkbox
v-for="area in surveyAreaList"
:key="area.id"
v-model="checkedAreaList"
:label="area.id"
class="filter-checkbox"
@change="handleCheckedAreasChange"
>
<div class="left-main">
<el-checkbox-group class="checkbox-group" v-model="checkedAreaList" @change="handleCheckedAreasChange">
<el-checkbox class="filter-checkbox" label="全部领域"> 全部领域 </el-checkbox>
<el-checkbox v-for="area in surveyAreaList" :key="area.id" :label="area.id" class="filter-checkbox">
{{ area.name }}
</el-checkbox>
</el-checkbox-group>
</div>
</div>
</div>
<div class="left-box1">
<div class="left-box1-header">
<!-- 发布时间 -->
<div class="left-box">
<div class="left-header">
<div class="icon"></div>
<div class="title">{{ "发布时间" }}</div>
</div>
<div class="left-box1-main">
<div class="checkbox-group">
<el-checkbox
v-model="checkAllYears"
:indeterminate="isIndeterminateYears"
@change="handleCheckAllYearsChange"
class="filter-checkbox"
>
全部时间
</el-checkbox>
<el-checkbox
v-for="year in displayedYearList"
:key="year.id"
v-model="checkedSurveyYears"
:label="year.id"
class="filter-checkbox"
@change="handleCheckedYearsChange"
>
<div class="left-main">
<el-checkbox-group class="checkbox-group" v-model="checkedSurveyYears" @change="handleCheckedYearsChange">
<el-checkbox class="filter-checkbox" label="全部时间"> 全部时间 </el-checkbox>
<el-checkbox v-for="year in displayedYearList" :key="year.id" :label="year.id" class="filter-checkbox">
{{ year.name }}
</el-checkbox>
<div
v-if="surveyYearList.length > 6"
class="expand-btn"
@click="isYearExpanded = !isYearExpanded"
>
<div v-if="surveyYearList.length > 6" class="expand-btn" @click="isYearExpanded = !isYearExpanded">
{{ isYearExpanded ? "收起" : "更早" }}
<el-icon>
<ArrowUp v-if="isYearExpanded" />
<ArrowDown v-else />
</el-icon>
</div>
</el-checkbox-group>
</div>
</div>
</div>
</div>
<div class="right">
<div class="right-header">
<div class="icon">
......@@ -106,53 +65,11 @@
</div>
<div class="title">{{ "301调查历程" }}</div>
</div>
<div class="right-main" v-loading="listLoading">
<div class="item" v-for="(item, index) in surveyInfoList" :key="index" @click="handleToSingleCase(item)">
<div class="item-left">{{ item.time }}</div>
<div class="item-center">
<div class="icon">
<!-- <img :src="item.img" alt="" /> -->
</div>
<div class="line" v-if="index !== surveyInfoList.length - 1"></div>
</div>
<div class="item-right">
<div class="item-right-header">
<div class="tag tag1">{{ item.tag }}</div>
<div class="title">{{ item.title }}</div>
<div class="status">
<div class="status-icon"></div>
<div class="status-text">{{ item.status }}</div>
</div>
</div>
<div class="item-right-content">
{{ item.content }}
</div>
<div class="item-right-footer">
<div class="area-box" v-for="(val, idx) in item.areaList" :key="idx">
<div class="area">{{ val }}</div>
</div>
<div class="flag-box">
<div class="flag" v-for="(vall, idxx) in item.countryList" :key="idxx">
<img :src="vall" alt="" style="width: 100%; height: 100%; border-radius: 50%;" />
</div>
</div>
</div>
</div>
</div>
</div>
<SurveyHistory v-loading="listLoading" :surveyList="surveyInfoList"></SurveyHistory>
<div class="right-footer">
<div class="footer-left">
{{ `共 ${totalDiscussNum} 项` }}
</div>
<div class="footer-left">{{ `共 ${totalDiscussNum} 项` }}</div>
<div class="footer-right">
<el-pagination
@current-change="handleCurrentChange"
:pageSize="pageSize"
:current-page="currentPage"
background
layout="prev, pager, next"
:total="totalDiscussNum"
/>
<el-pagination @current-change="handleCurrentChange" :pageSize="pageSize" :current-page="currentPage" background layout="prev, pager, next" :total="totalDiscussNum" />
</div>
</div>
</div>
......@@ -162,11 +79,9 @@
<script setup>
import { ref, onMounted, watch, computed } from "vue";
import router from "@/router";
import { useRoute } from "vue-router";
import { Search, ArrowDown, ArrowUp } from "@element-plus/icons-vue";
import { getSearchAllArea, getSearchAllYear, getSurveyList } from "@/api/marketAccessRestrictions";
const route = useRoute();
import SurveyHistory from "@/views/marketAccessRestrictions/com/SurveyHistory.vue"
const isSort = ref(true); // true 升序 false 倒序
const handleSwithSort = () => {
......@@ -238,18 +153,7 @@ const handleFetchSurveyList = async () => {
};
const res = await getSurveyList(params);
if (res.code === 200 && res.data) {
surveyInfoList.value = res.data.content.map(item => {
return {
time: item.SEARCHDATEZH || item.SEARCHDATE,
title: item.SEARCHNAME,
content: item.SEARCHNAME, // 或者其他字段,API文档没给出具体描述字段,暂用名称
status: item.CASESTATUS,
areaList: item.searchArea || [],
countryList: item.countryImage || [],
tag: item.SORTCODE || "301",
id: item.SEARCHID
};
});
surveyInfoList.value = res.data.content;
totalDiscussNum.value = res.data.totalElements || 0;
}
} catch (error) {
......@@ -281,19 +185,6 @@ watch(
}
);
const handleToSingleCase = item => {
console.log(item)
// router.push('/marketSingleCaseLayout/overview')
const curRoute = router.resolve({
path: "/marketSingleCaseLayout/overview",
query: {
id: route.query.id + '',
searchId: item.id + ''
}
});
window.open(curRoute.href, "_blank");
};
const handleGetSearchAllArea = async () => {
try {
const res = await getSearchAllArea({
......@@ -346,18 +237,20 @@ onMounted(async () => {
<style lang="scss" scoped>
.case-wrapper {
// width: 100%;
// height: 100%;
overflow: hidden;
.wrapper-header {
width: 1600px;
margin: 0 auto;
padding: 20px 0;
.wrapper-header {
width: 100%;
height: 32px;
margin: 16px auto;
display: flex;
margin-bottom: 16px;
justify-content: space-between;
.search-box {
width: 300px;
display: flex;
background-color: #fff;
border: 1px solid rgba(234, 236, 238, 1);
border-radius: 4px;
}
.select-box {
height: 32px;
......@@ -409,12 +302,11 @@ onMounted(async () => {
}
}
}
.wrapper-main {
width: 1600px;
// height: 900px;
margin: 0 auto;
display: flex;
justify-content: space-between;
gap: 16px;
.left {
width: 360px;
min-height: 560px;
......@@ -422,58 +314,62 @@ onMounted(async () => {
padding-bottom: 20px;
border-radius: 10px;
box-shadow: 0px 0px 15px 0px rgba(60, 87, 126, 0.2);
background: rgba(255, 255, 255, 1);
.left-box1 {
background: #fff;
.left-box {
margin-top: 17px;
// height: 260px;
.left-box1-header {
.left-header {
display: flex;
align-items: center;
.icon {
width: 8px;
height: 16px;
margin-top: 4px;
border-radius: 2px 2px 0 0;
background: var(--color-main-active);
border-radius: 0 2px 2px 0;
}
.title {
height: 2px;
margin-left: 17px;
color: var(--color-main-active);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 700;
line-height: 24px;
}
}
}
.left-box2 {
margin-top: 17px;
.left-box2-header {
display: flex;
.icon {
width: 8px;
height: 16px;
margin-top: 4px;
border-radius: 2px 2px 0 0;
background: var(--color-main-active);
.checkbox-group {
padding: 10px 0 0 25px;
.filter-checkbox {
width: 130px;
margin-bottom: 8px;
height: 32px;
:deep(.el-checkbox__label) {
font-size: 16px;
color: #5f656c;
}
.title {
height: 2px;
margin-left: 17px;
}
.expand-btn {
color: var(--color-main-active);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 700;
line-height: 24px;
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
margin-top: 4px;
.el-icon {
margin-left: 4px;
}
}
}
}
.right {
margin-left: 16px;
width: 1224px;
width: 20px;
flex: auto;
min-height: 700px;
height: 770px;
border-radius: 10px;
box-shadow: 0px 0px 15px 0px rgba(60, 87, 126, 0.2);
background: rgba(255, 255, 255, 1);
......@@ -482,7 +378,6 @@ onMounted(async () => {
height: 48px;
display: flex;
align-items: center;
border-bottom: 1px solid rgba(234, 236, 238, 1);
.icon {
width: 22px;
height: 18px;
......@@ -505,170 +400,6 @@ onMounted(async () => {
text-align: left;
}
}
.right-main {
padding-top: 6px;
height: 660px;
border-bottom: 1px solid rgba(230, 231, 232, 1);
overflow: auto;
.item {
height: 154px;
display: flex;
cursor: pointer;
&:hover {
background: var(--color-bg-hover) !important;
}
.item-left {
width: 100px;
color: var(--color-main-active);
font-family: Microsoft YaHei;
font-style: Bold;
font-size: 16px;
font-weight: 700;
line-height: 24px;
letter-spacing: 1px;
text-align: right;
padding-right: 16px;
}
.item-center {
width: 30px;
height: 154px;
.icon {
width: 30px;
height: 30px;
border-radius: 15px;
background: orange;
}
.line {
margin-left: 14px;
width: 2px;
height: 124px;
border-radius: 1.5px;
background: rgba(234, 236, 238, 1);
}
}
.item-right {
padding-left: 20px;
.item-right-header {
height: 26px;
display: flex;
align-items: center;
.tag {
width: 48px;
height: 24px;
padding: 0 8px;
box-sizing: border-box;
font-family: Microsoft YaHei;
font-style: Bold;
font-size: 16px;
font-weight: 700;
line-height: 24px;
letter-spacing: 1px;
text-align: left;
border-radius: 4px;
}
.tag1 {
border: 1px solid rgba(145, 202, 255, 1);
background: rgba(230, 244, 255, 1);
color: rgba(5, 95, 194, 1);
}
.tag2 {
border: 1px solid rgba(255, 213, 145, 1);
background: rgba(255, 247, 230, 1);
color: rgba(250, 140, 22, 1);
}
.tag3 {
border: 1px solid rgba(211, 173, 247, 1);
background: rgba(249, 240, 255, 1);
color: rgba(114, 46, 209, 1);
}
.title {
margin-left: 9px;
width: 880px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
height: 26px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-style: Bold;
font-size: 20px;
font-weight: 700;
line-height: 26px;
letter-spacing: 0px;
text-align: left;
}
.status {
margin-left: 9px;
display: flex;
width: 76px;
height: 24px;
justify-content: space-between;
align-items: center;
.status-icon {
width: 4px;
height: 4px;
border-radius: 2px;
background: var(--color-main-active);
}
.status-text {
color: var(--color-main-active);
font-family: Microsoft YaHei;
font-style: Regular;
font-size: 16px;
font-weight: 400;
line-height: 24px;
letter-spacing: 0px;
text-align: right;
}
}
}
.item-right-content {
margin-top: 9px;
width: 1022px;
height: 60px;
color: rgba(95, 101, 108, 1);
font-family: Microsoft YaHei;
font-style: Regular;
font-size: 16px;
font-weight: 400;
line-height: 30px;
letter-spacing: 0px;
text-align: justify;
}
.item-right-footer {
margin-top: 9px;
display: flex;
gap: 8px;
.area-box {
display: flex;
.area {
height: 24px;
padding: 0 8px;
border-radius: 4px;
background: rgba(231, 243, 255, 1);
color: var(--color-main-active);
font-family: Microsoft YaHei;
font-style: Regular;
font-size: 14px;
font-weight: 400;
line-height: 24px;
letter-spacing: 0px;
text-align: left;
}
}
.flag-box {
display: flex;
.flag {
width: 24px;
height: 24px;
border-radius: 12px;
background: orange;
}
}
}
}
}
}
.right-footer {
box-sizing: border-box;
padding-top: 15px;
......@@ -691,29 +422,4 @@ onMounted(async () => {
}
}
}
.checkbox-group{
padding-top: 10px;
margin-left: 16px;
.expand-btn {
width: 140px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-main-active);
font-size: 14px;
cursor: pointer;
user-select: none;
&:hover {
opacity: 0.8;
}
.el-icon {
margin-left: 4px;
}
}
}
.filter-checkbox {
width: 140px;
margin-bottom: 8px;
}
</style>
\ No newline at end of file
......@@ -3,14 +3,7 @@
<div class="wrapper-header">
<div class="header-filters">
<div class="search-box">
<el-input v-model="searchText" style="width: 360px; height: 32px" placeholder="搜索调查案件"
@keyup.enter="handleSearch">
<template #prefix>
<el-icon>
<Search />
</el-icon>
</template>
</el-input>
<el-input v-model="searchText" style="width: 360px; height: 32px" placeholder="搜索调查案件" @keyup.enter="handleSearch" :suffix-icon="Search"></el-input>
</div>
<div class="dropdown-filters">
<el-select v-model="filterStage" placeholder="全部阶段" class="filter-select" clearable>
......@@ -52,7 +45,6 @@
{{ area.name }}
</el-checkbox>
</el-checkbox-group>
</div>
</div>
<!-- 发布时间 -->
......@@ -95,106 +87,10 @@
</div>
</div>
<div class="right">
<!-- <div class="right-header">
<div class="icon">
<img src="./assets/images/right-header-icon.png" alt="" />
</div>
<div class="title">{{ "337调查历程" }}</div>
</div>
<div class="right-main" v-loading="listLoading">
<div class="timeline-item" v-for="(item, index) in surveyInfoList" :key="index"
@click="handleToSingleCase(item)">
<div class="timeline-date">
<div class="date-text">{{ item.timeZH.slice(0,4) }}</div>
<div class="date-text">{{ item.timeZH.slice(5) }}</div>
</div>
<div class="timeline-line-box">
<div class="timeline-icon">
<img v-if="item.sortImageUrl" :src="item.sortImageUrl" alt="" />
<div v-else class="default-dot"></div>
</div>
<div class="timeline-line" v-if="index !== surveyInfoList.length - 1"></div>
</div>
<div class="timeline-content-card">
<div class="card-header">
<div class="tag-box">
<div class="tag-337">{{ item.tag }}</div>
<div class="title">{{ item.title }}</div>
</div>
<div class="status" :class="{ 'status-active': true }">
<span class="dot">•</span> {{ item.status }}
</div>
</div>
<div class="card-body">
{{ item.content }}
</div>
<div class="card-footer">
<div class="footer-left-tags">
<AreaTag v-for="(area, idx) in item.areaList" :key="idx" :tagName="area"></AreaTag>
</div>
<div class="footer-right-flags">
<div class="flag-icon" v-for="(flag, fidx) in item.countryList" :key="fidx">
<img :src="flag" alt="" />
</div>
</div>
</div>
</div>
</div>
</div>
<AnalysisBox title="337调查历程" width="1224px" height="auto">
<SurveyHistory v-loading="listLoading" :surveyList="surveyInfoList"></SurveyHistory>
<div class="right-footer">
<div class="footer-left">
{{ `共${totalDiscussNum}项调查` }}
</div>
<div class="footer-right">
<el-pagination @current-change="handleCurrentChange" :pageSize="pageSize" :current-page="currentPage"
background layout="prev, pager, next" :total="totalDiscussNum" />
</div>
</div> -->
<AnalysisBox title="337调查历程" width="1224px">
<div class="right-main" v-loading="listLoading">
<div class="timeline-item" v-for="(item, index) in surveyInfoList" :key="index"
@click="handleToSingleCase(item)">
<div class="timeline-date">
<div class="date-text">{{ item.timeZH.slice(0,4) }}</div>
<div class="date-text">{{ item.timeZH.slice(5) }}</div>
</div>
<div class="timeline-line-box">
<div class="timeline-icon">
<img v-if="item.sortImageUrl" :src="item.sortImageUrl" alt="" />
<div v-else class="default-dot"></div>
</div>
<div class="timeline-line" v-if="index !== surveyInfoList.length - 1"></div>
</div>
<div class="timeline-content-card">
<div class="card-header">
<div class="tag-box">
<div class="tag-337">{{ item.tag }}</div>
<div class="title">{{ item.title }}</div>
</div>
<div class="status" :class="{ 'status-active': true }">
<span class="dot"></span> {{ item.status }}
</div>
</div>
<div class="card-body">
{{ item.content }}
</div>
<div class="card-footer">
<div class="footer-left-tags">
<AreaTag v-for="(area, idx) in item.areaList" :key="idx" :tagName="area"></AreaTag>
</div>
<div class="footer-right-flags">
<div class="flag-icon" v-for="(flag, fidx) in item.countryList" :key="fidx">
<img :src="flag" alt="" />
</div>
</div>
</div>
</div>
</div>
</div>
<div class="right-footer">
<div class="footer-left">
{{ `共${totalDiscussNum}项调查` }}
</div>
<div class="footer-left">{{ `共${totalDiscussNum}项调查` }}</div>
<div class="footer-right">
<el-pagination @current-change="handleCurrentChange" :pageSize="pageSize" :current-page="currentPage"
background layout="prev, pager, next" :total="totalDiscussNum" />
......@@ -208,14 +104,11 @@
<script setup>
import { ref, onMounted, watch, computed } from "vue";
import router from "@/router";
import { useRoute } from "vue-router";
import { Search, ArrowDown, ArrowUp } from "@element-plus/icons-vue";
import { getSearchAllArea, getSearchAllYear, getSurveyList, getSearchAllCountry } from "@/api/marketAccessRestrictions";
import AnalysisBox from "@/components/base/boxBackground/analysisBox.vue"
const route = useRoute();
import SurveyHistory from "@/views/marketAccessRestrictions/com/SurveyHistory.vue"
// 顶部过滤项
const searchText = ref("");
......@@ -319,22 +212,9 @@ const handleFetchSurveyList = async () => {
sortOrder: isSort.value ? "asc" : "desc"
};
const res = await getSurveyList(params);
if (res.code === 200 && res.data) {
surveyInfoList.value = res.data.content.map(item => {
return {
time: item.searchdate,
timeZH: item.searchdatezh,
title: item.searchname,
content: item.content,
status: item.casestatus,
areaList: item.searchArea || [],
countryList: item.countryImage || [],
tag: item.sortcode || "337",
id: item.searchid,
sortImageUrl: item.sortimageurl
};
});
totalDiscussNum.value = res.data.totalElements || 0;
if (res.code === 200) {
surveyInfoList.value = res.data?.content || [];
totalDiscussNum.value = res.data?.totalElements || 0;
}
} catch (error) {
console.error("获取调查列表失败", error);
......@@ -353,17 +233,6 @@ const handleSearch = () => {
handleFetchSurveyList();
};
const handleToSingleCase = item => {
const curRoute = router.resolve({
path: "/marketSingleCaseLayout/overview",
query: {
id: "337",
searchId: item.id + ""
}
});
window.open(curRoute.href, "_blank");
};
// 监听过滤条件
watch(
[checkedSurveyYears, checkedAreaList, checkedCountryList, isSort, filterStage, filterParty, filterReason],
......@@ -423,8 +292,7 @@ onMounted(async () => {
.case-wrapper {
width: 1600px;
margin: 0 auto;
padding-top: 20px;
padding-bottom: 35px;
padding: 20px 0;
.wrapper-header {
height: 32px;
......@@ -439,12 +307,10 @@ onMounted(async () => {
align-items: center;
.search-box {
:deep(.el-input__wrapper) {
background-color: #fff;
border: 1px solid rgba(234, 236, 238, 1);
border-radius: 4px;
}
}
.dropdown-filters {
display: flex;
......@@ -490,7 +356,7 @@ onMounted(async () => {
.wrapper-main {
display: flex;
justify-content: space-between;
gap: 16px;
.left {
width: 360px;
......@@ -526,21 +392,10 @@ onMounted(async () => {
.checkbox-group {
padding: 10px 0 0 25px;
// display: flex;
// flex-direction: column;
// .area-grid,
// .year-grid,
// .country-grid {
// display: grid;
// grid-template-columns: repeat(2, 1fr);
// gap: 4px;
// }
.filter-checkbox {
width: 130px;
margin-bottom: 8px;
height: 32px;
:deep(.el-checkbox__label) {
font-size: 16px;
color: #5f656c;
......@@ -563,210 +418,8 @@ onMounted(async () => {
}
.right {
width: 1224px;
// border-radius: 10px;
// box-shadow: 0px 0px 15px 0px rgba(60, 87, 126, 0.2);
// background: #fff;
// padding-bottom: 20px;
.right-header {
height: 48px;
display: flex;
align-items: center;
// border-bottom: 1px solid #eaeeef;
padding: 0 20px;
.icon {
width: 22px;
img {
width: 100%;
}
}
.title {
margin-left: 12px;
font-size: 20px;
font-weight: 700;
color: var(--color-main-active);
}
}
.right-main {
padding: 18px 27px 0 24px;
border-top: 1px solid var(--bg-black-5);
.timeline-item {
display: flex;
cursor: pointer;
.timeline-date {
width: 100px;
text-align: right;
padding-right: 20px;
.date-text {
height: 24px;
font-size: 16px;
font-weight: 700;
color: var(--color-main-active);
line-height: 24px;
letter-spacing: 1px;
}
}
.timeline-line-box {
width: 40px;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
.timeline-icon {
width: 32px;
height: 32px;
z-index: 2;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
img {
width: 100%;
height: 100%;
}
.default-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: orange;
}
}
.timeline-line {
width: 2px;
flex: 1;
background: #eaeeef;
margin: 4px 0;
}
}
.timeline-content-card {
flex: 1;
border-radius: 8px;
padding: 0 20px;
margin-left: 10px;
margin-bottom: 24px;
transition: all 0.3s;
&:hover {
.card-header>.tag-box>.title {
color: var(--color-main-active);
text-decoration: underline;
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
.tag-box {
display: flex;
align-items: center;
gap: 12px;
.tag-337 {
padding: 2px 10px;
border: 1px solid #91caff;
background: #e6f4ff;
color: #055fc2;
border-radius: 4px;
font-weight: 700;
font-size: 16px;
}
.title {
font-size: 18px;
font-weight: 700;
color: #3b414b;
}
}
.status {
font-size: 16px;
color: #84888e;
.dot {
margin-right: 4px;
}
}
.status-active {
color: var(--color-main-active);
}
}
.card-body {
font-size: 16px;
color: #5f656c;
line-height: 1.6;
margin-bottom: 16px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
overflow: hidden;
}
.card-footer {
display: flex;
gap: 8px;
align-items: center;
.footer-left-tags {
display: flex;
gap: 8px;
.area-tag {
padding: 2px 12px;
background: #e6f7ff;
border: 1px solid #91d5ff;
color: #1890ff;
border-radius: 4px;
font-size: 14px;
}
}
.footer-right-flags {
display: flex;
gap: 4px;
.flag-icon {
width: 24px;
height: 24px;
border-radius: 50%;
overflow: hidden;
border: 1px solid #eee;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
}
}
}
}
}
flex: auto;
width: 20px;
.right-footer {
height: 74px;
......@@ -774,7 +427,6 @@ onMounted(async () => {
justify-content: space-between;
align-items: center;
padding: 0 40px;
border-top: 1px solid rgba(234, 236, 238, 1);
.footer-left {
font-size: 14px;
......@@ -784,8 +436,4 @@ onMounted(async () => {
}
}
}
.filter-checkbox {
width: 130px;
}
</style>
......@@ -25,5 +25,9 @@ onMounted(() => {
</script>
<style lang="scss" scoped>
.case-wrap {
width: 100%;
height: 100%;
overflow-y: auto;
}
</style>
\ No newline at end of file
<template>
<div class="wrap">
<div class="header">
<div class="header-top">
<div class="left">
<div class="icon">
<div class="page-box">
<div class="page-top">
<div class="head-box">
<div class="head-icon">
<img :src="curSurvey.image" alt="" />
</div>
<div class="info">
<div class="title">{{ curSurvey.title }}</div>
<div class="content">
{{ curSurvey.desc }}
</div>
</div>
<div class="head-info">
<div class="head-name one-line-ellipsis">{{ curSurvey.title }}</div>
<div class="head-text one-line-ellipsis">{{ curSurvey.desc }}</div>
</div>
<!-- <div class="right">
<div class="icon">
<!-- <div class="head-button">
<div class="button-icon">
<img src="./assets/images/button-icon.png" alt="" />
</div>
<div class="text">{{ "查看官网" }}</div>
<div class="button-text">{{ "查看官网" }}</div>
</div> -->
</div>
<div class="header-footer">
<div
class="btn-item"
:class="{ btnItemActive: activeBtnName === item.name }"
v-for="(item, index) in btnList"
:key="index"
@click="handleClickBtn(item)"
>
<div class="page-tabs">
<div :class="['tab-item', {'tab-active': activeName==item.name}]" v-for="(item, index) in tabList" :key="index" @click="handleClickBtn(item)">
<div class="icon">
<img :src="item.acitveIcon" alt="" v-if="activeBtnName === item.name" />
<img :src="item.activeIcon" alt="" v-if="activeName==item.name" />
<img :src="item.icon" alt="" v-else />
</div>
<div class="text" :class="{ textActive: activeBtnName === item.name }">
<div class="text" :class="{ textActive: activeName==item.name }">
{{ item.name }}
</div>
</div>
......@@ -58,17 +48,17 @@ import Img301 from "./assets/images/301.png";
import { useRoute } from "vue-router";
const route = useRoute();
const btnList = ref([
const tabList = ref([
{
name: "调查案件",
icon: icon2,
acitveIcon: icon2Active,
activeIcon: icon2Active,
path: "/marketAccessLayout/case"
},
{
name: "数据统计",
icon: icon1,
acitveIcon: icon1Active,
activeIcon: icon1Active,
path: "/marketAccessLayout/overview"
}
]);
......@@ -78,89 +68,80 @@ const curSurvey = computed(() => {
return {
title: "301调查",
desc: '由美国贸易代表办公室依据《1974年贸易法》第301条针对"不合理或不公正贸易做法"发起的调查',
image: Img301
image: Img301,
};
} else if (route.query.id === "232") {
return {
title: "232调查",
desc: "依据《1962年贸易扩展法》第232条款,授权美国商务部对“特定进口产品是否威胁或损害美国国家安全”而开展的全面调查。",
image: Img232
image: Img232,
};
} else {
return {
title: "337调查",
desc: '美国国际贸易委员会根据《1930年关税法》第337节及相关修正案进行的调查,主要针对进口贸易中的知识产权侵权行为以及其他不公平竞争行为',
image: Img337
image: Img337,
};
}
});
const activeBtnName = ref("调查案件");
const activeName = ref("调查案件");
const handleClickBtn = item => {
activeBtnName.value = item.name;
activeName.value = item.name;
router.push({
path: item.path,
query: {
id: route.query.id
}
query: { id: route.query.id }
});
};
onMounted(() => {
if (route.path === "/marketAccessLayout/overview") {
activeBtnName.value = "数据统计";
activeName.value = "数据统计";
} else {
activeBtnName.value = "调查案件";
activeName.value = "调查案件";
}
});
</script>
<style lang="scss" scoped>
.wrap {
.page-box {
width: 100%;
height: 100%;
overflow: hidden;
overflow-y: auto;
.header {
width: 1920px;
height: 148px;
box-sizing: border-box;
border-bottom: 1px solid rgba(230, 231, 232, 1);
background: rgba(255, 255, 255, 1);
position: sticky;
top: 0;
z-index: 99999999;
.header-top {
display: flex;
height: 100px;
justify-content: space-between;
.left {
margin-left: 160px;
margin-top: 24px;
flex-direction: column;
background: rgba(255, 255, 255, 1);
.page-top {
width: 1600px;
margin: 0 auto;
box-sizing: border-box;
.head-box {
width: 100%;
display: flex;
.icon {
padding: 30px 0 20px;
.head-icon {
width: 54px;
height: 54px;
font-size: 0px;
margin-right: 16px;
img {
width: 100%;
height: 100%;
}
}
.info {
margin-left: 14px;
margin-top: -1px;
.title {
.head-info {
width: 20px;
flex: auto;
.head-name {
width: 100%;
height: 26px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 20px;
font-weight: 700;
font-weight: bold;
line-height: 26px;
}
.content {
margin-top: 1px;
.head-text {
width: 100%;
height: 24px;
color: rgba(95, 101, 108, 1);
font-family: Microsoft YaHei;
......@@ -169,61 +150,54 @@ onMounted(() => {
line-height: 24px;
}
}
}
.right {
margin-top: 24px;
margin-right: 160px;
.head-button {
margin-left: 100px;
width: 120px;
height: 36px;
border-radius: 6px;
background: var(--color-main-active);
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
.icon {
.button-icon {
width: 16px;
height: 16px;
margin-left: 16px;
margin-top: 10px;
position: relative;
z-index: 99;
font-size: 0;
img {
width: 100%;
height: 100%;
}
}
.text {
.button-text {
margin-left: 8px;
margin-top: 7px;
height: 22px;
color: rgba(255, 255, 255, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 22px;
}
}
}
.header-footer {
height: 48px;
margin-left: 160px;
.page-tabs {
display: flex;
gap: 24px;
.btn-item {
align-items: flex-start;
.tab-item {
height: 40px;
display: flex;
align-items: center;
gap: 4px;
cursor: pointer;
margin-right: 24px;
.icon {
width: 16px;
height: 16px;
margin-top: 16px;
font-size: 0;
img {
width: 100%;
height: 100%;
}
}
.text {
margin-left: 2px;
margin-top: 12px;
height: 24px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
......@@ -236,17 +210,26 @@ onMounted(() => {
font-weight: 700;
}
}
.btnItemActive {
border-bottom: 3px solid var(--color-main-active);
.tab-active {
position: relative;
&::after {
content: "";
position: absolute;
bottom: 0px;
left: 0;
width: 100%;
height: 3px;
background-color: var(--color-main-active);
}
}
}
}
.main {
width: 1920px;
height: 868px;
background: #f7f8f9;
overflow: hidden;
overflow-y: auto;
border-top: 1px solid rgba(230, 231, 232, 1);
background-color: #f7f8f9;
width: 100%;
height: 20px;
flex: auto;
}
}
</style>
\ No newline at end of file
......@@ -201,13 +201,16 @@ onMounted(() => {
<style lang="scss" scoped>
.wrap {
width: 100%;
height: 100%;
overflow: hidden;
width: 1600px;
margin: 0 auto;
padding: 20px 0;
display: flex;
flex-direction: column;
gap: 16px;
.top {
display: flex;
justify-content: space-between;
margin: 16px 160px;
.item {
width: 388px;
height: 80px;
......@@ -338,7 +341,6 @@ onMounted(() => {
.center {
display: flex;
justify-content: space-between;
margin: 0 160px;
.box1 {
width: 792px;
height: 360px;
......@@ -364,7 +366,6 @@ onMounted(() => {
}
}
.footer {
margin: 16px 160px;
display: flex;
justify-content: space-between;
.box3 {
......
<template>
<div class="wrap">
<!-- <div class="sider-tab-box">
<SiderTabs :siderList="siderTabList" @clickSiderItem="handleClickTabItem" />
</div> -->
<div class="top">
<div class="box1 box" v-loading="box1Loading">
<div class="box-header">
......@@ -143,28 +140,6 @@ import getSankeyChart from "./utils/sankey";
import getPieChart from "./utils/piechart";
import getbarChart from "@/views/bill/utils/barchart";
import { getSearchCountry, getStatArea, getStatNum, getSearchDirection } from "@/api/marketAccessRestrictions";
import SiderTabs from "@/components/base/SiderTabs/index.vue"
// const siderTabList = ref([
// {
// name: '分析内11111',
// active: false
// },
// {
// name: '分析内2',
// active: true
// },
// {
// name: '分析内3',
// active: false
// },
// ])
// const handleClickTabItem = (value) => {
// console.log('val',value);
// }
const selectYear = ref("2025");
......@@ -339,14 +314,12 @@ onMounted(() => {
<style lang="scss" scoped>
.wrap {
overflow: hidden;
position: relative;
.sider-tab-box{
position: absolute;
left: 20px;
top: 20px;
}
width: 1600px;
margin: 0 auto;
padding: 20px 0;
display: flex;
flex-direction: column;
gap: 16px;
.box {
width: 792px;
......@@ -473,7 +446,6 @@ onMounted(() => {
display: flex;
justify-content: center;
gap: 16px;
margin: 16px auto;
.box1 {
.box1-main {
......@@ -494,9 +466,6 @@ onMounted(() => {
display: flex;
justify-content: center;
gap: 16px;
margin: 0 auto;
margin-bottom: 16px;
.box3 {
.box3-main {
height: 304px;
......
......@@ -41,45 +41,6 @@
</div>
<div class="footer">
<div class="box3" v-loading="box3Loading">
<!-- <div class="box-header">
<div class="header-left"></div>
<div class="title">中国公司受调查情况</div>
<div class="header-btn-box">
<div class="btn" :class="{ btnActive: btnActiveName === '调查次数' }" @click="handleClickBox3Btn('调查次数')">
调查次数
</div>
<div class="btn" :class="{ btnActive: btnActiveName === '注册地分布' }" @click="handleClickBox3Btn('注册地分布')">
注册地分布
</div>
</div>
<div class="header-right">
<div class="icon">
<img src="@/assets/icons/box-header-icon1.png" alt="" />
</div>
<div class="icon">
<img src="@/assets/icons/box-header-icon2.png" alt="" />
</div>
<div class="icon">
<img src="@/assets/icons/box-header-icon3.png" alt="" />
</div>
</div>
</div>
<div v-show="btnActiveName === '调查次数'" class="box3-main" id="chart3"></div>
<div v-show="btnActiveName === '注册地分布'" class="box3-main1">
<div class="box3-main1-left">
<div class="box3-main1-left-item" v-for="(item, index) in mapData" :key="index">
<div class="box3-main1-left-item-left">{{ index + 1 }}</div>
<div class="box3-main1-left-item-center">{{ item.name }}</div>
<div class="box3-main1-left-item-right">
{{ item.value + "次" }}
</div>
</div>
</div>
<div class="box3-main1-right" id="chartMap"></div>
</div>
<div class="box3-footer">
<TipTab />
</div> -->
<AnalysisBox title="中国公司受调查情况">
<template #header-btn>
<div class="header-btn-box">
......@@ -118,9 +79,6 @@
</AnalysisBox>
</div>
</div>
<!-- <div class="graph-box" id="graphChart">
</div> -->
</div>
</template>
<script setup>
......@@ -523,26 +481,16 @@ onMounted(() => {
<style lang="scss" scoped>
.wrap {
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
.graph-box {
position: absolute;
top: 100px;
left: 100px;
width: 1000px;
height: 600px;
border-radius: 10px;
z-index: 9999;
background: #fff;
}
width: 1600px;
margin: 0 auto;
padding: 20px 0;
display: flex;
flex-direction: column;
gap: 16px;
.top {
display: flex;
justify-content: space-between;
margin: 16px 160px;
.item {
width: 388px;
......@@ -635,36 +583,6 @@ onMounted(() => {
font-weight: 700;
}
// .header-btn-box {
// position: absolute;
// top: 14px;
// right: 120px;
// display: flex;
// .btn {
// margin-left: 8px;
// height: 28px;
// padding: 0 8px;
// box-sizing: border-box;
// border: 1px solid rgba(230, 231, 232, 1);
// border-radius: 4px;
// background: rgba(255, 255, 255, 1);
// text-align: center;
// line-height: 28px;
// color: rgba(59, 65, 75, 1);
// font-family: Microsoft YaHei;
// font-size: 16px;
// font-weight: 400;
// cursor: pointer;
// }
// .btnActive {
// border: 1px solid var(--color-main-active);
// background: rgba(246, 250, 255, 1);
// color: var(--color-main-active);
// }
// }
.header-right {
position: absolute;
top: 14px;
......@@ -688,7 +606,6 @@ onMounted(() => {
.center {
display: flex;
justify-content: space-between;
margin: 0 160px;
.box1 {
width: 792px;
......@@ -721,7 +638,6 @@ onMounted(() => {
}
.footer {
margin: 16px 160px;
display: flex;
justify-content: space-between;
......
<template>
<div class="overview-wrap">
<Discussion1 v-if="showDiscussion ==='337'"></Discussion1>
<Discussion2 v-else-if="showDiscussion ==='232'"></Discussion2>
<Discussion3 v-else></Discussion3>
<Discussion2 v-if="showDiscussion ==='232'"></Discussion2>
<Discussion3 v-if="showDiscussion ==='301'"></Discussion3>
</div>
</template>
......@@ -28,5 +28,6 @@ onMounted(() => {
.overview-wrap {
width: 100%;
height: 100%;
overflow-y: auto;
}
</style>
\ No newline at end of file
<template>
<div class="wrapper">
<div class="box1 box">
<div class="box-header">
<div class="header-left"></div>
<div class="title">行业背景</div>
<div class="header-right">
<div class="icon">
<img src="@/assets/icons/box-header-icon2.png" alt="" />
</div>
<div class="icon">
<img src="@/assets/icons/box-header-icon3.png" alt="" />
</div>
</div>
</div>
<AnalysisBox title="行业背景" :showAllBtn="false" height="auto">
<div class="box1-main">
<div class="box1-main-item" v-for="(bg, index) in bgList" :key="index">
<!-- <div class="id">{{ index + 1 + "." }}</div> -->
<div class="title">{{ bg.title }}</div>
</div>
</div>
<div class="box-footer">
<div class="box-footer-left">
<img src="@/assets/icons/box-footer-left-icon.png" alt="" />
</div>
<div class="box-footer-center">
{{
`美国 232 调查认定,钕铁硼永磁体(关键领域核心材料)进口威胁其国家安全,因中国主导全球市场、美本土产能薄弱,未提关税,提议扶持本土供应链。`
}}
</div>
<div class="box-footer-right">
<img src="@/assets/icons/box-footer-right-icon.png" alt="" />
</div>
</div>
</div>
<div class="box2 box">
<div class="box-header">
<div class="header-left"></div>
<div class="title">市场需求</div>
<div class="header-right">
<div class="icon">
<img src="@/assets/icons/box-header-icon2.png" alt="" />
</div>
<div class="icon">
<img src="@/assets/icons/box-header-icon3.png" alt="" />
</div>
</div>
</div>
<div class="box2-main">
<div class="box2-main-item" v-for="(demand, index) in demandList" :key="index">
<div class="box2-main-item-header">
<div class="header-left">
{{ demand.title }}
</div>
<div class="header-right">
<div class="icon">
<img src="@/assets/images/icon-open.png" alt="" />
</div>
<div class="text">{{ "跳转原文" }}</div>
</div>
</div>
<div class="box2-main-item-content">
<div class="content-item" v-for="(val, idx) in demand.data" :key="idx">
<!-- <div class="id">{{ idx + 1 + "." }}</div> -->
<div class="title">{{ val.title }}</div>
</div>
</div>
</div>
</div>
<div class="box-footer">
<div class="box-footer-left">
<img src="@/assets/icons/box-footer-left-icon.png" alt="" />
</div>
<div class="box-footer-center">
{{
`美国 232 调查认定,钕铁硼永磁体(关键领域核心材料)进口威胁其国家安全,因中国主导全球市场、美本土产能薄弱,未提关税,提议扶持本土供应链。`
}}
</div>
<div class="box-footer-right">
<img src="@/assets/icons/box-footer-right-icon.png" alt="" />
</div>
</div>
</div>
<div class="box3 box">
<div class="box-header">
<div class="header-left"></div>
<div class="title">国内外供应情况</div>
<div class="header-right">
<div class="icon">
<img src="@/assets/icons/box-header-icon1.png" alt="" />
</div>
<div class="icon">
<img src="@/assets/icons/box-header-icon2.png" alt="" />
</div>
<div class="icon">
<img src="@/assets/icons/box-header-icon3.png" alt="" />
</div>
</div>
</div>
<div class="box3-main">
<div class="box3-main-item" v-for="(supply, index) in supplyList" :key="index">
<div class="box3-main-item-header">
<div class="header-left">
{{ supply.title }}
</div>
<div class="header-right">
<div class="icon">
<img src="@/assets/images/icon-open.png" alt="" />
</div>
<div class="text">{{ "跳转原文" }}</div>
</div>
</div>
<div class="box3-main-item-content">
<div class="content-item" v-for="(val, idx) in supply.data" :key="idx">
<!-- <div class="id">{{ idx + 1 + "." }}</div> -->
<div class="title">{{ val.title }}</div>
<div class="data-list">
<div class="data-item" v-for="(bg, index) in bgList" :key="index">{{ bg.title }}</div>
</div>
<AiTips tips="钒作为被美国列为对经济与国家安全至关重要且供应链易受中断的关键矿产,广泛应用于国防、关键基础设施等多个领域,美国钒产业以二次生产为主、产业高度集中且依赖进口原料,同时通过征收反倾销税、加征关税及签订国际合作协议等方式管控钒进口。"></AiTips>
</div>
</AnalysisBox>
<SurveyConclusion title="市场需求" :listData="demandList" tips="美国钒市场年均表观消费量约 8590 吨且进口依赖度超 80%,需求主要集中在钢铁产业(占 90%),钛产业和非冶金领域各占 5%,2020 年受新冠疫情冲击需求下滑;全球钒需求以钢铁产业为主(90%-93%),中国是最大消费国,航空航天领域需求稳定、钒液流电池为新兴增长点,化工领域提供稳定补充。"></SurveyConclusion>
<SurveyConclusion title="国内外供应情况" :listData="supplyList" tips="美国钒供应以二次生产为主、原生供应仅一家企业且已停产,存在潜在产能但需达到一定钒价才具经济性,原料进口依赖度高;全球钒产能集中在中国、俄罗斯等四国,中国主导供应,欧盟、南非等为主要出口方,美国进口来源集中且本土生产竞争力弱,多国规划新增产能有望大幅提升全球矿山产量。"></SurveyConclusion>
<SurveyConclusion title="未来产业格局" :listData="box4List" tips="美国钒产业短期将通过产能扩张提升自给率,中长期有望进一步降低进口依赖度,同时受政策驱动与多重风险挑战影响;全球钒市场需求将受钢铁产业结构调整和储能需求增长支撑,供应格局向多极化发展,价格受产能释放与储能需求影响呈现短期低位、中长期有望回升的态势,贸易与政策层面的竞争和管控将持续加剧。"></SurveyConclusion>
<AnalysisBox title="调查建议" :showAllBtn="false" height="auto">
<div class="box5-main">
<div class="data-list">
<div class="data-item" v-for="(item, index) in suggestionList" :key="index">
<div class="item-head">
<div class="item-name">{{ item.title }}</div>
<div class="button-box">
<div class="button-icon">
<img src="@/views/marketAccessRestrictions/assets/icons/open.png" alt="" />
</div>
</div>
<div class="box-footer">
<div class="box-footer-left">
<img src="@/assets/icons/box-footer-left-icon.png" alt="" />
</div>
<div class="box-footer-center">
{{
`美国 232 调查认定,钕铁硼永磁体(关键领域核心材料)进口威胁其国家安全,因中国主导全球市场、美本土产能薄弱,未提关税,提议扶持本土供应链。`
}}
</div>
<div class="box-footer-right">
<img src="@/assets/icons/box-footer-right-icon.png" alt="" />
</div>
</div>
</div>
<div class="box4 box">
<div class="box-header">
<div class="header-left"></div>
<div class="title">未来产业格局</div>
<div class="header-right">
<div class="icon">
<img src="@/assets/icons/box-header-icon1.png" alt="" />
</div>
<div class="icon">
<img src="@/assets/icons/box-header-icon2.png" alt="" />
</div>
<div class="icon">
<img src="@/assets/icons/box-header-icon3.png" alt="" />
</div>
</div>
</div>
<div class="box4-main">
<div class="box4-main-item" v-for="(item, index) in box4List" :key="index">
<div class="box4-main-item-header">
<div class="header-left">
{{ item.title }}
</div>
<div class="header-right">
<div class="icon">
<img src="@/assets/images/icon-open.png" alt="" />
</div>
<div class="text">{{ "跳转原文" }}</div>
<div class="button-text">跳转原文</div>
</div>
</div>
<div class="box4-main-item-content">
<div class="item-down">
<div class="content-item" v-for="(val, idx) in item.data" :key="idx">
<!-- <div class="id">{{ idx + 1 + "." }}</div> -->
<div class="title">{{ val.title }}</div>
</div>
</div>
</div>
</div>
<div class="box-footer">
<div class="box-footer-left">
<img src="@/assets/icons/box-footer-left-icon.png" alt="" />
</div>
<div class="box-footer-center">
{{
`美国 232 调查认定,钕铁硼永磁体(关键领域核心材料)进口威胁其国家安全,因中国主导全球市场、美本土产能薄弱,未提关税,提议扶持本土供应链。`
}}
</div>
<div class="box-footer-right">
<img src="@/assets/icons/box-footer-right-icon.png" alt="" />
</div>
</div>
</div>
<div class="box5 box">
<div class="box-header">
<div class="header-left"></div>
<div class="title">调查建议</div>
<div class="header-right">
<div class="icon">
<img src="@/assets/icons/box-header-icon1.png" alt="" />
</div>
<div class="icon">
<img src="@/assets/icons/box-header-icon2.png" alt="" />
</div>
<div class="icon">
<img src="@/assets/icons/box-header-icon3.png" alt="" />
</div>
</div>
</div>
<div class="box5-main">
<div class="box5-main-item" v-for="(suggestion, index) in suggestionList" :key="index">
<div class="box5-main-item-header">{{ suggestion.title }}</div>
<div class="box5-main-item-content">
<div class="content-item" v-for="(val, idx) in suggestion.data" :key="idx">
<div class="content-item-title">{{val.title }}</div>
<div class="content-item-info">
<div class="desc">{{ val.info.desc }}</div>
......@@ -221,20 +43,9 @@
</div>
</div>
</div>
<div class="box-footer">
<div class="box-footer-left">
<img src="@/assets/icons/box-footer-left-icon.png" alt="" />
</div>
<div class="box-footer-center">
{{
`美国 232 调查认定,钕铁硼永磁体(关键领域核心材料)进口威胁其国家安全,因中国主导全球市场、美本土产能薄弱,未提关税,提议扶持本土供应链。`
}}
</div>
<div class="box-footer-right">
<img src="@/assets/icons/box-footer-right-icon.png" alt="" />
</div>
</div>
<AiTips tips="美国为降低盟友及自身在钕铁硼磁体价值链对中国的依赖、保障国家安全,一方面推动关键矿物多边及双边合作,另一方面通过支持相关税收抵免法案、分配额外资金等立法与政策举措,强化稀土及非稀土磁体相关国内供应链建设。"></AiTips>
</div>
</AnalysisBox>
</div>
</template>
......@@ -242,6 +53,8 @@
import { ref, onMounted } from "vue";
import { useRoute } from "vue-router";
import { getReportAnalyze } from "@/api/marketAccessRestrictions/index.js";
import SurveyConclusion from "@/views/marketAccessRestrictions/com/SurveyConclusion.vue";
import AiTips from "@/views/marketAccessRestrictions/com/AiTips.vue";
const route = useRoute();
const searchId = route.query.searchId || "232";
......@@ -264,22 +77,37 @@ const getData = async () => {
// 市场需求
getReportAnalyze({ searchId, type: "02" }).then((res) => {
if (res.data) {
demandList.value = groupData(res.data);
if (res.code==200) {
demandList.value = res.data.map(item => {
return {
title: item.TITLE,
data: item.CONTENT?.split(/\r\n|\n/).filter(line => line.trim()) || [],
};
});
}
});
// 国内外供应情况
getReportAnalyze({ searchId, type: "03" }).then((res) => {
if (res.data) {
supplyList.value = groupData(res.data);
if (res.code==200) {
supplyList.value = res.data.map(item => {
return {
title: item.TITLE,
data: item.CONTENT?.split(/\r\n|\n/).filter(line => line.trim()) || [],
};
});
}
});
// 未来产业格局
getReportAnalyze({ searchId, type: "04" }).then((res) => {
if (res.data) {
box4List.value = groupData(res.data);
if (res.code==200) {
box4List.value = res.data.map(item => {
return {
title: item.TITLE,
data: item.CONTENT?.split(/\r\n|\n/).filter(line => line.trim()) || [],
};
});
}
});
......@@ -336,411 +164,79 @@ onMounted(() => {
<style lang="scss" scoped>
.wrapper {
width: 100%;
height: 100%;
overflow-y: auto;
.box-header {
height: 56px;
display: flex;
position: relative;
.header-left {
margin-top: 20px;
width: 8px;
height: 16px;
border-radius: 0 4px 4px 0;
background: var(--color-main-active);
}
.title {
margin-left: 14px;
margin-top: 16px;
height: 24px;
line-height: 24px;
color: var(--color-main-active);
font-family: Microsoft YaHei;
font-size: 18px;
font-weight: 700;
}
.header-btn-box {
position: absolute;
top: 14px;
right: 116px;
display: flex;
.btn {
margin-left: 8px;
height: 28px;
padding: 0 8px;
box-sizing: border-box;
border: 1px solid rgba(230, 231, 232, 1);
border-radius: 4px;
background: rgba(255, 255, 255, 1);
text-align: center;
line-height: 28px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
cursor: pointer;
}
.btnActive {
border: 1px solid var(--color-main-active);
background: rgba(246, 250, 255, 1);
color: var(--color-main-active);
}
}
.header-right {
position: absolute;
top: 14px;
right: 12px;
display: flex;
justify-content: flex-end;
gap: 4px;
.icon {
width: 28px;
height: 28px;
img {
width: 100%;
height: 100%;
}
}
}
}
.box-footer {
margin: 24px auto 0;
width: 1568px;
height: 64px;
box-sizing: border-box;
border: 1px solid rgba(231, 243, 255, 1);
border-radius: 4px;
background: rgba(246, 250, 255, 1);
display: flex;
align-items: center;
gap: 13px;
padding: 0 12px;
.box-footer-left {
width: 19px;
height: 20px;
img {
width: 100%;
height: 100%;
}
}
.box-footer-center {
width: 939px;
color: rgba(5, 95, 194, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 24px;
letter-spacing: 0px;
text-align: justify;
}
.box-footer-right {
width: 24px;
height: 24px;
img {
width: 100%;
height: 100%;
}
}
}
.box {
border: 1px solid rgba(234, 236, 238, 1);
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background: rgba(255, 255, 255, 1);
margin: 16px auto;
}
.box1 {
width: 1600px;
// height: 588px;
.box1-main {
// height: 425px;
margin-top: 2px;
overflow: hidden;
overflow-y: auto;
}
.box1-main-item {
margin: 0 auto;
width: 1552px;
padding: 12px 23px 12px 44px;
margin: 20px auto;
display: flex;
flex-direction: column;
gap: 16px;
}
.box1-main {
padding: 0 22px 20px;
.data-list {
margin-bottom: 16px;
border-top: 1px solid rgba(234, 236, 238, 1);
.data-item {
letter-spacing: 1px;
padding: 12px 20px 12px 40px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-family: Source Han Sans CN;
font-size: 16px;
font-weight: 400;
line-height: 30px;
letter-spacing: 0px;
text-align: justify;
display: flex;
box-sizing: border-box;
border-bottom: 1px solid rgba(234, 236, 238, 1);
.id {
width: 18px;
}
.title {
flex: 1;
}
}
}
.box2 {
width: 1600px;
// height: 921px;
.box2-main {
// height: 760px;
width: 1552px;
margin: 2px auto 0;
.box2-main-item {
width: 1552px;
.box2-main-item-header {
}
.box5-main {
padding: 0 22px 20px;
.data-list {
margin-bottom: 16px;
border-top: 1px solid rgba(234, 236, 238, 1);
.data-item {
.item-head {
padding: 0 20px;
height: 48px;
display: flex;
justify-content: space-between;
align-items: center;
box-sizing: border-box;
border-bottom: 1px solid rgba(234, 236, 238, 1);
border-top: 1px solid rgba(234, 236, 238, 1);
background: rgba(247, 248, 249, 1);
.header-left {
margin-left: 12px;
height: 24px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 18px;
font-weight: 700;
line-height: 24px;
letter-spacing: 0px;
text-align: justify;
}
.header-right {
margin-right: 22px;
display: flex;
gap: 5px;
justify-content: flex-end;
.icon {
width: 16px;
height: 16px;
img {
width: 100%;
height: 100%;
}
}
.text {
height: 14px;
color: rgba(5, 95, 194, 1);
font-family: Microsoft YaHei;
font-size: 12px;
font-weight: 400;
line-height: 14px;
letter-spacing: 0px;
text-align: justify;
}
}
}
.box2-main-item-content {
.content-item {
box-sizing: border-box;
padding: 12px 23px 12px 44px;
border-bottom: 1px solid rgba(234, 236, 238, 1);
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 30px;
letter-spacing: 0px;
text-align: justify;
display: flex;
.id {
width: 18px;
}
.title {
flex: 1;
}
}
}
}
}
}
.box3 {
width: 1600px;
// height: 957px;
.box3-main {
// height: 795px;
width: 1552px;
margin: 2px auto 0;
.box3-main-item {
width: 1552px;
.box3-main-item-header {
height: 48px;
display: flex;
justify-content: space-between;
align-items: center;
box-sizing: border-box;
border-bottom: 1px solid rgba(234, 236, 238, 1);
border-top: 1px solid rgba(234, 236, 238, 1);
background: rgba(247, 248, 249, 1);
.header-left {
margin-left: 12px;
height: 24px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
.item-name {
width: 20px;
flex: auto;
font-family: Source Han Sans CN;
font-size: 18px;
font-weight: 700;
line-height: 24px;
letter-spacing: 0px;
text-align: justify;
}
.header-right {
margin-right: 22px;
display: flex;
gap: 5px;
justify-content: flex-end;
.icon {
width: 16px;
height: 16px;
img {
width: 100%;
height: 100%;
}
}
.text {
height: 14px;
color: rgba(5, 95, 194, 1);
font-family: Microsoft YaHei;
font-size: 12px;
font-weight: 400;
line-height: 14px;
letter-spacing: 0px;
text-align: justify;
}
}
}
.box3-main-item-content {
.content-item {
box-sizing: border-box;
padding: 12px 23px 12px 44px;
border-bottom: 1px solid rgba(234, 236, 238, 1);
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
font-weight: bold;
line-height: 30px;
letter-spacing: 0px;
text-align: justify;
display: flex;
.id {
width: 18px;
}
.title {
flex: 1;
}
}
color: var(--text-primary-80-color);
}
}
}
}
.box4 {
width: 1600px;
// height: 954px;
.box4-main {
// height: 795px;
width: 1552px;
margin: 2px auto 0;
.box4-main-item {
width: 1552px;
.box4-main-item-header {
height: 48px;
.button-box {
display: flex;
justify-content: space-between;
align-items: center;
box-sizing: border-box;
border-bottom: 1px solid rgba(234, 236, 238, 1);
border-top: 1px solid rgba(234, 236, 238, 1);
background: rgba(247, 248, 249, 1);
.header-left {
margin-left: 12px;
height: 24px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 18px;
font-weight: 700;
line-height: 24px;
letter-spacing: 0px;
text-align: justify;
}
.header-right {
margin-right: 22px;
display: flex;
gap: 5px;
justify-content: flex-end;
.icon {
margin-left: 50px;
.button-icon {
width: 16px;
height: 16px;
font-size: 0;
margin-right: 4px;
img {
width: 100%;
height: 100%;
}
}
.text {
height: 14px;
color: rgba(5, 95, 194, 1);
.button-text {
color: var(--color-primary-100);
font-family: Microsoft YaHei;
font-size: 12px;
font-weight: 400;
line-height: 14px;
letter-spacing: 0px;
text-align: justify;
}
}
}
.box4-main-item-content {
.content-item {
box-sizing: border-box;
padding: 12px 23px 12px 44px;
border-bottom: 1px solid rgba(234, 236, 238, 1);
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 30px;
letter-spacing: 0px;
text-align: justify;
display: flex;
.id {
width: 18px;
}
.title {
flex: 1;
line-height: 12px;
}
}
}
}
}
}
.box5 {
width: 1600px;
// height: 813px;
.box5-main {
// height: 650px;
width: 1552px;
margin: 0 auto;
margin-top: 2px;
.box5-main-item {
.box5-main-item-header {
width: 1552px;
height: 48px;
box-sizing: border-box;
border-bottom: 1px solid rgba(234, 236, 238, 1);
border-top: 1px solid rgba(234, 236, 238, 1);
background: rgba(247, 248, 249, 1);
line-height: 48px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 18px;
font-weight: 700;
letter-spacing: 0px;
text-align: justify;
padding-left: 12px;
}
.box5-main-item-content {
.item-down {
margin-bottom: 10px;
.content-item {
.content-item-title {
......@@ -748,7 +244,7 @@ onMounted(() => {
box-sizing: border-box;
border-bottom: 1px solid rgba(234, 236, 238, 1);
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-family: Source Han Sans CN;
font-size: 16px;
font-weight: 700;
line-height: 30px;
......@@ -761,7 +257,7 @@ onMounted(() => {
border-bottom: 1px solid rgba(234, 236, 238, 1);
.desc {
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-family: Source Han Sans CN;
font-size: 16px;
font-weight: 400;
line-height: 30px;
......@@ -821,6 +317,5 @@ onMounted(() => {
}
}
}
}
}
</style>
\ No newline at end of file
......@@ -446,9 +446,9 @@ onMounted(() => {
<style lang="scss" scoped>
.deep-dig-container {
display: flex;
padding: 16px 160px;
width: 1600px;
margin: 20px auto;
gap: 16px;
background: #f7f8f9;
.box {
background: #ffffff;
......@@ -553,7 +553,7 @@ onMounted(() => {
}
.left-section {
width: 320px;
width: 480px;
flex-shrink: 0;
.company-list-box {
......@@ -565,11 +565,12 @@ onMounted(() => {
margin-bottom: 16px;
.area-select {
flex: 0 0 100px;
width: 150px;
}
.search-input {
flex: 1;
flex: auto;
width: 20px;
}
}
......@@ -624,7 +625,8 @@ onMounted(() => {
}
.right-section {
flex: 1;
flex: auto;
width: 20px;
display: flex;
flex-direction: column;
gap: 16px;
......
......@@ -26,5 +26,6 @@ onMounted(() => {
.deepdig-wrap {
width: 100%;
height: 100%;
overflow-y: auto;
}
</style>
\ No newline at end of file
<template>
<div class="wrapper">
<div class="header">
<div class="header-top">
<div class="header-top-left">{{ curSurvey.name }}</div>
<div class="header-top-right">
<div class="title">
{{ curSurvey.title }}
<div class="page-box">
<div class="page-top">
<div class="head-box">
<div class="head-icon">
<img :src="curSurvey.image" alt="" />
</div>
<div class="time">{{ curSurvey.time }}</div>
<div class="head-info">
<div class="head-name one-line-ellipsis">{{ curSurvey.title }}</div>
<div class="head-text">{{ curSurvey.time }}</div>
</div>
<div :class="`item-tag tag-${curSurvey.name}`">{{ curSurvey.name }}调查</div>
</div>
<div class="header-footer">
<div class="nav-box">
<div
class="nav-item"
:class="{ navItemActive: item.isActive }"
v-for="(item, index) in navList"
v-show="item.isShow"
:key="index"
@click="handleClickNav(index)"
>
<div class="page-tabs">
<div :class="['tab-item', {'tab-active': activeName==item.name}]" v-for="(item, index) in tabList" :key="index" @click="handleClickBtn(item)">
<div class="icon">
<img v-if="!item.isActive" :src="item.icon" alt="" />
<img v-else :src="item.activeIcon" alt="" />
<img :src="item.activeIcon" alt="" v-if="activeName==item.name" />
<img :src="item.icon" alt="" v-else />
</div>
<div class="text" :class="{ textActive: item.isActive }">
<div class="text" :class="{ textActive: activeName==item.name }">
{{ item.name }}
</div>
</div>
</div>
<div class="btn-box">
<!-- <div class="btn">
<div class="icon">
<img src="./assets/images/btn-icon1.png" alt="" />
</div>
<div class="text">{{ "查看官网" }}</div>
</div> -->
<div class="btn1">
<div class="icon">
<div style="width:20px; flex:auto;"></div>
<div class="head-button">
<div class="button-icon">
<img src="./assets/images/btn-icon2.png" alt="" />
</div>
<div class="text">{{ "分析报告" }}</div>
</div>
<div class="button-text">{{ "分析报告" }}</div>
</div>
</div>
</div>
......@@ -56,77 +41,50 @@ import { ref, onMounted, computed } from "vue";
import router from "@/router";
import NavIcon1 from "./assets/images/nav-icon1.png";
import NavIcon1Active from "./assets/images/nav-icon1-active.png";
import NavIcon2 from "./assets/images/nav-icon2.png";
import NavIcon2Active from "./assets/images/nav-icon2-active.png";
import NavIcon3 from "./assets/images/nav-icon2.png";
import NavIcon3Active from "./assets/images/nav-icon2-active.png";
import Img337 from "./assets/images/337.png";
import Img232 from "./assets/images/232.png";
import Img301 from "./assets/images/301.png";
import { useRoute } from "vue-router";
const route = useRoute();
const navList = ref([
const tabList = ref([
{
name: "调查概况",
icon: NavIcon1,
activeIcon: NavIcon1Active,
isActive: true,
isShow: true,
path: "/marketSingleCaseLayout/overview"
},
// {
// name: "报告解析",
// icon: NavIcon2,
// activeIcon: NavIcon2Active,
// isActive: false,
// isShow: route.query.id === "337",
// path: "/marketSingleCaseLayout/deepdig"
// },
{
name: "影响分析",
icon: NavIcon3,
activeIcon: NavIcon3Active,
isActive: false,
isShow: true,
path: "/marketSingleCaseLayout/deepdig"
}
]);
const handleClickNav = index => {
navList.value.forEach(item => {
item.isActive = false;
});
navList.value[index].isActive = true;
router.push({
path: navList.value[index].path,
query: {
...route.query
}
});
};
const surveyList = ref([
{
title: "337-TA-1443:外国制造的半导体器件及其下游产品和组件",
time: "2025年7月18日",
image: Img337,
name: "337"
},
{
title: "231-TA-1225:进口药及进口原材料的调查",
time: "2021年9月21日",
image: Img232,
name: "232"
},
{
title: "美国贸易代表第301条关于中国针对海事、物流和造船业以争取主导地位的行动",
time: "2025年4月17日",
image: Img301,
name: "301"
}
]);
// const curSurvey = ref( {
// title: "337-TA-1443:外国制造的半导体器件及其下游产品和组件",
// time: '2025年7月18日',
// name: '337'
// });
const curSurvey = computed(() => {
let survey;
if (route.query.id === "232") {
......@@ -139,175 +97,205 @@ const curSurvey = computed(() => {
return survey;
});
onMounted(() => {});
const activeName = ref("调查案件");
const handleClickBtn = item => {
activeName.value = item.name;
router.push({
path: item.path,
query: { ...route.query }
});
};
onMounted(() => {
console.log('route', route);
if (route.path === "/marketSingleCaseLayout/deepdig") {
activeName.value = "影响分析";
} else {
activeName.value = "调查概况";
}
});
</script>
<style lang="scss" scoped>
.wrapper {
.page-box {
width: 100%;
height: 100%;
overflow: hidden;
overflow-y: auto;
.header {
width: 1920px;
height: 148px;
box-sizing: border-box;
border-bottom: 1px solid rgba(230, 231, 232, 1);
background: rgba(255, 255, 255, 1);
padding: 24px 160px 0;
position: sticky;
top: 0;
z-index: 99999999;
.header-top {
display: flex;
.header-top-left {
width: 48px;
height: 48px;
flex-direction: column;
background: rgba(255, 255, 255, 1);
.page-top {
width: 1600px;
margin: 0 auto;
box-sizing: border-box;
border: 1px solid rgba(145, 202, 255, 1);
border-radius: 4px;
background: rgba(230, 244, 255, 1);
color: var(--color-main-active);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 700;
text-align: center;
line-height: 48px;
.head-box {
width: 100%;
display: flex;
padding: 30px 0 20px;
.head-icon {
width: 54px;
height: 54px;
font-size: 0px;
margin-right: 16px;
img {
width: 100%;
height: 100%;
}
}
.header-top-right {
margin-left: 20px;
.title {
.head-info {
width: 20px;
flex: auto;
.head-name {
width: 100%;
height: 26px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 20px;
font-weight: 700;
font-weight: bold;
line-height: 26px;
}
.time {
.head-text {
width: 100%;
height: 24px;
color: rgba(132, 136, 142, 1);
color: rgba(95, 101, 108, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 24px;
}
}
.item-tag {
height: 24px;
line-height: 24px;
padding: 0 8px;
border-radius: 4px;
font-weight: bold;
font-size: 16px;
letter-spacing: 2px;
}
.header-footer {
display: flex;
justify-content: space-between;
margin-top: 24px;
.nav-box {
height: 48px;
display: flex;
.navItemActive {
border-bottom: 3px solid var(--color-main-active);
.tag-337 {
border: 1px solid #91caff;
background: #e6f4ff;
color: #055fc2;
}
.nav-item {
.tag-232 {
border: 1px solid #b37feb;
background: #f9f0ff;
color: #722ed1;
}
.tag-301 {
border: 1px solid #ffd591;
background: #fff7e6;
color: #fa8c16;
}
.head-button {
margin-left: 100px;
width: 120px;
height: 36px;
border-radius: 6px;
background: var(--color-main-active);
display: flex;
width: 95px;
margin-right: 20px;
justify-content: center;
align-items: center;
cursor: pointer;
.icon {
.button-icon {
width: 16px;
height: 16px;
margin-top: 16px;
font-size: 0;
img {
width: 100%;
height: 100%;
}
}
.text {
width: 72px;
height: 24px;
margin-top: 12px;
margin-left: 4px;
color: rgba(59, 65, 75, 1);
.button-text {
margin-left: 8px;
color: rgba(255, 255, 255, 1);
font-family: Microsoft YaHei;
font-size: 18px;
font-size: 16px;
font-weight: 400;
line-height: 24px;
}
.textActive {
font-size: 18px;
font-weight: 700;
color: var(--color-main-active);
}
}
}
.btn-box {
height: 48px;
.page-tabs {
display: flex;
.btn {
margin-top: 2px;
width: 120px;
height: 36px;
box-sizing: border-box;
border: 1px solid rgba(230, 231, 232, 1);
border-radius: 6px;
background: rgba(255, 255, 255, 1);
align-items: flex-start;
.tab-item {
height: 40px;
display: flex;
align-items: center;
gap: 4px;
cursor: pointer;
margin-right: 24px;
.icon {
width: 16px;
height: 16px;
margin-left: 16px;
margin-top: 10px;
font-size: 0;
img {
width: 100%;
height: 100%;
}
}
.text {
margin-left: 8px;
margin-top: 7px;
height: 22px;
color: rgba(95, 101, 108, 1);
height: 24px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-size: 18px;
font-weight: 400;
line-height: 22px;
line-height: 24px;
}
.textActive {
color: var(--color-main-active);
font-weight: 700;
}
}
.tab-active {
position: relative;
&::after {
content: "";
position: absolute;
bottom: 0px;
left: 0;
width: 100%;
height: 3px;
background-color: var(--color-main-active);
}
}
.btn1 {
margin-left: 12px;
margin-top: 2px;
.head-button {
position: relative;
top: -5px;
width: 120px;
height: 36px;
border-radius: 6px;
background: var(--color-main-active);
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
.icon {
.button-icon {
width: 16px;
height: 16px;
margin-left: 16px;
margin-top: 10px;
font-size: 0;
img {
width: 100%;
height: 100%;
}
}
.text {
.button-text {
margin-left: 8px;
margin-top: 7px;
height: 22px;
color: #fff;
color: rgba(255, 255, 255, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 22px;
}
}
}
}
}
.main {
// height: 870px;
// width: 1920px;
// overflow-y: auto;
background: rgba(247, 248, 249, 1);
border-top: 1px solid rgba(230, 231, 232, 1);
background-color: #f7f8f9;
width: 100%;
height: 20px;
flex: auto;
}
}
</style>
\ No newline at end of file
<template>
<div class="wrapper" v-loading="loading">
<div class="wrapper">
<div class="left">
<div class="box1">
<div class="box-header">
<div class="header-left"></div>
<div class="title">基本信息</div>
<div class="header-right">
<div class="icon">
<img src="@/assets/icons/box-header-icon1.png" alt="" />
</div>
<div class="icon">
<img src="@/assets/icons/box-header-icon2.png" alt="" />
</div>
<div class="icon">
<img src="@/assets/icons/box-header-icon3.png" alt="" />
</div>
</div>
</div>
<div class="box1-main">
<AnalysisBox title="基本信息" :showAllBtn="false" height="auto">
<div class="box1-main" v-loading="loading">
<div class="box1-item">
<div class="box1-item-left">{{ "案卷编号:" }}</div>
<div class="box1-item-right">{{ baseInfo.SEARCHNUM || '-' }}</div>
......@@ -70,42 +55,20 @@
<div class="box1-item-right">{{ baseInfo.ISTARIFF === 'Y' ? `是,增加${baseInfo.ADDTARIFF}%` : '否' }}</div>
</div>
</div>
</div>
</AnalysisBox>
</div>
<div class="right">
<div class="box2" v-loading="box2Loading">
<div class="box-header">
<div class="header-left"></div>
<div class="title">调查脉络</div>
<div class="header-right">
<div class="icon">
<img src="@/assets/icons/box-header-icon1.png" alt="" />
</div>
<div class="icon">
<img src="@/assets/icons/box-header-icon2.png" alt="" />
</div>
<div class="icon">
<img src="@/assets/icons/box-header-icon3.png" alt="" />
</div>
</div>
</div>
<div class="box2-main">
<!-- <AnalysisBox title="调查脉络" :showAllBtn="false" height="auto">
<div class="box2-main" v-loading="box2Loading">
<div class="box2-line-box"></div>
<div
class="box2-item"
:class="{ box2ItemFooter: index % 2 }"
v-for="(item, index) in timeLineList"
:key="index"
>
<div class="box2-item" :class="{ box2ItemFooter: index % 2 }" v-for="(item, index) in timeLineList" :key="index">
<div class="point" :class="{ pointFooter: index % 2 }">
<img src="@/assets/images/dot.png" alt="" />
</div>
<div class="box2-item-header">
<div class="title">{{ item.time }}</div>
</div>
<div class="box2-item-content">
{{ item.content }}
</div>
<div class="box2-item-content">{{ item.content }}</div>
</div>
<div class="arrow-left">
<img src="@/assets/icons/card-btn-left.png" alt="" />
......@@ -127,50 +90,9 @@
<img src="@/assets/icons/box-footer-right-icon.png" alt="" />
</div>
</div>
</div>
<div class="box3" v-loading="box3Loading">
<div class="box-header">
<div class="header-left"></div>
<div class="title">调查结论</div>
<div class="header-right">
<div class="icon">
<img src="@/assets/icons/box-header-icon2.png" alt="" />
</div>
<div class="icon">
<img src="@/assets/icons/box-header-icon3.png" alt="" />
</div>
</div>
</div>
<div class="box3-main">
<div class="box3-main-item" v-for="(item, index) in surveyResult" :key="index">
<div class="box3-main-item-header">
<div class="header-left">{{ item.title }}</div>
<div class="header-right">
<div class="icon">
<img src="./assets/images/open.png" alt="" />
</div>
<div class="text">{{ "跳转原文" }}</div>
</div>
</div>
<div class="box3-main-item-content" v-for="(val, idx) in item.data" :key="idx">
<div class="content-item">{{ val }}</div>
</div>
</div>
</div>
<div class="box3-footer">
<div class="box3-footer-left">
<img src="@/assets/icons/box-footer-left-icon.png" alt="" />
</div>
<div class="box3-footer-center">
{{
`美国 232 调查认定,钕铁硼永磁体(关键领域核心材料)进口威胁其国家安全,因中国主导全球市场、美本土产能薄弱,未提关税,提议扶持本土供应链。`
}}
</div>
<div class="box3-footer-right">
<img src="@/assets/icons/box-footer-right-icon.png" alt="" />
</div>
</div>
</div>
</AnalysisBox> -->
<SurveyConclusion title="调查结论" :listData="surveyResult" :tips="tips"></SurveyConclusion>
<RelatedEvent title="相关行政举措" :listData="eventList"></RelatedEvent>
</div>
</div>
</template>
......@@ -179,6 +101,10 @@
import { ref, onMounted } from "vue";
import { useRoute } from "vue-router";
import { getSearchBlurb, getSearchContext, getSearchConclusion } from "@/api/marketAccessRestrictions";
import RelatedEvent from "@/views/marketAccessRestrictions/com/RelatedEvent.vue";
import SurveyConclusion from "@/views/marketAccessRestrictions/com/SurveyConclusion.vue";
const tips = "美国 232 调查认定,钕铁硼永磁体(关键领域核心材料)进口威胁其国家安全,因中国主导全球市场、美本土产能薄弱,未提关税,提议扶持本土供应链。"
const route = useRoute();
const loading = ref(false);
......@@ -239,13 +165,11 @@ const handleGetSearchConclusion = async () => {
const res = await getSearchConclusion({
searchId: route.query.searchId
});
if (res.code === 200 && res.data) {
if (res.code === 200) {
surveyResult.value = res.data.map(item => {
// 将 CONTENT 按换行符分割,并过滤掉空行
const contentList = item.CONTENT ? item.CONTENT.split(/\r\n|\n/).filter(line => line.trim()) : [];
return {
title: item.TITLE,
data: contentList
data: item.CONTENT?.split(/\r\n|\n/).filter(line => line.trim()) || [],
};
});
}
......@@ -256,6 +180,25 @@ const handleGetSearchConclusion = async () => {
}
};
// 相关行政举措
const eventList = ref([
{
name: "某些特定外国制造的半导体器件及其下游产品和组件;委员会最终裁定未违反第337条的通知;调查终止",
text: "特此通知,2025 年 2 月 18 日,根据 1930 年《关税法》第 337 条(经修订)已代表爱尔兰 Longitude Licensing Ltd.和爱尔",
time: "2025年4月15日"
},
{
name: "某些特定外国制造的半导体器件及其下游产品和组件;委员会最终裁定未违反第337条的通知;调查终止",
text: "特此通知,2025 年 2 月 18 日,根据 1930 年《关税法》第 337 条(经修订)已代表爱尔兰 Longitude Licensing Ltd.和爱尔",
time: "2025年4月15日"
},
{
name: "某些特定外国制造的半导体器件及其下游产品和组件;委员会最终裁定未违反第337条的通知;调查终止",
text: "特此通知,2025 年 2 月 18 日,根据 1930 年《关税法》第 337 条(经修订)已代表爱尔兰 Longitude Licensing Ltd.和爱尔",
time: "2025年4月15日"
},
])
onMounted(() => {
handleGetSearchBlurb();
handleGetSearchContext();
......@@ -265,113 +208,27 @@ onMounted(() => {
<style lang="scss" scoped>
.wrapper {
width: 100%;
// height: 1191px;
padding: 0 160px;
display: flex;
.box-header {
height: 56px;
display: flex;
position: relative;
.header-left {
margin-top: 20px;
width: 8px;
height: 16px;
border-radius: 0 4px 4px 0;
background: var(--color-main-active);
}
.title {
margin-left: 14px;
margin-top: 16px;
height: 24px;
line-height: 24px;
color: var(--color-main-active);
font-family: Microsoft YaHei;
font-size: 18px;
font-weight: 700;
}
.header-btn-box {
position: absolute;
top: 14px;
right: 52px;
width: 1600px;
margin: 20px auto;
display: flex;
.btn {
margin-left: 8px;
height: 28px;
padding: 0 8px;
box-sizing: border-box;
border: 1px solid rgba(230, 231, 232, 1);
border-radius: 4px;
background: rgba(255, 255, 255, 1);
text-align: center;
line-height: 28px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
cursor: pointer;
}
.btnActive {
border: 1px solid var(--color-main-active);
background: rgba(246, 250, 255, 1);
color: var(--color-main-active);
}
}
.header-info {
height: 22px;
position: absolute;
right: 84px;
top: 17px;
gap: 16px;
.left {
width: 520px;
display: flex;
justify-content: flex-end;
.icon {
margin-top: 3px;
width: 14px;
height: 14px;
margin-right: 8px;
img {
width: 100%;
height: 100%;
flex-direction: column;
gap: 16px;
}
}
.text {
height: 22px;
color: rgba(95, 101, 108, 1);
font-family: Microsoft YaHei;
font-size: 14px;
font-weight: 400;
line-height: 22px;
}
}
.header-right {
position: absolute;
top: 14px;
right: 12px;
.right {
width: 20px;
flex: auto;
display: flex;
justify-content: flex-end;
gap: 4px;
.icon {
width: 28px;
height: 28px;
img {
width: 100%;
height: 100%;
}
}
flex-direction: column;
gap: 16px;
}
}
.left {
width: 520px;
.box1 {
margin-top: 16px;
width: 520px;
height: 837px;
border-radius: 4px;
box-shadow: 0px 0px 15px 0px rgba(60, 87, 126, 0.2);
background: rgba(255, 255, 255, 1);
.box1-main {
margin-top: 6px;
margin-left: 22px;
}
.box1-main {
padding: 0 22px;
.box1-item {
display: flex;
margin-bottom: 16px;
......@@ -405,30 +262,14 @@ onMounted(() => {
}
}
}
}
}
}
.right {
width: 1064px;
margin-left: 16px;
.box2 {
margin-top: 16px;
width: 1064px;
height: 410px;
border-radius: 4px;
box-shadow: 0px 0px 15px 0px rgba(60, 87, 126, 0.2);
background: rgba(255, 255, 255, 1);
.box2-main {
margin: 0 auto;
margin-top: 10px;
}
.box2-main {
padding: 0 22px;
height: 250px;
// width: 1054px;
// overflow-x: auto;
// overflow-y: hidden;
display: flex;
justify-content: center;
position: relative;
padding-left: 120px;
.box2-line-box {
position: absolute;
left: 0;
......@@ -440,7 +281,6 @@ onMounted(() => {
.box2-item {
width: 320px;
height: 120px;
// background: pink;
position: relative;
box-sizing: border-box;
padding-left: 13px;
......@@ -532,10 +372,9 @@ onMounted(() => {
height: 100%;
}
}
}
.box2-footer {
margin: 24px auto 0;
width: 1032px;
}
.box2-footer {
margin: 20px 22px;
height: 64px;
box-sizing: border-box;
border: 1px solid rgba(231, 243, 255, 1);
......@@ -571,125 +410,5 @@ onMounted(() => {
height: 100%;
}
}
}
}
.box3 {
margin-top: 16px;
margin-bottom: 35px;
width: 1064px;
// height: 714px;
border-radius: 4px;
box-shadow: 0px 0px 15px 0px rgba(60, 87, 126, 0.2);
background: rgba(255, 255, 255, 1);
padding-bottom: 18px;
.box3-main {
width: 1016px;
// height: 550px;
margin: 0 auto;
overflow: hidden;
overflow-y: auto;
.box3-main-item {
.box3-main-item-header {
width: 1016px;
height: 48px;
box-sizing: border-box;
border-bottom: 1px solid rgba(234, 236, 238, 1);
border-top: 1px solid rgba(234, 236, 238, 1);
background: rgba(247, 248, 249, 1);
display: flex;
justify-content: space-between;
align-items: center;
.header-left {
margin-left: 12px;
height: 24px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 18px;
font-weight: 700;
line-height: 24px;
letter-spacing: 0px;
text-align: justify;
}
.header-right {
display: flex;
margin-right: 22px;
gap: 5px;
.icon {
width: 16px;
height: 16px;
img {
width: 100%;
height: 100%;
}
}
.text {
height: 14px;
color: rgba(5, 95, 194, 1);
font-family: Microsoft YaHei;
font-size: 12px;
font-weight: 400;
line-height: 14px;
letter-spacing: 0px;
text-align: justify;
}
}
}
.box3-main-item-content {
.content-item {
width: 1014px;
padding: 12px 23px 12px 44px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 30px;
letter-spacing: 0px;
text-align: justify;
border-bottom: 1px solid rgba(234, 236, 238, 1);
}
}
}
}
.box3-footer {
margin: 24px auto 0;
width: 1032px;
height: 64px;
box-sizing: border-box;
border: 1px solid rgba(231, 243, 255, 1);
border-radius: 4px;
background: rgba(246, 250, 255, 1);
display: flex;
align-items: center;
gap: 13px;
padding: 0 12px;
.box3-footer-left {
width: 19px;
height: 20px;
img {
width: 100%;
height: 100%;
}
}
.box3-footer-center {
width: 939px;
color: rgba(5, 95, 194, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 24px;
letter-spacing: 0px;
text-align: justify;
}
.box3-footer-right {
width: 24px;
height: 24px;
img {
width: 100%;
height: 100%;
}
}
}
}
}
}
</style>
......@@ -339,8 +339,8 @@ onMounted(() => {
<style lang="scss" scoped>
.wrapper {
width: 100%;
padding: 0 160px;
width: 1600px;
margin: 20px auto;
display: flex;
.box-header {
height: 56px;
......
<template>
<div class="wrapper" v-loading="loading">
<div class="left">
<div class="box1">
<div class="box-header">
<div class="header-left"></div>
<div class="title">基本信息</div>
<div class="header-right">
<div class="icon">
<img src="@/assets/icons/box-header-icon1.png" alt="" />
</div>
<div class="icon">
<img src="@/assets/icons/box-header-icon2.png" alt="" />
</div>
<div class="icon">
<img src="@/assets/icons/box-header-icon3.png" alt="" />
</div>
</div>
</div>
<AnalysisBox title="基本信息" :showAllBtn="false" height="auto">
<div class="box1-main">
<div class="box1-item">
<div class="box1-item-left">{{ "调查案号:" }}</div>
......@@ -52,11 +37,7 @@
<div class="box1-item">
<div class="box1-item-left">{{ "案件进展:" }}</div>
<div class="box1-item-right2">
<div
class="box1-item-right2-item"
v-for="(item, index) in processList"
:key="index"
>
<div class="box1-item-right2-item" v-for="(item, index) in processList" :key="index">
<div class="icon">
<img src="./assets/images/icon1.png" alt="" />
</div>
......@@ -66,72 +47,41 @@
</div>
</div>
</div>
</AnalysisBox>
<AnalysisBox title="调查公告" :showAllBtn="false" height="auto">
<div class="box4-main">
<div v-for="(item, index) in afficheList" :key="index" class="box4-item">
<div class="item-icon">
<img src="@/views/marketAccessRestrictions/singleCaseLayout/assets/images/icon_affiche.png" alt="">
</div>
</div>
<div class="right">
<div class="box2">
<div class="box-header">
<div class="header-left"></div>
<div class="title">原告信息</div>
<div class="header-right">
<div class="icon">
<img src="@/assets/icons/box-header-icon2.png" alt="" />
</div>
<div class="icon">
<img src="@/assets/icons/box-header-icon3.png" alt="" />
<div class="item-right">
<div class="item-time">{{ item.time }}</div>
<div class="item-text">{{ item.text }}</div>
</div>
</div>
</div>
<div class="box2-main">
<div class="box2-main-top">
<div class="left">
{{ baseInfo.ORGNAME ? baseInfo.ORGNAME.substring(0, 3) : '-' }}
</AnalysisBox>
</div>
<div class="right">
<div class="title">{{ baseInfo.ORGNAME || '-' }}</div>
<div class="info">{{ "-" }}</div>
</div>
</div>
<div class="box2-main-center">
{{ baseInfo.ORGBLURB || '-' }}
</div>
<div class="box2-main-footer">
<div class="box2-main-footer-left">
<img src="./assets/images/footer-icon1.png" alt="" />
</div>
<div class="box2-main-footer-center">
{{ baseInfo.ORGBLURB || '-' }}
</div>
<div class="box2-main-footer-right">
<img src="./assets/images/footer-icon2.png" alt="" />
</div>
</div>
</div>
</div>
<div class="box3">
<div class="box-header">
<div class="header-left"></div>
<div class="title">被告信息</div>
<div class="header-info">
<div class="icon">
<img src="./assets/images/header-info-icon.png" alt="">
</div>
<div class="text">{{ '以下为列名被告企业及其关联公司' }}</div>
</div>
<div class="header-right">
<div class="icon">
<img src="@/assets/icons/box-header-icon2.png" alt="" />
</div>
<div class="icon">
<img src="@/assets/icons/box-header-icon3.png" alt="" />
<AnalysisBox title="原告信息" :showAllBtn="false" height="auto">
<div class="box2-main">
<div class="data-head">
<div class="data-icon">{{ baseInfo.ORGNAME?.substring(0, 3) }}</div>
<div class="data-right">
<div class="data-name">{{ baseInfo.ORGNAME }}</div>
<div class="data-desc">{{ "" }}</div>
</div>
</div>
<div class="data-text">{{ baseInfo.ORGBLURB || '-' }}</div>
<AiTips :tips="tips"></AiTips>
</div>
</AnalysisBox>
<AnalysisBox title="被告信息" :showAllBtn="false" height="auto">
<div class="box3-main">
<div class="box3-main-box" v-for="(item, index) in caseList" :key="index">
<div class="box3-main-box-header">{{ item.title }}</div>
<div class="box3-main-box-content">
<div class="content-item" v-for="(val, idx) in item.companyList" :key="idx">
<div class="data-item" v-for="(item, index) in caseList" :key="index">
<div class="data-head">{{ item.title }}</div>
<div class="info-list">
<div class="info-item" v-for="(val, idx) in item.companyList" :key="idx">
<div class="icon">
<img :src="val.logo" alt="" v-if="val.logo">
<div class="default-logo" v-else>{{ val.name ? val.name.substring(0, 1) : '' }}</div>
......@@ -141,7 +91,8 @@
</div>
</div>
</div>
</div>
</AnalysisBox>
<RelatedEvent title="相关事件" :listData="eventList"></RelatedEvent>
</div>
</div>
</template>
......@@ -150,12 +101,17 @@ import { ref, onMounted } from "vue";
import { useRoute } from "vue-router";
import { getSearchBlurb } from "@/api/marketAccessRestrictions";
import AiTips from "@/views/marketAccessRestrictions/com/AiTips.vue";
import RelatedEvent from "@/views/marketAccessRestrictions/com/RelatedEvent.vue";
const tips = `Pantech是韩国的一家通信设备公司,曾经是手机制造商,但现在可能已转型为专利持有实体。这类公司常被称为"非执业实体"(NPE)或"专利断言实体"(PAE),通过专利授权和诉讼获取收益。这些企业曾经是行业龙头,但因科技和市场形态巨变,加上自身改革步伐缓慢,经营状况每况愈下。卖掉实体业务部门后,留下来的是高达几万件的专利。`
const route = useRoute();
const loading = ref(false);
const baseInfo = ref({});
const processList = ref([]);
const caseList = ref([]);
// 调查概况
const handleGetSearchBlurb = async () => {
loading.value = true;
try {
......@@ -163,6 +119,7 @@ const handleGetSearchBlurb = async () => {
searchId: route.query.searchId,
sortCode: "337"
});
console.log("调查概况", res)
if (res.code === 200 && res.data) {
const data = res.data;
baseInfo.value = data;
......@@ -195,12 +152,54 @@ const handleGetSearchBlurb = async () => {
}
}
} catch (error) {
console.error("获取调查详情失败", error);
} finally {
loading.value = false;
console.log("获取调查概况失败", error);
}
loading.value = false;
};
// 调查公告
const afficheList = ref([
{
time: "2025-03-30",
text: "美国ITC正式对外国制造的半导体器件及其下游产品和组件启动337调查"
},
{
time: "2025-03-30",
text: "美国ITC正式对外国制造的半导体器件及其下游产品和组件启动337调查"
},
{
time: "2025-03-30",
text: "美国ITC正式对外国制造的半导体器件及其下游产品和组件启动337调查"
},
{
time: "2025-03-30",
text: "美国ITC正式对外国制造的半导体器件及其下游产品和组件启动337调查"
},
{
time: "2025-03-30",
text: "美国ITC正式对外国制造的半导体器件及其下游产品和组件启动337调查"
},
]);
// 相关事件
const eventList = ref([
{
name: "某些特定外国制造的半导体器件及其下游产品和组件;委员会最终裁定未违反第337条的通知;调查终止",
text: "特此通知,2025 年 2 月 18 日,根据 1930 年《关税法》第 337 条(经修订)已代表爱尔兰 Longitude Licensing Ltd.和爱尔",
time: "2025年4月15日"
},
{
name: "某些特定外国制造的半导体器件及其下游产品和组件;委员会最终裁定未违反第337条的通知;调查终止",
text: "特此通知,2025 年 2 月 18 日,根据 1930 年《关税法》第 337 条(经修订)已代表爱尔兰 Longitude Licensing Ltd.和爱尔",
time: "2025年4月15日"
},
{
name: "某些特定外国制造的半导体器件及其下游产品和组件;委员会最终裁定未违反第337条的通知;调查终止",
text: "特此通知,2025 年 2 月 18 日,根据 1930 年《关税法》第 337 条(经修订)已代表爱尔兰 Longitude Licensing Ltd.和爱尔",
time: "2025年4月15日"
},
])
onMounted(() => {
handleGetSearchBlurb();
});
......@@ -208,9 +207,8 @@ onMounted(() => {
<style lang="scss" scoped>
.wrapper {
width: 100%;
height: 1191px;
padding: 0 160px;
width: 1600px;
margin: 20px auto;
display: flex;
.box-header {
height: 56px;
......@@ -305,22 +303,29 @@ onMounted(() => {
}
.left {
width: 520px;
.box1 {
margin-top: 16px;
width: 520px;
height: 837px;
border-radius: 4px;
box-shadow: 0px 0px 15px 0px rgba(60, 87, 126, 0.2);
background: rgba(255, 255, 255, 1);
.box1-main {
margin-top: 6px;
margin-left: 22px;
display: flex;
flex-direction: column;
gap: 16px;
}
.right {
width: 20px;
flex: auto;
margin-left: 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
}
.box1-main {
padding: 0 22px;
.box1-item {
display: flex;
margin-bottom: 16px;
.box1-item-left {
width: 100px;
height: 24px;
flex: none;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 16px;
......@@ -396,139 +401,59 @@ onMounted(() => {
}
}
}
}
}
}
.right {
width: 1064px;
margin-left: 16px;
.box2 {
margin-top: 16px;
width: 1064px;
height: 410px;
border-radius: 4px;
box-shadow: 0px 0px 15px 0px rgba(60, 87, 126, 0.2);
background: rgba(255, 255, 255, 1);
.box2-main {
.box2-main-top {
}
.box2-main {
padding: 0 22px 20px;
.data-head {
display: flex;
width: 1012px;
height: 69px;
border-bottom: 1px solid rgba(234, 236, 238, 1);
margin: 0 auto;
.left {
padding-bottom: 16px;
.data-icon {
width: 48px;
height: 48px;
text-align: center;
line-height: 48px;
background: rgba(231, 243, 255, 1);
color: var(--color-main-active);
font-family: Microsoft YaHei;
font-size: 14px;
font-weight: 700;
font-weight: bold;
font-family: Microsoft YaHei;
}
.right {
.data-right {
width: 20px;
flex: auto;
margin-left: 16px;
.title {
height: 24px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 700;
line-height: 24px;
font-family: Source Han Sans CN;
.data-name {
color: var(--text-primary-80-color);
font-weight: bold;
}
.info {
color: rgba(95, 101, 108, 1);
height: 24px;
font-family: Microsoft YaHei;
font-size: 16px;
.data-desc {
color: var(--text-primary-65-color);
font-weight: 400;
line-height: 24px;
}
}
}
.box2-main-center {
.data-text {
width: 1012px;
margin: 0 auto;
margin-top: 13px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-family: Source Han Sans CN;
font-size: 16px;
font-weight: 400;
line-height: 24px;
height: 160px;
overflow-y: auto;
}
.box2-main-footer {
margin: 0 auto;
margin-top: 10px;
width: 1034px;
height: 88px;
box-sizing: border-box;
border: 1px solid rgba(231, 243, 255, 1);
border-radius: 4px;
background: rgba(246, 251, 255, 1);
box-sizing: border-box;
padding: 6px 12px;
display: flex;
.box2-main-footer-left {
width: 19px;
height: 20px;
margin-top: 22px;
img {
width: 100%;
height: 100%;
}
}
.box2-main-footer-center {
margin-top: 2px;
margin-left: 13px;
width: 941px;
color: var(--color-main-active);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 24px;
display: -webkit-box;
-webkit-line-clamp: 3;
line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.box2-main-footer-right {
margin-left: 13px;
margin-top: 26px;
width: 24px;
height: 24px;
border-radius: 12px;
background: rgba(231, 243, 255, 1);
img {
width: 12px;
height: 12px;
margin-top: 6px;
margin-left: 6px;
}
}
}
}
}
.box3 {
margin-top: 16px;
margin-bottom: 35px;
width: 1064px;
height: 714px;
border-radius: 4px;
box-shadow: 0px 0px 15px 0px rgba(60, 87, 126, 0.2);
background: rgba(255, 255, 255, 1);
.box3-main {
margin-top: 1px;
margin-left: 24px;
margin-right: 27px;
height: 600px;
overflow-y: auto;
.box3-main-box {
margin-bottom: 18px;
.box3-main-box-header {
}
.box3-main {
padding: 0 22px 20px;
.data-item {
.data-head {
height: 36px;
border-bottom: 1px solid rgba(234, 236, 238, 1);
color: rgba(59, 65, 75, 1);
......@@ -537,15 +462,15 @@ onMounted(() => {
font-weight: 700;
line-height: 36px;
}
.box3-main-box-content {
.info-list {
padding-top: 13px;
display: flex;
flex-wrap: wrap;
.content-item {
.info-item {
display: flex;
height: 30px;
width: 50%;
margin-bottom: 11px;
margin-bottom: 10px;
.icon {
width: 24px;
height: 24px;
......@@ -587,8 +512,42 @@ onMounted(() => {
}
}
}
}
.box4-main {
padding: 0 16px 16px;
.box4-item {
border-top: 1px solid var(--bg-black-5);
padding: 6px;
display: flex;
.item-icon {
width: 15px;
height: 15px;
font-size: 0px;
margin-right: 16px;
margin-top: 7px;
img {
width: 100%;
height: 100%;
}
}
.item-right {
width: 20px;
flex: auto;
font-family: Source Han Sans CN;
font-size: 16px;
line-height: 30px;
.item-time {
font-weight: bold;
color: var(--text-primary-80-color);
}
.item-text {
color: var(--text-primary-65-color);
}
}
}
.box4-item:last-child {
border-bottom: 1px solid var(--bg-black-5);
}
}
</style>
<template>
<div class="overview-wrap">
<Discussion1 v-if="showSurvey ==='337'"></Discussion1>
<Discussion2 v-else-if="showSurvey ==='232'"></Discussion2>
<Discussion3 v-else-if="showSurvey ==='301'"></Discussion3>
<Discussion2 v-if="showSurvey ==='232'"></Discussion2>
<Discussion3 v-if="showSurvey ==='301'"></Discussion3>
</div>
</template>
......@@ -28,5 +28,6 @@ onMounted(() => {
.overview-wrap {
width: 100%;
height: 100%;
overflow-y: auto;
}
</style>
\ No newline at end of file
<template>
<div class="writtingBottom">
<!-- 文档停止解析 -->
<div class="parsed" v-if="store.bottomProgressNum>0&&store.bottomProgressNum!=100&&store.writeProgressNum<10">
<div class="analysis" @click="store.resetGenerateState">
<div class="icon"></div>
<span class="text-tip-2-bold">停止</span>
</div>
<div class="processLogBox" :class="{isProcessLog:store.isProcessLog}">
<div class="progress">
<div class="login">
<el-progress type="circle" :percentage="store.bottomProgressNum" :stroke-width="4" :width="24" :height="24" style="margin-right: 15px;" :show-text="false" color="rgb(5, 95, 194)"/>
<span class="text-tip-2-bold">文档翻译中</span>
</div>
<div class="text-tip-2" style="display: flex;align-items: center;">
<div class="processLogAlone" @click="progressAchieveClick">{{ storeProcessLogAlone }}</div>
<el-icon v-if="store.isProcessLog" @click="isProcessLogClick"><ArrowUpBold /></el-icon>
<el-icon v-else @click="isProcessLogClick"><ArrowDownBold /></el-icon>
</div>
</div>
<div class="processLogList" ref="processLogListRef">
<p>执行步骤:</p>
<div ref="processContainerRef" v-html="renderedProcess"></div>
</div>
</div>
</div>
<!-- 开始写报 -->
<div class="parsed" v-else-if="store.bottomProgressNum>=100">
<div class="analysis" v-if="store.writeProgressNum>0&&store.writeProgressNum!=100" @click="store.writeGenerateState">
<div class="icon"></div>
<span class="text-tip-2-bold">停止</span>
</div>
<div class="analysis" v-else-if="store.writeProgressNum>=100" @click="store.resetGenerateState">
<img src="@/assets/icons/aiBox/ai-logo-color.png" alt="">
<span class="text-tip-1">重新上传</span>
</div>
<div class="notAnalysis" v-else @click="onWriteClick()" >
<img src="@/assets/icons/aiBox/ai-logo.png" alt="">
<span class="text-tip-1">智能写报</span>
</div>
<div class="processLogBox" :class="{isProcessLog:store.isProcessLog}">
<div class="progress">
<!-- 如果store.writeProgressNum>=1 点击了写报 否则是思维导图已完成 -->
<div class="login" v-if="store.writeProgressNum>=1&&store.writeProgressNum<100">
<el-progress type="circle" :percentage="store.writeProgressNum" :stroke-width="4" :width="24" :height="24" style="margin-right: 15px;" :show-text="false" color="rgb(5, 95, 194)"/>
<span class="text-tip-2-bold">智能写报中</span>
</div>
<div class="login" v-else-if="store.writeProgressNum>=100||store.bottomProgressNum>=100">
<img class="steps" style="" src="@/assets/icons/aiBox/steps.png" alt="">
<span class="text-tip-2-bold" v-if="store.writeProgressNum>=100" > 智能写报完成</span>
<span class="text-tip-2-bold" v-else-if="store.bottomProgressNum>=100"> 思维导图已完成</span>
</div>
<div class="text-tip-2" style="display: flex;align-items: center;">
<div class="processLogAlone" @click="progressAchieveClick">{{ storeProcessLogAlone }}</div>
<el-icon v-if="store.isProcessLog" @click="isProcessLogClick"><ArrowUpBold /></el-icon>
<el-icon v-else @click="isProcessLogClick"><ArrowDownBold /></el-icon>
</div>
</div>
<div class="processLogList" ref="processLogListRef">
<p>执行步骤:</p>
<div ref="processContainerRef" v-html="renderedProcess"></div>
</div>
</div>
</div>
<!-- 开始解析文档 -->
<div class="parsed" v-else >
<div class="notAnalysis" @click="onAnalysisClick()">
<img src="@/assets/icons/aiBox/ai-logo.png" alt="">
<span class="text-tip-1">文档解析</span>
</div>
<div class="notProgress">
<img src="../assets/images/tips-icon.png" alt="">
<span class="text-tip-2">内容由AI生成,无法确保真实准确,仅供参考</span>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted, onUnmounted, ref, nextTick ,watch} from "vue";
import { useRoute } from "vue-router";
import { ElMessage } from "element-plus";
import { useWrittingAsstaintStore } from "@/stores/writtingAsstaintStore";
import { useStream } from "@/hooks/useStream";
// 子组件直接获取Pinia Store(核心优化)
const store = useWrittingAsstaintStore();
const emit = defineEmits(["generate","write"]);
const onAnalysisClick=()=>{
store.isShowSteps = !store.isShowSteps
emit("generate");
}
const onWriteClick=()=>{
store.isShowSteps = false
emit("write");
}
const { renderedProcess, updateProcess, clearContent } = useStream();
const processContainerRef = ref(null);
const processLogListRef=ref(null)
// 监听 store.processLog 变化,更新步骤内容并滚动
watch(
() => store.processLog,
async (newLog) => {
console.log(newLog,0)
if (newLog !== undefined && newLog !== null) {
await updateProcess(newLog, processContainerRef.value);
}
if(processLogListRef.value){
processLogListRef.value.scrollTo({
top:processLogListRef.value.scrollHeight,
behavior: 'smooth'
});
}
},
{ immediate: true }
);
defineExpose({
processContainerRef
});
const processLogAloneRef=ref(null)
const storeProcessLogAlone=ref()
watch(
() => store.processLogAlone,
async (newLog) => {
// console.log(newLog,1)
if (newLog !== undefined && newLog !== null) {
const lines = newLog.split('\n').filter(newLog => newLog.trim());
const lastLine = lines[lines.length - 1];
storeProcessLogAlone.value=lastLine
// await updateProcess(lastLine?lastLine:'', processLogAloneRef.value);
}
},
{ immediate: true }
);
const isProcessLogClick=()=>{
store.isProcessLog=!store.isProcessLog
nextTick(()=>{
processLogListRef.value.scrollTo({
top:processLogListRef.value.scrollHeight,
behavior: 'smooth'
});
})
}
// 进度完成点击切换
const progressAchieveClick= ()=>{
if(store.writeProgressNum>=100&&storeProcessLogAlone.value=='点击查看'){ //写报
console.log(store.headerTabType)
store.headerTabType='message'
}else if(store.bottomProgressNum>=100&&storeProcessLogAlone.value=='点击查看'){ //思维导图
console.log(store.headerTabType)
store.headerTabType='mind'
}
}
</script>
<style lang="scss" scoped>
.writtingBottom{
width: 100%;
background: #fff;
box-shadow: 0px 0px 3px 0px #d2d2d2;
position: relative;
padding: 0 22px;
.parsed{
display: flex;
justify-content: space-between;
// align-items: center;
height: 64px;
padding-top: 15px;
.notAnalysis{
display: flex;
justify-content: center;
align-items: center;
background-color: var(--color-primary-100);
border-radius: 4px;
width: 437px;
height: 36px;
color: #fff;
cursor: pointer;
img{
width: 21px;
height: 16px;
margin-right: 12px;
}
}
.notProgress{
background-color: #fff;
display: flex;
align-items: center;
justify-content: space-between;
color: rgba(132, 136, 142, 1);
img{
width: 16px;
height: 16px;
margin-right: 8px;
}
}
.analysis{
border: 1px solid var(--color-primary-100);
background-color: rgb(246, 250, 255);
color: var(--color-primary-100);
width: 437px;
height: 36px;
display: flex;
justify-content: center;
align-items: center;
margin-left: 22px;
cursor: pointer;
.icon{
width: 8px;
height: 8px;
border-radius: 2px;
background-color: var(--color-primary-100);
margin-right: 12px;
}
img{
width: 21px;
height: 16px;
margin-right: 12px;
}
}
.progress{
display: flex;
align-items: center;
justify-content: space-between;
color: var(--color-primary-100);
background-color: rgb(246, 250, 255);
border-radius: 50px;
height: 38px;
padding: 0 12px;
.login{
margin-right: 10px;
display: flex;
align-items: center;
.steps{
width: 16px;
height: 16px;
margin-right: 12px;
}
.text-tip-2-bold{
}
}
}
.processLogAlone{
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 300px;
margin-right: 15px;
cursor: pointer;
}
}
// 总步骤条
.processLogBox{
// background-color: rgba(246, 250, 255, 1);
border-radius: 5px;
transition: all 0.3s ease;
height: 300px;
transform: translateY(0);
.processLogList{
display: none;
padding: 0 40px;
font-size: 14px;
color: rgb(59, 65, 75);
line-height: 22px;
max-height: 240px;
overflow-y: auto;
width: 500px;
font-size: 16px;
}
}
.isProcessLog{
transform: translateY(-270px);
background: rgba(246, 250, 255, 1);
.progress{
background-color: rgba(246, 250, 255, 1);
}
.processLogList{
display: block;
}
}
}
</style>
\ No newline at end of file
<template>
<div class="headerBox">
<div class="tabBox" v-if="store.bottomProgressNum>0">
<div class="fileName">
<img src="@/assets/icons/pdf-icon.png" alt=" ">
<span class="text-tip-1-bold">{{ store.uploadFileList[0]?.name||'文件错误' }}</span>
</div>
<div class="tab">
<div class="tabList text-tip-1-bold" v-for="(item,index) in store.tabList" :key="index" :class="{'on':store.headerTabType==item.type}"
:style="!item.active?'color:#bfbfbf;cursor: no-drop;':''"
@click="onTabListClick(item.type,item.active)">{{ item.name }}</div>
</div>
<div class="switch" v-if="store.headerTabType=='translate'">
<el-switch v-model="store.isShowOriginal"/>
<div class="iconBOx">
<img src="@/assets/icons/translate-icon.png" alt="">
<span class="text-tip-1">显示原文</span>
</div>
<el-button @click="store.handleIsSsearchFor"><img style="width: 16px;" src="@/assets/icons/aiBox/search.png" alt=""> 查找</el-button>
</div>
<div v-else style="margin-right: 22px;">
<el-button @click="onExport">导出</el-button>
</div>
</div>
<div class="logo" v-else>
<img src="@/assets/icons/tool-item-icon1.png" alt="">
<span class="text-title-3-bold">智能写库</span>
</div>
</div>
</template>
<script setup>
import { onMounted, onUnmounted, ref, nextTick ,computed,watch} from "vue";
import { useRoute } from "vue-router";
import { ElMessage } from "element-plus";
import { useWrittingAsstaintStore } from "@/stores/writtingAsstaintStore";
const emit = defineEmits(["onExport"]);
// 子组件直接获取Pinia Store(核心优化)
const store = useWrittingAsstaintStore();
const onTabListClick= (type,active)=>{
if(!active) return
store.handleHeaderTab(type)
}
const onExport=()=>{
if(store.headerTabType=='mind'){
emit('onExport')
}else if(store.headerTabType=='message'){
store.exportContent
}
}
</script>
<style lang="scss" scoped>
.headerBox{
background-color: #fff;
border-bottom: 1px solid rgb(234, 236, 238);
.logo{
height: 60px;
display: flex;
align-items: center;
img{
width: 30px;
height: 30px;
margin-left: 28px;
margin-right: 20px;
}
}
.tabBox{
height: 60px;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
.fileName{
display: flex;
align-items: center;
img{
width: 24px;
height: 24px;
margin-left: 28px;
margin-right: 16px;
}
}
.tab{
width: 232px;
display: flex;
justify-content: space-between;
position: absolute;
left: 50%;
transform: translate(-50%);
.tabList{
cursor: pointer;
height: 59px;
line-height: 66px;
}
.on{
border-bottom: 4px solid var(--color-primary-100);
color:var(--color-primary-100) ;
transition: all 0.1s;
}
}
.btns{
margin-right: 23px;
}
}
.switch{
display: flex;
align-items: center;
margin-right: 23px;
.iconBOx{
display: flex;
align-items: center;
margin-right: 12px;
img{
width: 16px;
height: 16px;
margin:0 5px;
}
}
}
}
</style>
\ No newline at end of file
<template>
<div class="left-box-wrapper">
<div class="back" @click="store.resetGenerateState" v-if="store.isGenerating">&lt; 返回</div>
<!-- <div class="back" @click="store.resetGenerateState" v-if="store.isGenerating">&lt; 返回</div> -->
<div class="left-box" :class="{ 'has-back-btn': store.isGenerating }"
v-if="!store.isShowClauseTranslation && !store.isShowSteps">
<div class="left-box-input">
......@@ -11,15 +11,15 @@
<div class="header">报文主题</div>
<div class="title-box">
<div class="title">主题名称</div>
<el-input :disabled="store.isDisableTemplate" style="width: 476px; height: 32px"
<el-input :disabled="store.isDisableTemplate" style="width: 476px; height: 32px ;background: #f7f8f9;"
class="title-input" placeholder="输入主题名称,如:大而美法案" v-model="store.writtingTitle" />
</div>
<div class="description-box">
<!-- <div class="description-box">
<div class="title">主题描述</div>
<el-input :disabled="store.isDisableTemplate" class="description-input" type="textarea"
style="width: 476px" :rows="8" placeholder="输入报文主题描述,如:从科技领域方面分析大而美法案通过后对中国可能产生的影响"
v-model="store.descText" />
</div>
</div> -->
</div>
<!-- 报文模板 -->
......@@ -88,24 +88,24 @@
</div>
<!-- 提交区域 -->
<div class="submit-area">
<!-- <div class="submit-area">
<div class="tips">
<div class="tips-icon">
<img src="../assets/images/tips-icon.png" alt="" />
</div>
<div class="tips-text">内容由AI生成,无法确保真实准确,仅供参考</div>
</div>
</div> -->
<!-- 生成按钮 -->
<div class="submit-btn" @click="triggerGenerate" v-if="!store.isGenerating">
<!-- <div class="submit-btn" @click="triggerGenerate" v-if="!store.isGenerating">
<div class="submit-icon">
<img src="../assets/images/ai.png" alt="" />
</div>
<div class="submit-text">生成报文</div>
</div>
</div> -->
<!-- 生成中状态 -->
<div class="process-footer-box" v-else>
<!-- <div class="process-footer-box" v-else>
<div class="footer-left">
{{ store.isGenerating ? "报文生成中..." : "报文已生成" }}
</div>
......@@ -114,11 +114,11 @@
<div class="text">停止</div>
</div>
</div>
</div>
</div> end -->
</div>
<!-- 步骤侧边栏(拆分出来) -->
<div class="left-box process" :class="{ 'has-back-btn': store.isGenerating }" v-if="store.isShowSteps">
<!-- <div class="left-box process" :class="{ 'has-back-btn': store.isGenerating }" v-if="store.isShowSteps">
<div class="left-box-input">
<div class="process-box">
<div class="process-main-box">
......@@ -137,17 +137,17 @@
</div>
</div>
</div>
</div>
<div class="submit-area">
</div> -->
<!-- <div class="submit-area">
<div class="tips">
<div class="tips-icon">
<img src="../assets/images/tips-icon.png" alt="" />
</div>
<div class="tips-text">内容由AI生成,无法确保真实准确,仅供参考</div>
</div>
</div> -->
<!-- 生成中状态 -->
<div class="process-footer-box">
<!-- <div class="process-footer-box">
<div class="footer-left">
{{ store.isGenerating ? "报文生成中..." : "报文已生成" }}
</div>
......@@ -155,25 +155,23 @@
<div class="icon"></div>
<div class="text">停止</div>
</div>
</div>
</div>
</div>
<!-- 条款翻译侧边栏 -->
<div class="left-box translation-box" :class="{ 'has-back-btn': store.isGenerating }"
v-if="store.isShowClauseTranslation">
</div> -->
<!-- </div>
</div> -->
<!-- 条款翻译侧边栏 srot -->
<div class="left-box translation-box" :class="{ 'has-back-btn': store.isGenerating }" v-if="store.isShowSteps&&store.headerTabType=='message'">
<div class="translation-main-box">
<div class="translation-actions" v-if="!store.isGenerating">
<!-- <div class="translation-actions" v-if="!store.isGenerating">
<div class="back-input-btn" @click="store.backToInputAndClear">返回输入栏</div>
</div>
</div> -->
<!-- 政令标题卡片 -->
<div class="metadata-card" v-if="store.pdfMetadata">
<!-- <div class="metadata-card" v-if="store.pdfMetadata">
<div class="card-header">
<div class="chinese-name">{{ store.pdfMetadata.name }}</div>
<div class="type-tag">{{ store.pdfMetadata.signing_date }}</div>
</div>
<div class="english-name">{{ store.pdfMetadata.order_title }}</div>
</div>
</div> -->
<div class="translation-header-new">
<div class="header-left">共{{ store.clauseTranslationMessages.length }}章节</div>
<div class="header-right">
......@@ -200,7 +198,7 @@
</div>
</div>
<!-- 步骤侧边栏显隐按钮 -->
<div class="toggle-steps-btn" @click="store.isShowSteps = !store.isShowSteps">
<div class="toggle-steps-btn" @click=" isShowSteps ">
<div class="arrow" :class="{ 'is-active': store.isShowSteps }"></div>
</div>
</div>
......@@ -225,6 +223,10 @@ const emit = defineEmits(["generate"]);
const triggerGenerate = () => {
emit("generate");
};
const isShowSteps=()=>{
store.isShowSteps = !store.isShowSteps
store.highlightClauseId=''
}
// 数字转中文序号
const getChineseNumber = (num) => {
......@@ -245,13 +247,25 @@ const { renderedProcess, updateProcess, clearContent } = useStream();
watch(
() => store.highlightClauseId,
async (newId) => {
if (!newId || !translationContentRef.value) return;
console.log('789')
await nextTick();
if (!newId || !translationContentRef.value) return;
const container = translationContentRef.value;
const item = container.querySelector(`.translation-item[data-clause-number="${newId}"]`);
const result = newId.replace(/^\d+、/, '');
const item = container.querySelector(`.translation-item[data-clause-number="${newId.match(/^(\d+)/)[1]}"]`);
const itemHtml=item.querySelector(`.translated-text`);
if (!item) return;
// 你已经拿到的 外层大标签
const element =itemHtml
// 你要找的文字
const targetText = result
const location=findTextInElement(element, targetText);
const containerRect = container.getBoundingClientRect();
const itemRect = item.getBoundingClientRect();
const delta = itemRect.top - containerRect.top;
......@@ -264,6 +278,74 @@ watch(
});
}
);
// 👇 核心:在 element 内部找文字位置
// =========================================
const findTextInElement=(element, targetText )=> {
// 遍历标签内的所有内容
const nodes = element.childNodes;
let rect =''
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
// 只找纯文字
if (node.nodeType === 3) {
const originalText = node.textContent;
const cleanText=(str)=> {
return str
.replace(/(/g, '(')
.replace(/)/g, ')')
.replace(/,/g, ',')
.replace(/。/g, '.')
.replace(/:/g, ':')
.replace(/;/g, ';');
}
// 清理后的文字(无标点)
const nodeClean = cleanText(originalText);
const targetClean = cleanText(targetText);
// 用干净文字对比
if (nodeClean.includes(targetClean)) {
// 找到真实位置(用原始文本定位,不影响)
const index = originalText.indexOf(
originalText.includes(targetText) ? targetText : originalText
);
const range = document.createRange();
range.setStart(node, index);
range.setEnd(node, index + targetText.length);
// 拿到位置
rect = range.getBoundingClientRect();
const marks = element.querySelectorAll('mark');
marks.forEach(mark => {
// 把 mark 里的文字放回原位,删除标签
const parent = mark.parentNode;
while (mark.firstChild) {
parent.insertBefore(mark.firstChild, mark);
}
parent.removeChild(mark);
// 合并相邻文本节点(恢复页面原貌)
parent.normalize();
});
const mark = document.createElement("mark");
mark.style.backgroundColor = '#ffff00;';
mark.style.color = "#000"; // 文字颜色
range.surroundContents(mark);
break;
}
}
}
return rect
}
// 监听 store.processLog 变化,更新步骤内容并滚动
watch(
......@@ -308,7 +390,7 @@ defineExpose({
width: 521px;
height: 100%;
padding-top: 22px;
padding-bottom: 29px;
padding-bottom: 10px;
box-sizing: border-box;
border-right: 1px solid rgba(234, 236, 238, 1);
border-top: 1px solid rgba(234, 236, 238, 1);
......
......@@ -41,7 +41,9 @@ const handleGlobalClick = (e) => {
const clauseId = btn.getAttribute('data-clause');
if (clauseId) {
store.highlightClauseId = clauseId;
store.isShowSteps = true;
// 翻译栏一直显示,所以这里只需要确保它在视图内
console.log(store.highlightClauseId )
}
}
};
......@@ -88,6 +90,7 @@ watch(
},
{ immediate: true }
);
</script>
<style lang="scss" scoped>
......@@ -103,7 +106,7 @@ watch(
}
.content-box {
width: 1069px;
width: 100%;
height: 100%;
overflow-y: auto;
padding: 20px 80px;
......@@ -111,8 +114,8 @@ watch(
font-size: 16px;
box-sizing: border-box;
border: 1px solid rgba(234, 236, 238, 1);
border-radius: 10px;
// border: 1px solid rgba(234, 236, 238, 1);
// border-radius: 10px;
background: rgba(255, 255, 255, 1);
margin: 17px auto 0 auto;
......@@ -138,9 +141,10 @@ watch(
cursor: pointer;
vertical-align: middle;
transition: background-color 0.2s;
border-radius: 50%;
background: #818181;
&:hover {
background-color: #044da5;
background-color: #818181;
}
&:active {
......
<template>
<div style="width: 100%; display: flex;justify-content: center;align-items: stretch;">
<!-- 右侧子组件:绑定ref -->
<writtingMainBox v-show="!!store.reportContent" :report-content="store.reportContent" />
<div v-if="!store.reportContent" class="main-placeholder">
<img src="../assets/images/container-image.png" alt="无数据占位图" />
<div class="placeholder-text">
<div>智能体写报任务执行中...</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, watch, onMounted, onUnmounted } from "vue";
import { useWrittingAsstaintStore } from "@/stores/writtingAsstaintStore";
import writtingMainBox from "./WrittingMainBox.vue";
const store = useWrittingAsstaintStore();
const mainBoxRef = ref(null); // 右侧子组件ref
</script>
<style lang="scss" scoped>
.main-placeholder {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
background: #f7f8f9;
img {
max-width: 100%;
max-height: 80%;
object-fit: contain;
display: block;
}
.placeholder-text {
color: rgba(132, 136, 142, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 24px;
}
}
</style>
\ No newline at end of file
<template>
<div class="mind-map-container">
<div ref="containerRef" class="mind-map"></div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import G6 from '@antv/g6'
import { useWrittingAsstaintStore } from "@/stores/writtingAsstaintStore";
// 子组件直接获取Pinia Store(核心优化)
const store = useWrittingAsstaintStore();
// pushfenzhi
let mindMapData = {
id: 'root',
label: '名称',
children: [
]
}
let uniqueId = 1;
function convertMindMap(rawData) {
uniqueId = 1; // 每次重置
const tree = buildTreeRecursive(rawData.node, rawData.links);
return tree[0]; // 返回单个根节点
}
const raw = store.resultWriteData.政令深度分析.条款分析.科技条款内容[0].领域举措.data;
const treeData = convertMindMap(raw);
// 全局唯一自增计数器(最稳、最短、永不重复)
// 递归构建树 + 自动生成全局唯一数字ID
function buildTreeRecursive(nodes, links, parentId = null) {
const tree = [];
// 找到当前父节点的所有子节点
const childrenNodes = nodes.filter(node => {
if (parentId === null) {
return node.depth === 0;
}
return links.some(
link => link.source === parentId && link.target === node.id
);
});
// 遍历子节点,递归生成
for (const node of childrenNodes) {
const currentNode = {
id: `${uniqueId++}_`, // 🔥 纯数字自增,绝对唯一!
label: node.name,
children: buildTreeRecursive(nodes, links, node.id)
};
tree.push(currentNode);
}
return tree;
}
treeData.id= 'root',
console.log(treeData)
mindMapData=treeData
const containerRef = ref(null)
let graph = null
// 文字换行(不溢出)
function splitTextToLines(text, maxWidth, fontSize) {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
ctx.font = `${fontSize}px sans-serif`
const lines = []
let currentLine = ''
let width = 0
for (const char of text) {
const w = ctx.measureText(char).width
if (width + w > maxWidth) {
lines.push(currentLine)
currentLine = char
width = w
} else {
currentLine += char
width += w
}
}
if (currentLine) lines.push(currentLine)
const lineHeight = fontSize + 8
const totalHeight = lineHeight * lines.length + 24
return { lines, lineHeight, totalHeight }
}
onMounted(() => {
if (graph) graph.destroy()
const el = containerRef.value
if (!el) return
// 注册节点
G6.registerNode('custom-node', {
draw(cfg, group) {
const isRoot = cfg.id === 'root'
const isDept = cfg.id.startsWith('dept')
const MAX_WIDTH = isRoot ? 280 : isDept ? 240 : 400
const fontSize = isRoot ? 16 : isDept ? 15 : 14
const fontWeight = isRoot || isDept ? 'bold' : 'normal'
const color = 'rgba(5, 95, 194, 1)'
const padding = 16
const { lines, lineHeight, totalHeight } = splitTextToLines(cfg.label, MAX_WIDTH - padding * 2, fontSize)
const nodeW = MAX_WIDTH
const nodeH = totalHeight
// 节点背景
group.addShape('rect', {
attrs: {
x: -nodeW / 2,
y: -nodeH / 2,
width: nodeW,
height: nodeH,
fill: '#f8fcff',
stroke: color,
lineWidth: 1,
radius: 6
}
})
// 文字居中
lines.forEach((line, i) => {
group.addShape('text', {
attrs: {
text: line,
x: 0,
y: (i - (lines.length - 1) / 2) * lineHeight,
textAlign: 'center',
textBaseline: 'middle',
fontSize, fontWeight, fill: color
}
})
})
return group
},
getAnchorPoints: () => [[0, 0.5], [1, 0.5]]
}, 'single-node')
graph = new G6.TreeGraph({
container: el,
width: el.offsetWidth,
height: el.offsetHeight,
modes: {
default: ['drag-canvas', 'zoom-canvas', 'collapse-expand']
},
defaultNode: { type: 'custom-node' },
defaultEdge: {
type: 'cubic-horizontal',
style: { stroke: 'rgba(5, 95, 194, 0.4)', lineWidth: 1 }
},
layout: {
type: 'compactBox',
direction: 'LR',
getWidth: () => 400,
getHGap: () => 80,
getVGap: () => 50,
}
})
graph.data(mindMapData)
graph.render()
// 自适应显示
setTimeout(() => {
graph.fitCenter()
graph.zoomTo(0.65)
}, 200)
})
onUnmounted(() => graph?.destroy())
function exportGraph() {
if (!graph) return
graph.downloadFullImage('思维导图.png')
}
defineExpose({
exportGraph
});
</script>
<style scoped>
.mind-map-container {
width: 100%;
height: 100vh;
background: #f5f7fa;
}
.mind-map {
width: 92%;
height: 100%;
margin: 0 auto;
}
</style>
\ No newline at end of file
<template>
<div class="translation-content" ref="translationContentRef">
<!-- :class="{ active: store.highlightClauseId === item.payload?.clause_number }"
:data-clause-number="item.payload?.clause_number" -->
<!-- 查找 -->
<div class="searchFor" v-if="store.isSsearchFor">
<el-input v-model="keyword" style="width: 260px;" placeholder="查找原文内容" />
<div class="searchTextNum">
<span v-if="total==0">0</span>
<span v-else> {{ current + 1 }}</span>/{{ total }}
</div>
<div class="prev" @click="prev"><el-icon><ArrowUp /></el-icon></div>
<div class="next" @click="next"><el-icon><ArrowDown /></el-icon></div>
<div class="close" @click="closeClick"><el-icon><CloseBold /></el-icon></div>
</div>
<div class="content-box" ref="contentBox">
<div class="translation-item" v-for="(item, index) in renderList" :key="index">
<div class="item-body">
<div class="original-text" v-if="store.isShowOriginal">
<template v-for="(t, i) in item.fragments" :key="i">
<span :class="{ high: t.hit, current: t.hit && currentGlobalIndex === t.globalIndex}">
{{ t.text }}
</span>
</template>
</div>
<div class="translated-text">
<span class="clause-title">第{{ getChineseNumber(item.payload?.clause_number) }}节</span>{{ item.payload?.clause_content_zh }}
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted, onUnmounted, ref, nextTick ,watch,computed} from "vue";
import { useRoute } from "vue-router";
import { ElMessage } from "element-plus";
import { useWrittingAsstaintStore } from "@/stores/writtingAsstaintStore";
import { useStream } from "@/hooks/useStream";
// 子组件直接获取Pinia Store(核心优化)
const store = useWrittingAsstaintStore();
const translationContentRef = ref(null);
// 数字转中文序号
const getChineseNumber = (num) => {
const zh = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十'];
const n = parseInt(num);
if (n <= 10) return zh[n];
if (n < 20) return '十' + zh[n % 10];
if (n < 100) {
return zh[Math.floor(n / 10)] + '十' + (n % 10 === 0 ? '' : zh[n % 10]);
}
return num;
};
const closeClick=()=>{
store.handleIsSsearchFor()
current.value=0
total.value=0
keyword.value=''
}
const keyword = ref('')
const contentBox = ref(null)
const current = ref(0)
const total = ref(0)
// 渲染列表(不修改原数据,自动更新)
const renderList = computed(() => {
const key = keyword.value.trim()
return (store.clauseTranslationMessages || []).map(item => {
const section = item.payload?.clause_section || ''
const content = item.payload?.clause_content || ''
const fullText = section + ' ' + content
if (!key) {
return {
...item,
fragments: [{ text: fullText, hit: false, globalIndex: -1 }]
}
}
const parts = fullText.split(new RegExp(`(${key})`, 'g'))
const fragments = []
parts.forEach(t => {
fragments.push({
text: t,
hit: t === key,
globalIndex: -1
})
})
return { ...item, fragments }
})
})
// 全局匹配列表(只计算一次,修复蓝色全部选中BUG)
const globalMatchList = computed(() => {
const arr = []
renderList.value.forEach(item => {
item.fragments.forEach(f => {
if (f.hit) arr.push(f)
})
})
return arr
})
// 当前高亮的全局索引
const currentGlobalIndex = computed(() => {
if (!globalMatchList.value.length) return -1
return globalMatchList.value[current.value]?.globalIndex ?? -2
})
// 总数
watch(globalMatchList, (val) => {
total.value = val.length
current.value = 0
}, { immediate: true })
// 给每个命中项分配唯一 index
watch([renderList, globalMatchList], () => {
let idx = 0
const map = new Map()
globalMatchList.value.forEach(item => {
map.set(item, idx++)
})
renderList.value.forEach(item => {
item.fragments.forEach(f => {
if (f.hit) f.globalIndex = map.get(f)
})
})
})
function doSearch() {
current.value = 0
scrollTo(0)
}
function prev() {
if (!total.value) return
current.value = (current.value - 1 + total.value) % total.value
console.log(current.value )
scrollTo(current.value)
}
function next() {
if (!total.value) return
current.value = (current.value + 1) % total.value
scrollTo(current.value)
}
function scrollTo(idx) {
setTimeout(() => {
const all = contentBox.value?.querySelectorAll('.high')
if (all?.[idx]) {
contentBox.value.scrollTo({
top: all[idx].offsetTop - 100,
behavior: 'smooth'
})
}
}, 0)
}
</script>
<style lang="scss" scoped>
.content-box{
overflow-y: auto;
margin-top: 30px;
height: calc(100vh - 250px);
.translation-item{
margin-bottom: 24px;
.item-body{
display: flex;
justify-content: space-between;
width: 85%;
margin: 0 auto;
.original-text{
min-width: 48%;
line-height: 30px;
margin-right: 62px;
flex: 2;
}
.translated-text{
width: 100%;
line-height: 30px;
}
}
}
}
.searchFor{
display: flex;
align-items: center;
width: 430px;
height: 60px;
padding: 12px 0;
border-radius: 10px;
background-color: #fff;
border: 1px solid rgb(234, 236, 238);
position: fixed;
top: 120px;
right: 20px;
:deep(.el-input__wrapper){
background-color: #fff;
}
.searchTextNum{
width: 70px;
height: 100%;
border-right: 1px solid rgb(234, 236, 238);
line-height: 40px;
margin-right: 16px;
text-align: center;
}
.prev{
margin-right: 12px;
}
.next{
margin-right: 12px;
}
// position: absolute;
}
.high {
background: #ffeb3b;
}
.current {
background: #409eff !important;
color: #fff !important;
}
</style>
\ No newline at end of file
<template>
<div class="writting-wrapper">
<!-- 头部区域 -->
<div class="writting-header">
<!-- <div class="writting-header">
<div class="tab-box">
<div class="tab" :class="{ tabActive: item.active }" v-for="(item, index) in store.tabList"
:key="index">
......@@ -10,12 +10,6 @@
</div>
<div class="edit-box"></div>
<div class="btn-box">
<!-- <div class="btn" @click="store.exportContent">
<div class="icon">
<img src="./assets/images/export-icon.png" alt="" />
</div>
<div class="text">导出</div>
</div> -->
<div class="btn" @click="store.toggleEditMode">
<div class="icon">
<img v-if="store.isEditMode" src="./assets/images/preview-icon.png" alt="" />
......@@ -30,22 +24,38 @@
<div class="text text1">保存</div>
</div>
</div>
</div>
</div> -->
<IntelligenceLeftTabBar></IntelligenceLeftTabBar>
<!-- 主体区域:子组件 -->
<div style="width: 100%;">
<WrittingHeader @onExport="onExport"></WrittingHeader>
<div class="writting-main">
<!-- 左侧子组件:绑定ref -->
<writtingleftBox ref="leftBoxRef" @generate="handleGenerate" />
<!-- 右侧子组件:绑定ref -->
<writtingMainBox v-show="!!store.reportContent" ref="mainBoxRef" :report-content="store.reportContent" />
<!-- <writtingleftBox ref="leftBoxRef" @generate="handleGenerate" /> -->
<WrittingLeftBox ref="leftBoxRef" />
<!-- 翻译 -->
<WrittingTranslate v-if="store.isShowClauseTranslation&&store.headerTabType=='translate'"></WrittingTranslate>
<!-- 思维导图 " -->
<WrittingMind v-else-if="store.isShowClauseTranslation&&store.headerTabType=='mind' " ref="mindRef"></WrittingMind>
<!-- 写报 -->
<WrittingMessage v-else-if="store.isShowClauseTranslation&&store.headerTabType=='message'"></WrittingMessage>
<!-- 无数据时显示占位图 -->
<div v-show="!store.reportContent" class="main-placeholder">
<div v-else class="main-placeholder">
<img src="./assets/images/container-image.png" alt="无数据占位图" />
<div class="placeholder-text">
<div v-if="store.isGenerating">智能体写报任务执行中...</div>
<div v-else>上传文件后点击“生成报文”开始写报...</div>
</div>
</div>
<!-- 右侧子组件:绑定ref -->
<!-- <writtingMainBox v-show="!!store.reportContent" ref="mainBoxRef" :report-content="store.reportContent" /> -->
</div>
<WrittingBottom @generate="handleGenerate" @write="handleWrite"></WrittingBottom>
</div>
</div>
</template>
......@@ -55,14 +65,27 @@ import { onMounted, onUnmounted, ref, nextTick } from "vue";
import { useRoute } from "vue-router";
import { ElMessage } from "element-plus";
import { useWrittingAsstaintStore } from "@/stores/writtingAsstaintStore";
import writtingleftBox from "./components/WrittingLeftBox.vue";
import writtingMainBox from "./components/WrittingMainBox.vue";
import WrittingLeftBox from "./components/WrittingLeftBox.vue";
import WrittingHeader from "./components/WrittingHeader.vue"; //头
import WrittingBottom from "./components/WrittingBottom.vue"; //底部
import WrittingTranslate from "./components/WrittingTranslate.vue"; //翻译
import WrittingMind from "./components/WrittingMind.vue"; //思维导图
import WrittingMessage from "./components/WrittingMessage.vue"; //写报
// 获取路由实例(组件内读取)
const route = useRoute();
// 获取Pinia Store实例
const leftBoxRef = ref(null); // 左侧子组件ref
const mainBoxRef = ref(null); // 右侧子组件ref
const mindRef=ref(null) //思维导图ref
const onExport = () => {
mindRef.value.exportGraph()
}
const store = useWrittingAsstaintStore();
// 2. 核心:触发生成流程
......@@ -83,6 +106,19 @@ const handleGenerate = async () => {
console.error("生成报文失败:", error);
}
};
const handleWrite=async ()=>{
try {
// // 等待DOM更新(确保子组件DOM已挂载)
store.tabList[2].active=true //写报生成之后放开写报按钮
store.headerTabType='message'
await nextTick();
await store.generateWrite()
} catch (error) {
ElMessage.error(error.message);
console.error("生成写报失败:", error);
}
}
// 生命周期
onMounted(async () => {
......@@ -100,7 +136,7 @@ onUnmounted(() => {
.writting-wrapper {
width: 100%;
height: 100%;
display: flex;
.writting-header {
height: 60px;
box-sizing: border-box;
......@@ -191,7 +227,7 @@ onUnmounted(() => {
.writting-main {
display: flex;
height: calc(100% - 60px);
height: calc(100% - 126px);
position: relative;
.main-placeholder {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论