+32
-32
@@ -1,32 +1,32 @@
|
|||||||
# dependencies
|
# dependencies
|
||||||
/node_modules
|
/node_modules
|
||||||
/.pnp
|
/.pnp
|
||||||
.pnp.js
|
.pnp.js
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
/out
|
/out
|
||||||
/.next
|
/.next
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.env.local
|
.env.local
|
||||||
.env.development.local
|
.env.development.local
|
||||||
.env.test.local
|
.env.test.local
|
||||||
.env.production.local
|
.env.production.local
|
||||||
|
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
pnpm-debug.log*
|
pnpm-debug.log*
|
||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
*~
|
*~
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
---
|
---
|
||||||
title: GDP增长分析与展望
|
title: GDP增长分析与展望
|
||||||
---
|
---
|
||||||
|
|
||||||
GDP是衡量一个国家经济规模和增长速度的重要指标。本文将分析当前全球主要经济体的GDP增长情况,探讨影响GDP增长的因素,并对未来经济走势进行展望。
|
GDP是衡量一个国家经济规模和增长速度的重要指标。本文将分析当前全球主要经济体的GDP增长情况,探讨影响GDP增长的因素,并对未来经济走势进行展望。
|
||||||
|
|
||||||
首先,我们来看一下全球GDP增长的整体情况。根据国际货币基金组织(IMF)的最新数据,全球GDP增长率预计将在2026年达到3.5%左右,较2025年有所提升。
|
首先,我们来看一下全球GDP增长的整体情况。根据国际货币基金组织(IMF)的最新数据,全球GDP增长率预计将在2026年达到3.5%左右,较2025年有所提升。
|
||||||
|
|
||||||
美国作为全球最大的经济体,其GDP增长率预计将保持在2.5%左右。消费、投资和出口是美国经济增长的主要驱动力。美联储的货币政策、政府的财政政策以及地缘政治风险等因素将影响美国经济的表现。
|
美国作为全球最大的经济体,其GDP增长率预计将保持在2.5%左右。消费、投资和出口是美国经济增长的主要驱动力。美联储的货币政策、政府的财政政策以及地缘政治风险等因素将影响美国经济的表现。
|
||||||
|
|
||||||
中国的GDP增长率预计将在5.0%左右。中国经济正在从投资驱动型向消费驱动型转变,科技创新、绿色发展等新动能正在成为经济增长的重要支撑。
|
中国的GDP增长率预计将在5.0%左右。中国经济正在从投资驱动型向消费驱动型转变,科技创新、绿色发展等新动能正在成为经济增长的重要支撑。
|
||||||
|
|
||||||
欧元区的GDP增长率预计将在1.8%左右。欧元区经济面临着人口老龄化、结构性改革缓慢等挑战,但也受益于欧洲央行的宽松货币政策和复苏基金的支持。
|
欧元区的GDP增长率预计将在1.8%左右。欧元区经济面临着人口老龄化、结构性改革缓慢等挑战,但也受益于欧洲央行的宽松货币政策和复苏基金的支持。
|
||||||
|
|
||||||
影响GDP增长的因素是多方面的,包括货币政策、财政政策、技术进步、人口结构、地缘政治等。在当前全球经济不确定性增加的背景下,各国需要采取适当的政策措施来促进经济增长。
|
影响GDP增长的因素是多方面的,包括货币政策、财政政策、技术进步、人口结构、地缘政治等。在当前全球经济不确定性增加的背景下,各国需要采取适当的政策措施来促进经济增长。
|
||||||
|
|
||||||
展望未来,全球经济增长有望保持稳定,但仍面临着诸多挑战。科技创新、数字经济、绿色转型等将成为推动经济增长的重要力量。同时,各国需要加强国际合作,共同应对全球性挑战,如气候变化、贸易保护主义等。
|
展望未来,全球经济增长有望保持稳定,但仍面临着诸多挑战。科技创新、数字经济、绿色转型等将成为推动经济增长的重要力量。同时,各国需要加强国际合作,共同应对全球性挑战,如气候变化、贸易保护主义等。
|
||||||
@@ -1,19 +1,19 @@
|
|||||||
---
|
---
|
||||||
title: 消费趋势的变化与商业机会
|
title: 消费趋势的变化与商业机会
|
||||||
---
|
---
|
||||||
|
|
||||||
消费趋势的变化正在重塑商业格局。本文将分析当前消费市场的主要趋势,探讨这些变化对企业的影响,并为企业提供相应的战略建议。
|
消费趋势的变化正在重塑商业格局。本文将分析当前消费市场的主要趋势,探讨这些变化对企业的影响,并为企业提供相应的战略建议。
|
||||||
|
|
||||||
首先,我们来看一下消费市场的整体情况。随着经济的发展和居民收入水平的提高,消费结构正在从生存型消费向发展型和享受型消费转变。
|
首先,我们来看一下消费市场的整体情况。随着经济的发展和居民收入水平的提高,消费结构正在从生存型消费向发展型和享受型消费转变。
|
||||||
|
|
||||||
数字化消费已经成为主流。电子商务、移动支付、在线娱乐等数字消费方式正在改变人们的消费习惯。消费者越来越倾向于通过互联网和移动设备进行购物、娱乐和社交。
|
数字化消费已经成为主流。电子商务、移动支付、在线娱乐等数字消费方式正在改变人们的消费习惯。消费者越来越倾向于通过互联网和移动设备进行购物、娱乐和社交。
|
||||||
|
|
||||||
健康消费成为新的增长点。消费者对健康食品、健身服务、医疗保健等健康相关产品和服务的需求不断增加。这一趋势在疫情后更加明显,人们对健康的重视程度提高。
|
健康消费成为新的增长点。消费者对健康食品、健身服务、医疗保健等健康相关产品和服务的需求不断增加。这一趋势在疫情后更加明显,人们对健康的重视程度提高。
|
||||||
|
|
||||||
可持续消费理念逐渐深入人心。越来越多的消费者开始关注产品的环保性能,愿意为可持续产品支付溢价。企业的ESG表现也成为消费者选择品牌的重要因素。
|
可持续消费理念逐渐深入人心。越来越多的消费者开始关注产品的环保性能,愿意为可持续产品支付溢价。企业的ESG表现也成为消费者选择品牌的重要因素。
|
||||||
|
|
||||||
个性化消费需求日益凸显。消费者希望获得个性化的产品和服务,而不是千篇一律的标准化产品。企业需要通过大数据、人工智能等技术来满足消费者的个性化需求。
|
个性化消费需求日益凸显。消费者希望获得个性化的产品和服务,而不是千篇一律的标准化产品。企业需要通过大数据、人工智能等技术来满足消费者的个性化需求。
|
||||||
|
|
||||||
这些消费趋势的变化对企业提出了新的挑战和机遇。企业需要调整产品和服务策略,以适应消费者需求的变化。同时,企业也需要利用数字技术来提升运营效率,改善客户体验。
|
这些消费趋势的变化对企业提出了新的挑战和机遇。企业需要调整产品和服务策略,以适应消费者需求的变化。同时,企业也需要利用数字技术来提升运营效率,改善客户体验。
|
||||||
|
|
||||||
对于投资者而言,消费趋势的变化也意味着投资机会的转移。需要关注那些能够适应消费趋势变化、具有创新能力的企业。
|
对于投资者而言,消费趋势的变化也意味着投资机会的转移。需要关注那些能够适应消费趋势变化、具有创新能力的企业。
|
||||||
@@ -1,19 +1,19 @@
|
|||||||
---
|
---
|
||||||
title: 科技创新与经济增长
|
title: 科技创新与经济增长
|
||||||
---
|
---
|
||||||
|
|
||||||
科技创新是推动经济增长的重要动力。本文将分析当前科技创新的趋势,探讨其对经济增长的影响,并为政策制定者和投资者提供参考。
|
科技创新是推动经济增长的重要动力。本文将分析当前科技创新的趋势,探讨其对经济增长的影响,并为政策制定者和投资者提供参考。
|
||||||
|
|
||||||
首先,我们来看一下当前科技创新的主要领域。人工智能、区块链、生物技术、可再生能源等领域的技术突破正在改变我们的生活和工作方式。
|
首先,我们来看一下当前科技创新的主要领域。人工智能、区块链、生物技术、可再生能源等领域的技术突破正在改变我们的生活和工作方式。
|
||||||
|
|
||||||
人工智能技术的快速发展正在各个行业产生深远影响。从自动驾驶到医疗诊断,从金融服务到制造业,AI正在提高效率,创造新的商业模式。
|
人工智能技术的快速发展正在各个行业产生深远影响。从自动驾驶到医疗诊断,从金融服务到制造业,AI正在提高效率,创造新的商业模式。
|
||||||
|
|
||||||
区块链技术的应用也在不断扩展。除了加密货币,区块链在供应链管理、数字身份、金融服务等领域的应用正在逐步落地。
|
区块链技术的应用也在不断扩展。除了加密货币,区块链在供应链管理、数字身份、金融服务等领域的应用正在逐步落地。
|
||||||
|
|
||||||
生物技术的进步为医疗健康、农业等领域带来了新的机遇。基因编辑、精准医疗、合成生物学等技术的发展有望解决许多传统医学难以解决的问题。
|
生物技术的进步为医疗健康、农业等领域带来了新的机遇。基因编辑、精准医疗、合成生物学等技术的发展有望解决许多传统医学难以解决的问题。
|
||||||
|
|
||||||
可再生能源技术的突破正在加速能源转型。太阳能、风能、储能技术的成本不断下降,为实现碳中和目标提供了技术支持。
|
可再生能源技术的突破正在加速能源转型。太阳能、风能、储能技术的成本不断下降,为实现碳中和目标提供了技术支持。
|
||||||
|
|
||||||
这些科技创新对经济增长的影响是多方面的。它们不仅提高了生产效率,还创造了新的产业和就业机会。同时,科技创新也带来了一些挑战,如就业结构的变化、隐私保护等问题。
|
这些科技创新对经济增长的影响是多方面的。它们不仅提高了生产效率,还创造了新的产业和就业机会。同时,科技创新也带来了一些挑战,如就业结构的变化、隐私保护等问题。
|
||||||
|
|
||||||
对于政策制定者而言,需要制定有利于科技创新的政策环境,包括加大研发投入、培养创新人才、完善知识产权保护等。对于投资者而言,需要关注科技创新的趋势,寻找具有增长潜力的投资机会。
|
对于政策制定者而言,需要制定有利于科技创新的政策环境,包括加大研发投入、培养创新人才、完善知识产权保护等。对于投资者而言,需要关注科技创新的趋势,寻找具有增长潜力的投资机会。
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
---
|
---
|
||||||
title: 全球贸易格局的变化与影响
|
title: 全球贸易格局的变化与影响
|
||||||
---
|
---
|
||||||
|
|
||||||
全球贸易格局正在经历深刻的变化。本文将分析当前全球贸易的现状,探讨主要经济体的贸易政策,以及这些变化对全球经济的影响。
|
全球贸易格局正在经历深刻的变化。本文将分析当前全球贸易的现状,探讨主要经济体的贸易政策,以及这些变化对全球经济的影响。
|
||||||
|
|
||||||
首先,我们来看一下全球贸易的整体情况。尽管贸易保护主义有所抬头,但全球贸易总额仍然保持增长。新兴经济体在全球贸易中的比重不断提高,正在改变传统的贸易格局。
|
首先,我们来看一下全球贸易的整体情况。尽管贸易保护主义有所抬头,但全球贸易总额仍然保持增长。新兴经济体在全球贸易中的比重不断提高,正在改变传统的贸易格局。
|
||||||
|
|
||||||
美国、欧盟、中国等主要经济体的贸易政策对全球贸易产生重要影响。美国的贸易保护主义措施、欧盟的绿色贸易政策、中国的扩大开放政策等,都在塑造着新的贸易规则。
|
美国、欧盟、中国等主要经济体的贸易政策对全球贸易产生重要影响。美国的贸易保护主义措施、欧盟的绿色贸易政策、中国的扩大开放政策等,都在塑造着新的贸易规则。
|
||||||
|
|
||||||
全球供应链的重构也是当前贸易格局变化的重要特征。出于安全和效率的考虑,许多企业正在调整供应链布局,推动区域化和多元化。
|
全球供应链的重构也是当前贸易格局变化的重要特征。出于安全和效率的考虑,许多企业正在调整供应链布局,推动区域化和多元化。
|
||||||
|
|
||||||
这些变化对全球经济产生了深远的影响。一方面,贸易格局的调整可能会导致短期的经济波动;另一方面,新的贸易规则和供应链布局也为经济发展带来了新的机遇。
|
这些变化对全球经济产生了深远的影响。一方面,贸易格局的调整可能会导致短期的经济波动;另一方面,新的贸易规则和供应链布局也为经济发展带来了新的机遇。
|
||||||
|
|
||||||
对于企业和投资者而言,需要密切关注贸易格局的变化,调整战略以适应新的环境。同时,政府也需要制定相应的政策,促进贸易的健康发展。
|
对于企业和投资者而言,需要密切关注贸易格局的变化,调整战略以适应新的环境。同时,政府也需要制定相应的政策,促进贸易的健康发展。
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
---
|
---
|
||||||
title: 利率走势分析与投资策略
|
title: 利率走势分析与投资策略
|
||||||
---
|
---
|
||||||
|
|
||||||
近期全球主要经济体的利率走势引起了市场的广泛关注。本文将分析当前利率环境,探讨未来可能的变化趋势,并为投资者提供相应的投资策略建议。
|
近期全球主要经济体的利率走势引起了市场的广泛关注。本文将分析当前利率环境,探讨未来可能的变化趋势,并为投资者提供相应的投资策略建议。
|
||||||
|
|
||||||
首先,我们来看一下美联储的货币政策走向。随着通胀压力的缓解,美联储已经开始考虑降息,但步伐可能会比较谨慎。欧洲央行和日本央行也在调整各自的货币政策,以适应经济形势的变化。
|
首先,我们来看一下美联储的货币政策走向。随着通胀压力的缓解,美联储已经开始考虑降息,但步伐可能会比较谨慎。欧洲央行和日本央行也在调整各自的货币政策,以适应经济形势的变化。
|
||||||
|
|
||||||
在中国,央行通过调整政策利率和LPR(贷款市场报价利率)来引导市场利率水平。近期LPR的下调反映了货币政策的宽松取向,这将有利于降低企业融资成本,刺激经济增长。
|
在中国,央行通过调整政策利率和LPR(贷款市场报价利率)来引导市场利率水平。近期LPR的下调反映了货币政策的宽松取向,这将有利于降低企业融资成本,刺激经济增长。
|
||||||
|
|
||||||
对于债券投资者而言,利率下行意味着债券价格上涨,这为债券投资提供了机会。但需要注意的是,不同期限的债券对利率变化的敏感度不同,投资者需要根据自己的风险承受能力选择合适的债券品种。
|
对于债券投资者而言,利率下行意味着债券价格上涨,这为债券投资提供了机会。但需要注意的是,不同期限的债券对利率变化的敏感度不同,投资者需要根据自己的风险承受能力选择合适的债券品种。
|
||||||
|
|
||||||
对于股票投资者而言,利率下行通常有利于估值提升,特别是对成长型股票更为有利。但同时也需要关注经济基本面的变化,选择具有良好成长性和估值合理的股票。
|
对于股票投资者而言,利率下行通常有利于估值提升,特别是对成长型股票更为有利。但同时也需要关注经济基本面的变化,选择具有良好成长性和估值合理的股票。
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
---
|
---
|
||||||
title: 2026年中国经济展望
|
title: 2026年中国经济展望
|
||||||
---
|
---
|
||||||
|
|
||||||
2026年中国经济将面临诸多挑战与机遇。随着全球经济格局的变化,中国经济正在从高速增长向高质量发展转型。本文将分析当前经济形势,预测未来趋势,并提出相应的政策建议。
|
2026年中国经济将面临诸多挑战与机遇。随着全球经济格局的变化,中国经济正在从高速增长向高质量发展转型。本文将分析当前经济形势,预测未来趋势,并提出相应的政策建议。
|
||||||
|
|
||||||
首先,我们来看一下当前的宏观经济指标。GDP增长率、通胀水平、失业率等关键指标都显示中国经济正在稳步复苏。然而,外部环境的不确定性仍然存在,需要密切关注。
|
首先,我们来看一下当前的宏观经济指标。GDP增长率、通胀水平、失业率等关键指标都显示中国经济正在稳步复苏。然而,外部环境的不确定性仍然存在,需要密切关注。
|
||||||
|
|
||||||
在产业结构方面,数字经济、绿色经济等新兴产业正在成为经济增长的新动力。传统产业的转型升级也在加速,这将为经济可持续发展注入新的活力。
|
在产业结构方面,数字经济、绿色经济等新兴产业正在成为经济增长的新动力。传统产业的转型升级也在加速,这将为经济可持续发展注入新的活力。
|
||||||
|
|
||||||
对于投资者而言,2026年的投资策略需要更加注重长期价值,关注科技创新、消费升级等领域的投资机会。同时,风险管理也变得尤为重要,需要建立多元化的投资组合。
|
对于投资者而言,2026年的投资策略需要更加注重长期价值,关注科技创新、消费升级等领域的投资机会。同时,风险管理也变得尤为重要,需要建立多元化的投资组合。
|
||||||
|
|
||||||
总的来说,2026年中国经济有望保持稳健增长,但需要应对来自内外部的各种挑战。通过深化改革、扩大开放,中国经济将迎来新的发展机遇。
|
总的来说,2026年中国经济有望保持稳健增长,但需要应对来自内外部的各种挑战。通过深化改革、扩大开放,中国经济将迎来新的发展机遇。
|
||||||
@@ -1,117 +1,117 @@
|
|||||||
# 城投债化债:机制、影响与市场展望
|
# 城投债化债:机制、影响与市场展望
|
||||||
|
|
||||||
## 引言
|
## 引言
|
||||||
|
|
||||||
城投债作为地方政府融资的重要工具,在过去几十年为我国基础设施建设和经济发展做出了重要贡献。然而,随着债务规模的不断扩大,城投债风险逐渐引起市场关注。近期,市场对城投债化债的讨论升温,本文将深入分析城投债化债的核心机制、市场影响以及未来走势。
|
城投债作为地方政府融资的重要工具,在过去几十年为我国基础设施建设和经济发展做出了重要贡献。然而,随着债务规模的不断扩大,城投债风险逐渐引起市场关注。近期,市场对城投债化债的讨论升温,本文将深入分析城投债化债的核心机制、市场影响以及未来走势。
|
||||||
|
|
||||||
## 化债的核心逻辑:高利率换低利率
|
## 化债的核心逻辑:高利率换低利率
|
||||||
|
|
||||||
### 化债不是还本金
|
### 化债不是还本金
|
||||||
|
|
||||||
市场对城投债化债存在一个常见误解,认为化债就是偿还本金。事实上,化债的核心逻辑并非偿还本金,而是通过债务重组和利率优化,降低债务成本和风险。
|
市场对城投债化债存在一个常见误解,认为化债就是偿还本金。事实上,化债的核心逻辑并非偿还本金,而是通过债务重组和利率优化,降低债务成本和风险。
|
||||||
|
|
||||||
城投债的本质是地方政府的隐性债务,其规模庞大,短期内完全偿还本金并不现实。化债的目标是通过调整债务结构,将高成本债务置换为低成本债务,从而减轻地方政府的利息负担,为经济发展创造更有利的金融环境。
|
城投债的本质是地方政府的隐性债务,其规模庞大,短期内完全偿还本金并不现实。化债的目标是通过调整债务结构,将高成本债务置换为低成本债务,从而减轻地方政府的利息负担,为经济发展创造更有利的金融环境。
|
||||||
|
|
||||||
### 高利率向低利率的转化
|
### 高利率向低利率的转化
|
||||||
|
|
||||||
过去几年,城投债发行利率普遍较高,部分地区甚至超过6%。高利率不仅增加了地方政府的偿债压力,也限制了地方财政的可持续性。化债的关键在于将这些高利率债务转换为低利率债务。
|
过去几年,城投债发行利率普遍较高,部分地区甚至超过6%。高利率不仅增加了地方政府的偿债压力,也限制了地方财政的可持续性。化债的关键在于将这些高利率债务转换为低利率债务。
|
||||||
|
|
||||||
具体而言,化债通过以下方式实现利率优化:
|
具体而言,化债通过以下方式实现利率优化:
|
||||||
- **债务置换**:将高利率城投债置换为低利率的政府债券或银行贷款
|
- **债务置换**:将高利率城投债置换为低利率的政府债券或银行贷款
|
||||||
- **展期续作**:通过债务展期,延长还款期限,同时降低票面利率
|
- **展期续作**:通过债务展期,延长还款期限,同时降低票面利率
|
||||||
- **重组整合**:对城投平台进行整合,提高信用评级,从而降低融资成本
|
- **重组整合**:对城投平台进行整合,提高信用评级,从而降低融资成本
|
||||||
|
|
||||||
## 银行在化债中的角色:存款利率下调的逻辑
|
## 银行在化债中的角色:存款利率下调的逻辑
|
||||||
|
|
||||||
### 银行降低存款利率的必要性
|
### 银行降低存款利率的必要性
|
||||||
|
|
||||||
为了支持城投债化债,银行体系需要提供低成本资金。降低存款利率成为银行吸储并提供低利率资金的重要手段。
|
为了支持城投债化债,银行体系需要提供低成本资金。降低存款利率成为银行吸储并提供低利率资金的重要手段。
|
||||||
|
|
||||||
银行降低存款利率主要基于以下考虑:
|
银行降低存款利率主要基于以下考虑:
|
||||||
1. **降低负债成本**:通过下调存款利率,银行可以降低资金成本,为后续的低利率贷款创造空间
|
1. **降低负债成本**:通过下调存款利率,银行可以降低资金成本,为后续的低利率贷款创造空间
|
||||||
2. **引导资金流向**:存款利率下调后,部分资金可能流向债券市场,增加债券需求,从而降低债券发行利率
|
2. **引导资金流向**:存款利率下调后,部分资金可能流向债券市场,增加债券需求,从而降低债券发行利率
|
||||||
3. **支持实体经济**:低利率环境有助于降低企业融资成本,支持经济复苏
|
3. **支持实体经济**:低利率环境有助于降低企业融资成本,支持经济复苏
|
||||||
|
|
||||||
### 低利率置换城投债的机制
|
### 低利率置换城投债的机制
|
||||||
|
|
||||||
银行通过以下步骤实现低利率置换城投债:
|
银行通过以下步骤实现低利率置换城投债:
|
||||||
|
|
||||||
1. **吸收低成本存款**:通过降低存款利率,吸引储户存款,获取低成本资金
|
1. **吸收低成本存款**:通过降低存款利率,吸引储户存款,获取低成本资金
|
||||||
2. **提供低利率贷款**:向城投平台提供低利率贷款,用于置换高利率债务
|
2. **提供低利率贷款**:向城投平台提供低利率贷款,用于置换高利率债务
|
||||||
3. **参与债务重组**:直接购买或承销城投债,为城投平台提供资金支持
|
3. **参与债务重组**:直接购买或承销城投债,为城投平台提供资金支持
|
||||||
|
|
||||||
这种机制不仅帮助城投平台降低了融资成本,也为银行提供了相对安全的资产配置渠道,实现了双赢。
|
这种机制不仅帮助城投平台降低了融资成本,也为银行提供了相对安全的资产配置渠道,实现了双赢。
|
||||||
|
|
||||||
## 市场影响:中期低利率环境的形成
|
## 市场影响:中期低利率环境的形成
|
||||||
|
|
||||||
### 化债目标下的利率政策
|
### 化债目标下的利率政策
|
||||||
|
|
||||||
为了实现城投债化债的目标,货币政策需要维持宽松,市场中期将保持低利率或利率下行趋势。
|
为了实现城投债化债的目标,货币政策需要维持宽松,市场中期将保持低利率或利率下行趋势。
|
||||||
|
|
||||||
从政策层面看,央行通过以下措施引导利率下行:
|
从政策层面看,央行通过以下措施引导利率下行:
|
||||||
- **降准降息**:通过降低存款准备金率和政策利率,向市场释放流动性
|
- **降准降息**:通过降低存款准备金率和政策利率,向市场释放流动性
|
||||||
- **结构性货币政策**:通过再贷款、再贴现等工具,定向支持城投平台等重点领域
|
- **结构性货币政策**:通过再贷款、再贴现等工具,定向支持城投平台等重点领域
|
||||||
- **预期管理**:通过政策沟通,引导市场形成利率下行的预期
|
- **预期管理**:通过政策沟通,引导市场形成利率下行的预期
|
||||||
|
|
||||||
### 对不同市场主体的影响
|
### 对不同市场主体的影响
|
||||||
|
|
||||||
#### 对城投平台的影响
|
#### 对城投平台的影响
|
||||||
- **融资成本下降**:直接减轻利息负担,改善财务状况
|
- **融资成本下降**:直接减轻利息负担,改善财务状况
|
||||||
- **再融资环境改善**:低利率环境有助于城投债的顺利发行
|
- **再融资环境改善**:低利率环境有助于城投债的顺利发行
|
||||||
- **债务可持续性增强**:降低了债务违约风险,提高了信用评级
|
- **债务可持续性增强**:降低了债务违约风险,提高了信用评级
|
||||||
|
|
||||||
#### 对银行的影响
|
#### 对银行的影响
|
||||||
- **利差收窄**:存款利率下调的同时,贷款利率也会下降,可能压缩银行利差
|
- **利差收窄**:存款利率下调的同时,贷款利率也会下降,可能压缩银行利差
|
||||||
- **资产质量改善**:城投债风险降低,银行资产质量得到提升
|
- **资产质量改善**:城投债风险降低,银行资产质量得到提升
|
||||||
- **业务结构调整**:需要寻找新的利润增长点,如中间业务收入
|
- **业务结构调整**:需要寻找新的利润增长点,如中间业务收入
|
||||||
|
|
||||||
#### 对投资者的影响
|
#### 对投资者的影响
|
||||||
- **固定收益产品收益下降**:存款、债券等固定收益产品的收益率可能持续走低
|
- **固定收益产品收益下降**:存款、债券等固定收益产品的收益率可能持续走低
|
||||||
- **风险偏好变化**:部分投资者可能转向权益市场寻求更高收益
|
- **风险偏好变化**:部分投资者可能转向权益市场寻求更高收益
|
||||||
- **资产配置调整**:需要重新平衡资产配置,寻找风险与收益的平衡点
|
- **资产配置调整**:需要重新平衡资产配置,寻找风险与收益的平衡点
|
||||||
|
|
||||||
## 市场展望与投资策略
|
## 市场展望与投资策略
|
||||||
|
|
||||||
### 短期展望(1-2年)
|
### 短期展望(1-2年)
|
||||||
|
|
||||||
短期内,为了确保城投债化债的顺利推进,货币政策将保持宽松,市场利率有望继续下行。城投债违约风险将显著降低,信用利差可能收窄。
|
短期内,为了确保城投债化债的顺利推进,货币政策将保持宽松,市场利率有望继续下行。城投债违约风险将显著降低,信用利差可能收窄。
|
||||||
|
|
||||||
### 中期展望(2-5年)
|
### 中期展望(2-5年)
|
||||||
|
|
||||||
中期来看,随着化债工作的推进,城投债风险将进一步降低,但市场利率可能维持在较低水平。银行体系的资产负债结构将发生调整,金融市场的风险偏好可能逐步回升。
|
中期来看,随着化债工作的推进,城投债风险将进一步降低,但市场利率可能维持在较低水平。银行体系的资产负债结构将发生调整,金融市场的风险偏好可能逐步回升。
|
||||||
|
|
||||||
### 长期展望(5年以上)
|
### 长期展望(5年以上)
|
||||||
|
|
||||||
长期而言,城投债化债完成后,地方政府的债务结构将更加合理,财政可持续性将得到提升。市场利率可能回归正常水平,但仍将保持相对稳定。
|
长期而言,城投债化债完成后,地方政府的债务结构将更加合理,财政可持续性将得到提升。市场利率可能回归正常水平,但仍将保持相对稳定。
|
||||||
|
|
||||||
### 投资策略建议
|
### 投资策略建议
|
||||||
|
|
||||||
针对当前的市场环境,投资者可以考虑以下策略:
|
针对当前的市场环境,投资者可以考虑以下策略:
|
||||||
|
|
||||||
1. **固定收益投资**:
|
1. **固定收益投资**:
|
||||||
- 关注高评级城投债,受益于信用利差收窄
|
- 关注高评级城投债,受益于信用利差收窄
|
||||||
- 适当配置利率债,受益于利率下行
|
- 适当配置利率债,受益于利率下行
|
||||||
- 谨慎对待低评级城投债,仍需关注个体风险
|
- 谨慎对待低评级城投债,仍需关注个体风险
|
||||||
|
|
||||||
2. **权益投资**:
|
2. **权益投资**:
|
||||||
- 关注受益于低利率环境的行业,如房地产、公用事业等
|
- 关注受益于低利率环境的行业,如房地产、公用事业等
|
||||||
- 关注地方政府投资重点领域,如基础设施、新能源等
|
- 关注地方政府投资重点领域,如基础设施、新能源等
|
||||||
- 关注银行板块,受益于资产质量改善
|
- 关注银行板块,受益于资产质量改善
|
||||||
|
|
||||||
3. **资产配置**:
|
3. **资产配置**:
|
||||||
- 保持多元化配置,分散风险
|
- 保持多元化配置,分散风险
|
||||||
- 根据自身风险承受能力,调整股债比例
|
- 根据自身风险承受能力,调整股债比例
|
||||||
- 密切关注政策变化,及时调整投资策略
|
- 密切关注政策变化,及时调整投资策略
|
||||||
|
|
||||||
## 结论
|
## 结论
|
||||||
|
|
||||||
城投债化债是一个长期而复杂的过程,其核心逻辑是通过高利率换低利率来降低债务成本和风险。银行体系通过降低存款利率,为化债提供低成本资金支持。为了实现化债目标,市场中期将维持低利率或利率下行趋势。
|
城投债化债是一个长期而复杂的过程,其核心逻辑是通过高利率换低利率来降低债务成本和风险。银行体系通过降低存款利率,为化债提供低成本资金支持。为了实现化债目标,市场中期将维持低利率或利率下行趋势。
|
||||||
|
|
||||||
对于投资者而言,需要认识到化债过程中的投资机会和风险,制定相应的投资策略。同时,也要看到化债对经济长期发展的积极意义,为市场的稳定和繁荣创造有利条件。
|
对于投资者而言,需要认识到化债过程中的投资机会和风险,制定相应的投资策略。同时,也要看到化债对经济长期发展的积极意义,为市场的稳定和繁荣创造有利条件。
|
||||||
|
|
||||||
## 参考文献
|
## 参考文献
|
||||||
|
|
||||||
1. 《中国城投债市场发展报告》
|
1. 《中国城投债市场发展报告》
|
||||||
2. 《地方政府债务风险防控研究》
|
2. 《地方政府债务风险防控研究》
|
||||||
3. 央行货币政策执行报告
|
3. 央行货币政策执行报告
|
||||||
4. 财政部关于地方政府债务管理的相关文件
|
4. 财政部关于地方政府债务管理的相关文件
|
||||||
|
|||||||
+31
-13
@@ -3,31 +3,49 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"tauri": "tauri",
|
||||||
"build": "next build",
|
"dev:web": "vite",
|
||||||
"start": "next start",
|
"dev:server": "node --experimental-strip-types --no-warnings server/index.js",
|
||||||
|
"dev": "concurrently -n web,api -c blue,green \"npm run dev:web\" \"npm run dev:server\"",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"start": "node --experimental-strip-types --no-warnings server/index.js",
|
||||||
|
"db:migrate": "prisma migrate deploy",
|
||||||
|
"db:seed": "node scripts/seed.cjs",
|
||||||
|
"icons": "node scripts/generate-icons.mjs",
|
||||||
|
"tauri:dev": "tauri dev",
|
||||||
|
"tauri:build": "tauri build",
|
||||||
|
"tauri:android:init": "tauri android init",
|
||||||
|
"tauri:android:dev": "tauri android dev",
|
||||||
|
"tauri:android:build": "tauri android build",
|
||||||
|
"tauri:android:apk": "tauri android build --apk",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/adapter-mariadb": "^7.7.0",
|
"@prisma/adapter-mariadb": "^7.9.1",
|
||||||
"@prisma/client": "^7.7.0",
|
"@prisma/client": "^7.7.0",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"express": "^4.21.0",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"mariadb": "^3.5.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"marked": "^17.0.6",
|
"marked": "^17.0.6",
|
||||||
"next": "^14.2.3",
|
"mariadb": "^3.5.3",
|
||||||
"node-cron": "^4.2.1",
|
|
||||||
"prisma": "^7.7.0",
|
"prisma": "^7.7.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-router-dom": "^6.26.0",
|
||||||
"recharts": "^3.8.1"
|
"recharts": "^3.8.1"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
|
||||||
"author": "",
|
|
||||||
"license": "ISC",
|
|
||||||
"description": "",
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@tauri-apps/api": "^2.0.0",
|
||||||
|
"@tauri-apps/cli": "^2.0.0",
|
||||||
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
"axios": "^1.15.0",
|
"axios": "^1.15.0",
|
||||||
"cheerio": "^1.2.0",
|
"cheerio": "^1.2.0",
|
||||||
"csv-parser": "^3.2.0"
|
"concurrently": "^9.0.0",
|
||||||
|
"csv-parser": "^3.2.0",
|
||||||
|
"sharp": "^0.33.0",
|
||||||
|
"vite": "^5.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+14
-14
@@ -1,14 +1,14 @@
|
|||||||
// This file was generated by Prisma, and assumes you have installed the following:
|
// This file was generated by Prisma, and assumes you have installed the following:
|
||||||
// npm install --save-dev prisma dotenv
|
// npm install --save-dev prisma dotenv
|
||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
import { defineConfig } from "prisma/config";
|
import { defineConfig } from "prisma/config";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
schema: "prisma/schema.prisma",
|
schema: "prisma/schema.prisma",
|
||||||
migrations: {
|
migrations: {
|
||||||
path: "prisma/migrations",
|
path: "prisma/migrations",
|
||||||
},
|
},
|
||||||
datasource: {
|
datasource: {
|
||||||
url: process.env["DATABASE_URL"],
|
url: process.env["DATABASE_URL"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,62 +1,62 @@
|
|||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE `User` (
|
CREATE TABLE `User` (
|
||||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
`name` VARCHAR(191) NOT NULL,
|
`name` VARCHAR(191) NOT NULL,
|
||||||
`email` VARCHAR(191) NOT NULL,
|
`email` VARCHAR(191) NOT NULL,
|
||||||
`password` VARCHAR(191) NOT NULL,
|
`password` VARCHAR(191) NOT NULL,
|
||||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
`updatedAt` DATETIME(3) NOT NULL,
|
`updatedAt` DATETIME(3) NOT NULL,
|
||||||
|
|
||||||
UNIQUE INDEX `User_email_key`(`email`),
|
UNIQUE INDEX `User_email_key`(`email`),
|
||||||
PRIMARY KEY (`id`)
|
PRIMARY KEY (`id`)
|
||||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE `Article` (
|
CREATE TABLE `Article` (
|
||||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
`title` VARCHAR(191) NOT NULL,
|
`title` VARCHAR(191) NOT NULL,
|
||||||
`slug` VARCHAR(191) NOT NULL,
|
`slug` VARCHAR(191) NOT NULL,
|
||||||
`content` VARCHAR(191) NOT NULL,
|
`content` VARCHAR(191) NOT NULL,
|
||||||
`date` VARCHAR(191) NOT NULL,
|
`date` VARCHAR(191) NOT NULL,
|
||||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
`updatedAt` DATETIME(3) NOT NULL,
|
`updatedAt` DATETIME(3) NOT NULL,
|
||||||
|
|
||||||
UNIQUE INDEX `Article_slug_key`(`slug`),
|
UNIQUE INDEX `Article_slug_key`(`slug`),
|
||||||
PRIMARY KEY (`id`)
|
PRIMARY KEY (`id`)
|
||||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE `Strategy` (
|
CREATE TABLE `Strategy` (
|
||||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
`name` VARCHAR(191) NOT NULL,
|
`name` VARCHAR(191) NOT NULL,
|
||||||
`description` VARCHAR(191) NOT NULL,
|
`description` VARCHAR(191) NOT NULL,
|
||||||
`data` JSON NOT NULL,
|
`data` JSON NOT NULL,
|
||||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
`updatedAt` DATETIME(3) NOT NULL,
|
`updatedAt` DATETIME(3) NOT NULL,
|
||||||
|
|
||||||
PRIMARY KEY (`id`)
|
PRIMARY KEY (`id`)
|
||||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE `Indicator` (
|
CREATE TABLE `Indicator` (
|
||||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
`name` VARCHAR(191) NOT NULL,
|
`name` VARCHAR(191) NOT NULL,
|
||||||
`groupName` VARCHAR(191) NOT NULL,
|
`groupName` VARCHAR(191) NOT NULL,
|
||||||
`data` JSON NOT NULL,
|
`data` JSON NOT NULL,
|
||||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
`updatedAt` DATETIME(3) NOT NULL,
|
`updatedAt` DATETIME(3) NOT NULL,
|
||||||
|
|
||||||
PRIMARY KEY (`id`)
|
PRIMARY KEY (`id`)
|
||||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE `SowInventory` (
|
CREATE TABLE `SowInventory` (
|
||||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
`month` VARCHAR(191) NOT NULL,
|
`month` VARCHAR(191) NOT NULL,
|
||||||
`inventory` INTEGER NOT NULL,
|
`inventory` INTEGER NOT NULL,
|
||||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
`updatedAt` DATETIME(3) NOT NULL,
|
`updatedAt` DATETIME(3) NOT NULL,
|
||||||
|
|
||||||
UNIQUE INDEX `SowInventory_month_key`(`month`),
|
UNIQUE INDEX `SowInventory_month_key`(`month`),
|
||||||
PRIMARY KEY (`id`)
|
PRIMARY KEY (`id`)
|
||||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
Warnings:
|
Warnings:
|
||||||
|
|
||||||
- You are about to drop the `SowInventory` table. If the table is not empty, all the data it contains will be lost.
|
- You are about to drop the `SowInventory` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
|
||||||
*/
|
*/
|
||||||
-- DropTable
|
-- DropTable
|
||||||
DROP TABLE `SowInventory`;
|
DROP TABLE `SowInventory`;
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE `Econ_SowInventory` (
|
CREATE TABLE `Econ_SowInventory` (
|
||||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
`month` VARCHAR(191) NOT NULL,
|
`month` VARCHAR(191) NOT NULL,
|
||||||
`inventory` INTEGER NOT NULL,
|
`inventory` INTEGER NOT NULL,
|
||||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
`updatedAt` DATETIME(3) NOT NULL,
|
`updatedAt` DATETIME(3) NOT NULL,
|
||||||
|
|
||||||
UNIQUE INDEX `Econ_SowInventory_month_key`(`month`),
|
UNIQUE INDEX `Econ_SowInventory_month_key`(`month`),
|
||||||
PRIMARY KEY (`id`)
|
PRIMARY KEY (`id`)
|
||||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
# Please do not edit this file manually
|
# Please do not edit this file manually
|
||||||
# It should be added in your version-control system (e.g., Git)
|
# It should be added in your version-control system (e.g., Git)
|
||||||
provider = "mysql"
|
provider = "mysql"
|
||||||
|
|||||||
@@ -1,19 +1,6 @@
|
|||||||
# iBoard
|
# iBoard
|
||||||
用于收集和整理经济数据,但是以策略为主体重新组织。
|
用于收集和整理经济数据,但是以策略为主体重新组织。
|
||||||
- 首页主要是放经济文章
|
- 首页主要是放经济文章
|
||||||
- 数据主要是放各种经济数据和图表。
|
- 数据主要是放各种经济数据和图表。
|
||||||
- 策略是放按照经济理论组织的相关数据和图表的对比。
|
- 策略是放按照经济理论组织的相关数据和图表的对比。
|
||||||
|
|
||||||
|
|
||||||
## 爬虫
|
|
||||||
- 基于 bun 的定时任务
|
|
||||||
- 统一获取接口
|
|
||||||
- 统一爬取接口
|
|
||||||
```
|
|
||||||
async function fetch(startDate = null, endDate = null)
|
|
||||||
async function trigger(startDate = null, endDate = null)
|
|
||||||
```
|
|
||||||
不传递startDate和endDate,默认,默认获取最近30天的数据。
|
|
||||||
|
|
||||||
fecher 负责从目标网站获取原始数据
|
|
||||||
scraper 负责检查数据库中的数据是否完整,调用 fecher 将数据存储到数据库。
|
|
||||||
|
|||||||
@@ -14,4 +14,4 @@ const mysql = require('mysql2/promise');
|
|||||||
console.log('[' + t + '] 删除 ' + r.affectedRows + ' 条重复行');
|
console.log('[' + t + '] 删除 ' + r.affectedRows + ' 条重复行');
|
||||||
}
|
}
|
||||||
await c.end();
|
await c.end();
|
||||||
})().catch(e=>{console.error(e);process.exit(1)});
|
})().catch(e=>{console.error(e);process.exit(1)});
|
||||||
|
|||||||
+62
-62
@@ -1,62 +1,62 @@
|
|||||||
# 数据爬取脚本
|
# 数据爬取脚本
|
||||||
|
|
||||||
本目录包含用于从各种数据源爬取经济数据的脚本。
|
本目录包含用于从各种数据源爬取经济数据的脚本。
|
||||||
|
|
||||||
## 目录结构
|
## 目录结构
|
||||||
|
|
||||||
- `sow-inventory-scraper.js` - 爬取能繁母猪数量数据
|
- `sow-inventory-scraper.js` - 爬取能繁母猪数量数据
|
||||||
- `template-scraper.js` - 通用爬取脚本模板
|
- `template-scraper.js` - 通用爬取脚本模板
|
||||||
|
|
||||||
## 依赖
|
## 依赖
|
||||||
|
|
||||||
- axios - 用于发送HTTP请求
|
- axios - 用于发送HTTP请求
|
||||||
- cheerio - 用于解析HTML内容
|
- cheerio - 用于解析HTML内容
|
||||||
- csv-parser - 用于解析CSV格式数据
|
- csv-parser - 用于解析CSV格式数据
|
||||||
|
|
||||||
## 安装依赖
|
## 安装依赖
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
## 使用方法
|
## 使用方法
|
||||||
|
|
||||||
1. **修改脚本配置**:
|
1. **修改脚本配置**:
|
||||||
- 打开相应的爬取脚本
|
- 打开相应的爬取脚本
|
||||||
- 修改数据源URL和数据提取逻辑
|
- 修改数据源URL和数据提取逻辑
|
||||||
|
|
||||||
2. **运行爬取脚本**:
|
2. **运行爬取脚本**:
|
||||||
```bash
|
```bash
|
||||||
node scripts/scraper/sow-inventory-scraper.js
|
node scripts/scraper/sow-inventory-scraper.js
|
||||||
```
|
```
|
||||||
|
|
||||||
## 注意事项
|
## 注意事项
|
||||||
|
|
||||||
1. **数据来源合规性**:确保爬取的数据来源符合相关法律法规
|
1. **数据来源合规性**:确保爬取的数据来源符合相关法律法规
|
||||||
2. **爬虫礼仪**:设置合理的请求间隔,避免对目标网站造成压力
|
2. **爬虫礼仪**:设置合理的请求间隔,避免对目标网站造成压力
|
||||||
3. **数据验证**:爬取后对数据进行验证,确保数据准确性
|
3. **数据验证**:爬取后对数据进行验证,确保数据准确性
|
||||||
4. **错误处理**:脚本包含基本的错误处理,但可能需要根据实际情况进行调整
|
4. **错误处理**:脚本包含基本的错误处理,但可能需要根据实际情况进行调整
|
||||||
|
|
||||||
## 扩展指南
|
## 扩展指南
|
||||||
|
|
||||||
1. **添加新的爬取脚本**:
|
1. **添加新的爬取脚本**:
|
||||||
- 复制 `template-scraper.js` 模板
|
- 复制 `template-scraper.js` 模板
|
||||||
- 修改数据源URL和数据提取逻辑
|
- 修改数据源URL和数据提取逻辑
|
||||||
- 更新数据库存储逻辑
|
- 更新数据库存储逻辑
|
||||||
|
|
||||||
2. **修改数据提取逻辑**:
|
2. **修改数据提取逻辑**:
|
||||||
- 使用Cheerio选择器提取HTML中的数据
|
- 使用Cheerio选择器提取HTML中的数据
|
||||||
- 对于CSV数据,使用csv-parser解析
|
- 对于CSV数据,使用csv-parser解析
|
||||||
- 对于JSON数据,直接处理响应数据
|
- 对于JSON数据,直接处理响应数据
|
||||||
|
|
||||||
3. **数据存储**:
|
3. **数据存储**:
|
||||||
- 脚本会自动将数据存储到数据库
|
- 脚本会自动将数据存储到数据库
|
||||||
- 会检查数据是否已存在,存在则更新,不存在则创建
|
- 会检查数据是否已存在,存在则更新,不存在则创建
|
||||||
|
|
||||||
## 示例
|
## 示例
|
||||||
|
|
||||||
爬取浙江省能繁母猪数量数据:
|
爬取浙江省能繁母猪数量数据:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node scripts/scraper/sow-inventory-scraper.js
|
node scripts/scraper/sow-inventory-scraper.js
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,352 +1,95 @@
|
|||||||
import axios from 'axios';
|
const axios = require('axios');
|
||||||
import * as cheerio from 'cheerio';
|
const cheerio = require('cheerio');
|
||||||
import { createPool } from 'mariadb';
|
const fs = require('fs');
|
||||||
import fs from 'fs';
|
const path = require('path');
|
||||||
import path from 'path';
|
const { PrismaClient } = require('../../generated/prisma');
|
||||||
|
|
||||||
function loadDatabaseConfig() {
|
const prisma = new PrismaClient();
|
||||||
const envPath = path.join(process.cwd(), '.env');
|
|
||||||
if (fs.existsSync(envPath)) {
|
// 浙江数据开放网站URL(示例,实际需要根据真实网站修改)
|
||||||
console.log('读取 .env 文件...');
|
const ZHEJIANG_OPEN_DATA_URL = 'https://data.zj.gov.cn/';
|
||||||
const envContent = fs.readFileSync(envPath, 'utf-8');
|
|
||||||
const lines = envContent.split('\n');
|
// 能繁母猪数量数据爬取函数
|
||||||
|
async function scrapeSowInventory() {
|
||||||
for (const line of lines) {
|
try {
|
||||||
const trimmedLine = line.trim();
|
console.log('开始爬取浙江省能繁母猪数量数据...');
|
||||||
if (trimmedLine.startsWith('DATABASE_URL=')) {
|
|
||||||
const url = trimmedLine.substring('DATABASE_URL='.length).replace(/^"|"$/g, '');
|
// 1. 发送HTTP请求获取页面内容
|
||||||
console.log('从 .env 文件获取数据库配置成功!');
|
const response = await axios.get(ZHEJIANG_OPEN_DATA_URL, {
|
||||||
console.log(`DATABASE_URL: ${url}`);
|
headers: {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
||||||
// 解析URL
|
}
|
||||||
const parts = url.split('://')[1].split('@');
|
});
|
||||||
const auth = parts[0].split(':');
|
|
||||||
const hostAndDb = parts[1].split('/');
|
// 2. 解析HTML内容
|
||||||
const hostAndPort = hostAndDb[0].split(':');
|
const $ = cheerio.load(response.data);
|
||||||
|
|
||||||
return {
|
// 3. 提取数据(这里需要根据实际网站结构修改选择器)
|
||||||
user: auth[0],
|
// 示例:假设数据在表格中
|
||||||
password: auth[1],
|
const data = [];
|
||||||
host: hostAndPort[0],
|
|
||||||
port: parseInt(hostAndPort[1]),
|
$('table').each((tableIndex, table) => {
|
||||||
database: hostAndDb[1]
|
$(table).find('tr').each((rowIndex, row) => {
|
||||||
};
|
if (rowIndex > 0) { // 跳过表头
|
||||||
}
|
const cells = $(row).find('td');
|
||||||
}
|
if (cells.length >= 2) {
|
||||||
}
|
const month = $(cells[0]).text().trim();
|
||||||
|
const inventory = parseInt($(cells[1]).text().trim());
|
||||||
console.log('未找到 .env 文件或 DATABASE_URL 配置,使用默认配置');
|
|
||||||
return {
|
if (month && !isNaN(inventory)) {
|
||||||
host: '192.168.111.111',
|
data.push({
|
||||||
port: 3306,
|
month,
|
||||||
user: 'root',
|
inventory
|
||||||
password: 'fullstack',
|
});
|
||||||
database: 'iboard'
|
}
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
// 创建数据库连接池
|
});
|
||||||
const config = loadDatabaseConfig();
|
|
||||||
console.log('数据库配置:');
|
console.log(`爬取到 ${data.length} 条数据`);
|
||||||
console.log(` 主机: ${config.host}`);
|
|
||||||
console.log(` 端口: ${config.port}`);
|
// 4. 存储数据到数据库
|
||||||
console.log(` 用户: ${config.user}`);
|
for (const item of data) {
|
||||||
console.log(` 数据库: ${config.database}`);
|
try {
|
||||||
|
// 检查数据是否已存在
|
||||||
let pool;
|
const existing = await prisma.econ_SowInventory.findUnique({
|
||||||
|
where: { month: item.month }
|
||||||
async function initPool() {
|
});
|
||||||
if (!pool) {
|
|
||||||
pool = createPool({
|
if (existing) {
|
||||||
host: config.host,
|
// 更新现有数据
|
||||||
port: config.port,
|
await prisma.econ_SowInventory.update({
|
||||||
user: config.user,
|
where: { month: item.month },
|
||||||
password: config.password,
|
data: { inventory: item.inventory }
|
||||||
database: config.database,
|
});
|
||||||
connectionLimit: 5
|
console.log(`更新数据: ${item.month} - ${item.inventory}`);
|
||||||
});
|
} else {
|
||||||
|
// 创建新数据
|
||||||
try {
|
await prisma.econ_SowInventory.create({
|
||||||
const conn = await pool.getConnection();
|
data: item
|
||||||
console.log('数据库连接成功!');
|
});
|
||||||
conn.release();
|
console.log(`新增数据: ${item.month} - ${item.inventory}`);
|
||||||
} catch (error) {
|
}
|
||||||
console.error('数据库连接失败:', error.message);
|
} catch (error) {
|
||||||
throw error;
|
console.error(`处理数据 ${item.month} 时出错:`, error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return pool;
|
|
||||||
}
|
console.log('数据爬取和存储完成!');
|
||||||
|
|
||||||
// 浙江数据开放网站URL(示例,实际需要根据真实网站修改)
|
} catch (error) {
|
||||||
const ZHEJIANG_OPEN_DATA_URL = 'https://data.zj.gov.cn/';
|
console.error('爬取数据时出错:', error.message);
|
||||||
|
} finally {
|
||||||
// 爬虫名称,用于缓存表中的标识
|
// 关闭Prisma连接
|
||||||
const SCRAPER_NAME = 'sow-inventory';
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
// 最小更新间隔(毫秒),默认30分钟
|
}
|
||||||
const MIN_UPDATE_INTERVAL = 30 * 60 * 1000;
|
|
||||||
|
// 导出函数
|
||||||
// 从数据库读取缓存
|
module.exports = { scrapeSowInventory };
|
||||||
async function readCache() {
|
|
||||||
try {
|
// 如果直接运行此文件
|
||||||
await initPool();
|
if (require.main === module) {
|
||||||
const result = await pool.query('SELECT * FROM ScraperCache WHERE scraperName = ?', [SCRAPER_NAME]);
|
scrapeSowInventory();
|
||||||
return result.length > 0 ? result[0] : { scraperName: SCRAPER_NAME, lastDataHash: null, lastUpdateAt: null };
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('读取数据库缓存失败:', error.message);
|
|
||||||
return { scraperName: SCRAPER_NAME, lastDataHash: null, lastUpdateAt: null };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 写入数据库缓存
|
|
||||||
async function writeCache(lastDataHash) {
|
|
||||||
try {
|
|
||||||
await initPool();
|
|
||||||
await pool.query(
|
|
||||||
'INSERT INTO ScraperCache (scraperName, lastDataHash, lastUpdateAt) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE lastDataHash = VALUES(lastDataHash), lastUpdateAt = VALUES(lastUpdateAt)',
|
|
||||||
[SCRAPER_NAME, lastDataHash, new Date()]
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('写入数据库缓存失败:', error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算数据hash,用于检测数据是否变化
|
|
||||||
function computeDataHash(data) {
|
|
||||||
const str = JSON.stringify(data);
|
|
||||||
let hash = 0;
|
|
||||||
for (let i = 0; i < str.length; i++) {
|
|
||||||
const char = str.charCodeAt(i);
|
|
||||||
hash = ((hash << 5) - hash) + char;
|
|
||||||
hash = hash & hash;
|
|
||||||
}
|
|
||||||
return hash.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取数据库中最新的月份
|
|
||||||
async function getLatestMonth() {
|
|
||||||
try {
|
|
||||||
await initPool();
|
|
||||||
const result = await pool.query('SELECT MAX(month) as latestMonth FROM Econ_SowInventory');
|
|
||||||
return result[0].latestMonth || null;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('查询最新月份失败:', error.message);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 判断月份是否需要更新(只有比数据库中更新的月份才需要处理)
|
|
||||||
function shouldUpdateMonth(month, latestDbMonth) {
|
|
||||||
if (!latestDbMonth) return true;
|
|
||||||
return month > latestDbMonth;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否需要发起HTTP请求(基于更新间隔)
|
|
||||||
function shouldSkipRequest(cache) {
|
|
||||||
if (!cache.lastUpdateAt) return false;
|
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
const lastUpdate = new Date(cache.lastUpdateAt);
|
|
||||||
const interval = now - lastUpdate;
|
|
||||||
|
|
||||||
return interval < MIN_UPDATE_INTERVAL;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 能繁母猪数量数据爬取函数(增量更新版本)
|
|
||||||
async function scrapeSowInventory(incremental = true) {
|
|
||||||
try {
|
|
||||||
await initPool();
|
|
||||||
console.log('='.repeat(50));
|
|
||||||
console.log('开始爬取浙江省能繁母猪数量数据...');
|
|
||||||
console.log(`增量更新模式: ${incremental ? '开启' : '关闭'}`);
|
|
||||||
console.log('='.repeat(50));
|
|
||||||
|
|
||||||
// 0. 检查是否需要发起请求
|
|
||||||
if (incremental) {
|
|
||||||
const cache = await readCache();
|
|
||||||
|
|
||||||
if (shouldSkipRequest(cache)) {
|
|
||||||
const lastUpdate = cache.lastUpdateAt ? new Date(cache.lastUpdateAt) : null;
|
|
||||||
const hoursDiff = lastUpdate ? ((new Date() - lastUpdate) / (1000 * 60 * 60)).toFixed(2) : '未知';
|
|
||||||
console.log('\n' + '='.repeat(50));
|
|
||||||
console.log('跳过请求:距离上次更新不足30分钟');
|
|
||||||
console.log(`上次更新时间: ${cache.lastUpdateAt}`);
|
|
||||||
console.log(`距离现在: ${hoursDiff} 小时`);
|
|
||||||
console.log('='.repeat(50));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取数据库中最新的月份
|
|
||||||
const latestDbMonth = await getLatestMonth();
|
|
||||||
if (latestDbMonth) {
|
|
||||||
console.log(`数据库中最新月份: ${latestDbMonth}`);
|
|
||||||
} else {
|
|
||||||
console.log('数据库为空,将获取所有数据');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. 发送HTTP请求获取页面内容
|
|
||||||
console.log('\n正在请求数据源...');
|
|
||||||
const response = await axios.get(ZHEJIANG_OPEN_DATA_URL, {
|
|
||||||
headers: {
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 2. 解析HTML内容
|
|
||||||
const $ = cheerio.load(response.data);
|
|
||||||
|
|
||||||
// 3. 提取数据(这里需要根据实际网站结构修改选择器)
|
|
||||||
const allData = [];
|
|
||||||
|
|
||||||
$('table').each((tableIndex, table) => {
|
|
||||||
$(table).find('tr').each((rowIndex, row) => {
|
|
||||||
if (rowIndex > 0) { // 跳过表头
|
|
||||||
const cells = $(row).find('td');
|
|
||||||
if (cells.length >= 2) {
|
|
||||||
const month = $(cells[0]).text().trim();
|
|
||||||
const inventory = parseInt($(cells[1]).text().trim());
|
|
||||||
|
|
||||||
if (month && !isNaN(inventory)) {
|
|
||||||
allData.push({
|
|
||||||
month,
|
|
||||||
inventory
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`页面总数据量: ${allData.length} 条`);
|
|
||||||
|
|
||||||
// 4. 读取数据库缓存
|
|
||||||
const cache = await readCache();
|
|
||||||
|
|
||||||
// 5. 计算当前数据hash,检测数据是否变化
|
|
||||||
const currentDataHash = computeDataHash(allData);
|
|
||||||
|
|
||||||
// 6. 如果是增量更新且数据未变化,跳过更新
|
|
||||||
if (incremental && cache.lastDataHash === currentDataHash) {
|
|
||||||
console.log('\n' + '='.repeat(50));
|
|
||||||
console.log('数据未发生变化,跳过数据库更新!');
|
|
||||||
console.log(`上次更新时间: ${cache.lastUpdateAt || '未知'}`);
|
|
||||||
console.log('='.repeat(50));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. 如果是增量更新,获取数据库中最新的月份用于过滤
|
|
||||||
let latestDbMonth = null;
|
|
||||||
if (incremental) {
|
|
||||||
latestDbMonth = await getLatestMonth();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 8. 过滤需要更新的数据
|
|
||||||
let dataToUpdate = allData;
|
|
||||||
let skippedCount = 0;
|
|
||||||
|
|
||||||
if (incremental && latestDbMonth) {
|
|
||||||
dataToUpdate = allData.filter(item => shouldUpdateMonth(item.month, latestDbMonth));
|
|
||||||
skippedCount = allData.length - dataToUpdate.length;
|
|
||||||
console.log(`\n增量更新: 跳过 ${skippedCount} 条已存在的数据`);
|
|
||||||
console.log(`需要处理: ${dataToUpdate.length} 条新数据`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 9. 如果没有需要更新的数据
|
|
||||||
if (dataToUpdate.length === 0) {
|
|
||||||
console.log('\n没有需要更新的数据!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 10. 按月份排序(从旧到新)
|
|
||||||
dataToUpdate.sort((a, b) => a.month.localeCompare(b.month));
|
|
||||||
|
|
||||||
console.log('\n开始处理数据...');
|
|
||||||
console.log('-'.repeat(50));
|
|
||||||
|
|
||||||
let newCount = 0;
|
|
||||||
let updateCount = 0;
|
|
||||||
let errorCount = 0;
|
|
||||||
|
|
||||||
// 11. 存储数据到数据库
|
|
||||||
for (const item of dataToUpdate) {
|
|
||||||
try {
|
|
||||||
// 检查数据是否已存在
|
|
||||||
const existing = await pool.query('SELECT * FROM Econ_SowInventory WHERE month = ?', [item.month]);
|
|
||||||
|
|
||||||
if (existing.length > 0) {
|
|
||||||
// 检查数据是否真的需要更新(值变化了才更新)
|
|
||||||
if (existing[0].inventory !== item.inventory) {
|
|
||||||
await pool.query(
|
|
||||||
'UPDATE Econ_SowInventory SET inventory = ? WHERE month = ?',
|
|
||||||
[item.inventory, item.month]
|
|
||||||
);
|
|
||||||
updateCount++;
|
|
||||||
console.log(`更新数据: ${item.month} - ${item.inventory} (原值: ${existing[0].inventory})`);
|
|
||||||
} else {
|
|
||||||
console.log(`数据未变化,跳过: ${item.month}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await pool.query(
|
|
||||||
'INSERT INTO Econ_SowInventory (month, inventory) VALUES (?, ?)',
|
|
||||||
[item.month, item.inventory]
|
|
||||||
);
|
|
||||||
newCount++;
|
|
||||||
console.log(`新增数据: ${item.month} - ${item.inventory}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
errorCount++;
|
|
||||||
console.error(`处理数据 ${item.month} 时出错:`, error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 12. 更新数据库缓存
|
|
||||||
await writeCache(currentDataHash);
|
|
||||||
|
|
||||||
// 13. 输出统计信息
|
|
||||||
const now = new Date().toISOString();
|
|
||||||
console.log('\n' + '='.repeat(50));
|
|
||||||
console.log('数据爬取和存储完成!');
|
|
||||||
console.log('统计信息:');
|
|
||||||
console.log(` - 新增数据: ${newCount} 条`);
|
|
||||||
console.log(` - 更新数据: ${updateCount} 条`);
|
|
||||||
console.log(` - 跳过数据: ${skippedCount} 条`);
|
|
||||||
console.log(` - 错误数量: ${errorCount} 条`);
|
|
||||||
console.log(` - 本次更新时间: ${now}`);
|
|
||||||
console.log('='.repeat(50));
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('爬取数据时出错:', error.message);
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
// 关闭数据库连接池
|
|
||||||
if (pool) {
|
|
||||||
await pool.end();
|
|
||||||
console.log('数据库连接池已关闭');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 强制全量更新函数
|
|
||||||
async function scrapeSowInventoryFull() {
|
|
||||||
console.log('警告: 即将执行全量更新,这将更新所有数据!');
|
|
||||||
console.log('如果只是想获取新数据,请使用 scrapeSowInventory() 函数。\n');
|
|
||||||
await scrapeSowInventory(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导出函数
|
|
||||||
export {
|
|
||||||
scrapeSowInventory,
|
|
||||||
scrapeSowInventoryFull
|
|
||||||
};
|
|
||||||
|
|
||||||
// 如果直接运行此文件
|
|
||||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
||||||
// 默认执行增量更新
|
|
||||||
// 如果需要强制全量更新,使用: scrapeSowInventoryFull()
|
|
||||||
scrapeSowInventory().catch(error => {
|
|
||||||
console.error('执行失败:', error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
+100
-100
@@ -1,100 +1,100 @@
|
|||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const cheerio = require('cheerio');
|
const cheerio = require('cheerio');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { PrismaClient } = require('../../generated/prisma');
|
const { PrismaClient } = require('../../generated/prisma');
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
// 数据源URL(需要根据实际情况修改)
|
// 数据源URL(需要根据实际情况修改)
|
||||||
const DATA_SOURCE_URL = 'https://example.com/data';
|
const DATA_SOURCE_URL = 'https://example.com/data';
|
||||||
|
|
||||||
// 通用数据爬取函数
|
// 通用数据爬取函数
|
||||||
async function scrapeData() {
|
async function scrapeData() {
|
||||||
try {
|
try {
|
||||||
console.log('开始爬取数据...');
|
console.log('开始爬取数据...');
|
||||||
|
|
||||||
// 1. 发送HTTP请求获取页面内容
|
// 1. 发送HTTP请求获取页面内容
|
||||||
const response = await axios.get(DATA_SOURCE_URL, {
|
const response = await axios.get(DATA_SOURCE_URL, {
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. 解析HTML内容
|
// 2. 解析HTML内容
|
||||||
const $ = cheerio.load(response.data);
|
const $ = cheerio.load(response.data);
|
||||||
|
|
||||||
// 3. 提取数据(需要根据实际网站结构修改)
|
// 3. 提取数据(需要根据实际网站结构修改)
|
||||||
const data = [];
|
const data = [];
|
||||||
|
|
||||||
// 示例:从表格中提取数据
|
// 示例:从表格中提取数据
|
||||||
$('table').each((tableIndex, table) => {
|
$('table').each((tableIndex, table) => {
|
||||||
$(table).find('tr').each((rowIndex, row) => {
|
$(table).find('tr').each((rowIndex, row) => {
|
||||||
if (rowIndex > 0) { // 跳过表头
|
if (rowIndex > 0) { // 跳过表头
|
||||||
const cells = $(row).find('td');
|
const cells = $(row).find('td');
|
||||||
// 根据实际表格结构提取数据
|
// 根据实际表格结构提取数据
|
||||||
// 示例:假设第一列是日期,第二列是数值
|
// 示例:假设第一列是日期,第二列是数值
|
||||||
if (cells.length >= 2) {
|
if (cells.length >= 2) {
|
||||||
const date = $(cells[0]).text().trim();
|
const date = $(cells[0]).text().trim();
|
||||||
const value = parseFloat($(cells[1]).text().trim());
|
const value = parseFloat($(cells[1]).text().trim());
|
||||||
|
|
||||||
if (date && !isNaN(value)) {
|
if (date && !isNaN(value)) {
|
||||||
data.push({
|
data.push({
|
||||||
date,
|
date,
|
||||||
value
|
value
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`爬取到 ${data.length} 条数据`);
|
console.log(`爬取到 ${data.length} 条数据`);
|
||||||
|
|
||||||
// 4. 存储数据到数据库(需要根据实际表结构修改)
|
// 4. 存储数据到数据库(需要根据实际表结构修改)
|
||||||
for (const item of data) {
|
for (const item of data) {
|
||||||
try {
|
try {
|
||||||
// 这里需要根据实际表结构修改
|
// 这里需要根据实际表结构修改
|
||||||
// 示例:存储到某个表
|
// 示例:存储到某个表
|
||||||
/*
|
/*
|
||||||
const existing = await prisma.someTable.findUnique({
|
const existing = await prisma.someTable.findUnique({
|
||||||
where: { date: item.date }
|
where: { date: item.date }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
// 更新现有数据
|
// 更新现有数据
|
||||||
await prisma.someTable.update({
|
await prisma.someTable.update({
|
||||||
where: { date: item.date },
|
where: { date: item.date },
|
||||||
data: { value: item.value }
|
data: { value: item.value }
|
||||||
});
|
});
|
||||||
console.log(`更新数据: ${item.date} - ${item.value}`);
|
console.log(`更新数据: ${item.date} - ${item.value}`);
|
||||||
} else {
|
} else {
|
||||||
// 创建新数据
|
// 创建新数据
|
||||||
await prisma.someTable.create({
|
await prisma.someTable.create({
|
||||||
data: item
|
data: item
|
||||||
});
|
});
|
||||||
console.log(`新增数据: ${item.date} - ${item.value}`);
|
console.log(`新增数据: ${item.date} - ${item.value}`);
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`处理数据时出错:`, error.message);
|
console.error(`处理数据时出错:`, error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('数据爬取和存储完成!');
|
console.log('数据爬取和存储完成!');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('爬取数据时出错:', error.message);
|
console.error('爬取数据时出错:', error.message);
|
||||||
} finally {
|
} finally {
|
||||||
// 关闭Prisma连接
|
// 关闭Prisma连接
|
||||||
await prisma.$disconnect();
|
await prisma.$disconnect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出函数
|
// 导出函数
|
||||||
module.exports = { scrapeData };
|
module.exports = { scrapeData };
|
||||||
|
|
||||||
// 如果直接运行此文件
|
// 如果直接运行此文件
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
scrapeData();
|
scrapeData();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,4 +19,4 @@ const mysql = require('mysql2/promise');
|
|||||||
const [art] = await c.query('SELECT id,title,slug,date FROM Article');
|
const [art] = await c.query('SELECT id,title,slug,date FROM Article');
|
||||||
console.log('Article:'); console.table(art);
|
console.log('Article:'); console.table(art);
|
||||||
await c.end();
|
await c.end();
|
||||||
})().catch(e=>{console.error(e);process.exit(1)});
|
})().catch(e=>{console.error(e);process.exit(1)});
|
||||||
|
|||||||
@@ -19,4 +19,4 @@ const mysql = require('mysql2/promise');
|
|||||||
console.log(t + ': ' + r[0].c + ' rows');
|
console.log(t + ': ' + r[0].c + ' rows');
|
||||||
}
|
}
|
||||||
await conn.end();
|
await conn.end();
|
||||||
})().catch((e) => { console.error('ERR:', e.message); process.exit(1); });
|
})().catch((e) => { console.error('ERR:', e.message); process.exit(1); });
|
||||||
|
|||||||
@@ -12,4 +12,4 @@ const { PrismaClient } = require('../generated/prisma');
|
|||||||
};
|
};
|
||||||
console.log('COUNTS:', JSON.stringify(counts, null, 2));
|
console.log('COUNTS:', JSON.stringify(counts, null, 2));
|
||||||
await p.$disconnect();
|
await p.$disconnect();
|
||||||
})().catch((e) => { console.error(e); process.exit(1); });
|
})().catch((e) => { console.error(e); process.exit(1); });
|
||||||
|
|||||||
@@ -19,4 +19,4 @@ try {
|
|||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
} finally {
|
} finally {
|
||||||
await p.$disconnect();
|
await p.$disconnect();
|
||||||
}
|
}
|
||||||
|
|||||||
+66
-24
@@ -1,7 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# iBoard Android APK 打包
|
# iBoard Android APK 打包
|
||||||
# 在 WSL 内执行: bash scripts/wsl/build-apk.sh [debug|release]
|
# 用法: bash scripts/wsl/build-apk.sh [debug|release]
|
||||||
# 默认 debug, release 需要先配签名 (见末尾)
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
WIN_PROJECT='/mnt/d/workbench/iboard'
|
WIN_PROJECT='/mnt/d/workbench/iboard'
|
||||||
@@ -10,34 +9,77 @@ cd "$WIN_PROJECT"
|
|||||||
VARIANT="${1:-debug}"
|
VARIANT="${1:-debug}"
|
||||||
echo "构建变体: $VARIANT"
|
echo "构建变体: $VARIANT"
|
||||||
|
|
||||||
# 环境变量 (setup.sh 已写过, 防御性再写一次)
|
# 环境变量
|
||||||
export ANDROID_HOME="${ANDROID_HOME:-$HOME/android-sdk}"
|
export ANDROID_HOME="${ANDROID_HOME:-$HOME/android-sdk}"
|
||||||
export ANDROID_NDK_HOME="${ANDROID_NDK_HOME:-$ANDROID_HOME/ndk/26.1.10909125}"
|
export ANDROID_NDK_HOME="${ANDROID_NDK_HOME:-$ANDROID_HOME/ndk/27.2.12479018}"
|
||||||
export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$HOME/.cargo/bin:$PATH"
|
export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$HOME/.cargo/bin:$PATH"
|
||||||
|
|
||||||
# 1. 先确保前端 dist/ 最新
|
# 0. 首次需要 android 工程
|
||||||
|
cd src-tauri
|
||||||
|
if [ ! -d "gen/android" ]; then
|
||||||
|
echo "=== 0. 初始化 Android 工程 (一次性) ==="
|
||||||
|
npx tauri android init
|
||||||
|
fi
|
||||||
|
cd "$WIN_PROJECT"
|
||||||
|
|
||||||
|
# 1. 先构建前端, 让 Tauri android 工程能拿到 dist/
|
||||||
echo "=== 1. 构建前端 ==="
|
echo "=== 1. 构建前端 ==="
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
# 2. 进 src-tauri 跑 cargo tauri android build
|
# 2. 走不同路径
|
||||||
echo "=== 2. 构建 APK ==="
|
|
||||||
cd src-tauri
|
cd src-tauri
|
||||||
|
|
||||||
if [ "$VARIANT" = "release" ]; then
|
# 全部 4 个 Android 架构的 rustBuild 任务 (Tauri 2 默认会编 4 个)
|
||||||
# release 需要签名, 首次请先生成 keystore:
|
RUST_BUILD_TASKS=(
|
||||||
# keytool -genkey -v -keystore ~/iboard-release.keystore -alias iboard \
|
":app:rustBuildArm64Release"
|
||||||
# -keyalg RSA -keysize 2048 -validity 10000
|
":app:rustBuildArmv7Release"
|
||||||
# 然后在 ~/.gradle/gradle.properties 配:
|
":app:rustBuildX86_64Release"
|
||||||
# IBOARD_UPLOAD_STORE_FILE=/path/to/iboard-release.keystore
|
":app:rustBuildI686Release"
|
||||||
# IBOARD_UPLOAD_KEY_ALIAS=iboard
|
)
|
||||||
# IBOARD_UPLOAD_STORE_PASSWORD=...
|
RUST_BUILD_TASKS_DBG=(
|
||||||
# IBOARD_UPLOAD_KEY_PASSWORD=...
|
":app:rustBuildArm64Debug"
|
||||||
npx tauri android build -- --release
|
":app:rustBuildArmv7Debug"
|
||||||
else
|
":app:rustBuildX86_64Debug"
|
||||||
npx tauri android build -- --debug
|
":app:rustBuildI686Debug"
|
||||||
fi
|
)
|
||||||
|
SKIP_ARGS=()
|
||||||
|
for t in "${RUST_BUILD_TASKS[@]}"; do SKIP_ARGS+=(-x "${t#:}"); done
|
||||||
|
|
||||||
echo
|
if [ "$VARIANT" = "release" ]; then
|
||||||
echo "=== 产物位置 ==="
|
echo "[release] 手工 cargo 编 lib, gradle 只跑 packaging (跳过 rustBuild* 避免 --release 重复 bug)"
|
||||||
APK_DIR="$WIN_PROJECT/src-tauri/target/$( [ "$VARIANT" = "release" ] && echo release || echo debug )/bundle/android/app/build/outputs/apk/$VARIANT"
|
|
||||||
ls -lh "$APK_DIR"/*.apk 2>/dev/null || echo "(未找到 APK, 请检查上面日志)"
|
echo " [cargo] build lib release ..."
|
||||||
|
cargo build \
|
||||||
|
--target aarch64-linux-android \
|
||||||
|
--features custom-protocol \
|
||||||
|
--lib \
|
||||||
|
--release
|
||||||
|
|
||||||
|
echo " [gradle] assembleRelease -x rustBuild* ..."
|
||||||
|
cd gen/android
|
||||||
|
./gradlew --no-daemon assembleRelease "${SKIP_ARGS[@]}"
|
||||||
|
|
||||||
|
APK_DIR="$WIN_PROJECT/src-tauri/gen/android/app/build/outputs/apk/release"
|
||||||
|
echo
|
||||||
|
echo "=== 产物 (未签名) ==="
|
||||||
|
ls -lh "$APK_DIR"/*.apk 2>/dev/null || echo "(未找到 APK)"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "=== Release 签名 (按需) ==="
|
||||||
|
echo "1. 生成 keystore (一次性):"
|
||||||
|
echo " keytool -genkey -v -keystore ~/iboard-release.keystore -alias iboard -keyalg RSA -keysize 2048 -validity 10000"
|
||||||
|
echo "2. 在 ~/.gradle/gradle.properties 写:"
|
||||||
|
echo " IBOARD_UPLOAD_STORE_FILE=/home/$(whoami)/iboard-release.keystore"
|
||||||
|
echo " IBOARD_UPLOAD_KEY_ALIAS=iboard"
|
||||||
|
echo " IBOARD_UPLOAD_STORE_PASSWORD=..."
|
||||||
|
echo " IBOARD_UPLOAD_KEY_PASSWORD=..."
|
||||||
|
echo "3. 重跑本脚本"
|
||||||
|
else
|
||||||
|
echo "[debug] Tauri CLI debug 路径 (不传 --release 不会触发 bug), 直接走"
|
||||||
|
npx tauri android build --apk
|
||||||
|
|
||||||
|
APK_DIR="$WIN_PROJECT/src-tauri/target/debug/bundle/android/app/build/outputs/apk/debug"
|
||||||
|
echo
|
||||||
|
echo "=== 产物 ==="
|
||||||
|
ls -lh "$APK_DIR"/*.apk 2>/dev/null || echo "(未找到 APK)"
|
||||||
|
fi
|
||||||
Generated
+4394
File diff suppressed because it is too large
Load Diff
+496
-496
@@ -1,498 +1,498 @@
|
|||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
sans-serif;
|
sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Navbar */
|
/* Navbar */
|
||||||
.navbar {
|
.navbar {
|
||||||
background-color: #333;
|
background-color: #333;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo a {
|
.logo a {
|
||||||
color: white;
|
color: white;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-links {
|
.nav-links {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-links a {
|
.nav-links a {
|
||||||
color: white;
|
color: white;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-links a:hover {
|
.nav-links a:hover {
|
||||||
background-color: #555;
|
background-color: #555;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Global link styles */
|
/* Global link styles */
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Main content */
|
/* Main content */
|
||||||
main {
|
main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 2.5rem;
|
font-size: 2.5rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card:hover {
|
.card:hover {
|
||||||
transform: translateY(-5px);
|
transform: translateY(-5px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card h2 {
|
.card h2 {
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card p {
|
.card p {
|
||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Footer */
|
/* Footer */
|
||||||
.footer {
|
.footer {
|
||||||
background-color: #333;
|
background-color: #333;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-content {
|
.footer-content {
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Data page layout */
|
/* Data page layout */
|
||||||
.data-layout {
|
.data-layout {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-sidebar {
|
.data-sidebar {
|
||||||
width: 250px;
|
width: 250px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-content {
|
.data-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.indicator-group {
|
.indicator-group {
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.group-title {
|
.group-title {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.5rem;
|
||||||
border-bottom: 2px solid #ddd;
|
border-bottom: 2px solid #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
.indicator-list {
|
.indicator-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.indicator-item {
|
.indicator-item {
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.indicator-item:hover {
|
.indicator-item:hover {
|
||||||
background-color: #f0f0f0;
|
background-color: #f0f0f0;
|
||||||
transform: translateX(4px);
|
transform: translateX(4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.indicator-item.active {
|
.indicator-item.active {
|
||||||
background-color: #333;
|
background-color: #333;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.indicator-detail {
|
.indicator-detail {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.indicator-title {
|
.indicator-title {
|
||||||
font-size: 1.8rem;
|
font-size: 1.8rem;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.indicator-description {
|
.indicator-description {
|
||||||
color: #666;
|
color: #666;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-container {
|
.chart-container {
|
||||||
margin-bottom: 3rem;
|
margin-bottom: 3rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
background-color: #fafafa;
|
background-color: #fafafa;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-table-container {
|
.data-table-container {
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-title {
|
.table-title {
|
||||||
font-size: 1.3rem;
|
font-size: 1.3rem;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-table {
|
.data-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-table th,
|
.data-table th,
|
||||||
.data-table td {
|
.data-table td {
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border-bottom: 1px solid #eee;
|
border-bottom: 1px solid #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-table th {
|
.data-table th {
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-table tbody tr:hover {
|
.data-table tbody tr:hover {
|
||||||
background-color: #f9f9f9;
|
background-color: #f9f9f9;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Articles section */
|
/* Articles section */
|
||||||
.articles-section {
|
.articles-section {
|
||||||
margin-top: 4rem;
|
margin-top: 4rem;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.5rem;
|
||||||
border-bottom: 2px solid #ddd;
|
border-bottom: 2px solid #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
.articles-grid {
|
.articles-grid {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-card {
|
.article-card {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-card:hover {
|
.article-card:hover {
|
||||||
background-color: #f9f9f9;
|
background-color: #f9f9f9;
|
||||||
transform: translateX(5px);
|
transform: translateX(5px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-date {
|
.article-date {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
color: #666;
|
color: #666;
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-title {
|
.article-title {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-excerpt {
|
.article-excerpt {
|
||||||
color: #666;
|
color: #666;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Pagination */
|
/* Pagination */
|
||||||
.pagination {
|
.pagination {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-link {
|
.page-link {
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: #333;
|
color: #333;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-link:hover {
|
.page-link:hover {
|
||||||
background-color: #f0f0f0;
|
background-color: #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-link.active {
|
.page-link.active {
|
||||||
background-color: #333;
|
background-color: #333;
|
||||||
color: white;
|
color: white;
|
||||||
border-color: #333;
|
border-color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Article detail page */
|
/* Article detail page */
|
||||||
.article-detail {
|
.article-detail {
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-detail-title {
|
.article-detail-title {
|
||||||
font-size: 2.5rem;
|
font-size: 2.5rem;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-detail-date {
|
.article-detail-date {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
color: #666;
|
color: #666;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
padding-bottom: 1rem;
|
padding-bottom: 1rem;
|
||||||
border-bottom: 1px solid #eee;
|
border-bottom: 1px solid #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-detail-content {
|
.article-detail-content {
|
||||||
color: #333;
|
color: #333;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-detail-content h1,
|
.article-detail-content h1,
|
||||||
.article-detail-content h2,
|
.article-detail-content h2,
|
||||||
.article-detail-content h3,
|
.article-detail-content h3,
|
||||||
.article-detail-content h4,
|
.article-detail-content h4,
|
||||||
.article-detail-content h5,
|
.article-detail-content h5,
|
||||||
.article-detail-content h6 {
|
.article-detail-content h6 {
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-detail-content p {
|
.article-detail-content p {
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-detail-content ul,
|
.article-detail-content ul,
|
||||||
.article-detail-content ol {
|
.article-detail-content ol {
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
padding-left: 2rem;
|
padding-left: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-detail-content li {
|
.article-detail-content li {
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-detail-content img {
|
.article-detail-content img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
margin: 2rem 0;
|
margin: 2rem 0;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.data-layout {
|
.data-layout {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-sidebar {
|
.data-sidebar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination {
|
.pagination {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-detail {
|
.article-detail {
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-detail-title {
|
.article-detail-title {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.strategy-layout {
|
.strategy-layout {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.strategy-sidebar {
|
.strategy-sidebar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Strategy page layout */
|
/* Strategy page layout */
|
||||||
.strategy-layout {
|
.strategy-layout {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.strategy-sidebar {
|
.strategy-sidebar {
|
||||||
width: 250px;
|
width: 250px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.strategy-content {
|
.strategy-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-title {
|
.sidebar-title {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.5rem;
|
||||||
border-bottom: 2px solid #ddd;
|
border-bottom: 2px solid #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
.strategy-list {
|
.strategy-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.strategy-item {
|
.strategy-item {
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.strategy-item:hover {
|
.strategy-item:hover {
|
||||||
background-color: #f0f0f0;
|
background-color: #f0f0f0;
|
||||||
transform: translateX(4px);
|
transform: translateX(4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.strategy-item.active {
|
.strategy-item.active {
|
||||||
background-color: #333;
|
background-color: #333;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.strategy-detail {
|
.strategy-detail {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.strategy-title {
|
.strategy-title {
|
||||||
font-size: 1.8rem;
|
font-size: 1.8rem;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.strategy-description {
|
.strategy-description {
|
||||||
color: #666;
|
color: #666;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-section {
|
.chart-section {
|
||||||
margin-bottom: 3rem;
|
margin-bottom: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-title {
|
.chart-title {
|
||||||
font-size: 1.3rem;
|
font-size: 1.3rem;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ================== 移动端优先布局 (mobile-first) ================== */
|
/* ================== 移动端优先布局 (mobile-first) ================== */
|
||||||
|
|
||||||
@@ -714,4 +714,4 @@ input, textarea, select { font: inherit; }
|
|||||||
.chip-row { display: flex; gap: 8px; overflow-x: auto; padding: 8px 12px; }
|
.chip-row { display: flex; gap: 8px; overflow-x: auto; padding: 8px 12px; }
|
||||||
.chip-row::-webkit-scrollbar { display: none; }
|
.chip-row::-webkit-scrollbar { display: none; }
|
||||||
.chip { padding: 7px 12px; background: #fff; border: 1px solid var(--color-border); border-radius: 999px; font-size: 13px; white-space: nowrap; }
|
.chip { padding: 7px 12px; background: #fff; border: 1px solid var(--color-border); border-radius: 999px; font-size: 13px; white-space: nowrap; }
|
||||||
.chip.active { background: var(--color-primary); color: #fff; border-color: var(--color-primary); }
|
.chip.active { background: var(--color-primary); color: #fff; border-color: var(--color-primary); }
|
||||||
|
|||||||
Reference in New Issue
Block a user