chore: add and update gitignore rules for project files

add a comprehensive gitignore file to exclude unnecessary files like dependencies, runtime data, uploads, local configs and debug temporary files
This commit is contained in:
yangxiangyuan
2026-06-12 09:48:21 +08:00
parent c223c79b36
commit d3856cb52a
60 changed files with 13144 additions and 525 deletions
File diff suppressed because it is too large Load Diff
+126
View File
@@ -1221,6 +1221,46 @@ const lessons=[
exp:"每一次重新进入股市,风险意识都比上一次更强——从热点个股到宽基ETF,从频繁操作到条件单等待,从无仓位上限到六层金字塔。未来随着临近退休,防御仓位会进一步提高,进攻仓位继续收缩。这个方向是正确的,但节奏需要和实际需求挂钩,不能盲目加速。",
theory:"Inflation Risk(通胀风险)——不像市场下跌那样显眼,但对退休资产的长期侵蚀是真实的。中国历史通胀均值约2%~3%,实际需要名义收益率5%~6%才能保住购买力。如果风险收缩太快,全部转向防御资产,收益率可能跌到4%以下,看起来在赚钱,实际上购买力在下降。建议设动态收缩标准:每五年评估一次,根据当时的通胀率、退休时间表、现金流需求,决定防御仓位提高多少。不是线性收缩,是阶梯式的、有触发条件的收缩。Life-Cycle Investing核心原则:防御化的节奏要匹配你的负债端(退休后需要多少现金流),而不是主观上'觉得应该更稳了'。",
tags:["Inflation Risk","通胀侵蚀","Life-Cycle Investing","阶梯式收缩"]
},
{
theme:"第二十一课 · 六层仓位金字塔——风险分层是组合的骨架",
en:"仓位大小和风险特征完全反向对应。越稳的给越多钱,越进攻的给越少钱。",
zh:"仓位像盖楼打地基,越稳的层给的钱越多。大盘宽基是地基,10万打底稳稳当当;卫星博弈是楼顶的花园,1万够用,塌了也不影响整栋楼。每一层的亏损即使全亏完,也伤不到下面几层——这就是分层最大的价值,不是赚多少,是亏得起。",
exp:"2026年6月采摘节重新界定仓位分层,从大盘宽基到卫星博弈,金额从10万递减到1万,形成六级风险金字塔:大盘宽基¥10万 / 主题宽基¥8万 / 进取成长¥5万 / 优质进攻¥3.5万 / 弹性进攻¥2.5万 / 卫星博弈¥1万。通讯ETF定位在优质进攻层(¥3.5万),不是随手试错,是有真实信念支撑的标的。这个分层不是静态的,会随生命周期动态调整——临近退休,防御层比重上升,进攻层比重下降。",
theory:"Risk Pyramid(风险金字塔)——标准机构组合管理框架,底层是低风险高流动性资产,越往上风险越高、仓位越小。这个设计的价值不只是控制风险,而是让每一层的亏损即使最大化,也不会动摇下面几层的稳定性。六层架构比三层更精细,说明对持仓特征已经有了清晰的分类能力,不再是感性的配置,而是结构性的设计。Life-Cycle Investing补充:六层结构本身是动态的,防御层的比重应随退休时间临近而阶梯式上升,不是一劳永逸的固定比例。",
tags:["Risk Pyramid","仓位分层","风险金字塔","六级架构","Life-Cycle"]
},
{
theme:"第二十二课 · 锚定偏差——卖飞了是危险的买入动机",
en:"如果从来没有持有过它,今天第一次看到,你会买吗?",
zh:"追回一个之前卖飞了的标的,动机里混杂着信念和遗憾。过滤方法很简单:假设今天是第一次看到它,你会买吗?如果答案是会,才是真信念;如果犹豫,说明冲动里有锚定的成分。补充风险:估值基准可能重置,原来的伏击价格可能永远触发不了。",
exp:"通讯ETF设了条件单但一直涨,半年没有触发买入。采摘节考虑是否重新伏击时,发现追回的动机里混杂着两种成分:一是对这个板块真实的信念,二是对'卖便宜了'的遗憾。这两种动机看起来相似,但驱动力完全不同。在做任何分析之前,必须先过一道过滤:如果从来没有持有过它,今天第一次看到,你会买吗?答案是会,才继续分析;答案是不确定,说明追回来的冲动里有锚定的成分。补充风险:光模块板块在AI算力爆发背景下可能进入新估值区间,原来的伏击价格可能永远触发不了——这时需要判断是坚持等回调,还是接受新定价基准上移伏击价格。",
theory:"Anchoring Bias(锚定偏差)——Kahneman研究:人类决策天然会被第一个接触到的数字锚定。卖出价格成为锚点后,'追回来'的冲动看起来像逻辑,实际上是情绪。市场估值基准重置风险:某些板块在结构性利好驱动下会进入全新的估值区间,原来基于历史价格设定的伏击点可能永久失效。正确的应对不是硬等,而是重新评估:这个价格对应的是旧逻辑还是新逻辑?两个答案对应两种截然不同的操作。",
tags:["Anchoring Bias","锚定偏差","卖飞追回陷阱","估值基准重置"]
},
{
theme:"第二十三课 · 系统的终点是直觉专家——规则内化为本能",
en:"系统最高境界是成熟在胸,手到擒来。到了哪一步就走哪一步操作。",
zh:"投资系统就像开车,刚开始每一步都要想,十年后全部变肌肉记忆。达到这个境界靠的不是时间本身,而是系统化训练的积累——日志、条件单、复盘,把随机经验转化为真正的成长。没有日志,经验无法转化为进步,只是记忆在时间里的漂移。",
exp:"现阶段的系统需要刻意思考才能执行:查条件单、看仓位层级、对照多因子框架、写日志、复盘。每一个步骤都是有意识的动作。目标是10年后这些动作变成本能,不需要每次从头推演,规则已经编码进直觉——失败了怎么走,成功了怎么走,大盘跌了怎么走,大盘涨了怎么走,每种情景都有预设的路径,执行不需要临时判断。达到这个境界的条件,不是时间的积累,而是系统化训练的积累。日志、条件单、采摘节复盘,是这个训练的基础设施。",
theory:"Intuitive Expertise(直觉专家)——心理学家Klein研究:真正的专家直觉不是凭感觉,而是从系统化训练中提炼出来的模式识别能力。象棋大师看棋盘不需要逐步计算,是因为见过几十万个棋局,模式已经内化。投资的直觉专家同理。重要前提:直觉专家的直觉来自系统化训练,不是随机经验的积累。10年的随机经验只是一年经验的10次重复;10年的系统化记录和复盘,才是10年的真实成长。没有日志,经验无法转化为进步,只是记忆在时间里的漂移。",
tags:["Intuitive Expertise","直觉专家","系统化训练","模式内化"]
},
{
theme:"第二十四课 · 现金是期权,不是负担",
en:"现金放着就是未来盈利的期权。他没有之前想象中的白白躺在账户里。有大用处。",
zh:"判断对了但子弹不够,这是最难接受的一种输——输在准备上,不是输在判断上。现金不是等待中死去的钱,是等待中增值的权利。目标是把空闲现金仓提升至总仓位的20%,宁可不触发,也不提前消耗子弹。",
exp:"纳指科技ETF那笔操作,条件单触发、方向正确、执行完整,结果盈利。但回补时现金仓不足,只能降低金额。判断对了,子弹却不够用,这是最难承受的一种输——输在准备上,不是输在判断上。此后开始重新理解现金仓的本质:不是等待中死去的钱,而是等待中增值的权利。现金仓管理被列为采摘节的核心任务,目标是把空闲现金仓提升至总仓位的20%。条件单设置开始偏向保守,宁可不触发,也不提前消耗子弹。现金仓不足的三种模式:①子弹打在了优先级更低的目标上;②仓位上限设太满没给回补留空间;③月度子弹节奏和市场机会节奏不匹配。",
theory:"Option Value of Cash(现金的期权价值)——现金持有者拥有一张在任意时刻以任意价格买入任意资产的权利,这张权利没有行权成本,没有到期日。它的价值在危机时刻达到最高,恰恰是别人最缺现金的那一天。Capacity Constraint(容量约束):判断正确但资源不足,是一种不出现在账面上的机会成本损失。需要补充的批判:偏保守不是无限制的保守,第一档哨兵单需要设在合理但可触发的位置,否则等待本身会变成另一种形式的踏空——不是追高的踏空,是等待过度的踏空。",
tags:["Option Value of Cash","Dry Powder","期权价值","Capacity Constraint","现金仓三种不足"]
},
{
theme:"第二十五课 · 热点与非热点——聚合替代追逐",
en:"大家都知道的热点,早就不是最好的进入时机了。万变不离其宗,就是纳指科技、科创50、中证500这些聚合股票。",
zh:"追热点的本质是在消息已经定价之后入场。正确做法:热点用聚合型宽基ETF间接持有,指数自动做热点权重调整;非热点只选分红型蓝筹,焊着收分红,不依赖市场情绪。不认识的猎物不开枪,第一次遇到的猎物把打击距离拉远。",
exp:"两次追热点的教训:①航空航天ETF——举国体制非商业化,政策节奏无法预测,入场时消息已经定价,小仓位亏损。②中概互联网ETF——期待市场把腾讯阿里当AI公司定价,但市场定的是互联网平台估值,两套逻辑不匹配,仓位不小,一路下跌。形成的新原则:热点用聚合型宽基ETF(纳指科技、科创50、中证500)间接持有,指数自动做热点权重调整;非热点只选分红型蓝筹(中国石化、中国神华、中国银行),焊着收分红,不依赖市场情绪。稀土和电力设备下半年不碰。",
theory:"Catalyst Mismatch(催化剂错配):买入理由是期待A逻辑定价,市场实际采用B逻辑定价,两套逻辑不匹配,持有期间不会产生预期收益。Narrative Fallacy(叙事谬误):好故事不等于好投资。一个逻辑成立,不等于市场会在你预期的时间内为它定价。市场是前瞻性定价机器——当热点成为所有人都在讨论的话题,预期已经被定价了大半,你看到的是别人走过的路的终点,不是起点。Smart Beta via Index(指数替代选热点):通过持有聚合指数,间接持有所有热点的受益,同时不承担单一热点判断失误的风险。",
tags:["Catalyst Mismatch","Narrative Fallacy","叙事谬误","聚合替代追逐","Smart Beta"]
}
];
@@ -1821,6 +1861,10 @@ renderLesson();
<div class="p4-toc-item" data-ch="13"><span class="p4-toc-marker">⑭</span><span class="p4-toc-title">软件工程师的投资系统</span></div>
<div class="p4-toc-item" data-ch="14"><span class="p4-toc-marker">⑮</span><span class="p4-toc-title">系统的骨架</span></div>
<div class="p4-toc-item" data-ch="15"><span class="p4-toc-marker">⑯</span><span class="p4-toc-title">赢得不干净的那一次</span></div>
<div class="p4-toc-item" data-ch="16"><span class="p4-toc-marker">⑰</span><span class="p4-toc-title">散弹枪与狙击枪</span></div>
<div class="p4-toc-item" data-ch="17"><span class="p4-toc-marker">⑱</span><span class="p4-toc-title">三个隐蔽点</span></div>
<div class="p4-toc-item" data-ch="18"><span class="p4-toc-marker">⑲</span><span class="p4-toc-title">采摘节</span></div>
<div class="p4-toc-item" data-ch="19"><span class="p4-toc-marker">⑳</span><span class="p4-toc-title">热点与非热点</span></div>
</div>
</div>
@@ -2156,6 +2200,88 @@ const p4chapters = [
"盈利是结果,干净是过程。结果好,不代表过程没有问题。把过程里的问题找出来,是下一次赢得更干净的唯一方式。",
"告诫后来者: 在每一次盈利之后,也要认真复盘。问自己:这次赢得干净吗?如果不干净,原因是什么?好结果会掩盖坏过程,而坏过程,终有一天会在没有好结果的时候,完整地呈现出来。"
]
},
{
title: "散弹枪与狙击枪",
marker: "⑰",
paras: [
"如果要用最简单的语言,解释我的投资方法经历了什么样的转变,我会说:我从一个散弹枪手,变成了一个狙击手。",
"这不是隐喻的优劣之争,而是两种根本不同的作战哲学。",
"从前,我每天把枪拿出来,走进森林,听说哪里有猎物,就朝那个方向走过去。感觉是猎物了,开一枪。感觉还有猎物,再开一枪。回家的时候,子弹用了不少,背篓里却没什么东西。算下来,子弹的成本,远远超过了猎获的价值。",
"这就是散弹枪打法的本质问题:不是枪法不准,而是开枪太多。",
"每一颗子弹都有成本。显性的成本是手续费和价差,隐性的成本是注意力、判断力,以及每一次错误决策之后需要重建的信心。当你把精力分散在几十个方向上,表面上看起来勤奋,实际上是在用最高的代价,换取最低的期望回报。行为金融学的研究早已证实:交易最频繁的散户,年均收益率比交易最少的散户低6.5个百分点。勤奋,在这里是一个陷阱,不是优势。",
"后来,我换了一套作战方式。",
"我在森林里找了三个隐蔽点,分别叫中国、海外和防御。每一个隐蔽点,都是猎物必经之路——不是随机选择的,而是经过仔细勘察之后确定的战略阵地。然后,我趴下来。",
"趴着,是这套方法最反人性的部分。什么都不做,在心理上等同于浪费时间。大多数人无法忍受这种沉默,会忍不住起身,走动,找点什么事情做。但对狙击手来说,在猎物没有进入射程之前起身,才是真正的危险——不只是暴露位置,而是打乱了整个作战节奏。",
"我用条件单来维持这种趴伏状态。在平静的夜晚,根据每种猎物的特性,计算好打击距离,设定好每一枪的触发价格和仓位大小,然后把枪放在那里,让它自动开火。",
"不同的猎物,有不同的计算方式。带翅膀的飞禽,要用溢价率来判断它是否在合理的高度;善于奔跑的走兽,要用PE分位数看它是否已经跑到了估值的洼地;懂得潜水的水生动物,要用股息率判断它是否已经沉到了值得捕捞的深度。统一用箱体价格来瞄准所有猎物,就像用同一把标尺测量飞禽的速度和鱼的深度——工具用错了地方。",
"四枪的节奏,是这套打法的核心设计。第一枪在-10%开火,轻仓试探,这是哨兵枪;第二枪在-20%,加大仓位,这是主力枪;第三枪在-30%,是对极端情景的备用;第四枪尽量不开,留着看它是否真的需要。子弹是宝贵的,猎物挣扎五分钟自己倒下了,你还留着两颗子弹,这是最好的结局。",
"但这里有一处必须坦诚的局限:四枪之间的间距,应该根据猎物的特性来设定,不是所有猎物都适合10%的步长。低波动的猎物可以步长小一些;高速奔跑的猎物,步长太小会让你在它下跌的途中把子弹全部打光,等不到真正的底部。",
"还有两条铁律,是我用真实的代价换来的。不认识的猎物,不开枪。也许它看起来很诱人,但如果你不了解它的习性,你的第一枪很可能惊动了它,让它跑得更远。更糟糕的是,也许那根本不是猎物。第一次遇到的猎物,把打击距离拉远。不是-10%,而是-20%。你对它的了解打了折扣,安全边际就要相应地加宽。宁可少赚,不可不慎。",
"从散弹枪到狙击枪,不只是方法的改变,是心态的根本转变。以前,我害怕错过;现在,我更害怕打错。以前,我把每天开枪当成努力;现在,我把趴着等待当成工作。以前,我认为子弹用完就叫做积极;现在,我知道子弹留在手里,才叫做准备。",
"告诫后来者: 开枪之前,先问自己三个问题。这只猎物,我认识吗?它现在进入射程了吗?我剩下的子弹,够不够应对更好的机会?三个问题都有满意的答案,再扣动扳机。",
"---"
]
},
{
title: "三个隐蔽点",
marker: "⑱",
paras: [
"狙击手在出发之前,要做的第一件事不是磨枪,而是选阵地。",
"我选了三个隐蔽点。",
"一个叫中国,一个叫海外,一个叫防御。每一个都是猎物必经之路,每一个都有它独特的气候和季节规律。重要的是,我没有试图判断哪一个隐蔽点今天猎物最多——那是一个即使是最老练的猎人也常常判断错误的问题。我的选择,是在三个地方同时布点,让时间和市场来决定下一只猎物从哪里经过。",
"这是真正的分散逻辑。不是因为无知而分散,而是因为清醒而分散。",
"Buffett说,分散是对无知的对冲。这句话是对的,但有一个前提——他的分散标准,是基于深度研究之后仍然无法确定的不确定性,不是基于懒惰或者贪多。我的三个隐蔽点,每一个都有明确的逻辑支撑,不是随机撒网,是有意识的战略布局。这两种分散,有本质的区别。",
"隐蔽点没有好坏之分,只有时机的好坏之分。秋天大雁南飞,东边的隐蔽点是最佳位置;冬天狗熊出没,北边的林子更有机会。某一年龙卷风接连来袭,哪个隐蔽点都不安全。市场也是如此——中国有消费降级的周期,美国有选举年的震荡,防御资产有它自己的利率敏感性。阵地都没有问题,问题在于那一年的天气。",
"我不打算调整这三个位置。至少在现阶段,它们给我机会均等的布局——当其中一个阵地沉寂的时候,另外两个往往有猎物经过。这种低相关性,是组合里真正的稳定器。",
"有一句话值得认真对待:在不确定性最高的时候,最优仓位趋近于零。当你搞不清天气和季节,最好的做法是趴着,什么都不做。什么都不做,是一个主动的选择,不是被动的等待。等待本身,是这套系统最重要的工作内容之一。",
"偶尔,会有一只迷路的猎物,反常规地经过你的隐蔽点。2026年3月那次全球大跌,对很多人是灾难,对我是一只突然闯入的大猎物。它不在任何预测模型里,但我在那里,我有子弹,我趴着——这就是全部的条件。",
"退休之后,我可能会缩减到一个更安静的隐蔽点——少一些进攻性的布局,多一些风调雨顺的日子。防御型的阵地,猎物少一些,但天气稳定,偶尔还有日光浴。对那个年纪的我来说,这或许是最合适的节奏。",
"但有一点需要认真对待:防御不等于无风险。每一个隐蔽点都有它自己的风险,只是风险的形态不同。退休后的阵地,需要的不是没有风险,而是风险的形态符合那个阶段对现金流稳定性的需求。",
"告诫后来者: 选阵地,不是选哪里猎物最多,而是选哪里你最有把握在猎物经过时准确击发。了解自己的能力边界,比了解市场的机会,更重要。阵地选好了,趴着等,这是这套方法最难也最关键的部分。",
"---"
]
},
{
title: "采摘节",
marker: "⑲",
paras: [
"在我的投资系统里,有一个节点叫做采摘节。",
"不是每天,不是每周,而是每半年一次。时间通常定在六月底和十二月底。这个设计本身,就是一种表态:市场的节奏不是我的节奏,我有自己的节奏。",
"采摘节的第一个问题,永远是:我想要多少现金仓?",
"不是先看哪只股票涨了,不是先算账面盈利,而是先确定目标。这个顺序至关重要。大多数人是反过来的——看到涨了的就卖,卖完了发现手里多了些钱,然后找下一个买入目标。这是结果在驱动决策。我选择让目标驱动决策:先定20%的现金仓比例,再推导哪些仓位需要减,减多少。",
"从前,我把现金仓看成是没有被使用的资金,是等待中的浪费。这个认知,在某一次机会来临却子弹不足之后,彻底改变了。那次,条件单触发,方向正确,判断准确,但回补的资金不够,只能缩减仓位。账面上依然是盈利,但那个盈利,比它本可以达到的数字,少了一截。这种损失不出现在任何报表里,但它是真实的。它有一个名字,叫做机会成本。",
"从那之后,我把现金仓理解成一张期权。它赋予我在任何时刻以任何价格买入任何资产的权利。这张期权没有行权成本,没有到期日。它的价值,在市场最恐慌的时候达到顶点,恰恰是别人最缺现金的那一天。",
"采摘节的第二个任务,是做减法。三十个持仓,是这套系统走到今天积累下来的数字。每一个进入的时候都有理由,但不是每一个今天还有同样充分的理由留着。采摘节的核心问题只有一个:如果今天重新建仓,你还会买这个吗?答案是否定的,就是退出候选。不是因为它亏了,而是因为它占据了仓位、占据了注意力,却没有占据你的真实信念。",
"第三个任务,是重构伏击价格。从单一的箱体价格,升级到多因子判断——PE分位数、溢价率、股息率、均线,根据不同的猎物类型,选择不同的瞄准方式。箱体告诉你价格在哪里,估值告诉你价值在哪里。用价格瞄准,你打的是位置;用价值瞄准,你打的是意义。",
"还有一项容易被忽视的工作:逐一审查所有未触发的条件单。一张等待了六个月却没有触发的伏击单,有两种可能:一是原来的逻辑依然成立,市场只是还没有给你那个价格;二是底层逻辑已经发生了变化,那个价格可能永远不会再来。这两种情况需要不同的应对,而区分它们的唯一方式,是在采摘节认真地重新审视。",
"最后一个任务,是拿出盈利的10%,奖励自己。",
"这条是议程里最重要的,没有之一。不是因为钱的多少,而是因为这个动作本身的意义。它告诉你的大脑:这套系统是真实运转的,是有收获的,是值得继续维护的。采摘节不只是工作,它是你和自己的系统之间的一次仪式。仪式感,是长期坚持的燃料。",
"采摘节结束的那个晚上,我会去吃一顿好的。不是庆祝赚了多少,而是庆祝又认真地走完了一个完整的周期。",
"告诫后来者: 给你的投资系统设置一个有节奏的呼吸节点。不是为了频繁操作,而是为了让每一个持仓,在每一个时间点,都还有它存在的理由。采摘节的价值,不在于那一天做了什么,而在于你有这样一天。",
"---"
]
},
{
title: "热点与非热点",
marker: "⑳",
paras: [
"森林里每隔一段时间,就会出现一种奇特的现象。",
"某一块区域突然变得热闹起来。猎人们蜂拥而至,议论纷纷,说那里有稀有的猎物,有人已经打到了,有人正在赶去。整片森林都在谈论同一个名字。",
"我有过两次追进去的经历。",
"第一次是航空航天ETF。那段时间,相关新闻铺天盖地,政策利好接连出台,市场情绪高涨。我以为这是中国版的SpaceX时刻,追了进去。后来才明白,中国的航空航天是举国体制,不是商业化市场。它的估值逻辑不是由市场供需决定的,而是由政策节奏决定的。政策什么时候给信号,市场什么时候反应,这个节奏,局外人无从预测。进去的时机已经是消息兑现之后,最好的定价时机早已过去。幸好仓位小,代价有限。",
"第二次是中概互联网ETF。这一次,我以为自己想得很清楚——腾讯是中国互联网的基础设施,阿里是中国商业的底层平台,它们都有AI业务,是未来的受益者。逻辑严密,研究充分,仓位也不算小。但我犯了一个更隐蔽的错误。",
"我买入的理由,是期待市场把它们当作AI公司来定价。但市场在这段时间给它们的定价,是互联网平台公司——一个增长放缓、竞争加剧的行业框架。我预期的定价逻辑,和市场实际采用的定价逻辑,不是同一个。公司本身没有问题,但我等待的那个市场认可,迟迟没有到来。",
"这两次经历,让我想清楚了一件关于热点的根本性问题:大家都知道的热点,早就不是最好的进入时机了。",
"市场是一台高效的定价机器。当一个主题成为所有人都在讨论的话题,当财经媒体开始用\"风口\"这个词,当你在饭桌上听到有人推荐某只股票,这个信息的定价,往往已经发生了大半。你看到的,是别人已经走过的路的终点,不是起点。",
"真正的机会,往往安静得让人不安。",
"明白了这一点,我的应对方式变了。热点,我不再追单个板块。我用聚合型的宽基ETF来间接持有热点的受益——纳指科技、科创50、中证500。这些指数自动做了一件我做不到的事:当某个热点板块涨了,它在指数里的权重上升,我自然地获益;当热点退潮,权重下降,我的损失也被其他成分稀释。我通过持有指数,持有了所有热点,同时不需要为\"今年是半导体还是AI、是光模块还是机器人\"这个问题赌注。",
"非热点,我只选分红型蓝筹。中国石化、中国神华、中国银行——这些公司不需要热点来证明自己的价值。它们的价值来自稳定的现金流和持续的股息分配。如果焊在手上,最坏的结果是每年收分红。这是一种特殊的安全感:不依赖市场情绪,只依赖公司盈利。",
"进可攻,退可守。",
"这不是一个聪明的策略,而是一个诚实的策略。它承认了一个事实:我没有能力比市场更早知道下一个热点是什么,也没有时间逐一研究每一个新兴板块。既然如此,就用系统替代判断——指数替代选热点,分红替代判断估值。",
"有一点值得坦诚地说:这套方法,会让你错过一些真正的大机会。当某个板块从0涨到100,你持有的指数只涨了30。但它同样会保护你,当那个板块从100跌回30的时候,你的指数可能只跌了10。对于一个把投资定义为终身事业的人来说,少赚一些,是可以接受的代价。赔进去,才是真正的代价。",
"告诫后来者: 当你听说某个热点,问自己三个问题。第一,这个消息,有多少人已经知道了?第二,你比知道这件事的其他人,有什么额外的优势?第三,如果你的判断是错的,你能承受多大的损失?三个问题都有满意的答案,再考虑进场。如果答案是\"大家都知道了\"\"没有额外优势\"\"损失不确定\",那就让这只热点猎物,从你的隐蔽点旁边走过去。"
]
}
];
@@ -0,0 +1,15 @@
{
"_comment": "OSS 文件柜前端鉴权配置",
"enable_auth_comment": "true: 启用三层鉴权(cookie/GUID/token); false: 完全放开访问",
"enable_auth": true,
"max_age_days_comment": "鉴权 cookie 最大有效天数(1~365),过期后需重新获取入口授权",
"max_age_days": 30,
"cookieName_comment": "鉴权 Cookie 名称(gate 外层 cookie,不等于业务登录态)",
"cookieName": "oss_file_cabinet_gate",
"requireGuid_comment": "true:入口必须带 guid 参数,否则直接拒绝;false:允许仅凭 token 或已有会话访问",
"requireGuid": true,
"guidParam_comment": "URL 中的 GUID 参数名,建议使用小写 guid",
"guidParam": "guid",
"guidValue_comment": "GUID 参数值,建议使用高强度随机长串并定期更换,不在前端硬编码",
"guidValue": "db8a_2f1c-90e7-45a-a(d2-c3e4f50890"
}
+152
View File
@@ -0,0 +1,152 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OSS 文件柜</title>
<link rel="stylesheet" href="oss_file_cabinet.css">
</head>
<body>
<div id="loginScreen" class="login-screen">
<div class="login-card">
<div class="login-icon">🗂️</div>
<h1>OSS 文件柜</h1>
<p class="login-subtitle">请输入账号和密码登录</p>
<div class="login-form">
<div class="form-group">
<label>用户名</label>
<input type="text" id="loginUsername" autocomplete="username" autofocus>
</div>
<div class="form-group">
<label>密码</label>
<input type="password" id="loginPassword" autocomplete="current-password">
</div>
<button id="loginBtn" class="btn-primary">登录</button>
<div id="loginError" class="login-error"></div>
</div>
</div>
</div>
<div id="app" class="finder-app" style="display:none;">
<div class="finder-window">
<div class="finder-titlebar">
<div class="window-buttons">
<span class="btn-close"></span>
<span class="btn-mini"></span>
<span class="btn-max"></span>
</div>
<div class="finder-title">OSS 文件柜</div>
<div class="finder-actions">
<button id="btnThemeToggle" class="icon-btn" title="切换浅色/深色模式">🌓</button>
<button id="btnLogout" class="icon-btn" title="退出登录">⎋</button>
</div>
</div>
<div class="finder-toolbar">
<div class="nav-buttons">
<button id="btnBack" class="nav-btn" title="返回">◀</button>
<button id="btnForward" class="nav-btn" title="前进">▶</button>
</div>
<div class="path-bar" id="pathBar"></div>
<div class="view-switcher">
<button class="view-btn active" data-view="icon" title="图标视图">▦</button>
<button class="view-btn" data-view="list" title="列表视图">☰</button>
<button class="view-btn" data-view="column" title="分栏视图">▥</button>
</div>
<div class="search-box">
<input type="text" id="searchInput" placeholder="搜索">
</div>
</div>
<div class="finder-body">
<div class="finder-sidebar">
<div class="sidebar-section-title">个人收藏</div>
<div class="sidebar-item active" data-target="home">
<span class="sidebar-icon">🏠</span><span>全部文件</span>
</div>
<div class="sidebar-item" data-target="trash">
<span class="sidebar-icon">🗑️</span><span>废纸篓</span>
</div>
<div class="sidebar-item" data-target="images">
<span class="sidebar-icon">🖼️</span><span>图片</span>
</div>
</div>
<div class="finder-content">
<div id="fileArea" class="file-area icon-view">
<div class="empty-state" style="display:none;">
<div class="empty-icon">📂</div>
<div class="empty-title">空文件夹</div>
<div class="empty-hint">拖拽文件到此处或点击「上传」</div>
</div>
</div>
</div>
</div>
<div class="finder-statusbar">
<span id="statusInfo">0 个项目</span>
<span id="statusSelect"></span>
</div>
</div>
<div id="dropOverlay" class="drop-overlay">
<div class="drop-box">
<div class="drop-icon">⬆️</div>
<div class="drop-text">松开以将文件上传到当前目录</div>
</div>
</div>
</div>
<div id="filePreview" class="modal" style="display:none;">
<div class="modal-backdrop"></div>
<div class="modal-content image-preview">
<button class="modal-close" id="closePreview">✕</button>
<div id="previewContainer"></div>
</div>
</div>
<div id="contextMenu" class="context-menu" style="display:none;">
<div class="cm-item" data-action="open">打开</div>
<div class="cm-item" data-action="preview">预览</div>
<div class="cm-item" data-action="rename">重命名</div>
<div class="cm-item" data-action="info">显示简介</div>
<div class="cm-sep"></div>
<div class="cm-item" data-action="copy">复制</div>
<div class="cm-item" data-action="move">移动到…</div>
<div class="cm-item" data-action="download">下载</div>
<div class="cm-item" data-action="compress">压缩</div>
<div class="cm-item" data-action="share">分享临时链接…</div>
<div class="cm-item" data-action="raw_url">原始OSS链接(不建议)</div>
<div class="cm-sep"></div>
<div class="cm-item danger" data-action="delete">移到废纸篓</div>
</div>
<div id="folderContextMenu" class="context-menu" style="display:none;">
<div class="cm-item" data-action="new_folder">新建文件夹</div>
<div class="cm-item" data-action="upload">上传文件…</div>
<div class="cm-sep"></div>
<div class="cm-item" data-action="paste">粘贴</div>
<div class="cm-item" data-action="refresh">刷新</div>
</div>
<div id="trashContextMenu" class="context-menu" style="display:none;">
<div class="cm-item" data-action="restore">放回</div>
<div class="cm-item danger" data-action="empty_perm">彻底删除</div>
</div>
<div id="modalDialog" class="modal" style="display:none;">
<div class="modal-backdrop"></div>
<div class="modal-content">
<div class="modal-title" id="modalTitle"></div>
<div class="modal-body" id="modalBody"></div>
<div class="modal-footer">
<button class="btn-ghost" id="modalCancel">取消</button>
<button class="btn-primary" id="modalOk">确定</button>
</div>
</div>
</div>
<input type="file" id="fileInput" multiple style="display:none;">
<script src="oss_file_cabinet.js"></script>
</body>
</html>
@@ -0,0 +1,688 @@
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #f5f5f7;
--window-bg: #ffffff;
--sidebar-bg: #ececec;
--header-bg: #f0f0f2;
--text: #1d1d1f;
--text-secondary: #6e6e73;
--border: rgba(0,0,0,0.08);
--selection: #0a84ff;
--selection-bg: rgba(10,132,255,0.14);
--selection-border: rgba(10,132,255,0.35);
--hover-bg: rgba(0,0,0,0.04);
--danger: #ff3b30;
--shadow: 0 10px 40px rgba(0,0,0,0.12), 0 2px 10px rgba(0,0,0,0.08);
--radius: 10px;
--radius-sm: 6px;
}
:root.theme-dark {
--bg: #1e1e1e;
--window-bg: #2c2c2e;
--sidebar-bg: #232325;
--header-bg: #2a2a2c;
--text: #f5f5f7;
--text-secondary: #a1a1a6;
--border: rgba(255,255,255,0.08);
--selection: #0a84ff;
--selection-bg: rgba(10,132,255,0.25);
--selection-border: rgba(10,132,255,0.5);
--hover-bg: rgba(255,255,255,0.05);
--shadow: 0 10px 40px rgba(0,0,0,0.5), 0 2px 10px rgba(0,0,0,0.3);
}
html, body {
height: 100%;
width: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', 'PingFang SC', 'Microsoft YaHei', sans-serif;
font-size: 13px;
color: var(--text);
background: var(--bg);
-webkit-font-smoothing: antialiased;
overflow: hidden;
}
button { font-family: inherit; cursor: pointer; border: none; outline: none; background: none; color: inherit; }
input, select, textarea { font-family: inherit; outline: none; }
/* ===== 登录页 ===== */
.login-screen {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100vh;
background: linear-gradient(180deg, #6b7dff 0%, #b066ff 50%, #ff6ec7 100%);
}
:root.theme-dark .login-screen { background: linear-gradient(180deg, #1a1d2e 0%, #2a1d3a 50%, #3a1d2e 100%); }
.login-card {
width: 320px;
padding: 32px 28px;
background: var(--window-bg);
border-radius: 14px;
box-shadow: var(--shadow);
text-align: center;
backdrop-filter: blur(20px) saturate(180%);
}
.login-icon { font-size: 48px; margin-bottom: 8px; }
.login-card h1 { font-size: 20px; font-weight: 600; margin-bottom: 4px; }
.login-subtitle { font-size: 12px; color: var(--text-secondary); margin-bottom: 20px; }
.login-form { text-align: left; }
.form-group { margin-bottom: 12px; }
.form-group label { display: block; font-size: 12px; color: var(--text-secondary); margin-bottom: 6px; }
.form-group input {
width: 100%;
padding: 8px 12px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--bg);
color: var(--text);
font-size: 13px;
}
.form-group input:focus { border-color: var(--selection); }
.btn-primary {
width: 100%;
padding: 9px 16px;
background: var(--selection);
color: #fff;
border-radius: var(--radius-sm);
font-size: 13px;
font-weight: 500;
margin-top: 6px;
transition: opacity 0.15s;
}
.btn-primary:hover { opacity: 0.88; }
.btn-ghost {
padding: 8px 16px;
background: transparent;
color: var(--text);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
font-size: 13px;
}
.btn-ghost:hover { background: var(--hover-bg); }
.login-error { margin-top: 12px; color: var(--danger); font-size: 12px; text-align: center; min-height: 16px; }
/* ===== Finder 窗口 ===== */
.finder-app { width: 100%; height: 100vh; padding: 12px; }
.finder-window {
width: 100%;
height: 100%;
background: var(--window-bg);
border-radius: var(--radius);
box-shadow: var(--shadow);
display: flex;
flex-direction: column;
overflow: hidden;
}
.finder-titlebar {
height: 38px;
display: flex;
align-items: center;
padding: 0 14px;
background: var(--header-bg);
border-bottom: 1px solid var(--border);
position: relative;
}
.window-buttons { display: flex; gap: 8px; }
.window-buttons span {
width: 12px; height: 12px;
border-radius: 50%;
display: block;
}
.btn-close { background: #ff5f57; }
.btn-mini { background: #febc2e; }
.btn-max { background: #28c840; }
.finder-title {
flex: 1;
text-align: center;
font-size: 13px;
font-weight: 600;
color: var(--text);
}
.finder-actions { display: flex; gap: 6px; }
.icon-btn {
padding: 4px 8px;
font-size: 14px;
border-radius: var(--radius-sm);
color: var(--text-secondary);
}
.icon-btn:hover { background: var(--hover-bg); color: var(--text); }
/* ===== Toolbar ===== */
.finder-toolbar {
height: 44px;
display: flex;
align-items: center;
padding: 0 14px;
gap: 12px;
background: var(--header-bg);
border-bottom: 1px solid var(--border);
}
.nav-buttons { display: flex; gap: 4px; }
.nav-btn {
width: 28px; height: 28px;
border-radius: var(--radius-sm);
font-size: 14px;
color: var(--text-secondary);
display: flex; align-items: center; justify-content: center;
}
.nav-btn:hover { background: var(--hover-bg); color: var(--text); }
.path-bar {
flex: 1;
display: flex;
align-items: center;
gap: 4px;
padding: 4px 12px;
background: var(--bg);
border-radius: var(--radius-sm);
font-size: 13px;
min-height: 28px;
overflow-x: auto;
white-space: nowrap;
}
.crumb {
padding: 2px 8px;
border-radius: 4px;
color: var(--text-secondary);
cursor: pointer;
white-space: nowrap;
}
.crumb.active { color: var(--text); font-weight: 500; }
.crumb:hover:not(.active) { background: var(--hover-bg); color: var(--text); }
.crumb-sep { color: var(--text-secondary); opacity: 0.5; }
.view-switcher {
display: flex;
background: var(--bg);
border-radius: var(--radius-sm);
padding: 2px;
}
.view-btn {
width: 28px; height: 24px;
border-radius: 4px;
font-size: 12px;
color: var(--text-secondary);
display: flex; align-items: center; justify-content: center;
}
.view-btn:hover { color: var(--text); }
.view-btn.active { background: var(--window-bg); color: var(--text); box-shadow: 0 1px 2px rgba(0,0,0,0.06); }
.search-box input {
width: 180px;
padding: 6px 10px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text);
font-size: 12px;
}
.search-box input:focus { border-color: var(--selection); }
/* ===== Body ===== */
.finder-body {
flex: 1;
display: flex;
overflow: hidden;
}
.finder-sidebar {
width: 180px;
background: var(--sidebar-bg);
padding: 10px 6px;
overflow-y: auto;
border-right: 1px solid var(--border);
}
.sidebar-section-title {
font-size: 11px;
color: var(--text-secondary);
padding: 8px 10px 4px;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 600;
}
.sidebar-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
margin: 1px 0;
border-radius: 5px;
cursor: pointer;
font-size: 13px;
color: var(--text);
}
.sidebar-item:hover { background: var(--hover-bg); }
.sidebar-item.active { background: var(--selection); color: #fff; }
.sidebar-icon { font-size: 14px; }
.finder-content {
flex: 1;
overflow: hidden;
background: var(--window-bg);
}
/* ===== 文件区 ===== */
.file-area {
width: 100%;
height: 100%;
padding: 18px;
overflow-y: auto;
position: relative;
}
/* 图标视图 */
.icon-view {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(92px, 1fr));
gap: 16px;
align-content: start;
}
.icon-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 8px 6px;
border-radius: 8px;
cursor: pointer;
text-align: center;
transition: background 0.12s;
user-select: none;
}
.icon-item:hover { background: var(--hover-bg); }
.icon-item.selected { background: var(--selection-bg); }
.icon-wrap {
width: 64px; height: 64px;
display: flex; align-items: center; justify-content: center;
font-size: 34px;
margin-bottom: 4px;
border-radius: 12px;
background: var(--file-icon-bg, #f0f0f0);
overflow: hidden;
}
.icon-wrap.file-red { background: #ffebee; }
.icon-wrap.file-blue { background: #e3f2fd; }
.icon-wrap.file-green { background: #e8f5e9; }
.icon-wrap.file-orange { background: #fff3e0; }
.icon-wrap.file-purple { background: #f3e5f5; }
.icon-wrap.file-pink { background: #fce4ec; }
.icon-wrap.file-yellow { background: #fffde7; }
.icon-wrap.file-image { background: #e8eaf6; }
.icon-wrap.file-text { background: #f5f5f5; }
.icon-wrap.file-gray { background: #f0f0f0; }
.icon-thumb {
width: 100%; height: 100%;
object-fit: cover;
}
.icon-name {
font-size: 12px;
line-height: 1.3;
max-width: 80px;
word-break: break-all;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-align: center;
}
/* 列表视图 */
.list-table {
width: 100%;
display: flex;
flex-direction: column;
}
.list-row {
display: grid;
grid-template-columns: minmax(280px, 1fr) 100px 180px 100px;
padding: 6px 12px;
font-size: 12px;
cursor: pointer;
border-bottom: 1px solid var(--border);
align-items: center;
user-select: none;
}
.list-row:hover { background: var(--hover-bg); }
.list-row.selected { background: var(--selection-bg); }
.list-header {
background: var(--header-bg);
font-weight: 600;
color: var(--text-secondary);
position: sticky;
top: 0;
cursor: default;
border-bottom: 1px solid var(--border);
z-index: 1;
}
.list-header:hover { background: var(--header-bg); }
.col-name { display: flex; align-items: center; gap: 8px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.col-size, .col-modified, .col-kind { color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.row-icon {
width: 22px; height: 22px;
display: inline-flex; align-items: center; justify-content: center;
font-size: 13px;
border-radius: 5px;
flex-shrink: 0;
background: var(--file-icon-bg, #f0f0f0);
}
.row-icon.file-red { background: #ffebee; }
.row-icon.file-blue { background: #e3f2fd; }
.row-icon.file-green { background: #e8f5e9; }
.row-icon.file-orange { background: #fff3e0; }
.row-icon.file-purple { background: #f3e5f5; }
.row-icon.file-pink { background: #fce4ec; }
.row-icon.file-yellow { background: #fffde7; }
.row-icon.file-image { background: #e8eaf6; }
.row-icon.file-text { background: #f5f5f5; }
.row-icon.file-gray { background: #f0f0f0; }
/* 分栏视图 */
.file-area.column-view {
display: flex;
padding: 0;
}
.column {
flex: 1;
min-width: 200px;
border-right: 1px solid var(--border);
overflow-y: auto;
}
.column:last-child { border-right: none; }
.column-row {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
font-size: 12px;
cursor: pointer;
border-bottom: 1px solid var(--border);
user-select: none;
}
.column-row:hover { background: var(--hover-bg); }
.column-row.selected { background: var(--selection); color: #fff; }
.column-hint { flex: 2; display: flex; align-items: center; justify-content: center; }
.column-empty { color: var(--text-secondary); font-size: 12px; }
.col-name-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.column-info-panel { padding: 20px; text-align: center; }
.column-info-name { font-size: 14px; font-weight: 600; margin-bottom: 12px; word-break: break-all; }
.column-info-row { font-size: 12px; color: var(--text-secondary); margin-bottom: 6px; }
.file-area.column-view { overflow-x: auto; }
.column { min-width: 180px; max-width: 280px; }
/* 状态栏 */
.finder-statusbar {
height: 24px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 14px;
background: var(--header-bg);
border-top: 1px solid var(--border);
font-size: 11px;
color: var(--text-secondary);
}
/* 空状态 */
.empty-state {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
text-align: center;
color: var(--text-secondary);
}
.empty-icon { font-size: 48px; margin-bottom: 12px; opacity: 0.5; }
.empty-title { font-size: 15px; margin-bottom: 4px; font-weight: 500; color: var(--text); opacity: 0.7; }
.empty-hint { font-size: 12px; }
/* 拖拽蒙层 */
.drop-overlay {
position: fixed;
inset: 0;
background: rgba(10,132,255,0.15);
backdrop-filter: blur(8px);
display: none;
align-items: center;
justify-content: center;
z-index: 999;
pointer-events: none;
border: 3px dashed var(--selection);
}
.drop-box {
background: var(--window-bg);
padding: 32px 48px;
border-radius: 12px;
text-align: center;
box-shadow: var(--shadow);
pointer-events: auto;
}
.drop-icon { font-size: 48px; margin-bottom: 8px; }
.drop-text { font-size: 14px; color: var(--text); }
/* 选框 */
.selection-rect {
position: fixed;
border: 1px solid var(--selection);
background: var(--selection-bg);
border-radius: 2px;
pointer-events: none;
z-index: 998;
}
/* ===== 右键菜单 ===== */
.context-menu {
position: fixed;
min-width: 200px;
background: var(--window-bg);
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0,0,0,0.25), 0 2px 8px rgba(0,0,0,0.1);
padding: 4px;
z-index: 1000;
backdrop-filter: blur(30px) saturate(180%);
border: 1px solid var(--border);
}
.cm-item {
padding: 7px 12px;
font-size: 13px;
border-radius: 4px;
cursor: pointer;
color: var(--text);
}
.cm-item:hover { background: var(--selection); color: #fff; }
.cm-item.danger { color: var(--danger); }
.cm-item.danger:hover { background: var(--danger); color: #fff; }
.cm-sep {
height: 1px;
background: var(--border);
margin: 4px 8px;
}
/* ===== 弹窗 ===== */
.modal {
position: fixed;
inset: 0;
display: none;
align-items: center;
justify-content: center;
z-index: 2000;
}
.modal-backdrop {
position: absolute;
inset: 0;
background: rgba(0,0,0,0.3);
backdrop-filter: blur(8px);
}
.modal-content {
position: relative;
background: var(--window-bg);
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
padding: 20px 24px;
min-width: 360px;
max-width: 520px;
max-height: 80vh;
overflow-y: auto;
}
.modal-title {
font-size: 15px;
font-weight: 600;
margin-bottom: 14px;
color: var(--text);
}
.modal-body {
font-size: 13px;
color: var(--text);
line-height: 1.6;
margin-bottom: 18px;
}
.modal-body label {
display: block;
font-size: 12px;
color: var(--text-secondary);
margin-bottom: 6px;
margin-top: 8px;
}
.modal-body label:first-child { margin-top: 0; }
.modal-body input, .modal-body select {
width: 100%;
padding: 7px 10px;
border: 1px solid var(--border);
border-radius: 5px;
background: var(--bg);
color: var(--text);
font-size: 13px;
}
.modal-body input:focus, .modal-body select:focus { border-color: var(--selection); }
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.modal-footer .btn-primary, .modal-footer .btn-ghost {
width: auto;
min-width: 72px;
margin-top: 0;
}
/* 简介 */
.info-row {
display: flex;
padding: 4px 0;
border-bottom: 1px solid var(--border);
font-size: 12px;
}
.info-row:last-child { border-bottom: none; }
.info-label {
width: 80px;
color: var(--text-secondary);
flex-shrink: 0;
}
.info-value {
flex: 1;
color: var(--text);
overflow-wrap: break-word;
word-break: break-all;
}
/* 分享输出 */
.share-output { margin-top: 8px; }
.share-title { font-size: 12px; color: var(--text-secondary); margin-bottom: 6px; }
.share-url-box {
padding: 8px 10px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 5px;
font-size: 11px;
font-family: 'SF Mono', Menlo, monospace;
color: var(--text);
word-break: break-all;
line-height: 1.5;
}
/* 图片预览 */
.modal-content.image-preview {
background: transparent;
box-shadow: none;
padding: 0;
min-width: auto;
max-width: 90vw;
max-height: 90vh;
overflow: visible;
}
.modal-content.image-preview img {
max-width: 90vw;
max-height: 85vh;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0,0,0,0.5);
transition: transform 0.08s ease-out;
cursor: zoom-in;
}
.modal-close {
position: absolute;
top: -10px;
right: -10px;
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--window-bg);
color: var(--text);
font-size: 14px;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
.modal-close:hover { background: var(--selection); color: #fff; }
/* ===== Toast ===== */
.toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
background: var(--window-bg);
color: var(--text);
padding: 10px 20px;
border-radius: 20px;
box-shadow: 0 8px 32px rgba(0,0,0,0.15);
font-size: 13px;
z-index: 9999;
display: none;
align-items: center;
border: 1px solid var(--border);
backdrop-filter: blur(20px);
}
.toast-success { border-left: 3px solid #28c840; }
.toast-error { border-left: 3px solid var(--danger); }
.toast-info { border-left: 3px solid var(--selection); }
/* 滚动条 */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.15); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.3); }
:root.theme-dark ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); }
:root.theme-dark ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.3); }
@media (max-width: 900px) {
.finder-sidebar { width: 140px; }
.list-row { grid-template-columns: minmax(180px, 1fr) 80px 140px 80px; font-size: 11px; }
.search-box input { width: 120px; }
}
File diff suppressed because it is too large Load Diff
+8
View File
@@ -206,6 +206,14 @@ textarea {
margin-bottom: 8px;
}
.weather-data-timestamp {
font-size: 11px;
color: rgba(63, 95, 45, 0.5);
margin-bottom: 4px;
font-family: "Consolas", "Monaco", monospace;
letter-spacing: 0.5px;
}
.expand-btn {
align-self: flex-end;
background: transparent;
+12
View File
@@ -567,6 +567,17 @@ const renderWeather = weather => {
const adviceText = escapeHtml(weather.advice_text || '暂无建议')
const weatherIcon = getWeatherIcon(weather.icon)
// 转换 data_gateway 更新时间为北京时间
const formatBeijingTime = (utcStr) => {
if (!utcStr) return ''
const d = new Date(utcStr.replace(' ', 'T') + '+00:00')
if (isNaN(d.getTime())) return ''
const pad = n => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds())}`
}
const dataTimestamp = formatBeijingTime(weather._data_gateway_updated_at)
const dataSourceLabel = dataTimestamp ? `数据更新:${dataTimestamp}` : ''
// 检查温度是否已经包含°C,如果没有则添加
if (temp && !temp.includes('°C')) {
temp = temp + '°C'
@@ -602,6 +613,7 @@ const renderWeather = weather => {
<div class="weather-advice">
<div class="advice-text" id="advice-text">${adviceText}</div>
${dataSourceLabel ? `<div class="weather-data-timestamp">${escapeHtml(dataSourceLabel)}</div>` : ''}
<button class="expand-btn" id="expand-advice-btn">展开</button>
</div>
`
@@ -2,8 +2,8 @@
"_comment": "私人剪贴板工具的前端鉴权配置",
"enable_auth_comment": "true:启用鉴权;false:关闭鉴权并放开访问",
"enable_auth": true,
"max_age_days_comment": "鉴权 Cookie 有效期(天),范围建议 1~365",
"max_age_days": 30,
"max_age_days_comment": "鉴权 Cookie 有效期(天),1天后过期需重新登录",
"max_age_days": 1,
"cookieName_comment": "鉴权 Cookie 名称,建议保持默认",
"cookieName": "private_clipboard_gate",
"requireGuid_comment": "true:入口必须带 guid 参数;false:允许仅凭 token 或会话访问",
@@ -11,5 +11,5 @@
"guidParam_comment": "URL 中的 GUID 参数名,建议使用小写 guid",
"guidParam": "guid",
"guidValue_comment": "GUID 参数值,建议使用高强度随机长串并定期更换",
"guidValue": "SD9KK-LW32S-9KJJH-JJH43-LJ432J"
"guidValue": "db8a2f1c-90e7-4b5a-a1d2-c3e4f5067890"
}
@@ -4,6 +4,68 @@ body {
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
}
/* ===== 登录界面 ===== */
.pc-auth-card {
max-width: 360px;
margin: 120px auto 0;
padding: 32px 28px;
background: #1a1d24;
border: 1px solid #2a2d35;
border-radius: 12px;
text-align: center;
}
.pc-auth-card h2 {
margin: 0 0 24px;
font-size: 20px;
font-weight: 600;
}
.pc-auth-form {
display: flex;
flex-direction: column;
gap: 12px;
}
.pc-auth-form input {
padding: 10px 12px;
background: #0f1115;
border: 1px solid #2a2d35;
border-radius: 6px;
color: #e6e8ee;
font-size: 14px;
outline: none;
}
.pc-auth-form input:focus {
border-color: #4a9eff;
}
.pc-auth-status {
margin-top: 12px;
font-size: 13px;
color: #ff6b6b;
}
.pc-auth-status.ok {
color: #4ade80;
}
/* ===== 顶部栏 ===== */
.pc-topbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.pc-topbar-title {
font-size: 20px;
font-weight: 600;
}
.pc-btn-logout {
padding: 6px 16px;
font-size: 13px;
background: #2a2d35;
border: 1px solid #3a3d45;
}
.pc-btn-logout:hover {
background: #3a3d45;
}
.pc-main {
max-width: 1100px;
margin: 0 auto;
@@ -736,6 +736,99 @@ const createFolder = async () => {
}
const init = async () => {
// ===== set_token 处理:必须在 clearUrlQuery 之前 =====
try {
const params = new URLSearchParams(window.location.search)
const setToken = params.get('set_token')
if (setToken) {
localStorage.setItem('toolbox_creds_token', setToken)
}
} catch {}
clearUrlQuery()
// ===== 登录逻辑 =====
const $auth = $('pc-auth')
const $app = $('pc-app')
const $status = $('pc-auth-status')
const $user = $('pc-login-user')
const $pass = $('pc-login-pass')
async function doLogin() {
try {
const username = $user.value.trim()
const password = $pass.value
const res = await fetch(apiBase + '/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
credentials: 'same-origin',
cache: 'no-store'
})
const data = await res.json()
if (!data.ok) throw new Error(data.error || '登录失败')
$auth.style.display = 'none'
$app.style.display = ''
initApp()
} catch (e) {
$status.textContent = String(e.message || e)
$status.style.display = ''
$status.className = 'pc-auth-status'
}
}
async function doLogout() {
try {
await fetch(apiBase + '/auth/logout', { method: 'POST', credentials: 'same-origin' })
} catch {}
$app.style.display = 'none'
$auth.style.display = ''
$pass.value = ''
$status.textContent = '已注销'
$status.className = 'pc-auth-status ok'
$status.style.display = ''
}
// 自动填充
async function tryAutoFill() {
try {
const token = localStorage.getItem('toolbox_creds_token') || ''
if (!token) return
const res = await fetch(apiBase + '/auth/local_creds?token=' + encodeURIComponent(token), {
credentials: 'same-origin',
cache: 'no-store'
})
const data = await res.json().catch(() => null)
if (data && data.ok && data.creds) {
if (data.creds.username) $user.value = data.creds.username
if (data.creds.password) $pass.value = data.creds.password
}
} catch {}
}
$('pc-btn-login').onclick = doLogin
$('pc-btn-logout').onclick = doLogout
$user.removeAttribute('readonly')
$pass.removeAttribute('readonly')
$pass.addEventListener('keydown', e => { if (e.key === 'Enter') doLogin() })
// 先检查是否已登录
try {
const res = await fetch(apiBase + '/auth/me', { credentials: 'same-origin', cache: 'no-store' })
const data = await res.json()
if (data && data.ok) {
$auth.style.display = 'none'
$app.style.display = ''
initApp()
return
}
} catch {}
// 未登录,尝试自动填充
await tryAutoFill()
}
const initApp = async () => {
clearUrlQuery()
const isMobile = detectMobile()
const refreshBtn = $('pc-refresh')
+17 -1
View File
@@ -9,7 +9,23 @@
<link rel="stylesheet" href="/tools/private_clipboard/clipboard.css" />
</head>
<body>
<main class="pc-main">
<!-- 登录界面 -->
<section id="pc-auth" class="pc-auth-card">
<h2>私人剪贴板 - 登录</h2>
<div class="pc-auth-form" autocomplete="off">
<input id="pc-login-user" placeholder="用户名" autocomplete="off" readonly />
<input id="pc-login-pass" placeholder="密码" type="password" autocomplete="off" readonly />
<button id="pc-btn-login" class="btn">登录</button>
</div>
<div id="pc-auth-status" class="pc-auth-status" style="display:none"></div>
</section>
<!-- 主应用界面 -->
<main id="pc-app" class="pc-main" style="display:none">
<div class="pc-topbar">
<span class="pc-topbar-title">私人剪贴板</span>
<button id="pc-btn-logout" class="btn pc-btn-logout" type="button">注销</button>
</div>
<section class="pc-input">
<div id="paste-zone" class="pc-paste-zone pc-desktop-only" tabindex="0">
<div class="pc-paste-text">在这里粘贴文字 / 图片 / 文件</div>
File diff suppressed because it is too large Load Diff
+131
View File
@@ -0,0 +1,131 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>打谷场 - 塔防合集</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{
--bg:#0d1117;--card-bg:#161b22;--border:#30363d;
--accent:#e94560;--accent2:#4caf50;--gold:#f0c040;
--text:#e6edf3;--text2:#8b949e;
}
body{
font-family:'Segoe UI','Microsoft YaHei',sans-serif;
background:var(--bg);color:var(--text);min-height:100vh;
}
header{
text-align:center;padding:40px 20px 20px;
background:linear-gradient(180deg, #1a1a2e 0%, var(--bg) 100%);
border-bottom:1px solid var(--border);
}
header h1{font-size:32px;color:var(--accent);margin-bottom:8px}
header p{font-size:14px;color:var(--text2)}
.container{
max-width:900px;margin:0 auto;padding:24px 16px;
display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));
gap:16px;
}
.card{
background:var(--card-bg);border:1px solid var(--border);
border-radius:10px;padding:20px;cursor:pointer;
transition:all 0.2s;text-decoration:none;color:var(--text);
position:relative;overflow:hidden;
}
.card:hover{transform:translateY(-3px);border-color:var(--accent);box-shadow:0 4px 20px rgba(233,69,96,0.2)}
.card .icon{font-size:36px;margin-bottom:12px}
.card h3{font-size:16px;margin-bottom:6px}
.card .diff{display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;margin-bottom:8px}
.card .diff.easy{background:#4caf50;color:#fff}
.card .diff.hell{background:#e94560;color:#fff}
.card .tags{display:flex;flex-wrap:wrap;gap:4px;margin-top:10px}
.card .tag{font-size:10px;padding:2px 6px;border-radius:3px;background:rgba(255,255,255,0.06);color:var(--text2)}
.card .desc{font-size:12px;color:var(--text2);line-height:1.5}
.card.coming-soon{opacity:0.5;cursor:default}
.card.coming-soon:hover{transform:none;border-color:var(--border);box-shadow:none}
.card.coming-soon .badge{position:absolute;top:10px;right:10px;font-size:10px;background:var(--border);color:var(--text2);padding:2px 8px;border-radius:10px}
footer{
text-align:center;padding:20px;color:var(--text2);font-size:12px;
border-top:1px solid var(--border);margin-top:20px;
}
</style>
</head>
<body>
<header>
<h1>打谷场</h1>
<p>军事风自定义路线塔防游戏合集 - 多个难度版本,自由绘制敌军路径</p>
</header>
<div class="container">
<!-- 简单版 -->
<a class="card" href="simple.html">
<div class="icon">🎯</div>
<h3>简单版</h3>
<span class="diff easy">入门</span>
<p class="desc">标准难度,500金币起步,20生命值。BOSS每10波出现。适合新手熟悉游戏机制。</p>
<div class="tags">
<span class="tag">金币500</span><span class="tag">生命20</span><span class="tag">BOSS=10波</span>
</div>
</a>
<!-- 地狱版 -->
<a class="card" href="hell.html">
<div class="icon">💀</div>
<h3>地狱版</h3>
<span class="diff hell">地狱</span>
<p class="desc">极难难度!300金币紧巴巴,仅10生命值。BOSS每5波就来,敌人1.5倍数量+1.3倍速度,塔造价涨30%。</p>
<div class="tags">
<span class="tag">金币300</span><span class="tag">生命10</span><span class="tag">BOSS=5波</span><span class="tag">1.5x敌军</span><span class="tag">1.3x塔价</span>
</div>
</a>
<!-- 占位:更多版本 -->
<div class="card coming-soon">
<div class="icon">⚡</div>
<h3>闪电战</h3>
<span class="badge">即将推出</span>
<p class="desc">敌人速度翻倍,但塔攻击力也翻倍。快节奏极限操作。</p>
<div class="tags">
<span class="tag">2x速度</span><span class="tag">2x伤害</span>
</div>
</div>
<div class="card coming-soon">
<div class="icon">♾️</div>
<h3>无尽模式</h3>
<span class="badge">即将推出</span>
<p class="desc">无波次限制,敌人无限刷新,难度持续递增,挑战最高击杀数。</p>
<div class="tags">
<span class="tag">无限波</span><span class="tag">排行榜</span>
</div>
</div>
<div class="card coming-soon">
<div class="icon">🌙</div>
<h3>噩梦版</h3>
<span class="badge">即将推出</span>
<p class="desc">比地狱更恐怖。0金币起步,杀敌才得金币。每波随机出特殊兵种。</p>
<div class="tags">
<span class="tag">0金币</span><span class="tag">特殊兵种</span>
</div>
</div>
<div class="card coming-soon">
<div class="icon">🛡️</div>
<h3>坚守阵地</h3>
<span class="badge">即将推出</span>
<p class="desc">固定路线+预设塔布局,纯策略防御。能用有限的塔撑多久?</p>
<div class="tags">
<span class="tag">固定路线</span><span class="tag">预设塔</span>
</div>
</div>
</div>
<footer>
<p>所有版本离线可玩 · 存档永久保留在浏览器中 · 无贴图无音效极简风格</p>
</footer>
</body>
</html>
File diff suppressed because it is too large Load Diff