date: 2026-05-27 updated: 2026-06-01 conversation_id: b60ad0b6-0300-4914-ab3d-b021cdaac425 title: "网页选股模块需求文档" tags: [deepseek, conversation] ---
网页选股模块需求文档
创建时间: 2026-05-27 18:58
📎 处理文件中...
👤 用户:
解析图中内容,帮我撰写需求文档
💭 思考过程
我们被要求解析图中内容并撰写需求文档。图片描述的是一个表格界面,有两个模块:基础版“拉萨二哥精选”和高级版“拉萨二哥主导”。需要根据这些信息撰写需求文档。需求文档应包含功能概述、用户角色、功能细节、数据字段、操作按钮等。输出格式应为markdown。
以下是根据图片内容整理的需求文档。
需求文档:网页选股模块
1. 概述
本模块用于网页端选股管理,支持用户添加个股,记录入池时间、价格、最大涨幅、持仓时间等信息,并提供汇总统计。分为基础版与高级版两个功能模块。
2. 用户角色
- 普通用户:使用基础版功能。
- 高级用户:使用高级版功能(包含更多字段与筛选能力)。
3. 功能模块
3.1 基础版 —— 【拉萨二哥精选】
3.1.1 功能描述
用户可以添加个股进入汇总列表,系统自动计算并展示相关统计数据。
3.1.2 汇总列表字段
| 字段名 | 说明 |
|---|---|
| 代码 | 股票代码 |
| 名称 | 股票名称 |
| 入池时间 | 添加至列表的时间 |
| 入池价格 | 添加时的股价 |
| 最大涨幅 | 入池后至今的最大涨幅百分比 |
| 持仓时间 | 从入池到当前(或卖出)的持仓天数 |
| 选择时间段个股入池情况按钮 | 按钮,用于筛选查看指定时间段内的入池记录 |
3.1.3 汇总统计数据(底部栏)
| 统计项 | 说明 |
|---|---|
| 入池总数 | 累计添加的个股数量 |
| 盈利胜率 | 盈利个股数 / 总个股数 × 100% |
| 综合收益率 | 所有个股的平均收益率(或总收益/总成本) |
3.1.4 操作说明
- 用户可通过界面上的“添加个股”入口(图中未绘出,需隐含)填写代码、名称、入池价格,系统自动记录入池时间(当前时间),持仓时间随交易日动态更新,最大涨幅每日更新。
3.2 高级版 —— 【拉萨二哥主导】
3.2.1 功能描述
在基础版基础上增加更多字段与分析能力,允许更精细的数据管理。
3.2.2 汇总列表字段
与基础版相同,但额外包含“代码、名称”字段(基础版已包含,此处可能强调顺序或完整展示)。实际上高级版列表字段为:
| 字段名 | 说明 |
|---|---|
| 代码 | 股票代码 |
| 名称 | 股票名称 |
| 入池时间 | 添加时间 |
| 入池价格 | 添加时股价 |
| 最大涨幅 | 入池后最大涨幅 |
| 持仓时间 | 持仓天数 |
| 选择时间段个股入池情况按钮 | 筛选按钮 |
(注:基础版与高级版字段展示一致,但高级版可能支持更多数据分析或导出功能,图中未明确,可推断高级版有额外能力。)
3.2.3 汇总统计数据
同基础版:
- 入池总数
- 盈利胜率
- 综合收益率
4. 非功能性需求
- 实时性:最大涨幅、持仓时间需每日/实时更新。
- 数据存储:支持持久化存储用户添加的个股记录。
- 筛选功能:点击“选择时间段个股入池情况按钮”后,弹出时间范围选择器,列表仅显示该时间段内的入池记录,统计数据同步更新。
5. 界面示意(文字描述)
页面分为两个卡片或区域:
- 基础版区域:标题“拉萨二哥精选”,下方为表格,展示代码、名称、入池时间、入池价格、最大涨幅、持仓时间,最后一列包含筛选按钮。表格下方显示入池总数、盈利胜率、综合收益率。
- 高级版区域:标题“拉萨二哥主导”,布局与基础版类似,字段相同,但功能更强大(如可编辑、导出、多条件筛选等,图中未详述,可根据实际需求补充)。
6. 待补充说明
- 最大涨幅的计算基准:基于入池价格的最高价涨幅。
- 持仓时间计算:自然日或交易日?建议统一为交易日。
- 盈利胜率:以当前价格 > 入池价格为盈利,或最大涨幅 > 0% 为盈利?建议定义为当前价高于入池价。
- 综合收益率计算方式:平均涨幅或加权收益率,需明确。
以上文档基于图片内容推断撰写,实际开发时可根据详细业务规则补充。
👤 用户:
制作为网页原型
💭 思考过程
我们需要制作网页原型。根据前面的需求文档,实现一个HTML/CSS/JS前端原型,包括基础版【拉萨二哥精选】和高级版【拉萨二哥主导】。需要具有添加个股的功能,展示表格和统计。按照原型要求:能够添加个股进入,汇总列表显示添加时间、添加价格、最大涨幅、持仓时间等。另外还要有“选择时间段个股入池情况按钮”,用于筛选时间段的记录。入池总数、盈利胜率、综合收益率动态更新。
由于是原型,可以用静态前端,JavaScript模拟数据存储,实现添加、删除(可选),但需求里没有明确删除,但一般原型需要管理列表。但主要基于图中描述:新增网页选股,能添加个股进入,汇总列表展示字段,展示统计数据,以及时间段筛选按钮。最好支持示例数据展示。
另外最大涨幅和持仓时间需要根据当前时间和价格动态计算?原型中可以模拟:每天当前价格假设最新价?为了简化,可以在添加时设置当前价?或者设一个当前价默认为入池价格+随机涨幅展示最大涨幅?由于最大涨幅需要追踪历史最高价,原型中可以设计为编辑或者预先定义,或者通过点击更新行情来模拟。不过既然是原型,可以设计为添加时输入“当前最新价”或“最高价”?但更合理的原型:最大涨幅 = max(当前价格与入池价格之比 - 1,0),但最大涨幅应该是历史最高价相对入池价格的涨幅,但原型简单可模拟一个最大涨幅字段,允许编辑?但最好按照标准:添加个股时,设定当前价和最高价?或模拟每日更新比较复杂。但网页原型可以每次手动“刷新行情”模拟计算?需求文档中希望真实体现最大涨幅和持仓时间可更新。为了展示完整性,我可以在每个股票的行内提供一个“刷新”按钮来模拟价格更新?但会增加复杂性。更简洁:在添加个股时,除了代码、名称、入池价格外,还需输入“当前价格”和“历史最高价”,从而计算最大涨幅,持仓时间使用入池时间到当前系统时间的差值(天数),按交易日?简单按自然日天数显示。这样最大涨幅可以通过(历史最高价 - 入池价格)/入池价格计算。也可以让系统自动生成示例数据,并提供编辑功能。由于用户需要演示,可以设计每个股票添加后默认当前价格=入池价格,历史最高价=入池价格,最大涨幅0,用户可以通过编辑历史最高价来模拟。但是为了符合快速原型,我可以支持添加时填写初始最高价?但太麻烦。简单点:最大涨幅可手动输入,但不太专业。更佳做法:添加个股时,只填代码、名称、入池价格,入池时间自动当前时间,持仓时间自动计算,最大涨幅通过一个“更新涨幅”按钮来随机或让用户输入最高价。但用户需要看到“最大涨幅”真实反映变化,由于原型只能展示概念,我提供一个模拟更新按钮,用户点击更新行情时随机调整最大涨幅? 这样不严谨。考虑到需求描述中未提供价格接口,作为网页原型可以带有模拟数据演示,并且展示添加功能。我们设计每个股票条目中“最大涨幅”显示百分比,并提供“修改最高价”功能?或者设计一个简单面板来设置个股最高价? 最好还是允许用户双击涨幅修改?过于复杂。为了满足“最大涨幅”可显示并直观,我将在添加个股时允许用户输入“当前最高价”(可选,默认识别),若不填则初始最高价为入池价格,涨幅0。但是用户可能理解。更友好:添加对话框中包含“历史最高价”字段,用于计算最大涨幅(基于入池价格)。另外也可以提供“更新最高价”按钮进行修改持仓时间和涨幅的相关逻辑。同时持仓时间用当前日期减去入池时间自然日天数显示,每日自动变化。最大涨幅也可以手动重新计算/重设。由于是原型,合理就行,确保每个个股可以添加后显示入池时间、入池价格,最大涨幅字段。还需要统计盈利胜率和综合收益率,盈利胜率根据当前价格(当前价可基于最新价)> 入池价格来计算盈利。但是当前价从哪里来?没有实时行情。为了简单,我们在每个股票的列表里增加“当前价格”列,用户可以手动更新当前价格,从而重新计算盈利胜率和综合收益率,以及最大涨幅(最大涨幅单独计算?最大涨幅一般依据历史最高价和入池价,最好保留历史最高价列并提供编辑。因此表格可能需要增加“历史最高价”列或“当前价/最高价”,但是原需求表格只包含代码名称入池时间入池价格最大涨幅持仓时间,还包括选择时间段按钮。我们可以在高级版与基础版中同样字段(但高级版可能有额外功能但保持一样可以)。因为我们为了实现需求,在界面上只展示这些列,但为了提高数据完整性,可以在内部维护每个股票最高价。添加个股时提供“最高价”或每日更新? 做最简原型:设置最大涨幅为手动输入或计算? 为了使演示方便且不复杂,我决定每个股票行增加一个操作列“设置最高价”按钮,设置该股票的历史最高价,然后计算最大涨幅。同时也可以设置“当前价格”用于胜率和综合收益率。但是胜率一般依赖于盈亏状态(当前价是否大于入池价)。可定义“最新价”,默认等于入池价。提供“设置最新价”按钮。这样用户可以手动调整数据观察统计变化。这也符合原型交互演示。
但基础版需求文档中没有明确说明当前价,但盈利胜率需要知道盈利,所以必须有一个价格比较,因此必须维护最新价(或当前市价)。根据常见选股池,用户可能手动更新持仓的最新价,原型中每只股票有最新价字段(在表格中显示但不一定要求显示在汇总列表中?需求显示汇总列表字段只提到代码、名称、入池时间、入池价格、最大涨幅、持仓时间。为了不增加视觉混乱,可以把最新价作为隐藏维护数据,但让用户通过点击“更新盈利”等方式修改最新价,并用单独控件。为了简单易懂,在每个股票旁边设置最新价编辑,也可以用内联显示。但表格会超出预期列。可以增加一列“当前价”,并在需求上适度扩展,但原型着重展示功能,添加个股时填当前价(默认入池价),这样用户可以调整当前价,立马影响盈利胜率和综合收益率计算,同时最大涨幅通过单独提供“记录最高价”功能来更新。那么需要额外列“历史最高价”或通过最大涨幅反向计算,但最大涨幅显示的百分比根据(历史最高价 - 入池价)/入池价。为了灵活性,在股票每一项提供一个“更新最高价”的图标,设置最高价。同样盈利胜率依赖当前价,用户可编辑当前价,这能够最大程度模拟真实场景同时代码量合理。
我需要输出纯HTML/CSS/JS,支持基础版和高级版两个表格,但两个表格数据应该独立?需求中两个模块各自独立添加个股,基础版与高级版数据不互通,分别管理。因为是两个独立功能,所以基础版(拉萨二哥精选)是一个单独的个股池;高级版(拉萨二哥主导)是另一个单独的池子。每个池子各自有添加记录,各自统计。筛选时间段按钮需要基于该表格内的记录进行时间筛选。
整体需要实现:
两个版块,每个版块包含:
- 添加表单:输入代码,名称,入池价格(入池时间自动当前datetime),另外可以额外让用户输入初始最高价(默认入池价)和初始当前价(默认入池价),但最好简化,添加时直接设置当前价和最高价同入池价,最大涨幅0,持仓时间根据当前日期计算。为后期修改,在表格内为每行提供“修改最高价”和“修改当前价”按钮/输入框,以及可单独“更新持仓时间”随日期刷新,持仓时间每天自动基于当前日期计算。最大涨幅修改最高价后重新计算。
尽可能无后端,使用localStorage或内存数组存储。
另外需求中有“最大涨幅”和“持仓时间”需动态更新,最大涨幅基于最高价,持仓时间基于当前日期持续变化。每行显示最大涨幅百分比,持仓天数。此外还需要盈利胜率根据当前价>入池价判定盈利。综合收益率计算方式:所有股票的(当前价-入池价)/入池价的平均(或者总和收益率?需求文档说综合收益率,建议采用平均收益率(%)求和/股票数量)。合理。
同时入池总数轻松统计。添加时还需入池时间,使用JavaScript的本地时间字符串(YYYY-MM-DD HH:MM)。持仓时间计算天数差,以自然日(或交易日原型简单)。筛选时间段按钮:点击弹出日期范围选择器,筛选入池时间在范围内的记录,并更新表格显示并重新统计。最好显示筛选状态,并可以重置。添加高级版功能类似。
另外高级版与基础版都支持相同的功能,但我们可以实现代码重用,通过不同容器分别处理数据数组和渲染。实际实现可封装为组件函数,但为了简洁,两个独立操作。
由于时间:需完成HTML,CSS,表格,交互,JavaScript。
还要注意界面的"选择时间段个股入池情况按钮"一般在表格外单独按钮,用于筛选显示该池子的个股入池情况。每个模块有一个筛选按钮。
需求中还显示汇总列表中有入池总数、盈利胜率、综合收益率以及底部统计。表格上还需要展示最大涨幅,持仓时间等。
另外基础版和高级版布局区别(高级版可能有更多功能),但根据需求,高级版可能将来功能增强,但原型保持一致,可额外注明高级版支持更多数据分析等,但为了美观,可以细微样式差异。
实现复杂程度适中。
我将编写一个完整html,包含style,modern设计,暗色或亮色,卡片样式。
实现步骤:
- 页面标题,显示两个卡片。
- 基础版卡片: 标题"【拉萨二哥精选】-基础版",添加个股表单(代码,名称,入池价格),其他字段自动生成。另外还可以加设置初始当前价和最高价可选,但可先不暴露给用户,添加后默认当前价=入池价,最高价=入池价。然后提供每个股票的操作:设置当前价(弹出提示或inline),设置最高价(弹出提示)。为简化交互,每行有“设置当前价”和“设置最高价”两个小按钮,点击时通过prompt输入数字,更新后重新渲染表格和统计。这样便于演示最大涨幅和盈利胜率修改。
此外持仓时间每次重新渲染时基于当前日期计算(每次更新渲染都会重新计算天数)。最大涨幅根据最高价计算百分比。
还要注意修改个股当前价和最高价后,实时更新,保证统计正确。
表格列:代码、名称、入池时间、入池价格、最大涨幅、持仓时间、操作(设置当前价、设置最高价、删除)。另外需要一个筛选时间段按钮,在表格上方或下方,选择时间段筛选。也需要一个显示所有记录的按钮。
由于有筛选状态,基础版维护原始数据数组,筛选后显示数据,统计基于筛选后数据(还是基于全部?一般选股情况按钮作用展示指定时间入池情况,统计应该针对筛选后显示的数据?但需求展示汇总列表时统计指标一般基于当前展示的记录。我们可以做成基于筛选后显示的记录进行统计(入池总数、盈利胜率、综合收益率)。同时筛选按钮也是改变显示。
但入池总数计数为筛选后个数,这个合理,可以切换时间段查看统计。实现:筛选功能基于日期范围,更新表格并重新统计。
另外需求“选择时间段个股入池情况按钮”可以用于展示入池情况,我们实现。
高级版类似。为了简化代码,我会独立写两个类似块,但JavaScript部分分别维护两个数据模型,各自渲染,独立筛选。为了提高代码复用,可能会写一个类,但直接各自函数也可以。
由于时间,确保功能完整。
实现:
- 定义baseStocks = [] 和 advancedStocks = []数组。
- 分别为基础版和高级版提供添加,删除,更新当前价,更新最高价,筛选等功能。
- 渲染函数:renderBaseTable(filteredStocks) 和 renderAdvancedTable,统计时基于传入数组(筛选后的或全部)。
- 筛选:每个版块有一个显示时间段筛选的按钮和一个日期范围选择器(两个input date)。或点击按钮,弹出两个日期输入框选择。为了简便,增加两个日期输入,一个开始日期,一个结束日期,再有一个“筛选”按钮。并提供重置。
实现时两个模块分别有开始日期结束日期筛选器。
还要展示额外汇总统计。
另外为了体现自动计算最大涨幅和持仓时间,需要基于最高价和当前日期。修改当前价时影响盈利胜率和综合收益率,修改最高价影响最大涨幅,此外持仓时间无需编辑(根据入池时间动态)。注意入池时间不可修改,删除功能。
操作按钮触发重新渲染。
另外,添加个股时需检验代码名称价格有效,入池时间使用new Date(),格式化显示YYYY-MM-DD HH:MM:SS,入池价格保存数字。持仓时间每次用当前日期(new Date())差值天数。
最大涨幅计算:max((最高价 - 入池价) / 入池价 * 100,0)。如果最高价低于入池价,最大涨幅为0。给定默认最高价等于入池价。
此外要保存最高价字段,当前价字段。每个股票对象包含code,name,entryTime,entryPrice,currentPrice,highPrice。初始currentPrice=entryPrice,highPrice=entryPrice,最大涨幅动态计算,表内显示基于highPrice计算百分比。另外盈利基于currentPrice。
综合收益率:所有股票(当前价-入池价)/入池价 * 100的平均值。
盈利胜率:currentPrice>entryPrice的占比。
注意筛选功能基于entryTime的日期字符串比较。还要保留原始数据以便重置筛选。
同时添加按钮后自动重置筛选(清除筛选),或者不清除但添加后重新显示筛选结果。最好添加后重新应用筛选条件(保留当前时间段筛选)。所以筛选状态需要存储当前基础版的筛选起止日期值,每次数据变化重新根据筛选条件过滤刷新。实现复杂度适中,我们可以管理每个模块的filterStart, filterEnd。添加股票、修改、删除等操作后,重新应用筛选条件。并且日期筛选的输入框值变化后手动点击筛选更新过滤显示。另外重置筛选清除日期过滤条件显示所有。
还需要考虑,默认基础版数据示例或许初始化展示几个示例数据。高级版展示示例数据,方便测试。
另外注意持仓时间排序等不需要。
写代码。由于代码规模较大,注意不要冲突。
开始编写。
以下是您所需的网页选股模块的HTML代码。它实现了基础版“拉萨二哥精选”和高级版“拉萨二哥主导”两个独立的个股管理池,支持添加股票、动态计算涨跌幅与胜率,并可按时间段筛选数据。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<title>选股决策系统 - 拉萨二哥系列</title>
<style>
* {
box-sizing: border-box;
font-family: system-ui, 'Segoe UI', 'Roboto', 'Helvetica Neue', sans-serif;
}
body {
background: #f0f2f5;
margin: 0;
padding: 24px 20px;
}
.container {
max-width: 1600px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 32px;
}
/* 卡片通用样式 */
.card {
background: #ffffff;
border-radius: 24px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.05);
overflow: hidden;
transition: all 0.2s;
border: 1px solid #e9ecef;
}
.card-header {
padding: 20px 24px 12px 24px;
border-bottom: 2px solid #f1f3f5;
display: flex;
justify-content: space-between;
align-items: baseline;
flex-wrap: wrap;
gap: 12px;
background: #fefefe;
}
.card-header h2 {
margin: 0;
font-size: 1.6rem;
font-weight: 600;
background: linear-gradient(135deg, #1e2a3e, #0f172a);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
letter-spacing: -0.3px;
}
.badge-version {
background: #eef2ff;
padding: 4px 12px;
border-radius: 40px;
font-size: 0.75rem;
font-weight: 500;
color: #1e40af;
}
.add-panel {
padding: 20px 24px;
background: #f8fafc;
border-bottom: 1px solid #e2e8f0;
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: flex-end;
}
.input-group {
display: flex;
flex-direction: column;
gap: 6px;
font-size: 0.8rem;
font-weight: 500;
color: #334155;
}
.input-group label {
font-size: 0.75rem;
letter-spacing: 0.3px;
}
.input-group input {
padding: 8px 12px;
border-radius: 12px;
border: 1px solid #cbd5e1;
background: white;
font-size: 0.85rem;
width: 130px;
transition: 0.2s;
}
.input-group input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59,130,246,0.2);
}
button {
background: #1e293b;
border: none;
padding: 8px 18px;
border-radius: 40px;
font-weight: 500;
font-size: 0.8rem;
color: white;
cursor: pointer;
transition: 0.2s;
display: inline-flex;
align-items: center;
gap: 6px;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
button.primary {
background: #3b82f6;
}
button.primary:hover {
background: #2563eb;
transform: translateY(-1px);
}
button:hover {
background: #0f172a;
}
button.small {
padding: 4px 12px;
font-size: 0.7rem;
}
button.warning {
background: #eab308;
color: #1e293b;
}
button.warning:hover {
background: #ca8a04;
}
button.danger {
background: #ef4444;
}
button.danger:hover {
background: #dc2626;
}
.filter-bar {
padding: 12px 24px;
background: #ffffff;
border-bottom: 1px solid #edf2f7;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 15px;
}
.filter-group {
display: flex;
align-items: center;
gap: 8px;
background: #f1f5f9;
padding: 5px 12px;
border-radius: 40px;
}
.filter-group label {
font-size: 0.75rem;
font-weight: 500;
}
.filter-group input {
border: 1px solid #cbd5e1;
border-radius: 20px;
padding: 6px 10px;
font-size: 0.75rem;
background: white;
}
.stats-bar {
padding: 12px 24px;
background: #fefce8;
border-top: 1px solid #f1f3f5;
display: flex;
gap: 32px;
flex-wrap: wrap;
font-weight: 500;
font-size: 0.9rem;
}
.stat {
background: white;
padding: 5px 16px;
border-radius: 40px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.stat span:first-child {
color: #475569;
}
.stat span:last-child {
font-weight: 700;
color: #0f172a;
margin-left: 8px;
}
.table-wrapper {
overflow-x: auto;
padding: 0 0 12px 0;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.8rem;
min-width: 800px;
}
th {
text-align: left;
padding: 14px 12px;
background: #f8fafc;
color: #1e293b;
font-weight: 600;
border-bottom: 1px solid #e2e8f0;
}
td {
padding: 12px;
border-bottom: 1px solid #f1f5f9;
vertical-align: middle;
}
.actions {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.profit-positive {
color: #10b981;
font-weight: 600;
}
.profit-negative {
color: #ef4444;
}
.tooltip-info {
cursor: help;
border-bottom: 1px dashed #94a3b8;
}
footer {
text-align: center;
font-size: 0.7rem;
color: #94a3b8;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<!-- 基础版模块:拉萨二哥精选 -->
<div class="card">
<div class="card-header">
<div style="display: flex; align-items: center; gap: 12px;">
<h2>📋 拉萨二哥精选 · 基础版</h2>
<div class="badge-version">动态池|实时胜率</div>
</div>
<button id="resetBaseFilterBtn" class="small" style="background:#e2e8f0; color:#1e293b;">🔄 重置筛选</button>
</div>
<div class="add-panel">
<div class="input-group">
<label>📟 代码</label>
<input type="text" id="baseCode" placeholder="例: 600036" autocomplete="off">
</div>
<div class="input-group">
<label>🏷️ 名称</label>
<input type="text" id="baseName" placeholder="招商银行">
</div>
<div class="input-group">
<label>💰 入池价格</label>
<input type="number" id="basePrice" step="0.01" placeholder="价格">
</div>
<button id="addBaseStockBtn" class="primary">➕ 添加个股</button>
<div style="font-size:0.7rem; color:#5b6e8c; margin-left: auto;">⚡ 添加后默认当前价=入池价,最高价=入池价</div>
</div>
<div class="filter-bar">
<div class="filter-group">
<label>📅 时间段筛选:</label>
<input type="date" id="baseStartDate">
<span>—</span>
<input type="date" id="baseEndDate">
<button id="applyBaseFilterBtn" class="small" style="background:#3b82f6;">筛选入池情况</button>
</div>
<div class="tooltip-info" style="font-size:0.7rem; color:#475569;">* 点击筛选后显示该区间入池个股,统计同步更新</div>
</div>
<div class="table-wrapper">
<table id="baseTable">
<thead>
<tr><th>代码</th><th>名称</th><th>入池时间</th><th>入池价格</th><th>最大涨幅</th><th>持仓时间(天)</th><th>操作</th></tr>
</thead>
<tbody id="baseTbody">
<tr><td colspan="7" style="text-align:center">暂无数据,请添加个股</td></tr>
</tbody>
</table>
</div>
<div class="stats-bar" id="baseStats">
<div class="stat">📊 入池总数:<span id="baseTotalCount">0</span></div>
<div class="stat">🏆 盈利胜率:<span id="baseWinRate">0%</span></div>
<div class="stat">📈 综合收益率:<span id="baseAvgReturn">0%</span></div>
</div>
</div>
<!-- 高级版模块:拉萨二哥主导 -->
<div class="card">
<div class="card-header">
<div style="display: flex; align-items: center; gap: 12px;">
<h2>⚡ 拉萨二哥主导 · 高级版</h2>
<div class="badge-version" style="background:#e0f2fe; color:#0c4a6e;">增强分析|动态调价</div>
</div>
<button id="resetAdvancedFilterBtn" class="small" style="background:#e2e8f0; color:#1e293b;">🔄 重置筛选</button>
</div>
<div class="add-panel">
<div class="input-group">
<label>📟 代码</label>
<input type="text" id="advCode" placeholder="例: 000858">
</div>
<div class="input-group">
<label>🏷️ 名称</label>
<input type="text" id="advName" placeholder="五粮液">
</div>
<div class="input-group">
<label>💰 入池价格</label>
<input type="number" id="advPrice" step="0.01" placeholder="价格">
</div>
<button id="addAdvStockBtn" class="primary">✨ 高级添加</button>
<div style="font-size:0.7rem; color:#5b6e8c; margin-left: auto;">🎯 可随时修改当前价/最高价,动态计算涨幅&胜率</div>
</div>
<div class="filter-bar">
<div class="filter-group">
<label>📅 时间段筛选:</label>
<input type="date" id="advStartDate">
<span>—</span>
<input type="date" id="advEndDate">
<button id="applyAdvFilterBtn" class="small" style="background:#3b82f6;">筛选入池情况</button>
</div>
<div class="tooltip-info" style="font-size:0.7rem;">* 支持股票最高价/当前价独立调节,模拟真实场景</div>
</div>
<div class="table-wrapper">
<table id="advTable">
<thead>
<tr><th>代码</th><th>名称</th><th>入池时间</th><th>入池价格</th><th>最大涨幅</th><th>持仓时间(天)</th><th>操作</th></tr>
</thead>
<tbody id="advTbody">
<tr><td colspan="7" style="text-align:center">暂无数据,请添加个股</td></tr>
</tbody>
</table>
</div>
<div class="stats-bar" id="advStats">
<div class="stat">📊 入池总数:<span id="advTotalCount">0</span></div>
<div class="stat">🏆 盈利胜率:<span id="advWinRate">0%</span></div>
<div class="stat">📈 综合收益率:<span id="advAvgReturn">0%</span></div>
</div>
</div>
<footer>💡 提示:点击「设置当前价/最高价」模拟行情变动 → 最大涨幅基于最高价计算,盈利基于当前价,持仓天数随系统日期动态更新。</footer>
</div>
<script>
// ---------- 基础版数据管理 ----------
let baseStocks = []; // 每个元素: { id, code, name, entryTime, entryPrice, currentPrice, highPrice }
let baseFilter = { start: null, end: null }; // 存储日期范围字符串 'YYYY-MM-DD'
// 高级版数据
let advancedStocks = [];
let advFilter = { start: null, end: null };
// ================= 辅助函数 =================
// 格式化日期时间 (入池时间显示)
function formatDateTime(dateObj) {
let d = new Date(dateObj);
let year = d.getFullYear();
let month = String(d.getMonth() + 1).padStart(2, '0');
let day = String(d.getDate()).padStart(2, '0');
let hour = String(d.getHours()).padStart(2, '0');
let minute = String(d.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hour}:${minute}`;
}
// 计算持仓天数 (基于当前系统时间 和 entryTime)
function calcHoldDays(entryTimeStr) {
const entry = new Date(entryTimeStr);
const now = new Date();
const diffTime = Math.abs(now - entry);
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
return diffDays;
}
// 最大涨幅百分比 (基于历史最高价 vs 入池价)
function calcMaxGainPercent(entryPrice, highPrice) {
if (!entryPrice || entryPrice <= 0) return 0;
let gain = ((highPrice - entryPrice) / entryPrice) * 100;
return gain > 0 ? gain : 0;
}
// 当前收益率 (基于当前价)
function currReturnPercent(entryPrice, currentPrice) {
if (!entryPrice || entryPrice <= 0) return 0;
return ((currentPrice - entryPrice) / entryPrice) * 100;
}
// 筛选股票 (根据入池时间是否在区间内)
function filterStocksByDate(stocks, startDate, endDate) {
if (!startDate && !endDate) return [...stocks];
return stocks.filter(stock => {
let entryDateStr = stock.entryTime.split(' ')[0]; // YYYY-MM-DD
if (startDate && entryDateStr < startDate) return false;
if (endDate && entryDateStr > endDate) return false;
return true;
});
}
// 渲染基础版表格 & 统计 (基于当前筛选)
function renderBase() {
let filtered = filterStocksByDate(baseStocks, baseFilter.start, baseFilter.end);
const tbody = document.getElementById('baseTbody');
if (filtered.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center">📭 暂无符合条件个股,请添加或调整筛选</td></tr>';
document.getElementById('baseTotalCount').innerText = '0';
document.getElementById('baseWinRate').innerText = '0%';
document.getElementById('baseAvgReturn').innerText = '0%';
return;
}
let html = '';
let totalReturnSum = 0;
let winCount = 0;
filtered.forEach(stock => {
const holdDays = calcHoldDays(stock.entryTime);
const maxGain = calcMaxGainPercent(stock.entryPrice, stock.highPrice);
const currRet = currReturnPercent(stock.entryPrice, stock.currentPrice);
totalReturnSum += currRet;
if (currRet > 0) winCount++;
html += `<tr>
<td style="font-weight:500;">${escapeHtml(stock.code)}</td>
<td>${escapeHtml(stock.name)}</td>
<td>${escapeHtml(stock.entryTime)}</td>
<td>${stock.entryPrice.toFixed(2)}</td>
<td class="${maxGain > 0 ? 'profit-positive' : ''}">${maxGain.toFixed(2)}%</td>
<td>${holdDays} 天</td>
<td class="actions">
<button class="small" data-type="base" data-id="${stock.id}" data-action="setPrice">💰 设当前价</button>
<button class="small warning" data-type="base" data-id="${stock.id}" data-action="setHigh">📈 设最高价</button>
<button class="small danger" data-type="base" data-id="${stock.id}" data-action="del">🗑️ 删除</button>
</td>
</tr>`;
});
tbody.innerHTML = html;
const totalCount = filtered.length;
const winRate = totalCount === 0 ? 0 : (winCount / totalCount) * 100;
const avgReturn = totalCount === 0 ? 0 : (totalReturnSum / totalCount);
document.getElementById('baseTotalCount').innerText = totalCount;
document.getElementById('baseWinRate').innerText = winRate.toFixed(2) + '%';
document.getElementById('baseAvgReturn').innerText = (avgReturn > 0 ? '+' : '') + avgReturn.toFixed(2) + '%';
// 绑定动态按钮事件 (基础版)
document.querySelectorAll('#baseTbody button').forEach(btn => {
btn.removeEventListener('click', handleBaseAction);
btn.addEventListener('click', handleBaseAction);
});
}
function handleBaseAction(e) {
const btn = e.currentTarget;
const stockId = parseInt(btn.dataset.id);
const action = btn.dataset.action;
const stock = baseStocks.find(s => s.id === stockId);
if (!stock) return;
if (action === 'del') {
if (confirm(`确定删除 ${stock.name}(${stock.code}) 吗?`)) {
baseStocks = baseStocks.filter(s => s.id !== stockId);
renderBase();
}
} else if (action === 'setPrice') {
let newPrice = parseFloat(prompt(`请输入 ${stock.name} 的【最新当前价】`, stock.currentPrice.toFixed(2)));
if (!isNaN(newPrice) && newPrice > 0) {
stock.currentPrice = newPrice;
renderBase();
} else { alert("请输入有效价格 (>0)"); }
} else if (action === 'setHigh') {
let newHigh = parseFloat(prompt(`设定 ${stock.name} 的【历史最高价】`, stock.highPrice.toFixed(2)));
if (!isNaN(newHigh) && newHigh > 0) {
stock.highPrice = newHigh;
renderBase();
} else { alert("请输入有效最高价"); }
}
}
// 高级版渲染 逻辑完全一致,但数据源 advancedStocks
function renderAdvanced() {
let filtered = filterStocksByDate(advancedStocks, advFilter.start, advFilter.end);
const tbody = document.getElementById('advTbody');
if (filtered.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center">✨ 暂无个股,高级版支持灵活调仓</td></tr>';
document.getElementById('advTotalCount').innerText = '0';
document.getElementById('advWinRate').innerText = '0%';
document.getElementById('advAvgReturn').innerText = '0%';
return;
}
let html = '';
let totalReturnSum = 0;
let winCount = 0;
filtered.forEach(stock => {
const holdDays = calcHoldDays(stock.entryTime);
const maxGain = calcMaxGainPercent(stock.entryPrice, stock.highPrice);
const currRet = currReturnPercent(stock.entryPrice, stock.currentPrice);
totalReturnSum += currRet;
if (currRet > 0) winCount++;
html += `<tr>
<td style="font-weight:500;">${escapeHtml(stock.code)}</td>
<td>${escapeHtml(stock.name)}</td>
<td>${escapeHtml(stock.entryTime)}</td>
<td>${stock.entryPrice.toFixed(2)}</td>
<td class="${maxGain > 0 ? 'profit-positive' : ''}">${maxGain.toFixed(2)}%</td>
<td>${holdDays} 天</td>
<td class="actions">
<button class="small" data-type="adv" data-id="${stock.id}" data-action="setPrice">💰 设当前价</button>
<button class="small warning" data-type="adv" data-id="${stock.id}" data-action="setHigh">📈 设最高价</button>
<button class="small danger" data-type="adv" data-id="${stock.id}" data-action="del">🗑️ 删除</button>
</td>
</tr>`;
});
tbody.innerHTML = html;
const totalCount = filtered.length;
const winRate = totalCount === 0 ? 0 : (winCount / totalCount) * 100;
const avgReturn = totalCount === 0 ? 0 : (totalReturnSum / totalCount);
document.getElementById('advTotalCount').innerText = totalCount;
document.getElementById('advWinRate').innerText = winRate.toFixed(2) + '%';
document.getElementById('advAvgReturn').innerText = (avgReturn > 0 ? '+' : '') + avgReturn.toFixed(2) + '%';
// 绑定高级版按钮事件
document.querySelectorAll('#advTbody button').forEach(btn => {
btn.removeEventListener('click', handleAdvAction);
btn.addEventListener('click', handleAdvAction);
});
}
function handleAdvAction(e) {
const btn = e.currentTarget;
const stockId = parseInt(btn.dataset.id);
const action = btn.dataset.action;
const stock = advancedStocks.find(s => s.id === stockId);
if (!stock) return;
if (action === 'del') {
if (confirm(`确定从高级池移除 ${stock.name}(${stock.code}) 吗?`)) {
advancedStocks = advancedStocks.filter(s => s.id !== stockId);
renderAdvanced();
}
} else if (action === 'setPrice') {
let newPrice = parseFloat(prompt(`设置【当前价】用于计算盈利胜率`, stock.currentPrice.toFixed(2)));
if (!isNaN(newPrice) && newPrice > 0) {
stock.currentPrice = newPrice;
renderAdvanced();
} else alert("无效价格");
} else if (action === 'setHigh') {
let newHigh = parseFloat(prompt(`设置【历史最高价】决定最大涨幅`, stock.highPrice.toFixed(2)));
if (!isNaN(newHigh) && newHigh > 0) {
stock.highPrice = newHigh;
renderAdvanced();
} else alert("无效最高价");
}
}
// 添加基础版个股
function addBaseStock(code, name, price) {
if (!code.trim() || !name.trim() || isNaN(price) || price <= 0) {
alert("请完整填写:代码/名称/有效价格");
return false;
}
const now = new Date();
const entryTimeStr = formatDateTime(now);
const entryPriceVal = parseFloat(price);
const newId = Date.now() + Math.random() * 1000 + baseStocks.length;
baseStocks.push({
id: newId,
code: code.trim(),
name: name.trim(),
entryTime: entryTimeStr,
entryPrice: entryPriceVal,
currentPrice: entryPriceVal, // 初始当前价 = 入池价
highPrice: entryPriceVal // 初始最高价 = 入池价
});
renderBase();
return true;
}
function addAdvancedStock(code, name, price) {
if (!code.trim() || !name.trim() || isNaN(price) || price <= 0) {
alert("请填写完整:代码/名称/有效价格");
return false;
}
const now = new Date();
const entryTimeStr = formatDateTime(now);
const entryPriceVal = parseFloat(price);
const newId = Date.now() + Math.random() * 1000 + advancedStocks.length;
advancedStocks.push({
id: newId,
code: code.trim(),
name: name.trim(),
entryTime: entryTimeStr,
entryPrice: entryPriceVal,
currentPrice: entryPriceVal,
highPrice: entryPriceVal
});
renderAdvanced();
return true;
}
// 筛选事件绑定
function setupFilters() {
// 基础版 筛选
const baseApply = document.getElementById('applyBaseFilterBtn');
const baseStart = document.getElementById('baseStartDate');
const baseEnd = document.getElementById('baseEndDate');
const resetBase = document.getElementById('resetBaseFilterBtn');
baseApply.addEventListener('click', () => {
baseFilter.start = baseStart.value || null;
baseFilter.end = baseEnd.value || null;
renderBase();
});
resetBase.addEventListener('click', () => {
baseStart.value = '';
baseEnd.value = '';
baseFilter.start = null;
baseFilter.end = null;
renderBase();
});
// 高级版筛选
const advApply = document.getElementById('applyAdvFilterBtn');
const advStart = document.getElementById('advStartDate');
const advEnd = document.getElementById('advEndDate');
const resetAdv = document.getElementById('resetAdvancedFilterBtn');
advApply.addEventListener('click', () => {
advFilter.start = advStart.value || null;
advFilter.end = advEnd.value || null;
renderAdvanced();
});
resetAdv.addEventListener('click', () => {
advStart.value = '';
advEnd.value = '';
advFilter.start = null;
advFilter.end = null;
renderAdvanced();
});
}
// 添加按钮事件
function bindAddButtons() {
document.getElementById('addBaseStockBtn').addEventListener('click', () => {
const code = document.getElementById('baseCode').value;
const name = document.getElementById('baseName').value;
const price = document.getElementById('basePrice').value;
if (addBaseStock(code, name, price)) {
document.getElementById('baseCode').value = '';
document.getElementById('baseName').value = '';
document.getElementById('basePrice').value = '';
}
});
document.getElementById('addAdvStockBtn').addEventListener('click', () => {
const code = document.getElementById('advCode').value;
const name = document.getElementById('advName').value;
const price = document.getElementById('advPrice').value;
if (addAdvancedStock(code, name, price)) {
document.getElementById('advCode').value = '';
document.getElementById('advName').value = '';
document.getElementById('advPrice').value = '';
}
});
}
function escapeHtml(str) {
if (!str) return '';
return str.replace(/[&<>]/g, function(m) {
if (m === '&') return '&';
if (m === '<') return '<';
if (m === '>') return '>';
return m;
});
}
// 演示示例数据 (让界面有初始参考)
function initSampleData() {
// 基础版示例
const sampleBase = [
{ code: '000001', name: '平安银行', entryPrice: 12.5, entryTimeAdj: -3, currentPriceAdj: 0.15, highAdj: 0.28 },
{ code: '600519', name: '贵州茅台', entryPrice: 1680, entryTimeAdj: -8, currentPriceAdj: 0.05, highAdj: 0.12 }
];
sampleBase.forEach((item, idx) => {
const entryDate = new Date();
entryDate.setDate(entryDate.getDate() + item.entryTimeAdj);
const entryTimeStr = formatDateTime(entryDate);
const entryPr = item.entryPrice;
const currentPrice = entryPr * (1 + item.currentPriceAdj);
const highPrice = entryPr * (1 + item.highAdj);
baseStocks.push({
id: 1000 + idx,
code: item.code,
name: item.name,
entryTime: entryTimeStr,
entryPrice: entryPr,
currentPrice: currentPrice,
highPrice: highPrice,
});
});
// 高级版示例
const sampleAdv = [
{ code: '300750', name: '宁德时代', entryPrice: 185, entryTimeAdj: -5, currentPriceAdj: 0.08, highAdj: 0.22 },
{ code: '002594', name: '比亚迪', entryPrice: 245, entryTimeAdj: -2, currentPriceAdj: -0.03, highAdj: 0.05 }
];
sampleAdv.forEach((item, idx) => {
const entryDate = new Date();
entryDate.setDate(entryDate.getDate() + item.entryTimeAdj);
const entryTimeStr = formatDateTime(entryDate);
const entryPr = item.entryPrice;
const currentPrice = entryPr * (1 + item.currentPriceAdj);
const highPrice = entryPr * (1 + item.highAdj);
advancedStocks.push({
id: 2000 + idx,
code: item.code,
name: item.name,
entryTime: entryTimeStr,
entryPrice: entryPr,
currentPrice: currentPrice,
highPrice: highPrice,
});
});
renderBase();
renderAdvanced();
}
// 初始化
function init() {
bindAddButtons();
setupFilters();
initSampleData();
// 额外加上全局日期变化时,持仓天数自动更新 (每次渲染即基于最新时间)
setInterval(() => {
renderBase();
renderAdvanced();
}, 60000); // 每分钟重绘,保证“持仓时间”动态更新
}
init();
</script>
</body>
</html>
👤 用户:
仅作为展示股票池,不需要操作和添加
💭 思考过程
我们需要修改需求: 用户要求 "仅作为展示股票池,不需要操作和添加"。基于之前的html原型,去掉所有添加按钮、编辑/设置价格/删除等操作功能,只是静态展示股票池示例数据。同时保留表格字段、统计模块、时间段筛选按钮?但题目仅要求展示股票池,不需要操作,可能筛选还可以保留但要看是否属于操作。但用户说"不需要操作和添加",避免任何交互式增加或修改。最好直接展示固定的示例数据,筛选功能可能也属于操作,可以保留为纯前端筛选(仅展示筛选结果),但不允许添加/删除/修改个股,这应该可以。为了稳妥,去掉所有添加面板,去掉每个股票对应的操作按钮(设当前价、设最高价、删除)。只保留展示表格、统计汇总和时间段筛选按钮(筛选仅影响展示的个股)。另外,要保证没有任何可编辑或添加的功能。而且两个版本基础版和高级版都保留,但都只用于展示数据。
同时初始示例数据要体现“入池时间、入池价格、最大涨幅、持仓时间”等字段,涨幅和持仓时间是动态计算的(基于当前时间与最高价模拟),由于没有操作,我们需要模拟展示一些股票。最大涨幅根据固定示例数据中的历史最高价或动态生成演示值。但为了简单且不依赖用户输入,使用静态示例数据集,但持仓时间可以用当前日期减去入池时间计算(动态),最大涨幅可以使用一个预设的涨幅,因为不需要用户设置最高价,我们可以直接存储预设的最大涨幅值。不过为了完整性,可保留“最大涨幅”基于演示数据的最高价和入池价计算,但用户不能修改。很好。因为只是展示,所有动作都不需要,也没有"添加个股"的表单。
调整:移除添加面板及相关代码、移除表格中操作列(所有按钮)、移除统计中没有任何按钮绑定。筛选功能保留(时间段筛选),但仍然无法添加或修改。同时基础版和高级版的表头中移除“操作”列。调整表为6列。统计模块保留,动态计算盈利胜率和综合收益率,基于展示的股票,根据当前最新价格(或收益率)。为了演示,我们需要给示例股票设置currentPrice(当前价)以及最高价来计算最大涨幅和当前收益率。这样既符合展示需求,又无添加/删除/修改控件。
另外确保"入池价格"和"当前价格"差异及最大涨幅展示。
需要生成全新html代码,不含任何添加/编辑/删除的交互。但筛选和时间段按钮、重置筛选可保留,因为属于查看功能,用户没有要求完全静止,只是说不需要操作(不能添加个股或修改数据)。所以筛选是合理的。
还要注意"持仓时间"字段:基于入池时间到现在的自然天数,动态更新。
并且满足基础版和高级版卡片样式,两个数据表可以单独展示,最好展示不同的预设数据。基础版展示示例股票池A,高级版展示示例股票池B。
因为不需要添加,但用户可能期望看到已有的股票清单。我需要设计几组预定义股票,涵盖正收益负收益,使统计有意义。
另外字段要求代码、名称、入池时间、入池价格、最大涨幅、持仓时间。高级版相同,高级版标题下可多一项额外信息?但不需要,按要求基础版和高级版字段基本一致,但需求文档里高级版描述多"代码 名称",实际表格已经包含。保持一致即可。
还需要“选择时间段个股入池情况按钮”,即时间段筛选按钮,也要提供。写清楚。
同时注意没有操作按钮后,表格最后一列是筛选相关的按钮位置?需求表里面最后一列是"选择时间段个股入池情况按钮",并不是针对每行,而是全局时间段筛选,我们已经在表格上方做了筛选控件,就不需要每一行再放按钮。原需求中的表格最后一列标题可能是"选择时间段个股入池情况按钮",描述有误,全局按钮更好实现。就这样做。
综上所述,生成新的静态页面,核心展示两个股票池,支持按日期筛选入池个股,统计数据随筛选变化,没有添加、修改、删除功能。动态持仓时间,基于当前日期。预置4-6只股票示例。
编写代码时注意避免所有button或input引发数据修改,只有筛选日期输入框和筛选按钮,重置按钮无副作用。确保无localStorage或用户新增的入口。纯粹展示。
现在按要求生成最终的html。
以下是仅用于展示股票池的静态网页原型,包含基础版和高级版两个模块,支持按时间段筛选查看,但不提供任何添加或编辑股票的操作。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<title>股票池展示 | 拉萨二哥系列 · 只读版</title>
<style>
* {
box-sizing: border-box;
font-family: system-ui, 'Segoe UI', 'Roboto', 'Helvetica Neue', sans-serif;
}
body {
background: #f0f2f5;
margin: 0;
padding: 24px 20px;
}
.container {
max-width: 1500px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 32px;
}
/* 卡片样式 */
.card {
background: #ffffff;
border-radius: 24px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.05);
overflow: hidden;
border: 1px solid #e9ecef;
}
.card-header {
padding: 20px 24px 12px 24px;
border-bottom: 2px solid #f1f3f5;
display: flex;
justify-content: space-between;
align-items: baseline;
flex-wrap: wrap;
gap: 12px;
background: #fefefe;
}
.card-header h2 {
margin: 0;
font-size: 1.6rem;
font-weight: 600;
background: linear-gradient(135deg, #1e2a3e, #0f172a);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
letter-spacing: -0.3px;
}
.badge-version {
background: #eef2ff;
padding: 4px 12px;
border-radius: 40px;
font-size: 0.75rem;
font-weight: 500;
color: #1e40af;
}
.filter-bar {
padding: 14px 24px;
background: #ffffff;
border-bottom: 1px solid #edf2f7;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 15px;
}
.filter-group {
display: flex;
align-items: center;
gap: 8px;
background: #f1f5f9;
padding: 5px 14px;
border-radius: 40px;
}
.filter-group label {
font-size: 0.75rem;
font-weight: 500;
color: #1e293b;
}
.filter-group input {
border: 1px solid #cbd5e1;
border-radius: 20px;
padding: 6px 10px;
font-size: 0.75rem;
background: white;
}
button {
background: #1e293b;
border: none;
padding: 6px 16px;
border-radius: 40px;
font-weight: 500;
font-size: 0.75rem;
color: white;
cursor: pointer;
transition: 0.2s;
display: inline-flex;
align-items: center;
gap: 6px;
}
button.primary {
background: #3b82f6;
}
button.primary:hover {
background: #2563eb;
}
button.reset {
background: #e2e8f0;
color: #1e293b;
}
button.reset:hover {
background: #cbd5e1;
}
.stats-bar {
padding: 12px 24px;
background: #fefce8;
border-top: 1px solid #f1f3f5;
display: flex;
gap: 32px;
flex-wrap: wrap;
font-weight: 500;
font-size: 0.9rem;
}
.stat {
background: white;
padding: 5px 16px;
border-radius: 40px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.stat span:first-child {
color: #475569;
}
.stat span:last-child {
font-weight: 700;
color: #0f172a;
margin-left: 8px;
}
.table-wrapper {
overflow-x: auto;
padding: 0 0 12px 0;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.8rem;
min-width: 750px;
}
th {
text-align: left;
padding: 14px 12px;
background: #f8fafc;
color: #1e293b;
font-weight: 600;
border-bottom: 1px solid #e2e8f0;
}
td {
padding: 12px;
border-bottom: 1px solid #f1f5f9;
vertical-align: middle;
}
.profit-positive {
color: #10b981;
font-weight: 600;
}
.profit-negative {
color: #ef4444;
}
.info-note {
font-size: 0.7rem;
color: #5b6e8c;
margin-left: 16px;
}
footer {
text-align: center;
font-size: 0.7rem;
color: #94a3b8;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<!-- 基础版模块:拉萨二哥精选 · 只读展示 -->
<div class="card">
<div class="card-header">
<div style="display: flex; align-items: center; gap: 12px;">
<h2>📋 拉萨二哥精选 · 基础版</h2>
<div class="badge-version">展示池 · 只读</div>
</div>
<button id="resetBaseFilterBtn" class="reset">🔄 重置筛选</button>
</div>
<div class="filter-bar">
<div class="filter-group">
<label>📅 时间段筛选 (入池时间):</label>
<input type="date" id="baseStartDate">
<span>—</span>
<input type="date" id="baseEndDate">
<button id="applyBaseFilterBtn" class="primary">🔍 筛选个股入池情况</button>
</div>
<div class="info-note">* 筛选后仅显示该区间内入池的股票,统计数据同步更新</div>
</div>
<div class="table-wrapper">
<table>
<thead>
<tr><th>代码</th><th>名称</th><th>入池时间</th><th>入池价格(元)</th><th>最大涨幅(%)</th><th>持仓时间(天)</th></tr>
</thead>
<tbody id="baseTbody">
<tr><td colspan="6" style="text-align:center">加载数据中...</td></tr>
</tbody>
</table>
</div>
<div class="stats-bar" id="baseStats">
<div class="stat">📊 入池总数:<span id="baseTotalCount">0</span></div>
<div class="stat">🏆 盈利胜率:<span id="baseWinRate">0%</span></div>
<div class="stat">📈 综合收益率:<span id="baseAvgReturn">0%</span></div>
</div>
</div>
<!-- 高级版模块:拉萨二哥主导 · 只读展示 -->
<div class="card">
<div class="card-header">
<div style="display: flex; align-items: center; gap: 12px;">
<h2>⚡ 拉萨二哥主导 · 高级版</h2>
<div class="badge-version" style="background:#e0f2fe; color:#0c4a6e;">增强数据池 · 只读</div>
</div>
<button id="resetAdvancedFilterBtn" class="reset">🔄 重置筛选</button>
</div>
<div class="filter-bar">
<div class="filter-group">
<label>📅 时间段筛选 (入池时间):</label>
<input type="date" id="advStartDate">
<span>—</span>
<input type="date" id="advEndDate">
<button id="applyAdvFilterBtn" class="primary">🔍 筛选个股入池情况</button>
</div>
<div class="info-note">* 持仓天数每日自动更新,最大涨幅基于历史最高价计算</div>
</div>
<div class="table-wrapper">
<table>
<thead>
<tr><th>代码</th><th>名称</th><th>入池时间</th><th>入池价格(元)</th><th>最大涨幅(%)</th><th>持仓时间(天)</th></tr>
</thead>
<tbody id="advTbody">
<tr><td colspan="6" style="text-align:center">加载数据中...</td></tr>
</tbody>
</table>
</div>
<div class="stats-bar" id="advStats">
<div class="stat">📊 入池总数:<span id="advTotalCount">0</span></div>
<div class="stat">🏆 盈利胜率:<span id="advWinRate">0%</span></div>
<div class="stat">📈 综合收益率:<span id="advAvgReturn">0%</span></div>
</div>
</div>
<footer>📌 说明:本页面仅为股票池展示,不包含添加/编辑/删除操作。最大涨幅及盈利胜率根据预设数据动态计算,持仓天数基于当前日期与入池时间自动更新。</footer>
</div>
<script>
// --------------------------------------------------------------
// 基础版 展示数据 (固定股票池,无任何修改/添加入口)
// 结构: 代码,名称,入池时间字符串,入池价格,当前价格,历史最高价
// 最大涨幅 = (历史最高价 - 入池价)/入池价 * 100 , 收益率 = (当前价-入池价)/入池价*100
// --------------------------------------------------------------
const baseStockData = [
{
code: "000858",
name: "五粮液",
entryDateStr: "2025-03-10 09:35:00", // 入池时间
entryPrice: 148.50,
currentPrice: 168.30, // 最新价
highPrice: 175.20 // 历史最高价
},
{
code: "600519",
name: "贵州茅台",
entryDateStr: "2025-02-18 14:20:00",
entryPrice: 1720.00,
currentPrice: 1690.00,
highPrice: 1850.00
},
{
code: "300750",
name: "宁德时代",
entryDateStr: "2025-04-01 10:15:00",
entryPrice: 195.00,
currentPrice: 212.50,
highPrice: 228.00
},
{
code: "002415",
name: "海康威视",
entryDateStr: "2025-03-25 13:45:00",
entryPrice: 32.80,
currentPrice: 31.20,
highPrice: 34.10
},
{
code: "601318",
name: "中国平安",
entryDateStr: "2025-01-20 11:05:00",
entryPrice: 42.60,
currentPrice: 46.80,
highPrice: 48.90
},
{
code: "000001",
name: "平安银行",
entryDateStr: "2025-04-12 09:58:00",
entryPrice: 11.20,
currentPrice: 12.05,
highPrice: 12.95
}
];
// 高级版 展示数据 (不同的股票池)
const advancedStockData = [
{
code: "002594",
name: "比亚迪",
entryDateStr: "2025-03-05 10:30:00",
entryPrice: 235.00,
currentPrice: 262.50,
highPrice: 285.00
},
{
code: "688981",
name: "中芯国际",
entryDateStr: "2025-02-10 14:10:00",
entryPrice: 52.30,
currentPrice: 48.90,
highPrice: 59.80
},
{
code: "300059",
name: "东方财富",
entryDateStr: "2025-04-08 11:20:00",
entryPrice: 22.50,
currentPrice: 24.30,
highPrice: 27.50
},
{
code: "600036",
name: "招商银行",
entryDateStr: "2025-01-15 09:45:00",
entryPrice: 34.20,
currentPrice: 38.60,
highPrice: 40.20
},
{
code: "002475",
name: "立讯精密",
entryDateStr: "2025-03-28 13:20:00",
entryPrice: 31.60,
currentPrice: 33.80,
highPrice: 36.10
},
{
code: "601899",
name: "紫金矿业",
entryDateStr: "2025-04-18 15:00:00",
entryPrice: 17.80,
currentPrice: 19.20,
highPrice: 20.50
}
];
// ---------- 通用工具函数 ----------
// 解析入池时间字符串 (格式 "YYYY-MM-DD HH:MM:SS")
function parseEntryDate(dateTimeStr) {
return new Date(dateTimeStr.replace(' ', 'T'));
}
// 计算持仓天数 (基于当前系统时间)
function calcHoldDays(entryDateTimeStr) {
const entry = parseEntryDate(entryDateTimeStr);
const now = new Date();
const diffMs = now - entry;
const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
return days >= 0 ? days : 0;
}
// 计算最大涨幅 (%)
function calcMaxGain(entryPrice, highPrice) {
if (!entryPrice || entryPrice <= 0) return 0;
const gain = ((highPrice - entryPrice) / entryPrice) * 100;
return gain > 0 ? gain : 0;
}
// 当前收益率 (基于当前价格)
function calcCurrentReturn(entryPrice, currentPrice) {
if (!entryPrice || entryPrice <= 0) return 0;
return ((currentPrice - entryPrice) / entryPrice) * 100;
}
// 筛选股票池 (根据入池时间的日期部分是否在 [startDate, endDate] 区间内)
function filterStocksByDateRange(stocks, startDate, endDate) {
if (!startDate && !endDate) return [...stocks];
return stocks.filter(stock => {
// 提取日期部分 "YYYY-MM-DD"
const datePart = stock.entryDateStr.split(' ')[0];
if (startDate && datePart < startDate) return false;
if (endDate && datePart > endDate) return false;
return true;
});
}
// 渲染基础版表格 + 统计 (只读,无任何操作按钮)
function renderBase() {
const startInput = document.getElementById('baseStartDate');
const endInput = document.getElementById('baseEndDate');
const startDate = startInput.value || null;
const endDate = endInput.value || null;
let filtered = filterStocksByDateRange(baseStockData, startDate, endDate);
const tbody = document.getElementById('baseTbody');
if (filtered.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center; padding: 32px;">📭 该时间段内暂无入池股票,请调整筛选范围</td></tr>';
document.getElementById('baseTotalCount').innerText = '0';
document.getElementById('baseWinRate').innerText = '0%';
document.getElementById('baseAvgReturn').innerText = '0%';
return;
}
let totalReturnSum = 0; // 累计收益率
let winCount = 0; // 盈利个数 (当前收益率 > 0)
let htmlRows = '';
for (let stock of filtered) {
const holdDays = calcHoldDays(stock.entryDateStr);
const maxGain = calcMaxGain(stock.entryPrice, stock.highPrice);
const currRet = calcCurrentReturn(stock.entryPrice, stock.currentPrice);
totalReturnSum += currRet;
if (currRet > 0) winCount++;
const maxGainClass = maxGain > 0 ? 'profit-positive' : '';
const formattedMaxGain = maxGain.toFixed(2) + '%';
const holdDaysText = holdDays + ' 天';
htmlRows += `<tr>
<td style="font-weight:500;">${escapeHtml(stock.code)}</td>
<td>${escapeHtml(stock.name)}</td>
<td>${escapeHtml(stock.entryDateStr)}</td>
<td>${stock.entryPrice.toFixed(2)}</td>
<td class="${maxGainClass}">${formattedMaxGain}</td>
<td>${holdDaysText}</td>
</tr>`;
}
tbody.innerHTML = htmlRows;
const totalCount = filtered.length;
const winRate = totalCount === 0 ? 0 : (winCount / totalCount) * 100;
const avgReturn = totalCount === 0 ? 0 : (totalReturnSum / totalCount);
document.getElementById('baseTotalCount').innerText = totalCount;
document.getElementById('baseWinRate').innerText = winRate.toFixed(2) + '%';
const avgReturnStr = (avgReturn > 0 ? '+' : '') + avgReturn.toFixed(2) + '%';
document.getElementById('baseAvgReturn').innerText = avgReturnStr;
}
// 渲染高级版表格 + 统计
function renderAdvanced() {
const startInput = document.getElementById('advStartDate');
const endInput = document.getElementById('advEndDate');
const startDate = startInput.value || null;
const endDate = endInput.value || null;
let filtered = filterStocksByDateRange(advancedStockData, startDate, endDate);
const tbody = document.getElementById('advTbody');
if (filtered.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center; padding: 32px;">✨ 该时间段内暂无股票,请调整筛选日期</td></tr>';
document.getElementById('advTotalCount').innerText = '0';
document.getElementById('advWinRate').innerText = '0%';
document.getElementById('advAvgReturn').innerText = '0%';
return;
}
let totalReturnSum = 0;
let winCount = 0;
let htmlRows = '';
for (let stock of filtered) {
const holdDays = calcHoldDays(stock.entryDateStr);
const maxGain = calcMaxGain(stock.entryPrice, stock.highPrice);
const currRet = calcCurrentReturn(stock.entryPrice, stock.currentPrice);
totalReturnSum += currRet;
if (currRet > 0) winCount++;
const maxGainClass = maxGain > 0 ? 'profit-positive' : '';
htmlRows += `<tr>
<td style="font-weight:500;">${escapeHtml(stock.code)}</td>
<td>${escapeHtml(stock.name)}</td>
<td>${escapeHtml(stock.entryDateStr)}</td>
<td>${stock.entryPrice.toFixed(2)}</td>
<td class="${maxGainClass}">${maxGain.toFixed(2)}%</td>
<td>${holdDays} 天</td>
</tr>`;
}
tbody.innerHTML = htmlRows;
const totalCount = filtered.length;
const winRate = totalCount === 0 ? 0 : (winCount / totalCount) * 100;
const avgReturn = totalCount === 0 ? 0 : (totalReturnSum / totalCount);
document.getElementById('advTotalCount').innerText = totalCount;
document.getElementById('advWinRate').innerText = winRate.toFixed(2) + '%';
const avgReturnStr = (avgReturn > 0 ? '+' : '') + avgReturn.toFixed(2) + '%';
document.getElementById('advAvgReturn').innerText = avgReturnStr;
}
// 基础版筛选事件及重置
function bindFilterEvents() {
const baseApply = document.getElementById('applyBaseFilterBtn');
const baseReset = document.getElementById('resetBaseFilterBtn');
const baseStart = document.getElementById('baseStartDate');
const baseEnd = document.getElementById('baseEndDate');
baseApply.addEventListener('click', () => {
renderBase();
});
baseReset.addEventListener('click', () => {
baseStart.value = '';
baseEnd.value = '';
renderBase();
});
const advApply = document.getElementById('applyAdvFilterBtn');
const advReset = document.getElementById('resetAdvancedFilterBtn');
const advStart = document.getElementById('advStartDate');
const advEnd = document.getElementById('advEndDate');
advApply.addEventListener('click', () => {
renderAdvanced();
});
advReset.addEventListener('click', () => {
advStart.value = '';
advEnd.value = '';
renderAdvanced();
});
}
// 简单的防XSS
function escapeHtml(str) {
if (!str) return '';
return str.replace(/[&<>]/g, function(m) {
if (m === '&') return '&';
if (m === '<') return '<';
if (m === '>') return '>';
return m;
});
}
// 初始化所有展示,并启动定时器更新持仓天数(每日效果)
function initDisplay() {
renderBase();
renderAdvanced();
// 每分钟重新渲染一次,使持仓天数动态刷新(不改变任何数据,仅更新时间字段)
setInterval(() => {
renderBase();
renderAdvanced();
}, 60000); // 60秒自动刷新,保证"持仓时间"基于最新日期
}
// 页面加载执行
bindFilterEvents();
initDisplay();
</script>
</body>
</html>
👤 用户:
时间筛选改为单日期筛选,不是日期区间
💭 思考过程
我们需理解用户需求:当前网页原型中的时间筛选是日期区间(开始日期和结束日期)。用户要求改为单日期筛选,即只选择一个日期,筛选出入池时间等于该日期的个股(或入池时间在某一天?常见逻辑是筛选出指定日期当天入池的个股)。根据需求文档中的“选择时间段个股入池情况按钮”原来可能是区间,现在改为单日期。需要调整UI:将两个日期输入框改成一个日期选择器,筛选逻辑改为匹配入池时间的日期部分等于选定日期。
需修改HTML和JS。需要输出完整代码,保证功能正确。
我将修改两个模块(基础版和高级版)的筛选部分:将“开始日期—结束日期”替换为单个“入池日期”选择,筛选按钮文字可保持不变或改为“筛选”。重置按钮清空该日期。
同时修改JS中的筛选函数,filterStocksByDateRange改为按单日期匹配(精确匹配日期字符串)。保持其他统计和展示不变。
重新生成完整HTML代码,保留只读展示特性,无添加编辑功能。
以下是改为单日期筛选的股票池展示页面。您可以选择一个具体日期,筛选出入池时间等于该日期的个股,统计数据同步更新。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<title>股票池展示 | 拉萨二哥系列 · 单日期筛选版</title>
<style>
* {
box-sizing: border-box;
font-family: system-ui, 'Segoe UI', 'Roboto', 'Helvetica Neue', sans-serif;
}
body {
background: #f0f2f5;
margin: 0;
padding: 24px 20px;
}
.container {
max-width: 1500px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 32px;
}
/* 卡片样式 */
.card {
background: #ffffff;
border-radius: 24px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.05);
overflow: hidden;
border: 1px solid #e9ecef;
}
.card-header {
padding: 20px 24px 12px 24px;
border-bottom: 2px solid #f1f3f5;
display: flex;
justify-content: space-between;
align-items: baseline;
flex-wrap: wrap;
gap: 12px;
background: #fefefe;
}
.card-header h2 {
margin: 0;
font-size: 1.6rem;
font-weight: 600;
background: linear-gradient(135deg, #1e2a3e, #0f172a);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
letter-spacing: -0.3px;
}
.badge-version {
background: #eef2ff;
padding: 4px 12px;
border-radius: 40px;
font-size: 0.75rem;
font-weight: 500;
color: #1e40af;
}
.filter-bar {
padding: 14px 24px;
background: #ffffff;
border-bottom: 1px solid #edf2f7;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 15px;
}
.filter-group {
display: flex;
align-items: center;
gap: 8px;
background: #f1f5f9;
padding: 5px 14px;
border-radius: 40px;
}
.filter-group label {
font-size: 0.75rem;
font-weight: 500;
color: #1e293b;
}
.filter-group input {
border: 1px solid #cbd5e1;
border-radius: 20px;
padding: 6px 10px;
font-size: 0.75rem;
background: white;
}
button {
background: #1e293b;
border: none;
padding: 6px 16px;
border-radius: 40px;
font-weight: 500;
font-size: 0.75rem;
color: white;
cursor: pointer;
transition: 0.2s;
display: inline-flex;
align-items: center;
gap: 6px;
}
button.primary {
background: #3b82f6;
}
button.primary:hover {
background: #2563eb;
}
button.reset {
background: #e2e8f0;
color: #1e293b;
}
button.reset:hover {
background: #cbd5e1;
}
.stats-bar {
padding: 12px 24px;
background: #fefce8;
border-top: 1px solid #f1f3f5;
display: flex;
gap: 32px;
flex-wrap: wrap;
font-weight: 500;
font-size: 0.9rem;
}
.stat {
background: white;
padding: 5px 16px;
border-radius: 40px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.stat span:first-child {
color: #475569;
}
.stat span:last-child {
font-weight: 700;
color: #0f172a;
margin-left: 8px;
}
.table-wrapper {
overflow-x: auto;
padding: 0 0 12px 0;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.8rem;
min-width: 750px;
}
th {
text-align: left;
padding: 14px 12px;
background: #f8fafc;
color: #1e293b;
font-weight: 600;
border-bottom: 1px solid #e2e8f0;
}
td {
padding: 12px;
border-bottom: 1px solid #f1f5f9;
vertical-align: middle;
}
.profit-positive {
color: #10b981;
font-weight: 600;
}
.profit-negative {
color: #ef4444;
}
.info-note {
font-size: 0.7rem;
color: #5b6e8c;
margin-left: 16px;
}
footer {
text-align: center;
font-size: 0.7rem;
color: #94a3b8;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<!-- 基础版模块:拉萨二哥精选 · 只读展示 · 单日期筛选 -->
<div class="card">
<div class="card-header">
<div style="display: flex; align-items: center; gap: 12px;">
<h2>📋 拉萨二哥精选 · 基础版</h2>
<div class="badge-version">展示池 · 只读</div>
</div>
<button id="resetBaseFilterBtn" class="reset">🔄 重置筛选</button>
</div>
<div class="filter-bar">
<div class="filter-group">
<label>📅 入池日期筛选:</label>
<input type="date" id="baseSingleDate">
<button id="applyBaseFilterBtn" class="primary">🔍 筛选个股</button>
</div>
<div class="info-note">* 选择日期后仅显示当天入池的股票,统计数据同步更新</div>
</div>
<div class="table-wrapper">
<table>
<thead>
<tr><th>代码</th><th>名称</th><th>入池时间</th><th>入池价格(元)</th><th>最大涨幅(%)</th><th>持仓时间(天)</th></tr>
</thead>
<tbody id="baseTbody">
<tr><td colspan="6" style="text-align:center">加载数据中...</td></tr>
</tbody>
</table>
</div>
<div class="stats-bar" id="baseStats">
<div class="stat">📊 入池总数:<span id="baseTotalCount">0</span></div>
<div class="stat">🏆 盈利胜率:<span id="baseWinRate">0%</span></div>
<div class="stat">📈 综合收益率:<span id="baseAvgReturn">0%</span></div>
</div>
</div>
<!-- 高级版模块:拉萨二哥主导 · 只读展示 · 单日期筛选 -->
<div class="card">
<div class="card-header">
<div style="display: flex; align-items: center; gap: 12px;">
<h2>⚡ 拉萨二哥主导 · 高级版</h2>
<div class="badge-version" style="background:#e0f2fe; color:#0c4a6e;">增强数据池 · 只读</div>
</div>
<button id="resetAdvancedFilterBtn" class="reset">🔄 重置筛选</button>
</div>
<div class="filter-bar">
<div class="filter-group">
<label>📅 入池日期筛选:</label>
<input type="date" id="advSingleDate">
<button id="applyAdvFilterBtn" class="primary">🔍 筛选个股</button>
</div>
<div class="info-note">* 持仓天数每日自动更新,最大涨幅基于历史最高价计算</div>
</div>
<div class="table-wrapper">
<table>
<thead>
<tr><th>代码</th><th>名称</th><th>入池时间</th><th>入池价格(元)</th><th>最大涨幅(%)</th><th>持仓时间(天)</th></tr>
</thead>
<tbody id="advTbody">
<tr><td colspan="6" style="text-align:center">加载数据中...</td></tr>
</tbody>
</table>
</div>
<div class="stats-bar" id="advStats">
<div class="stat">📊 入池总数:<span id="advTotalCount">0</span></div>
<div class="stat">🏆 盈利胜率:<span id="advWinRate">0%</span></div>
<div class="stat">📈 综合收益率:<span id="advAvgReturn">0%</span></div>
</div>
</div>
<footer>📌 说明:本页面仅为股票池展示,不包含添加/编辑/删除操作。最大涨幅及盈利胜率根据预设数据动态计算,持仓天数基于当前日期与入池时间自动更新。筛选改为「单日期」模式,仅显示所选日期当天入池的个股。</footer>
</div>
<script>
// --------------------------------------------------------------
// 基础版 展示数据 (固定股票池,无修改/添加入口)
// 包含不同的入池日期,用于演示单日期筛选效果
// --------------------------------------------------------------
const baseStockData = [
{
code: "000858",
name: "五粮液",
entryDateStr: "2025-03-10 09:35:00", // 入池时间 (日期 2025-03-10)
entryPrice: 148.50,
currentPrice: 168.30,
highPrice: 175.20
},
{
code: "600519",
name: "贵州茅台",
entryDateStr: "2025-02-18 14:20:00", // 2025-02-18
entryPrice: 1720.00,
currentPrice: 1690.00,
highPrice: 1850.00
},
{
code: "300750",
name: "宁德时代",
entryDateStr: "2025-04-01 10:15:00", // 2025-04-01
entryPrice: 195.00,
currentPrice: 212.50,
highPrice: 228.00
},
{
code: "002415",
name: "海康威视",
entryDateStr: "2025-03-25 13:45:00", // 2025-03-25
entryPrice: 32.80,
currentPrice: 31.20,
highPrice: 34.10
},
{
code: "601318",
name: "中国平安",
entryDateStr: "2025-01-20 11:05:00", // 2025-01-20
entryPrice: 42.60,
currentPrice: 46.80,
highPrice: 48.90
},
{
code: "000001",
name: "平安银行",
entryDateStr: "2025-04-12 09:58:00", // 2025-04-12
entryPrice: 11.20,
currentPrice: 12.05,
highPrice: 12.95
},
{
code: "002050",
name: "三花智控",
entryDateStr: "2025-03-10 14:30:00", // 与五粮液同一天 2025-03-10
entryPrice: 22.30,
currentPrice: 24.60,
highPrice: 26.40
}
];
// 高级版 展示数据 (不同的股票池)
const advancedStockData = [
{
code: "002594",
name: "比亚迪",
entryDateStr: "2025-03-05 10:30:00", // 2025-03-05
entryPrice: 235.00,
currentPrice: 262.50,
highPrice: 285.00
},
{
code: "688981",
name: "中芯国际",
entryDateStr: "2025-02-10 14:10:00", // 2025-02-10
entryPrice: 52.30,
currentPrice: 48.90,
highPrice: 59.80
},
{
code: "300059",
name: "东方财富",
entryDateStr: "2025-04-08 11:20:00", // 2025-04-08
entryPrice: 22.50,
currentPrice: 24.30,
highPrice: 27.50
},
{
code: "600036",
name: "招商银行",
entryDateStr: "2025-01-15 09:45:00", // 2025-01-15
entryPrice: 34.20,
currentPrice: 38.60,
highPrice: 40.20
},
{
code: "002475",
name: "立讯精密",
entryDateStr: "2025-03-28 13:20:00", // 2025-03-28
entryPrice: 31.60,
currentPrice: 33.80,
highPrice: 36.10
},
{
code: "601899",
name: "紫金矿业",
entryDateStr: "2025-04-18 15:00:00", // 2025-04-18
entryPrice: 17.80,
currentPrice: 19.20,
highPrice: 20.50
},
{
code: "000333",
name: "美的集团",
entryDateStr: "2025-03-05 09:20:00", // 与比亚迪同一天 2025-03-05
entryPrice: 68.50,
currentPrice: 72.30,
highPrice: 76.00
}
];
// ---------- 通用工具函数 ----------
// 解析入池时间字符串 (格式 "YYYY-MM-DD HH:MM:SS")
function parseEntryDate(dateTimeStr) {
return new Date(dateTimeStr.replace(' ', 'T'));
}
// 计算持仓天数 (基于当前系统时间)
function calcHoldDays(entryDateTimeStr) {
const entry = parseEntryDate(entryDateTimeStr);
const now = new Date();
const diffMs = now - entry;
const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
return days >= 0 ? days : 0;
}
// 计算最大涨幅 (%)
function calcMaxGain(entryPrice, highPrice) {
if (!entryPrice || entryPrice <= 0) return 0;
const gain = ((highPrice - entryPrice) / entryPrice) * 100;
return gain > 0 ? gain : 0;
}
// 当前收益率 (基于当前价格)
function calcCurrentReturn(entryPrice, currentPrice) {
if (!entryPrice || entryPrice <= 0) return 0;
return ((currentPrice - entryPrice) / entryPrice) * 100;
}
// 单日期筛选: 根据入池时间字符串的日期部分 (YYYY-MM-DD) 是否等于 selectedDate
function filterStocksBySingleDate(stocks, selectedDate) {
if (!selectedDate) return [...stocks]; // 未选择日期时展示全部
return stocks.filter(stock => {
const datePart = stock.entryDateStr.split(' ')[0];
return datePart === selectedDate;
});
}
// 渲染基础版表格 + 统计 (只读,无任何操作按钮)
function renderBase() {
const dateInput = document.getElementById('baseSingleDate');
const selectedDate = dateInput.value || null; // 格式 YYYY-MM-DD
let filtered = filterStocksBySingleDate(baseStockData, selectedDate);
const tbody = document.getElementById('baseTbody');
if (filtered.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center; padding: 32px;">📭 该日期无入池股票,请调整筛选日期或清空筛选</td></tr>';
document.getElementById('baseTotalCount').innerText = '0';
document.getElementById('baseWinRate').innerText = '0%';
document.getElementById('baseAvgReturn').innerText = '0%';
return;
}
let totalReturnSum = 0; // 累计收益率
let winCount = 0; // 盈利个数 (当前收益率 > 0)
let htmlRows = '';
for (let stock of filtered) {
const holdDays = calcHoldDays(stock.entryDateStr);
const maxGain = calcMaxGain(stock.entryPrice, stock.highPrice);
const currRet = calcCurrentReturn(stock.entryPrice, stock.currentPrice);
totalReturnSum += currRet;
if (currRet > 0) winCount++;
const maxGainClass = maxGain > 0 ? 'profit-positive' : '';
const formattedMaxGain = maxGain.toFixed(2) + '%';
const holdDaysText = holdDays + ' 天';
htmlRows += `<tr>
<td style="font-weight:500;">${escapeHtml(stock.code)}</td>
<td>${escapeHtml(stock.name)}</td>
<td>${escapeHtml(stock.entryDateStr)}</td>
<td>${stock.entryPrice.toFixed(2)}</td>
<td class="${maxGainClass}">${formattedMaxGain}</td>
<td>${holdDaysText}</td>
</tr>`;
}
tbody.innerHTML = htmlRows;
const totalCount = filtered.length;
const winRate = totalCount === 0 ? 0 : (winCount / totalCount) * 100;
const avgReturn = totalCount === 0 ? 0 : (totalReturnSum / totalCount);
document.getElementById('baseTotalCount').innerText = totalCount;
document.getElementById('baseWinRate').innerText = winRate.toFixed(2) + '%';
const avgReturnStr = (avgReturn > 0 ? '+' : '') + avgReturn.toFixed(2) + '%';
document.getElementById('baseAvgReturn').innerText = avgReturnStr;
}
// 渲染高级版表格 + 统计
function renderAdvanced() {
const dateInput = document.getElementById('advSingleDate');
const selectedDate = dateInput.value || null;
let filtered = filterStocksBySingleDate(advancedStockData, selectedDate);
const tbody = document.getElementById('advTbody');
if (filtered.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center; padding: 32px;">✨ 该日期无入池股票,请调整筛选日期或清空筛选</td></tr>';
document.getElementById('advTotalCount').innerText = '0';
document.getElementById('advWinRate').innerText = '0%';
document.getElementById('advAvgReturn').innerText = '0%';
return;
}
let totalReturnSum = 0;
let winCount = 0;
let htmlRows = '';
for (let stock of filtered) {
const holdDays = calcHoldDays(stock.entryDateStr);
const maxGain = calcMaxGain(stock.entryPrice, stock.highPrice);
const currRet = calcCurrentReturn(stock.entryPrice, stock.currentPrice);
totalReturnSum += currRet;
if (currRet > 0) winCount++;
const maxGainClass = maxGain > 0 ? 'profit-positive' : '';
htmlRows += `<tr>
<td style="font-weight:500;">${escapeHtml(stock.code)}</td>
<td>${escapeHtml(stock.name)}</td>
<td>${escapeHtml(stock.entryDateStr)}</td>
<td>${stock.entryPrice.toFixed(2)}</td>
<td class="${maxGainClass}">${maxGain.toFixed(2)}%</td>
<td>${holdDays} 天</td>
</tr>`;
}
tbody.innerHTML = htmlRows;
const totalCount = filtered.length;
const winRate = totalCount === 0 ? 0 : (winCount / totalCount) * 100;
const avgReturn = totalCount === 0 ? 0 : (totalReturnSum / totalCount);
document.getElementById('advTotalCount').innerText = totalCount;
document.getElementById('advWinRate').innerText = winRate.toFixed(2) + '%';
const avgReturnStr = (avgReturn > 0 ? '+' : '') + avgReturn.toFixed(2) + '%';
document.getElementById('advAvgReturn').innerText = avgReturnStr;
}
// 绑定筛选事件和重置按钮 (单日期模式)
function bindFilterEvents() {
// 基础版
const baseApply = document.getElementById('applyBaseFilterBtn');
const baseReset = document.getElementById('resetBaseFilterBtn');
const baseDateInput = document.getElementById('baseSingleDate');
baseApply.addEventListener('click', () => {
renderBase();
});
baseReset.addEventListener('click', () => {
baseDateInput.value = '';
renderBase();
});
// 高级版
const advApply = document.getElementById('applyAdvFilterBtn');
const advReset = document.getElementById('resetAdvancedFilterBtn');
const advDateInput = document.getElementById('advSingleDate');
advApply.addEventListener('click', () => {
renderAdvanced();
});
advReset.addEventListener('click', () => {
advDateInput.value = '';
renderAdvanced();
});
}
// 简单的防XSS
function escapeHtml(str) {
if (!str) return '';
return str.replace(/[&<>]/g, function(m) {
if (m === '&') return '&';
if (m === '<') return '<';
if (m === '>') return '>';
return m;
});
}
// 初始化所有展示,并启动定时器更新持仓天数(模拟自然日变化)
function initDisplay() {
renderBase();
renderAdvanced();
// 每分钟重新渲染一次,使持仓天数动态刷新(不改变任何数据,仅更新时间字段)
setInterval(() => {
renderBase();
renderAdvanced();
}, 60000);
}
// 页面加载执行
bindFilterEvents();
initDisplay();
</script>
</body>
</html>
👤 用户:
去掉筛选个股按钮和重置筛选按钮
💭 思考过程
我们需要修改HTML: 去掉筛选个股按钮和重置筛选按钮。保留单日期选择器,但不需要按钮?注意需求: 时间筛选改为单日期筛选,但去掉按钮,意味着日期选择变化时自动触发筛选(或不需要触发,但通常需要自动刷新)。不过给定要求“去掉筛选个股按钮和重置筛选按钮”,但保留单日期筛选功能。可能需要日期选择变化时自动筛选。我将实现:隐藏两个按钮,同时日期选择input监听change事件自动刷新。同时重置按钮也去掉,不过重置功能可通过清空日期自动显示全部?但没有了重置按钮,用户可以手动清空日期,然后自动刷新。最好清空日期时也自动展示全部。所以给日期input添加事件。另外清除筛选的方式: 用户删除日期值,触发change事件,重新渲染。这样既符合去掉按钮,又保留筛选能力。另外需要删除对应的按钮元素和绑定逻辑。同时保留筛选的功能。
但界面设计上,我们只需删除按钮的HTML和对应的JavaScript事件监听,修改为监听日期变化。同时确保重置功能不再需要按钮,因为清空日期即可。这样更简洁。
同时也要删除“重置筛选按钮”的DOM。更新页面。
注意:之前代码中 filter-bar 内有按钮,现在需要移除它们,仅保留日期选择器。另外“选择时间段个股入池情况按钮”在需求图中存在,但我们已经是单日期筛选了,没有按钮会简化。符合“去掉筛选个股按钮和重置筛选按钮”。
我将修改HTML: 删除“筛选个股”按钮和“重置筛选”按钮,在filter-bar中只保留日期选择input和提示文字。然后JS中不再绑定按钮事件,改为监听日期input的change事件,自动调用渲染。同时为了用户体验,当手动清空日期时,自动显示全部数据。
此外也要移除卡头部的重置筛选按钮。卡片头部的重置筛选按钮也要去掉。
因此修改步骤:
- 基础版和高级版卡header中移除重置按钮。
- filter-bar中移除按钮,只留下日期选择器。
- 修改JavaScript: 移除bindFilterEvents中的按钮监听,添加日期input的change事件监听,同时初始渲染调用一次。日期变化时重新渲染对应表格。
- 确保初始化时无筛选显示全部。
- 另外为了支持清空日期后自动刷新,input事件用change或input均可,用change较好。
最后测试。
已按照您的要求,移除了所有的“筛选个股”按钮和“重置筛选”按钮。现在单日期筛选器会随着日期选择的变化自动刷新股票池,无需手动点击任何按钮。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<title>股票池展示 | 拉萨二哥系列 · 自动日期筛选</title>
<style>
* {
box-sizing: border-box;
font-family: system-ui, 'Segoe UI', 'Roboto', 'Helvetica Neue', sans-serif;
}
body {
background: #f0f2f5;
margin: 0;
padding: 24px 20px;
}
.container {
max-width: 1500px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 32px;
}
/* 卡片样式 */
.card {
background: #ffffff;
border-radius: 24px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.05);
overflow: hidden;
border: 1px solid #e9ecef;
}
.card-header {
padding: 20px 24px 12px 24px;
border-bottom: 2px solid #f1f3f5;
display: flex;
justify-content: space-between;
align-items: baseline;
flex-wrap: wrap;
gap: 12px;
background: #fefefe;
}
.card-header h2 {
margin: 0;
font-size: 1.6rem;
font-weight: 600;
background: linear-gradient(135deg, #1e2a3e, #0f172a);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
letter-spacing: -0.3px;
}
.badge-version {
background: #eef2ff;
padding: 4px 12px;
border-radius: 40px;
font-size: 0.75rem;
font-weight: 500;
color: #1e40af;
}
.filter-bar {
padding: 14px 24px;
background: #ffffff;
border-bottom: 1px solid #edf2f7;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 15px;
}
.filter-group {
display: flex;
align-items: center;
gap: 12px;
background: #f1f5f9;
padding: 5px 18px;
border-radius: 40px;
}
.filter-group label {
font-size: 0.8rem;
font-weight: 500;
color: #1e293b;
}
.filter-group input {
border: 1px solid #cbd5e1;
border-radius: 20px;
padding: 8px 12px;
font-size: 0.85rem;
background: white;
width: 160px;
transition: 0.2s;
}
.filter-group input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59,130,246,0.2);
}
.stats-bar {
padding: 12px 24px;
background: #fefce8;
border-top: 1px solid #f1f3f5;
display: flex;
gap: 32px;
flex-wrap: wrap;
font-weight: 500;
font-size: 0.9rem;
}
.stat {
background: white;
padding: 5px 16px;
border-radius: 40px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.stat span:first-child {
color: #475569;
}
.stat span:last-child {
font-weight: 700;
color: #0f172a;
margin-left: 8px;
}
.table-wrapper {
overflow-x: auto;
padding: 0 0 12px 0;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.8rem;
min-width: 750px;
}
th {
text-align: left;
padding: 14px 12px;
background: #f8fafc;
color: #1e293b;
font-weight: 600;
border-bottom: 1px solid #e2e8f0;
}
td {
padding: 12px;
border-bottom: 1px solid #f1f5f9;
vertical-align: middle;
}
.profit-positive {
color: #10b981;
font-weight: 600;
}
.profit-negative {
color: #ef4444;
}
.info-note {
font-size: 0.7rem;
color: #5b6e8c;
margin-left: 16px;
}
footer {
text-align: center;
font-size: 0.7rem;
color: #94a3b8;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<!-- 基础版模块:拉萨二哥精选 · 只读展示 · 自动单日期筛选 -->
<div class="card">
<div class="card-header">
<div style="display: flex; align-items: center; gap: 12px;">
<h2>📋 拉萨二哥精选 · 基础版</h2>
<div class="badge-version">展示池 · 只读</div>
</div>
<!-- 重置按钮已移除 -->
</div>
<div class="filter-bar">
<div class="filter-group">
<label>📅 入池日期筛选:</label>
<input type="date" id="baseSingleDate" placeholder="选择日期">
</div>
<div class="info-note">* 选择日期后自动筛选当天入池股票,清空日期则显示全部</div>
</div>
<div class="table-wrapper">
<table>
<thead>
<tr><th>代码</th><th>名称</th><th>入池时间</th><th>入池价格(元)</th><th>最大涨幅(%)</th><th>持仓时间(天)</th></tr>
</thead>
<tbody id="baseTbody">
<tr><td colspan="6" style="text-align:center">加载数据中...</td></tr>
</tbody>
</table>
</div>
<div class="stats-bar" id="baseStats">
<div class="stat">📊 入池总数:<span id="baseTotalCount">0</span></div>
<div class="stat">🏆 盈利胜率:<span id="baseWinRate">0%</span></div>
<div class="stat">📈 综合收益率:<span id="baseAvgReturn">0%</span></div>
</div>
</div>
<!-- 高级版模块:拉萨二哥主导 · 只读展示 · 自动单日期筛选 -->
<div class="card">
<div class="card-header">
<div style="display: flex; align-items: center; gap: 12px;">
<h2>⚡ 拉萨二哥主导 · 高级版</h2>
<div class="badge-version" style="background:#e0f2fe; color:#0c4a6e;">增强数据池 · 只读</div>
</div>
<!-- 重置按钮已移除 -->
</div>
<div class="filter-bar">
<div class="filter-group">
<label>📅 入池日期筛选:</label>
<input type="date" id="advSingleDate" placeholder="选择日期">
</div>
<div class="info-note">* 持仓天数每日自动更新,最大涨幅基于历史最高价计算</div>
</div>
<div class="table-wrapper">
<table>
<thead>
<tr><th>代码</th><th>名称</th><th>入池时间</th><th>入池价格(元)</th><th>最大涨幅(%)</th><th>持仓时间(天)</th></tr>
</thead>
<tbody id="advTbody">
<tr><td colspan="6" style="text-align:center">加载数据中...</td></tr>
</tbody>
</table>
</div>
<div class="stats-bar" id="advStats">
<div class="stat">📊 入池总数:<span id="advTotalCount">0</span></div>
<div class="stat">🏆 盈利胜率:<span id="advWinRate">0%</span></div>
<div class="stat">📈 综合收益率:<span id="advAvgReturn">0%</span></div>
</div>
</div>
<footer>📌 说明:仅展示股票池,无任何添加/编辑/删除操作。选择日期后立即筛选,清空日期则显示全部。最大涨幅和盈利胜率动态计算,持仓天数按自然日自动更新。</footer>
</div>
<script>
// --------------------------------------------------------------
// 基础版 展示数据 (固定股票池)
// --------------------------------------------------------------
const baseStockData = [
{
code: "000858",
name: "五粮液",
entryDateStr: "2025-03-10 09:35:00",
entryPrice: 148.50,
currentPrice: 168.30,
highPrice: 175.20
},
{
code: "600519",
name: "贵州茅台",
entryDateStr: "2025-02-18 14:20:00",
entryPrice: 1720.00,
currentPrice: 1690.00,
highPrice: 1850.00
},
{
code: "300750",
name: "宁德时代",
entryDateStr: "2025-04-01 10:15:00",
entryPrice: 195.00,
currentPrice: 212.50,
highPrice: 228.00
},
{
code: "002415",
name: "海康威视",
entryDateStr: "2025-03-25 13:45:00",
entryPrice: 32.80,
currentPrice: 31.20,
highPrice: 34.10
},
{
code: "601318",
name: "中国平安",
entryDateStr: "2025-01-20 11:05:00",
entryPrice: 42.60,
currentPrice: 46.80,
highPrice: 48.90
},
{
code: "000001",
name: "平安银行",
entryDateStr: "2025-04-12 09:58:00",
entryPrice: 11.20,
currentPrice: 12.05,
highPrice: 12.95
},
{
code: "002050",
name: "三花智控",
entryDateStr: "2025-03-10 14:30:00", // 同2025-03-10
entryPrice: 22.30,
currentPrice: 24.60,
highPrice: 26.40
}
];
// 高级版 展示数据
const advancedStockData = [
{
code: "002594",
name: "比亚迪",
entryDateStr: "2025-03-05 10:30:00",
entryPrice: 235.00,
currentPrice: 262.50,
highPrice: 285.00
},
{
code: "688981",
name: "中芯国际",
entryDateStr: "2025-02-10 14:10:00",
entryPrice: 52.30,
currentPrice: 48.90,
highPrice: 59.80
},
{
code: "300059",
name: "东方财富",
entryDateStr: "2025-04-08 11:20:00",
entryPrice: 22.50,
currentPrice: 24.30,
highPrice: 27.50
},
{
code: "600036",
name: "招商银行",
entryDateStr: "2025-01-15 09:45:00",
entryPrice: 34.20,
currentPrice: 38.60,
highPrice: 40.20
},
{
code: "002475",
name: "立讯精密",
entryDateStr: "2025-03-28 13:20:00",
entryPrice: 31.60,
currentPrice: 33.80,
highPrice: 36.10
},
{
code: "601899",
name: "紫金矿业",
entryDateStr: "2025-04-18 15:00:00",
entryPrice: 17.80,
currentPrice: 19.20,
highPrice: 20.50
},
{
code: "000333",
name: "美的集团",
entryDateStr: "2025-03-05 09:20:00", // 同2025-03-05
entryPrice: 68.50,
currentPrice: 72.30,
highPrice: 76.00
}
];
// ---------- 通用工具函数 ----------
function parseEntryDate(dateTimeStr) {
return new Date(dateTimeStr.replace(' ', 'T'));
}
function calcHoldDays(entryDateTimeStr) {
const entry = parseEntryDate(entryDateTimeStr);
const now = new Date();
const diffMs = now - entry;
const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
return days >= 0 ? days : 0;
}
function calcMaxGain(entryPrice, highPrice) {
if (!entryPrice || entryPrice <= 0) return 0;
const gain = ((highPrice - entryPrice) / entryPrice) * 100;
return gain > 0 ? gain : 0;
}
function calcCurrentReturn(entryPrice, currentPrice) {
if (!entryPrice || entryPrice <= 0) return 0;
return ((currentPrice - entryPrice) / entryPrice) * 100;
}
// 单日期筛选: 未选择日期则返回全部,否则只返回入池日期等于所选日期的股票
function filterStocksBySingleDate(stocks, selectedDate) {
if (!selectedDate) return [...stocks];
return stocks.filter(stock => {
const datePart = stock.entryDateStr.split(' ')[0];
return datePart === selectedDate;
});
}
// 渲染基础版
function renderBase() {
const dateInput = document.getElementById('baseSingleDate');
const selectedDate = dateInput.value || null;
let filtered = filterStocksBySingleDate(baseStockData, selectedDate);
const tbody = document.getElementById('baseTbody');
if (filtered.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center; padding: 32px;">📭 该日期无入池股票,请调整日期或清空选择</td></tr>';
document.getElementById('baseTotalCount').innerText = '0';
document.getElementById('baseWinRate').innerText = '0%';
document.getElementById('baseAvgReturn').innerText = '0%';
return;
}
let totalReturnSum = 0;
let winCount = 0;
let htmlRows = '';
for (let stock of filtered) {
const holdDays = calcHoldDays(stock.entryDateStr);
const maxGain = calcMaxGain(stock.entryPrice, stock.highPrice);
const currRet = calcCurrentReturn(stock.entryPrice, stock.currentPrice);
totalReturnSum += currRet;
if (currRet > 0) winCount++;
const maxGainClass = maxGain > 0 ? 'profit-positive' : '';
htmlRows += `<tr>
<td style="font-weight:500;">${escapeHtml(stock.code)}</td>
<td>${escapeHtml(stock.name)}</td>
<td>${escapeHtml(stock.entryDateStr)}</td>
<td>${stock.entryPrice.toFixed(2)}</td>
<td class="${maxGainClass}">${maxGain.toFixed(2)}%</td>
<td>${holdDays} 天</td>
</tr>`;
}
tbody.innerHTML = htmlRows;
const totalCount = filtered.length;
const winRate = totalCount === 0 ? 0 : (winCount / totalCount) * 100;
const avgReturn = totalCount === 0 ? 0 : (totalReturnSum / totalCount);
document.getElementById('baseTotalCount').innerText = totalCount;
document.getElementById('baseWinRate').innerText = winRate.toFixed(2) + '%';
const avgReturnStr = (avgReturn > 0 ? '+' : '') + avgReturn.toFixed(2) + '%';
document.getElementById('baseAvgReturn').innerText = avgReturnStr;
}
// 渲染高级版
function renderAdvanced() {
const dateInput = document.getElementById('advSingleDate');
const selectedDate = dateInput.value || null;
let filtered = filterStocksBySingleDate(advancedStockData, selectedDate);
const tbody = document.getElementById('advTbody');
if (filtered.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center; padding: 32px;">✨ 该日期无入池股票,请调整日期或清空选择</td></tr>';
document.getElementById('advTotalCount').innerText = '0';
document.getElementById('advWinRate').innerText = '0%';
document.getElementById('advAvgReturn').innerText = '0%';
return;
}
let totalReturnSum = 0;
let winCount = 0;
let htmlRows = '';
for (let stock of filtered) {
const holdDays = calcHoldDays(stock.entryDateStr);
const maxGain = calcMaxGain(stock.entryPrice, stock.highPrice);
const currRet = calcCurrentReturn(stock.entryPrice, stock.currentPrice);
totalReturnSum += currRet;
if (currRet > 0) winCount++;
const maxGainClass = maxGain > 0 ? 'profit-positive' : '';
htmlRows += `<tr>
<td style="font-weight:500;">${escapeHtml(stock.code)}</td>
<td>${escapeHtml(stock.name)}</td>
<td>${escapeHtml(stock.entryDateStr)}</td>
<td>${stock.entryPrice.toFixed(2)}</td>
<td class="${maxGainClass}">${maxGain.toFixed(2)}%</td>
<td>${holdDays} 天</td>
</tr>`;
}
tbody.innerHTML = htmlRows;
const totalCount = filtered.length;
const winRate = totalCount === 0 ? 0 : (winCount / totalCount) * 100;
const avgReturn = totalCount === 0 ? 0 : (totalReturnSum / totalCount);
document.getElementById('advTotalCount').innerText = totalCount;
document.getElementById('advWinRate').innerText = winRate.toFixed(2) + '%';
const avgReturnStr = (avgReturn > 0 ? '+' : '') + avgReturn.toFixed(2) + '%';
document.getElementById('advAvgReturn').innerText = avgReturnStr;
}
// 设置自动筛选(监听日期变化,去掉按钮)
function bindAutoFilter() {
const baseDateInput = document.getElementById('baseSingleDate');
const advDateInput = document.getElementById('advSingleDate');
// 基础版:日期变化时自动刷新
baseDateInput.addEventListener('change', () => {
renderBase();
});
// 高级版:日期变化时自动刷新
advDateInput.addEventListener('change', () => {
renderAdvanced();
});
}
function escapeHtml(str) {
if (!str) return '';
return str.replace(/[&<>]/g, function(m) {
if (m === '&') return '&';
if (m === '<') return '<';
if (m === '>') return '>';
return m;
});
}
// 初始化与自动刷新持仓天数
function initDisplay() {
renderBase();
renderAdvanced();
// 每分钟重新渲染一次,使持仓天数保持最新(仅更新时间数值,不改变数据)
setInterval(() => {
renderBase();
renderAdvanced();
}, 60000);
}
bindAutoFilter();
initDisplay();
</script>
</body>
</html>
👤 用户:
列表新增当前价,当前涨幅
💭 思考过程
我们收到了用户需求:在现有的股票池展示列表中,新增“当前价”和“当前涨幅”两列。当前原型已经包含入池价格、最大涨幅等,需要增加这两项。同时要保持单日期筛选功能,无添加/编辑按钮,只读展示。
我们需要修改HTML表格,增加表头列“当前价(元)”和“当前涨幅(%)”,并在每行数据中显示对应值。当前涨幅基于当前价格和入池价格计算。
注意:高级版和基础版都需要增加。同时底部统计中的“综合收益率”已经基于当前收益率计算,无需改动。确保数据中已有currentPrice字段。
修改renderBase和renderAdvanced函数中的表头和数据行生成逻辑。
另外注意样式适应,列宽自动。
由于用户未要求其他修改,直接生成完整HTML。
同时可以保留现有股票数据中的currentPrice。当前涨幅 = (currentPrice - entryPrice)/entryPrice * 100,保留两位小数。
现在生成最终代码。
以下是新增“当前价”和“当前涨幅”列后的股票池展示页面。两个版本均添加了这两列,数据基于现有股票价格动态计算,并保留了单日期自动筛选功能。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<title>股票池展示 | 拉萨二哥系列 · 新增当前价/涨幅</title>
<style>
* {
box-sizing: border-box;
font-family: system-ui, 'Segoe UI', 'Roboto', 'Helvetica Neue', sans-serif;
}
body {
background: #f0f2f5;
margin: 0;
padding: 24px 20px;
}
.container {
max-width: 1600px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 32px;
}
/* 卡片样式 */
.card {
background: #ffffff;
border-radius: 24px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.05);
overflow: hidden;
border: 1px solid #e9ecef;
}
.card-header {
padding: 20px 24px 12px 24px;
border-bottom: 2px solid #f1f3f5;
display: flex;
justify-content: space-between;
align-items: baseline;
flex-wrap: wrap;
gap: 12px;
background: #fefefe;
}
.card-header h2 {
margin: 0;
font-size: 1.6rem;
font-weight: 600;
background: linear-gradient(135deg, #1e2a3e, #0f172a);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
letter-spacing: -0.3px;
}
.badge-version {
background: #eef2ff;
padding: 4px 12px;
border-radius: 40px;
font-size: 0.75rem;
font-weight: 500;
color: #1e40af;
}
.filter-bar {
padding: 14px 24px;
background: #ffffff;
border-bottom: 1px solid #edf2f7;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 15px;
}
.filter-group {
display: flex;
align-items: center;
gap: 12px;
background: #f1f5f9;
padding: 5px 18px;
border-radius: 40px;
}
.filter-group label {
font-size: 0.8rem;
font-weight: 500;
color: #1e293b;
}
.filter-group input {
border: 1px solid #cbd5e1;
border-radius: 20px;
padding: 8px 12px;
font-size: 0.85rem;
background: white;
width: 160px;
transition: 0.2s;
}
.filter-group input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59,130,246,0.2);
}
.stats-bar {
padding: 12px 24px;
background: #fefce8;
border-top: 1px solid #f1f3f5;
display: flex;
gap: 32px;
flex-wrap: wrap;
font-weight: 500;
font-size: 0.9rem;
}
.stat {
background: white;
padding: 5px 16px;
border-radius: 40px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.stat span:first-child {
color: #475569;
}
.stat span:last-child {
font-weight: 700;
color: #0f172a;
margin-left: 8px;
}
.table-wrapper {
overflow-x: auto;
padding: 0 0 12px 0;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.8rem;
min-width: 950px;
}
th {
text-align: left;
padding: 14px 12px;
background: #f8fafc;
color: #1e293b;
font-weight: 600;
border-bottom: 1px solid #e2e8f0;
}
td {
padding: 12px;
border-bottom: 1px solid #f1f5f9;
vertical-align: middle;
}
.profit-positive {
color: #10b981;
font-weight: 600;
}
.profit-negative {
color: #ef4444;
}
.info-note {
font-size: 0.7rem;
color: #5b6e8c;
margin-left: 16px;
}
footer {
text-align: center;
font-size: 0.7rem;
color: #94a3b8;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<!-- 基础版模块:新增当前价、当前涨幅 -->
<div class="card">
<div class="card-header">
<div style="display: flex; align-items: center; gap: 12px;">
<h2>📋 拉萨二哥精选 · 基础版</h2>
<div class="badge-version">展示池 · 只读</div>
</div>
</div>
<div class="filter-bar">
<div class="filter-group">
<label>📅 入池日期筛选:</label>
<input type="date" id="baseSingleDate" placeholder="选择日期">
</div>
<div class="info-note">* 选择日期后自动筛选当天入池股票,清空日期则显示全部</div>
</div>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>代码</th><th>名称</th><th>入池时间</th><th>入池价格(元)</th>
<th>当前价(元)</th><th>当前涨幅(%)</th><th>最大涨幅(%)</th><th>持仓时间(天)</th>
</tr>
</thead>
<tbody id="baseTbody">
<tr><td colspan="8" style="text-align:center">加载数据中...</td></tr>
</tbody>
</table>
</div>
<div class="stats-bar" id="baseStats">
<div class="stat">📊 入池总数:<span id="baseTotalCount">0</span></div>
<div class="stat">🏆 盈利胜率:<span id="baseWinRate">0%</span></div>
<div class="stat">📈 综合收益率:<span id="baseAvgReturn">0%</span></div>
</div>
</div>
<!-- 高级版模块:新增当前价、当前涨幅 -->
<div class="card">
<div class="card-header">
<div style="display: flex; align-items: center; gap: 12px;">
<h2>⚡ 拉萨二哥主导 · 高级版</h2>
<div class="badge-version" style="background:#e0f2fe; color:#0c4a6e;">增强数据池 · 只读</div>
</div>
</div>
<div class="filter-bar">
<div class="filter-group">
<label>📅 入池日期筛选:</label>
<input type="date" id="advSingleDate" placeholder="选择日期">
</div>
<div class="info-note">* 持仓天数每日自动更新,最大涨幅基于历史最高价计算</div>
</div>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>代码</th><th>名称</th><th>入池时间</th><th>入池价格(元)</th>
<th>当前价(元)</th><th>当前涨幅(%)</th><th>最大涨幅(%)</th><th>持仓时间(天)</th>
</tr>
</thead>
<tbody id="advTbody">
<tr><td colspan="8" style="text-align:center">加载数据中...</td></tr>
</tbody>
</table>
</div>
<div class="stats-bar" id="advStats">
<div class="stat">📊 入池总数:<span id="advTotalCount">0</span></div>
<div class="stat">🏆 盈利胜率:<span id="advWinRate">0%</span></div>
<div class="stat">📈 综合收益率:<span id="advAvgReturn">0%</span></div>
</div>
</div>
<footer>📌 说明:仅展示股票池,无任何添加/编辑/删除操作。当前涨幅 = (当前价-入池价)/入池价×100%,盈利胜率基于当前涨幅>0计算,综合收益率为平均当前涨幅。</footer>
</div>
<script>
// --------------------------------------------------------------
// 基础版 展示数据 (固定股票池)
// --------------------------------------------------------------
const baseStockData = [
{
code: "000858",
name: "五粮液",
entryDateStr: "2025-03-10 09:35:00",
entryPrice: 148.50,
currentPrice: 168.30,
highPrice: 175.20
},
{
code: "600519",
name: "贵州茅台",
entryDateStr: "2025-02-18 14:20:00",
entryPrice: 1720.00,
currentPrice: 1690.00,
highPrice: 1850.00
},
{
code: "300750",
name: "宁德时代",
entryDateStr: "2025-04-01 10:15:00",
entryPrice: 195.00,
currentPrice: 212.50,
highPrice: 228.00
},
{
code: "002415",
name: "海康威视",
entryDateStr: "2025-03-25 13:45:00",
entryPrice: 32.80,
currentPrice: 31.20,
highPrice: 34.10
},
{
code: "601318",
name: "中国平安",
entryDateStr: "2025-01-20 11:05:00",
entryPrice: 42.60,
currentPrice: 46.80,
highPrice: 48.90
},
{
code: "000001",
name: "平安银行",
entryDateStr: "2025-04-12 09:58:00",
entryPrice: 11.20,
currentPrice: 12.05,
highPrice: 12.95
},
{
code: "002050",
name: "三花智控",
entryDateStr: "2025-03-10 14:30:00", // 同2025-03-10
entryPrice: 22.30,
currentPrice: 24.60,
highPrice: 26.40
}
];
// 高级版 展示数据
const advancedStockData = [
{
code: "002594",
name: "比亚迪",
entryDateStr: "2025-03-05 10:30:00",
entryPrice: 235.00,
currentPrice: 262.50,
highPrice: 285.00
},
{
code: "688981",
name: "中芯国际",
entryDateStr: "2025-02-10 14:10:00",
entryPrice: 52.30,
currentPrice: 48.90,
highPrice: 59.80
},
{
code: "300059",
name: "东方财富",
entryDateStr: "2025-04-08 11:20:00",
entryPrice: 22.50,
currentPrice: 24.30,
highPrice: 27.50
},
{
code: "600036",
name: "招商银行",
entryDateStr: "2025-01-15 09:45:00",
entryPrice: 34.20,
currentPrice: 38.60,
highPrice: 40.20
},
{
code: "002475",
name: "立讯精密",
entryDateStr: "2025-03-28 13:20:00",
entryPrice: 31.60,
currentPrice: 33.80,
highPrice: 36.10
},
{
code: "601899",
name: "紫金矿业",
entryDateStr: "2025-04-18 15:00:00",
entryPrice: 17.80,
currentPrice: 19.20,
highPrice: 20.50
},
{
code: "000333",
name: "美的集团",
entryDateStr: "2025-03-05 09:20:00", // 同2025-03-05
entryPrice: 68.50,
currentPrice: 72.30,
highPrice: 76.00
}
];
// ---------- 通用工具函数 ----------
function parseEntryDate(dateTimeStr) {
return new Date(dateTimeStr.replace(' ', 'T'));
}
function calcHoldDays(entryDateTimeStr) {
const entry = parseEntryDate(entryDateTimeStr);
const now = new Date();
const diffMs = now - entry;
const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
return days >= 0 ? days : 0;
}
function calcMaxGain(entryPrice, highPrice) {
if (!entryPrice || entryPrice <= 0) return 0;
const gain = ((highPrice - entryPrice) / entryPrice) * 100;
return gain > 0 ? gain : 0;
}
function calcCurrentReturn(entryPrice, currentPrice) {
if (!entryPrice || entryPrice <= 0) return 0;
return ((currentPrice - entryPrice) / entryPrice) * 100;
}
// 单日期筛选: 未选择日期则返回全部
function filterStocksBySingleDate(stocks, selectedDate) {
if (!selectedDate) return [...stocks];
return stocks.filter(stock => {
const datePart = stock.entryDateStr.split(' ')[0];
return datePart === selectedDate;
});
}
// 渲染基础版(新增当前价、当前涨幅列)
function renderBase() {
const dateInput = document.getElementById('baseSingleDate');
const selectedDate = dateInput.value || null;
let filtered = filterStocksBySingleDate(baseStockData, selectedDate);
const tbody = document.getElementById('baseTbody');
if (filtered.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" style="text-align:center; padding: 32px;">📭 该日期无入池股票,请调整日期或清空选择</td></tr>';
document.getElementById('baseTotalCount').innerText = '0';
document.getElementById('baseWinRate').innerText = '0%';
document.getElementById('baseAvgReturn').innerText = '0%';
return;
}
let totalReturnSum = 0; // 累计当前收益率
let winCount = 0;
let htmlRows = '';
for (let stock of filtered) {
const holdDays = calcHoldDays(stock.entryDateStr);
const maxGain = calcMaxGain(stock.entryPrice, stock.highPrice);
const currRet = calcCurrentReturn(stock.entryPrice, stock.currentPrice);
totalReturnSum += currRet;
if (currRet > 0) winCount++;
const currRetClass = currRet >= 0 ? 'profit-positive' : 'profit-negative';
const maxGainClass = maxGain > 0 ? 'profit-positive' : '';
htmlRows += `
<tr>
<td style="font-weight:500;">${escapeHtml(stock.code)}</td>
<td>${escapeHtml(stock.name)}</td>
<td>${escapeHtml(stock.entryDateStr)}</td>
<td>${stock.entryPrice.toFixed(2)}</td>
<td>${stock.currentPrice.toFixed(2)}</td>
<td class="${currRetClass}">${(currRet > 0 ? '+' : '')}${currRet.toFixed(2)}%</td>
<td class="${maxGainClass}">${maxGain.toFixed(2)}%</td>
<td>${holdDays} 天</td>
</tr>
`;
}
tbody.innerHTML = htmlRows;
const totalCount = filtered.length;
const winRate = totalCount === 0 ? 0 : (winCount / totalCount) * 100;
const avgReturn = totalCount === 0 ? 0 : (totalReturnSum / totalCount);
document.getElementById('baseTotalCount').innerText = totalCount;
document.getElementById('baseWinRate').innerText = winRate.toFixed(2) + '%';
const avgReturnStr = (avgReturn > 0 ? '+' : '') + avgReturn.toFixed(2) + '%';
document.getElementById('baseAvgReturn').innerText = avgReturnStr;
}
// 渲染高级版(同样新增两列)
function renderAdvanced() {
const dateInput = document.getElementById('advSingleDate');
const selectedDate = dateInput.value || null;
let filtered = filterStocksBySingleDate(advancedStockData, selectedDate);
const tbody = document.getElementById('advTbody');
if (filtered.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" style="text-align:center; padding: 32px;">✨ 该日期无入池股票,请调整日期或清空选择</td></tr>';
document.getElementById('advTotalCount').innerText = '0';
document.getElementById('advWinRate').innerText = '0%';
document.getElementById('advAvgReturn').innerText = '0%';
return;
}
let totalReturnSum = 0;
let winCount = 0;
let htmlRows = '';
for (let stock of filtered) {
const holdDays = calcHoldDays(stock.entryDateStr);
const maxGain = calcMaxGain(stock.entryPrice, stock.highPrice);
const currRet = calcCurrentReturn(stock.entryPrice, stock.currentPrice);
totalReturnSum += currRet;
if (currRet > 0) winCount++;
const currRetClass = currRet >= 0 ? 'profit-positive' : 'profit-negative';
const maxGainClass = maxGain > 0 ? 'profit-positive' : '';
htmlRows += `
<tr>
<td style="font-weight:500;">${escapeHtml(stock.code)}</td>
<td>${escapeHtml(stock.name)}</td>
<td>${escapeHtml(stock.entryDateStr)}</td>
<td>${stock.entryPrice.toFixed(2)}</td>
<td>${stock.currentPrice.toFixed(2)}</td>
<td class="${currRetClass}">${(currRet > 0 ? '+' : '')}${currRet.toFixed(2)}%</td>
<td class="${maxGainClass}">${maxGain.toFixed(2)}%</td>
<td>${holdDays} 天</td>
</tr>
`;
}
tbody.innerHTML = htmlRows;
const totalCount = filtered.length;
const winRate = totalCount === 0 ? 0 : (winCount / totalCount) * 100;
const avgReturn = totalCount === 0 ? 0 : (totalReturnSum / totalCount);
document.getElementById('advTotalCount').innerText = totalCount;
document.getElementById('advWinRate').innerText = winRate.toFixed(2) + '%';
const avgReturnStr = (avgReturn > 0 ? '+' : '') + avgReturn.toFixed(2) + '%';
document.getElementById('advAvgReturn').innerText = avgReturnStr;
}
// 自动筛选:监听日期变化
function bindAutoFilter() {
const baseDateInput = document.getElementById('baseSingleDate');
const advDateInput = document.getElementById('advSingleDate');
baseDateInput.addEventListener('change', () => {
renderBase();
});
advDateInput.addEventListener('change', () => {
renderAdvanced();
});
}
function escapeHtml(str) {
if (!str) return '';
return str.replace(/[&<>]/g, function(m) {
if (m === '&') return '&';
if (m === '<') return '<';
if (m === '>') return '>';
return m;
});
}
// 初始化并定时刷新持仓天数
function initDisplay() {
renderBase();
renderAdvanced();
setInterval(() => {
renderBase();
renderAdvanced();
}, 60000);
}
bindAutoFilter();
initDisplay();
</script>
</body>
</html>
👤 用户:
再新增当前收益,为当前价/入池价格*100-100
💭 思考过程
我们理解了需求:在股票列表新增“当前收益”,计算公式为 (当前价 / 入池价格 100 - 100) ,其实就是当前涨幅百分比(与之前的当前涨幅本质相同)。但用户可能想要一个不同表述,也可能是为了强化展示。不过目前已有“当前涨幅(%)”列,计算公式就是 (当前价-入池价)/入池价100 = 当前价/入池价100 -100。为避免重复,也许用户想要绝对收益金额?需求里写“当前收益,为当前价/入池价格100-100”,实际上还是百分比,与当前涨幅重复。但可能想要单独的“当前收益”列显示百分比并保留两位小数。为了满足要求,我们可以在表格中新增一列“当前收益(%)”与“当前涨幅(%)”完全一样,但这没有意义。更好的理解:用户可能想要的是绝对收益金额(例如当前价-入池价格)?但需求描述明确写“当前价/入池价格*100-100”,即百分比。不过为了演示,我们仍按公式新增一列“当前收益(%)”,与“当前涨幅”并列,用户可自行决定保留哪个。或者我们替换掉当前涨幅。但需求说“再新增当前收益”,而不是替换。由于界面已存在当前涨幅,再新增相同数值会冗余。可能用户没有注意到已有当前涨幅,但以需求为准。我们选择新增一列,放在当前价之后,当前涨幅之前。这样表格列顺序为:代码、名称、入池时间、入池价格、当前价、当前收益(%)、当前涨幅(%)、最大涨幅(%)、持仓时间。其中当前涨幅与当前收益数值完全一致。但我们会按照公式计算并显示。为用户体验,可以隐藏原有当前涨幅?不能擅自隐藏,但严格遵循新增。但也可以改为“绝对收益(元)”更合理?但公式明确,我们按公式展示。
另:考虑到用户之前要求“列表新增当前价,当前涨幅”,现在又新增当前收益(重复),是否笔误?但作为AI,忠实实现:新增一列“当前收益” = (当前价 / 入池价格) * 100 - 100,显示为百分比(保留两位小数,正负号)。为了避免过度冗余,我们在基础版和高级版都增加该列。
同时因为列增多,调整表格最小宽度。
重新生成HTML。
以下是新增“当前收益”列的股票池页面。该列按您指定的公式(当前价 ÷ 入池价格 × 100 - 100)计算,与当前涨幅数值一致。表格列顺序为:代码、名称、入池时间、入池价格、当前价、当前收益、当前涨幅、最大涨幅、持仓时间。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<title>股票池展示 | 拉萨二哥系列 · 新增当前收益</title>
<style>
* {
box-sizing: border-box;
font-family: system-ui, 'Segoe UI', 'Roboto', 'Helvetica Neue', sans-serif;
}
body {
background: #f0f2f5;
margin: 0;
padding: 24px 20px;
}
.container {
max-width: 1700px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 32px;
}
.card {
background: #ffffff;
border-radius: 24px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.05);
overflow: hidden;
border: 1px solid #e9ecef;
}
.card-header {
padding: 20px 24px 12px 24px;
border-bottom: 2px solid #f1f3f5;
display: flex;
justify-content: space-between;
align-items: baseline;
flex-wrap: wrap;
gap: 12px;
background: #fefefe;
}
.card-header h2 {
margin: 0;
font-size: 1.6rem;
font-weight: 600;
background: linear-gradient(135deg, #1e2a3e, #0f172a);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
letter-spacing: -0.3px;
}
.badge-version {
background: #eef2ff;
padding: 4px 12px;
border-radius: 40px;
font-size: 0.75rem;
font-weight: 500;
color: #1e40af;
}
.filter-bar {
padding: 14px 24px;
background: #ffffff;
border-bottom: 1px solid #edf2f7;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 15px;
}
.filter-group {
display: flex;
align-items: center;
gap: 12px;
background: #f1f5f9;
padding: 5px 18px;
border-radius: 40px;
}
.filter-group label {
font-size: 0.8rem;
font-weight: 500;
color: #1e293b;
}
.filter-group input {
border: 1px solid #cbd5e1;
border-radius: 20px;
padding: 8px 12px;
font-size: 0.85rem;
background: white;
width: 160px;
transition: 0.2s;
}
.filter-group input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59,130,246,0.2);
}
.stats-bar {
padding: 12px 24px;
background: #fefce8;
border-top: 1px solid #f1f3f5;
display: flex;
gap: 32px;
flex-wrap: wrap;
font-weight: 500;
font-size: 0.9rem;
}
.stat {
background: white;
padding: 5px 16px;
border-radius: 40px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.stat span:first-child {
color: #475569;
}
.stat span:last-child {
font-weight: 700;
color: #0f172a;
margin-left: 8px;
}
.table-wrapper {
overflow-x: auto;
padding: 0 0 12px 0;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.8rem;
min-width: 1100px;
}
th {
text-align: left;
padding: 14px 12px;
background: #f8fafc;
color: #1e293b;
font-weight: 600;
border-bottom: 1px solid #e2e8f0;
}
td {
padding: 12px;
border-bottom: 1px solid #f1f5f9;
vertical-align: middle;
}
.profit-positive {
color: #10b981;
font-weight: 600;
}
.profit-negative {
color: #ef4444;
}
.info-note {
font-size: 0.7rem;
color: #5b6e8c;
margin-left: 16px;
}
footer {
text-align: center;
font-size: 0.7rem;
color: #94a3b8;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<!-- 基础版模块:新增“当前收益”列 (公式:当前价/入池价*100-100) -->
<div class="card">
<div class="card-header">
<div style="display: flex; align-items: center; gap: 12px;">
<h2>📋 拉萨二哥精选 · 基础版</h2>
<div class="badge-version">展示池 · 只读</div>
</div>
</div>
<div class="filter-bar">
<div class="filter-group">
<label>📅 入池日期筛选:</label>
<input type="date" id="baseSingleDate" placeholder="选择日期">
</div>
<div class="info-note">* 选择日期后自动筛选当天入池股票,清空日期则显示全部</div>
</div>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>代码</th><th>名称</th><th>入池时间</th><th>入池价格(元)</th>
<th>当前价(元)</th><th>当前收益(%)</th><th>当前涨幅(%)</th><th>最大涨幅(%)</th><th>持仓时间(天)</th>
</tr>
</thead>
<tbody id="baseTbody">
<tr><td colspan="9" style="text-align:center">加载数据中...</td></tr>
</tbody>
</table>
</div>
<div class="stats-bar" id="baseStats">
<div class="stat">📊 入池总数:<span id="baseTotalCount">0</span></div>
<div class="stat">🏆 盈利胜率:<span id="baseWinRate">0%</span></div>
<div class="stat">📈 综合收益率:<span id="baseAvgReturn">0%</span></div>
</div>
</div>
<!-- 高级版模块:新增“当前收益”列 -->
<div class="card">
<div class="card-header">
<div style="display: flex; align-items: center; gap: 12px;">
<h2>⚡ 拉萨二哥主导 · 高级版</h2>
<div class="badge-version" style="background:#e0f2fe; color:#0c4a6e;">增强数据池 · 只读</div>
</div>
</div>
<div class="filter-bar">
<div class="filter-group">
<label>📅 入池日期筛选:</label>
<input type="date" id="advSingleDate" placeholder="选择日期">
</div>
<div class="info-note">* 持仓天数每日自动更新,最大涨幅基于历史最高价计算</div>
</div>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>代码</th><th>名称</th><th>入池时间</th><th>入池价格(元)</th>
<th>当前价(元)</th><th>当前收益(%)</th><th>当前涨幅(%)</th><th>最大涨幅(%)</th><th>持仓时间(天)</th>
</tr>
</thead>
<tbody id="advTbody">
<tr><td colspan="9" style="text-align:center">加载数据中...</td></tr>
</tbody>
</table>
</div>
<div class="stats-bar" id="advStats">
<div class="stat">📊 入池总数:<span id="advTotalCount">0</span></div>
<div class="stat">🏆 盈利胜率:<span id="advWinRate">0%</span></div>
<div class="stat">📈 综合收益率:<span id="advAvgReturn">0%</span></div>
</div>
</div>
<footer>📌 说明:「当前收益」计算公式为 (当前价 ÷ 入池价格 × 100 - 100) ,与「当前涨幅」本质相同,按需求额外展示。盈利胜率基于当前收益 > 0 计算,综合收益率为所有个股当前收益的平均值。</footer>
</div>
<script>
// --------------------------------------------------------------
// 基础版 展示数据
// --------------------------------------------------------------
const baseStockData = [
{
code: "000858",
name: "五粮液",
entryDateStr: "2025-03-10 09:35:00",
entryPrice: 148.50,
currentPrice: 168.30,
highPrice: 175.20
},
{
code: "600519",
name: "贵州茅台",
entryDateStr: "2025-02-18 14:20:00",
entryPrice: 1720.00,
currentPrice: 1690.00,
highPrice: 1850.00
},
{
code: "300750",
name: "宁德时代",
entryDateStr: "2025-04-01 10:15:00",
entryPrice: 195.00,
currentPrice: 212.50,
highPrice: 228.00
},
{
code: "002415",
name: "海康威视",
entryDateStr: "2025-03-25 13:45:00",
entryPrice: 32.80,
currentPrice: 31.20,
highPrice: 34.10
},
{
code: "601318",
name: "中国平安",
entryDateStr: "2025-01-20 11:05:00",
entryPrice: 42.60,
currentPrice: 46.80,
highPrice: 48.90
},
{
code: "000001",
name: "平安银行",
entryDateStr: "2025-04-12 09:58:00",
entryPrice: 11.20,
currentPrice: 12.05,
highPrice: 12.95
},
{
code: "002050",
name: "三花智控",
entryDateStr: "2025-03-10 14:30:00",
entryPrice: 22.30,
currentPrice: 24.60,
highPrice: 26.40
}
];
// 高级版 展示数据
const advancedStockData = [
{
code: "002594",
name: "比亚迪",
entryDateStr: "2025-03-05 10:30:00",
entryPrice: 235.00,
currentPrice: 262.50,
highPrice: 285.00
},
{
code: "688981",
name: "中芯国际",
entryDateStr: "2025-02-10 14:10:00",
entryPrice: 52.30,
currentPrice: 48.90,
highPrice: 59.80
},
{
code: "300059",
name: "东方财富",
entryDateStr: "2025-04-08 11:20:00",
entryPrice: 22.50,
currentPrice: 24.30,
highPrice: 27.50
},
{
code: "600036",
name: "招商银行",
entryDateStr: "2025-01-15 09:45:00",
entryPrice: 34.20,
currentPrice: 38.60,
highPrice: 40.20
},
{
code: "002475",
name: "立讯精密",
entryDateStr: "2025-03-28 13:20:00",
entryPrice: 31.60,
currentPrice: 33.80,
highPrice: 36.10
},
{
code: "601899",
name: "紫金矿业",
entryDateStr: "2025-04-18 15:00:00",
entryPrice: 17.80,
currentPrice: 19.20,
highPrice: 20.50
},
{
code: "000333",
name: "美的集团",
entryDateStr: "2025-03-05 09:20:00",
entryPrice: 68.50,
currentPrice: 72.30,
highPrice: 76.00
}
];
// 计算当前收益(公式:当前价/入池价格*100 - 100)
function calcCurrentEarningPercent(entryPrice, currentPrice) {
if (!entryPrice || entryPrice <= 0) return 0;
return (currentPrice / entryPrice) * 100 - 100;
}
// 计算当前涨幅(与原当前涨幅一致,但为了保持独立,使用相同公式)
function calcCurrentReturnPercent(entryPrice, currentPrice) {
if (!entryPrice || entryPrice <= 0) return 0;
return ((currentPrice - entryPrice) / entryPrice) * 100;
}
function parseEntryDate(dateTimeStr) {
return new Date(dateTimeStr.replace(' ', 'T'));
}
function calcHoldDays(entryDateTimeStr) {
const entry = parseEntryDate(entryDateTimeStr);
const now = new Date();
const diffMs = now - entry;
const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
return days >= 0 ? days : 0;
}
function calcMaxGain(entryPrice, highPrice) {
if (!entryPrice || entryPrice <= 0) return 0;
const gain = ((highPrice - entryPrice) / entryPrice) * 100;
return gain > 0 ? gain : 0;
}
// 单日期筛选
function filterStocksBySingleDate(stocks, selectedDate) {
if (!selectedDate) return [...stocks];
return stocks.filter(stock => {
const datePart = stock.entryDateStr.split(' ')[0];
return datePart === selectedDate;
});
}
// 渲染基础版(新增当前收益列)
function renderBase() {
const dateInput = document.getElementById('baseSingleDate');
const selectedDate = dateInput.value || null;
let filtered = filterStocksBySingleDate(baseStockData, selectedDate);
const tbody = document.getElementById('baseTbody');
if (filtered.length === 0) {
tbody.innerHTML = '<tr><td colspan="9" style="text-align:center; padding: 32px;">📭 该日期无入池股票,请调整日期或清空选择</td></tr>';
document.getElementById('baseTotalCount').innerText = '0';
document.getElementById('baseWinRate').innerText = '0%';
document.getElementById('baseAvgReturn').innerText = '0%';
return;
}
let totalEarningSum = 0; // 当前收益总和(用于综合收益率)
let winCount = 0;
let htmlRows = '';
for (let stock of filtered) {
const holdDays = calcHoldDays(stock.entryDateStr);
const maxGain = calcMaxGain(stock.entryPrice, stock.highPrice);
// 当前收益(按新公式)
const currentEarning = calcCurrentEarningPercent(stock.entryPrice, stock.currentPrice);
// 当前涨幅(标准公式)
const currentReturn = calcCurrentReturnPercent(stock.entryPrice, stock.currentPrice);
totalEarningSum += currentEarning;
if (currentEarning > 0) winCount++;
const earningClass = currentEarning >= 0 ? 'profit-positive' : 'profit-negative';
const returnClass = currentReturn >= 0 ? 'profit-positive' : 'profit-negative';
const maxGainClass = maxGain > 0 ? 'profit-positive' : '';
htmlRows += `
<tr>
<td style="font-weight:500;">${escapeHtml(stock.code)}</td>
<td>${escapeHtml(stock.name)}</td>
<td>${escapeHtml(stock.entryDateStr)}</td>
<td>${stock.entryPrice.toFixed(2)}</td>
<td>${stock.currentPrice.toFixed(2)}</td>
<td class="${earningClass}">${(currentEarning > 0 ? '+' : '')}${currentEarning.toFixed(2)}%</td>
<td class="${returnClass}">${(currentReturn > 0 ? '+' : '')}${currentReturn.toFixed(2)}%</td>
<td class="${maxGainClass}">${maxGain.toFixed(2)}%</td>
<td>${holdDays} 天</td>
</tr>
`;
}
tbody.innerHTML = htmlRows;
const totalCount = filtered.length;
const winRate = totalCount === 0 ? 0 : (winCount / totalCount) * 100;
const avgEarning = totalCount === 0 ? 0 : (totalEarningSum / totalCount);
document.getElementById('baseTotalCount').innerText = totalCount;
document.getElementById('baseWinRate').innerText = winRate.toFixed(2) + '%';
const avgEarningStr = (avgEarning > 0 ? '+' : '') + avgEarning.toFixed(2) + '%';
document.getElementById('baseAvgReturn').innerText = avgEarningStr;
}
// 渲染高级版
function renderAdvanced() {
const dateInput = document.getElementById('advSingleDate');
const selectedDate = dateInput.value || null;
let filtered = filterStocksBySingleDate(advancedStockData, selectedDate);
const tbody = document.getElementById('advTbody');
if (filtered.length === 0) {
tbody.innerHTML = '<tr><td colspan="9" style="text-align:center; padding: 32px;">✨ 该日期无入池股票,请调整日期或清空选择</td></tr>';
document.getElementById('advTotalCount').innerText = '0';
document.getElementById('advWinRate').innerText = '0%';
document.getElementById('advAvgReturn').innerText = '0%';
return;
}
let totalEarningSum = 0;
let winCount = 0;
let htmlRows = '';
for (let stock of filtered) {
const holdDays = calcHoldDays(stock.entryDateStr);
const maxGain = calcMaxGain(stock.entryPrice, stock.highPrice);
const currentEarning = calcCurrentEarningPercent(stock.entryPrice, stock.currentPrice);
const currentReturn = calcCurrentReturnPercent(stock.entryPrice, stock.currentPrice);
totalEarningSum += currentEarning;
if (currentEarning > 0) winCount++;
const earningClass = currentEarning >= 0 ? 'profit-positive' : 'profit-negative';
const returnClass = currentReturn >= 0 ? 'profit-positive' : 'profit-negative';
const maxGainClass = maxGain > 0 ? 'profit-positive' : '';
htmlRows += `
<tr>
<td style="font-weight:500;">${escapeHtml(stock.code)}</td>
<td>${escapeHtml(stock.name)}</td>
<td>${escapeHtml(stock.entryDateStr)}</td>
<td>${stock.entryPrice.toFixed(2)}</td>
<td>${stock.currentPrice.toFixed(2)}</td>
<td class="${earningClass}">${(currentEarning > 0 ? '+' : '')}${currentEarning.toFixed(2)}%</td>
<td class="${returnClass}">${(currentReturn > 0 ? '+' : '')}${currentReturn.toFixed(2)}%</td>
<td class="${maxGainClass}">${maxGain.toFixed(2)}%</td>
<td>${holdDays} 天</td>
</tr>
`;
}
tbody.innerHTML = htmlRows;
const totalCount = filtered.length;
const winRate = totalCount === 0 ? 0 : (winCount / totalCount) * 100;
const avgEarning = totalCount === 0 ? 0 : (totalEarningSum / totalCount);
document.getElementById('advTotalCount').innerText = totalCount;
document.getElementById('advWinRate').innerText = winRate.toFixed(2) + '%';
const avgEarningStr = (avgEarning > 0 ? '+' : '') + avgEarning.toFixed(2) + '%';
document.getElementById('advAvgReturn').innerText = avgEarningStr;
}
// 自动筛选:监听日期变化
function bindAutoFilter() {
const baseDateInput = document.getElementById('baseSingleDate');
const advDateInput = document.getElementById('advSingleDate');
baseDateInput.addEventListener('change', () => {
renderBase();
});
advDateInput.addEventListener('change', () => {
renderAdvanced();
});
}
function escapeHtml(str) {
if (!str) return '';
return str.replace(/[&<>]/g, function(m) {
if (m === '&') return '&';
if (m === '<') return '<';
if (m === '>') return '>';
return m;
});
}
// 初始化并定时刷新持仓天数
function initDisplay() {
renderBase();
renderAdvanced();
setInterval(() => {
renderBase();
renderAdvanced();
}, 60000);
}
bindAutoFilter();
initDisplay();
</script>
</body>
</html>
👤 用户:
盈利胜率:当前盈利>0的股票总数/当日入池股票总数 那么综合收益率如何计算比较好呢
💭 思考过程
我们被问到:盈利胜率定义为当前盈利>0的股票总数 / 当日入池股票总数。那么综合收益率如何计算比较好呢?
用户正在设计一个股票池展示页面,已经有了盈利胜率,现在想确定综合收益率的合适计算方法。需要给出建议。
可能的计算方式:
- 平均收益率(所有入池股票的当前收益率的算术平均)—— 简单直观。
- 总收益率(总收益/总成本)—— 加权平均,反映整体资金效率。
- 中位数收益率 —— 抗极端值。
- 超额收益率相对基准等。
根据之前页面,综合收益率显示的是平均收益率(之前代码中用的是平均当前收益率)。用户问如何计算比较好,应该给出合理建议。
需要回答:考虑到胜率衡量的是盈利股票的比例,综合收益率可以衡量整体收益水平,建议使用简单平均收益率(所有股票当前收益率的平均值)或总收益率(总盈利/总成本)。同时说明优缺点。
提供清晰建议。
关于“综合收益率”的计算,通常有两种常见且合理的方式,你可以根据使用场景选择:
1. 简单平均收益率(推荐)
公式: \[ \text{综合收益率} = \frac{\sum (\text{每只股票的当前收益率})}{\text{入池股票总数}} \]
- 其中当前收益率 = (当前价 / 入池价) × 100% – 100%
- 优点:直观反映每只股票的平均盈利水平,与“盈利胜率”形成互补(胜率看比例,平均收益率看强度)。
- 缺点:极端值(如某只股票暴涨)会拉高整体平均值,可能掩盖多数股票的实际表现。
- 适用:一般展示场景,用户容易理解。
2. 总资金收益率(加权平均)
公式: \[ \text{综合收益率} = \frac{\sum (\text{当前价} - \text{入池价})}{\sum \text{入池价}} \times 100\% \]
- 即总盈亏 ÷ 总成本,相当于按入池金额等权重(假设每只股票投入相同资金)计算的整体收益率。
- 优点:更贴近真实账户的总收益表现,不受个股价格高低影响(例如高价股和低价股等权重下,盈亏额直接相加)。
- 缺点:需要假设每只股票投入相同金额,但实际可能不同;计算结果可能比平均收益率更“保守”。
- 适用:模拟等额投资组合的整体收益。
3. 中位数收益率(抗极端值)
公式:将当前收益率从小到大排序,取中间值。
- 优点:能反映大多数股票的“典型”收益,不受少数暴涨暴跌股干扰。
- 缺点:无法体现整体盈利规模,与胜率信息部分重叠。
- 适用:当池中存在极端值(如涨幅超过100%的妖股)时,中位数比平均值更稳健。
推荐方案
日常展示建议使用“简单平均收益率”,因为它:
- 计算简单,用户一看就懂(所有股票的平均赚/亏百分比)。
- 与“盈利胜率”搭配自然:胜率高但平均收益率低 → 说明多数股票微赚;胜率低但平均收益率高 → 说明少数股票大赚。
- 你之前代码中实际上已经采用了平均当前收益率,符合直觉。
如果需要更专业的分析(如资金管理),可以在高级版中额外展示“总资金收益率”作为参考。
修改建议:保持现有平均收益率计算不变,但在统计栏旁添加一个小问号提示:“综合收益率 = 所有入池股票当前收益率的平均值”。这样既清晰又专业。
👤 用户:
如果是计算历史股池的综合收益率呢
💭 思考过程
我们理解问题:用户问的是“如果是计算历史股池的综合收益率呢”。之前讨论的场景是展示当前股票池(筛选当日入池个股),现在扩展到历史股池——即考虑不同时间入池的股票,如何计算综合收益率?可能涉及时间加权、不同持有期等问题。
需要给出合理的计算方法建议。
如果是计算历史股池(即包含不同时间入池、不同持仓周期的股票)的综合收益率,就不能简单地用当前收益率的简单平均了,因为每只股票的持有时间不同,直接平均会忽略时间价值,也无法真实反映策略的整体表现。
以下是几种常用的计算方式,适用于历史股池:
1. 简单平均收益率(忽略时间)
直接对股池中所有股票的当前收益率取算术平均。
- 优点:计算简单,快速了解股票池的整体盈亏水平。
- 缺点:忽略了不同股票持仓时间长短的影响。例如一只持有1天赚5%的股票,和一只持有1年赚5%的股票,平均后都是5%,但年化收益天差地别。
- 适用:仅用于快速概览,或当所有股票入池时间相近时。
2. 年化平均收益率(推荐)
将每只股票的收益率年化后再取平均。 公式: \[ \text{年化收益率}i = \left(1 + \frac{\text{当前收益率}i}{100}\right)^{\frac{365}{\text{持仓天数}i}} - 1 \] \[ \text{综合收益率(年化平均)} = \frac{\sum \text{年化收益率}i}{N} \times 100\% \]
- 优点:消除持仓时间差异,公平比较不同入池时间的股票表现。
- 缺点:短期暴涨的股票年化后会非常夸张(例如1天涨10%,年化约为 \(1.1^{365}-1 \approx 1.3\times10^{14}\%\)),需要结合最大涨幅或波动率等辅助判断。
- 适用:长期投资组合评估,或需要时间标准化对比的场景。
3. 资金加权总收益率(实际账户视角)
假设每只股票投入相同金额(例如每只1万元),计算总体盈亏: \[ \text{总收益率} = \frac{\sum (\text{当前价} - \text{入池价})}{\sum \text{入池价}} \times 100\% \] 这与之前提到的总资金收益率公式完全一样,无论入池时间先后,只要金额相同,总收益率就是所有股票盈亏之和除以总成本。
- 优点:模拟真实等额投资的总回报,直观反映策略的整体赚钱效应。
- 缺点:不体现时间因素,长期持有的股票如果波动大可能掩盖短期机会成本。
- 适用:展示实际资金的最终收益,是投资者最关心的指标之一。
4. 持有期加权平均收益率
以持仓天数为权重,对当前收益率进行加权平均: \[ \text{加权平均收益率} = \frac{\sum (\text{当前收益率}i \times \text{持仓天数}i)}{\sum \text{持仓天数}_i} \]
- 优点:持有时间越长的股票对综合收益影响越大,符合“长期暴露”的逻辑。
- 缺点:与年化不同,它没有消除时间单位,仅调整权重。
- 适用:当你想强调长期持仓的贡献时。
5. 滚动区间收益率(进阶)
对于历史股池,可以按入池日期分组,分别计算每组至今的平均收益率,然后绘制成时间序列图,或者计算整个时间段的累计收益曲线。 这通常需要更详细的数据结构(每日记录股池变动),适合做策略回测。
推荐方案
| 场景 | 推荐指标 |
|---|---|
| 快速了解股票池赚钱比例 | 盈利胜率 + 简单平均收益率 |
| 比较不同入池时间的股票表现 | 年化平均收益率 |
| 模拟等额投资的总回报 | 资金加权总收益率 |
| 向非专业用户展示 | 资金加权总收益率(最直观) |
在你的网页原型中,如果历史股池是指所有曾经入池、已退池或仍在池中的股票,建议在表格上方增加一个切换按钮,让用户选择查看“当前持股”或“全部历史股池”,然后对历史股池采用资金加权总收益率(假设等额买入),并在说明中写明计算方式。
如果你希望我帮你修改网页代码,增加一个“历史股池”视图并采用上述某种综合收益率计算方法,请告诉我具体需求。
👤 用户:
根据以上股池展示功能,新增绘制一个股池管理后台,可以选定日期,增加或删除股票。
💭 思考过程
我们为用户提供了一个股票池展示页面,现在需要新增一个股池管理后台,可以选定日期,增加或删除股票。需要基于现有功能扩展,保持基础版和高级版两个模块,并且管理后台应该允许用户添加股票(代码、名称、入池价格、入池日期等),删除股票,可能还需要编辑当前价/最高价等。因为用户要求“可以选定日期,增加或删除股票”,我们需要设计一个独立的管理区域或弹窗,或者直接在表格中增加操作列。但之前要求“仅作为展示股票池,不需要操作和添加”,现在反过来要求管理后台,所以应该新增一个管理界面,与展示区分开,或者将现有页面改造为可管理。由于没有明确要求分离,我们可以将现有页面增强,增加“管理模式”开关,或者提供一套独立的管理后台页面。为了清晰,可以新增一个“管理后台”按钮,点击后弹出侧边栏或模态框来添加/删除股票,同时表格中增加删除按钮。但要注意不要破坏原有的展示简洁性。最好创建一个独立的“后台管理”页面或卡片,包含增删改查功能。考虑到用户要求“绘制一个股池管理后台”,我们可以设计一个新的卡片区域,包含添加股票的表单(代码、名称、入池价格、入池日期(日期选择器),以及一个股票列表(可删除))。同时要确保基础版和高级版的数据能够被管理。可能需要将数据存储到localStorage或前端数组。
由于之前已经有两个版本(基础版和高级版),管理后台应该同时管理两个池?或者分别管理?用户没有明确,建议分别给两个池各自的管理界面,或者统一管理但区分版本。为了简单,我们可以在每个卡片内增加“管理”按钮,展开表单进行添加/删除,这样功能内聚。
综合考虑,我将修改页面:在基础版和高级版卡片的头部添加“管理”按钮,点击后显示/隐藏管理面板(添加股票的表单),并在表格每一行增加删除图标(仅在管理模式下显示)。同时支持选定日期(入池日期)添加股票。这样既保留了展示的简洁,又提供了管理功能。
另外,为了满足“选定日期”,添加股票时可以让用户选择入池日期(默认为当天),入池时间精确到时分秒可以设为所选日期的00:00:00或当前时间。
我们来实现这个增强版页面。
以下是支持管理功能(增加/删除股票)的股池管理后台页面。您可以在基础版和高级版中分别添加股票(可指定入池日期),并删除已有股票,所有数据实时更新且持久化保存至浏览器本地。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<title>股池管理后台 | 拉萨二哥系列 · 增删改查</title>
<style>
* {
box-sizing: border-box;
font-family: system-ui, 'Segoe UI', 'Roboto', 'Helvetica Neue', sans-serif;
}
body {
background: #f0f2f5;
margin: 0;
padding: 24px 20px;
}
.container {
max-width: 1700px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 32px;
}
/* 卡片样式 */
.card {
background: #ffffff;
border-radius: 24px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.05);
overflow: hidden;
border: 1px solid #e9ecef;
transition: all 0.2s;
}
.card-header {
padding: 20px 24px 12px 24px;
border-bottom: 2px solid #f1f3f5;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
background: #fefefe;
}
.card-header h2 {
margin: 0;
font-size: 1.6rem;
font-weight: 600;
background: linear-gradient(135deg, #1e2a3e, #0f172a);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
letter-spacing: -0.3px;
}
.badge-version {
background: #eef2ff;
padding: 4px 12px;
border-radius: 40px;
font-size: 0.75rem;
font-weight: 500;
color: #1e40af;
}
.filter-bar {
padding: 14px 24px;
background: #ffffff;
border-bottom: 1px solid #edf2f7;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 15px;
justify-content: space-between;
}
.filter-group {
display: flex;
align-items: center;
gap: 12px;
background: #f1f5f9;
padding: 5px 18px;
border-radius: 40px;
}
.filter-group label {
font-size: 0.8rem;
font-weight: 500;
color: #1e293b;
}
.filter-group input {
border: 1px solid #cbd5e1;
border-radius: 20px;
padding: 8px 12px;
font-size: 0.85rem;
background: white;
width: 160px;
transition: 0.2s;
}
.filter-group input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59,130,246,0.2);
}
.manage-toggle {
background: #1e293b;
border: none;
padding: 6px 16px;
border-radius: 40px;
font-weight: 500;
font-size: 0.75rem;
color: white;
cursor: pointer;
transition: 0.2s;
display: inline-flex;
align-items: center;
gap: 6px;
}
.manage-toggle.active {
background: #3b82f6;
}
.manage-panel {
background: #f8fafc;
border-bottom: 1px solid #e2e8f0;
padding: 20px 24px;
display: flex;
flex-wrap: wrap;
gap: 16px;
align-items: flex-end;
}
.manage-panel .input-group {
display: flex;
flex-direction: column;
gap: 6px;
font-size: 0.75rem;
font-weight: 500;
color: #334155;
}
.manage-panel input {
padding: 8px 12px;
border-radius: 12px;
border: 1px solid #cbd5e1;
font-size: 0.85rem;
width: 150px;
}
.manage-panel button {
background: #3b82f6;
border: none;
padding: 8px 20px;
border-radius: 40px;
color: white;
font-weight: 500;
cursor: pointer;
}
.stats-bar {
padding: 12px 24px;
background: #fefce8;
border-top: 1px solid #f1f3f5;
display: flex;
gap: 32px;
flex-wrap: wrap;
font-weight: 500;
font-size: 0.9rem;
}
.stat {
background: white;
padding: 5px 16px;
border-radius: 40px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.stat span:first-child {
color: #475569;
}
.stat span:last-child {
font-weight: 700;
color: #0f172a;
margin-left: 8px;
}
.table-wrapper {
overflow-x: auto;
padding: 0 0 12px 0;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.8rem;
min-width: 1100px;
}
th {
text-align: left;
padding: 14px 12px;
background: #f8fafc;
color: #1e293b;
font-weight: 600;
border-bottom: 1px solid #e2e8f0;
}
td {
padding: 12px;
border-bottom: 1px solid #f1f5f9;
vertical-align: middle;
}
.profit-positive {
color: #10b981;
font-weight: 600;
}
.profit-negative {
color: #ef4444;
}
.delete-btn {
background: #ef4444;
color: white;
border: none;
padding: 4px 10px;
border-radius: 30px;
font-size: 0.7rem;
cursor: pointer;
transition: 0.2s;
}
.delete-btn:hover {
background: #dc2626;
}
.info-note {
font-size: 0.7rem;
color: #5b6e8c;
margin-left: 16px;
}
footer {
text-align: center;
font-size: 0.7rem;
color: #94a3b8;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<!-- 基础版模块:带管理功能 -->
<div class="card" id="cardBase">
<div class="card-header">
<div style="display: flex; align-items: center; gap: 12px;">
<h2>📋 拉萨二哥精选 · 基础版</h2>
<div class="badge-version">展示池 · 可管理</div>
</div>
<button class="manage-toggle" data-target="base">🔧 管理股票</button>
</div>
<!-- 管理面板(默认隐藏) -->
<div id="baseManagePanel" class="manage-panel" style="display: none;">
<div class="input-group">
<label>📟 代码</label>
<input type="text" id="baseCode" placeholder="例: 600036">
</div>
<div class="input-group">
<label>🏷️ 名称</label>
<input type="text" id="baseName" placeholder="招商银行">
</div>
<div class="input-group">
<label>💰 入池价格</label>
<input type="number" id="basePrice" step="0.01" placeholder="价格">
</div>
<div class="input-group">
<label>📅 入池日期</label>
<input type="date" id="baseEntryDate" value="2025-06-01">
</div>
<button id="addBaseStockBtn">➕ 添加个股</button>
<div style="font-size:0.7rem; color:#5b6e8c;">* 当前价/最高价初始为入池价,可后续修改(暂未开放编辑)</div>
</div>
<div class="filter-bar">
<div class="filter-group">
<label>📅 入池日期筛选:</label>
<input type="date" id="baseSingleDate" placeholder="选择日期">
</div>
<div class="info-note">* 选择日期后自动筛选当天入池股票,清空日期则显示全部</div>
</div>
<div class="table-wrapper">
<table id="baseTable">
<thead>
<tr>
<th>代码</th><th>名称</th><th>入池时间</th><th>入池价格(元)</th>
<th>当前价(元)</th><th>当前收益(%)</th><th>当前涨幅(%)</th><th>最大涨幅(%)</th><th>持仓时间(天)</th>
<th style="width:70px">操作</th>
</tr>
</thead>
<tbody id="baseTbody">
<tr><td colspan="10" style="text-align:center">加载数据中...</td></tr>
</tbody>
</table>
</div>
<div class="stats-bar" id="baseStats">
<div class="stat">📊 入池总数:<span id="baseTotalCount">0</span></div>
<div class="stat">🏆 盈利胜率:<span id="baseWinRate">0%</span></div>
<div class="stat">📈 综合收益率(平均):<span id="baseAvgReturn">0%</span></div>
</div>
</div>
<!-- 高级版模块:带管理功能 -->
<div class="card" id="cardAdv">
<div class="card-header">
<div style="display: flex; align-items: center; gap: 12px;">
<h2>⚡ 拉萨二哥主导 · 高级版</h2>
<div class="badge-version" style="background:#e0f2fe; color:#0c4a6e;">增强数据池 · 可管理</div>
</div>
<button class="manage-toggle" data-target="adv">🔧 管理股票</button>
</div>
<div id="advManagePanel" class="manage-panel" style="display: none;">
<div class="input-group">
<label>📟 代码</label>
<input type="text" id="advCode" placeholder="例: 002594">
</div>
<div class="input-group">
<label>🏷️ 名称</label>
<input type="text" id="advName" placeholder="比亚迪">
</div>
<div class="input-group">
<label>💰 入池价格</label>
<input type="number" id="advPrice" step="0.01" placeholder="价格">
</div>
<div class="input-group">
<label>📅 入池日期</label>
<input type="date" id="advEntryDate" value="2025-06-01">
</div>
<button id="addAdvStockBtn">➕ 添加个股</button>
<div style="font-size:0.7rem; color:#5b6e8c;">* 当前价/最高价初始为入池价,可后续修改(暂未开放编辑)</div>
</div>
<div class="filter-bar">
<div class="filter-group">
<label>📅 入池日期筛选:</label>
<input type="date" id="advSingleDate" placeholder="选择日期">
</div>
<div class="info-note">* 持仓天数每日自动更新,最大涨幅基于历史最高价计算</div>
</div>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>代码</th><th>名称</th><th>入池时间</th><th>入池价格(元)</th>
<th>当前价(元)</th><th>当前收益(%)</th><th>当前涨幅(%)</th><th>最大涨幅(%)</th><th>持仓时间(天)</th>
<th style="width:70px">操作</th>
</tr>
</thead>
<tbody id="advTbody">
<tr><td colspan="10" style="text-align:center">加载数据中...</td></tr>
</tbody>
</table>
</div>
<div class="stats-bar" id="advStats">
<div class="stat">📊 入池总数:<span id="advTotalCount">0</span></div>
<div class="stat">🏆 盈利胜率:<span id="advWinRate">0%</span></div>
<div class="stat">📈 综合收益率(平均):<span id="advAvgReturn">0%</span></div>
</div>
</div>
<footer>📌 说明:点击「管理股票」按钮可展开添加面板,表格中每行右侧有删除按钮。数据自动保存至浏览器本地,刷新不丢失。当前收益 = (当前价/入池价*100-100)%,综合收益率为简单平均。</footer>
</div>
<script>
// ---------- 数据存储 key ----------
const STORAGE_BASE = 'lasha_base_stocks_v2';
const STORAGE_ADV = 'lasha_adv_stocks_v2';
// 初始化默认数据(仅当 localStorage 为空时使用示例数据)
function getDefaultBaseStocks() {
return [
{ id: 1001, code: "000858", name: "五粮液", entryDateStr: "2025-03-10 09:35:00", entryPrice: 148.50, currentPrice: 168.30, highPrice: 175.20 },
{ id: 1002, code: "600519", name: "贵州茅台", entryDateStr: "2025-02-18 14:20:00", entryPrice: 1720.00, currentPrice: 1690.00, highPrice: 1850.00 },
{ id: 1003, code: "300750", name: "宁德时代", entryDateStr: "2025-04-01 10:15:00", entryPrice: 195.00, currentPrice: 212.50, highPrice: 228.00 },
{ id: 1004, code: "002415", name: "海康威视", entryDateStr: "2025-03-25 13:45:00", entryPrice: 32.80, currentPrice: 31.20, highPrice: 34.10 },
{ id: 1005, code: "601318", name: "中国平安", entryDateStr: "2025-01-20 11:05:00", entryPrice: 42.60, currentPrice: 46.80, highPrice: 48.90 },
{ id: 1006, code: "000001", name: "平安银行", entryDateStr: "2025-04-12 09:58:00", entryPrice: 11.20, currentPrice: 12.05, highPrice: 12.95 },
{ id: 1007, code: "002050", name: "三花智控", entryDateStr: "2025-03-10 14:30:00", entryPrice: 22.30, currentPrice: 24.60, highPrice: 26.40 }
];
}
function getDefaultAdvStocks() {
return [
{ id: 2001, code: "002594", name: "比亚迪", entryDateStr: "2025-03-05 10:30:00", entryPrice: 235.00, currentPrice: 262.50, highPrice: 285.00 },
{ id: 2002, code: "688981", name: "中芯国际", entryDateStr: "2025-02-10 14:10:00", entryPrice: 52.30, currentPrice: 48.90, highPrice: 59.80 },
{ id: 2003, code: "300059", name: "东方财富", entryDateStr: "2025-04-08 11:20:00", entryPrice: 22.50, currentPrice: 24.30, highPrice: 27.50 },
{ id: 2004, code: "600036", name: "招商银行", entryDateStr: "2025-01-15 09:45:00", entryPrice: 34.20, currentPrice: 38.60, highPrice: 40.20 },
{ id: 2005, code: "002475", name: "立讯精密", entryDateStr: "2025-03-28 13:20:00", entryPrice: 31.60, currentPrice: 33.80, highPrice: 36.10 },
{ id: 2006, code: "601899", name: "紫金矿业", entryDateStr: "2025-04-18 15:00:00", entryPrice: 17.80, currentPrice: 19.20, highPrice: 20.50 },
{ id: 2007, code: "000333", name: "美的集团", entryDateStr: "2025-03-05 09:20:00", entryPrice: 68.50, currentPrice: 72.30, highPrice: 76.00 }
];
}
// 加载数据
function loadBaseStocks() {
const stored = localStorage.getItem(STORAGE_BASE);
if (stored) {
return JSON.parse(stored);
} else {
const defaultData = getDefaultBaseStocks();
localStorage.setItem(STORAGE_BASE, JSON.stringify(defaultData));
return defaultData;
}
}
function loadAdvStocks() {
const stored = localStorage.getItem(STORAGE_ADV);
if (stored) {
return JSON.parse(stored);
} else {
const defaultData = getDefaultAdvStocks();
localStorage.setItem(STORAGE_ADV, JSON.stringify(defaultData));
return defaultData;
}
}
function saveBaseStocks(stocks) {
localStorage.setItem(STORAGE_BASE, JSON.stringify(stocks));
}
function saveAdvStocks(stocks) {
localStorage.setItem(STORAGE_ADV, JSON.stringify(stocks));
}
// 全局数组
let baseStocks = loadBaseStocks();
let advancedStocks = loadAdvStocks();
// ---------- 工具函数 ----------
function parseEntryDate(dateTimeStr) {
return new Date(dateTimeStr.replace(' ', 'T'));
}
function calcHoldDays(entryDateTimeStr) {
const entry = parseEntryDate(entryDateTimeStr);
const now = new Date();
const diffMs = now - entry;
const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
return days >= 0 ? days : 0;
}
function calcMaxGain(entryPrice, highPrice) {
if (!entryPrice || entryPrice <= 0) return 0;
const gain = ((highPrice - entryPrice) / entryPrice) * 100;
return gain > 0 ? gain : 0;
}
function calcCurrentEarningPercent(entryPrice, currentPrice) {
if (!entryPrice || entryPrice <= 0) return 0;
return (currentPrice / entryPrice) * 100 - 100;
}
function calcCurrentReturnPercent(entryPrice, currentPrice) {
if (!entryPrice || entryPrice <= 0) return 0;
return ((currentPrice - entryPrice) / entryPrice) * 100;
}
function filterStocksBySingleDate(stocks, selectedDate) {
if (!selectedDate) return [...stocks];
return stocks.filter(stock => {
const datePart = stock.entryDateStr.split(' ')[0];
return datePart === selectedDate;
});
}
// 渲染基础版(带删除按钮)
function renderBase() {
const dateInput = document.getElementById('baseSingleDate');
const selectedDate = dateInput.value || null;
let filtered = filterStocksBySingleDate(baseStocks, selectedDate);
const tbody = document.getElementById('baseTbody');
if (filtered.length === 0) {
tbody.innerHTML = '<tr><td colspan="10" style="text-align:center; padding: 32px;">📭 该日期无入池股票,请调整日期或添加</td></tr>';
document.getElementById('baseTotalCount').innerText = '0';
document.getElementById('baseWinRate').innerText = '0%';
document.getElementById('baseAvgReturn').innerText = '0%';
return;
}
let totalEarningSum = 0;
let winCount = 0;
let htmlRows = '';
for (let stock of filtered) {
const holdDays = calcHoldDays(stock.entryDateStr);
const maxGain = calcMaxGain(stock.entryPrice, stock.highPrice);
const currentEarning = calcCurrentEarningPercent(stock.entryPrice, stock.currentPrice);
const currentReturn = calcCurrentReturnPercent(stock.entryPrice, stock.currentPrice);
totalEarningSum += currentEarning;
if (currentEarning > 0) winCount++;
const earningClass = currentEarning >= 0 ? 'profit-positive' : 'profit-negative';
const returnClass = currentReturn >= 0 ? 'profit-positive' : 'profit-negative';
const maxGainClass = maxGain > 0 ? 'profit-positive' : '';
htmlRows += `
<tr>
<td style="font-weight:500;">${escapeHtml(stock.code)}</td>
<td>${escapeHtml(stock.name)}</td>
<td>${escapeHtml(stock.entryDateStr)}</td>
<td>${stock.entryPrice.toFixed(2)}</td>
<td>${stock.currentPrice.toFixed(2)}</td>
<td class="${earningClass}">${(currentEarning > 0 ? '+' : '')}${currentEarning.toFixed(2)}%</td>
<td class="${returnClass}">${(currentReturn > 0 ? '+' : '')}${currentReturn.toFixed(2)}%</td>
<td class="${maxGainClass}">${maxGain.toFixed(2)}%</td>
<td>${holdDays} 天</td>
<td><button class="delete-btn" data-id="${stock.id}" data-type="base">删除</button></td>
</tr>
`;
}
tbody.innerHTML = htmlRows;
const totalCount = filtered.length;
const winRate = totalCount === 0 ? 0 : (winCount / totalCount) * 100;
const avgEarning = totalCount === 0 ? 0 : (totalEarningSum / totalCount);
document.getElementById('baseTotalCount').innerText = totalCount;
document.getElementById('baseWinRate').innerText = winRate.toFixed(2) + '%';
document.getElementById('baseAvgReturn').innerText = (avgEarning > 0 ? '+' : '') + avgEarning.toFixed(2) + '%';
// 绑定删除事件
document.querySelectorAll('#baseTbody .delete-btn').forEach(btn => {
btn.removeEventListener('click', handleDelete);
btn.addEventListener('click', handleDelete);
});
}
function renderAdvanced() {
const dateInput = document.getElementById('advSingleDate');
const selectedDate = dateInput.value || null;
let filtered = filterStocksBySingleDate(advancedStocks, selectedDate);
const tbody = document.getElementById('advTbody');
if (filtered.length === 0) {
tbody.innerHTML = '<tr><td colspan="10" style="text-align:center; padding: 32px;">✨ 该日期无入池股票,请添加</td></tr>';
document.getElementById('advTotalCount').innerText = '0';
document.getElementById('advWinRate').innerText = '0%';
document.getElementById('advAvgReturn').innerText = '0%';
return;
}
let totalEarningSum = 0;
let winCount = 0;
let htmlRows = '';
for (let stock of filtered) {
const holdDays = calcHoldDays(stock.entryDateStr);
const maxGain = calcMaxGain(stock.entryPrice, stock.highPrice);
const currentEarning = calcCurrentEarningPercent(stock.entryPrice, stock.currentPrice);
const currentReturn = calcCurrentReturnPercent(stock.entryPrice, stock.currentPrice);
totalEarningSum += currentEarning;
if (currentEarning > 0) winCount++;
const earningClass = currentEarning >= 0 ? 'profit-positive' : 'profit-negative';
const returnClass = currentReturn >= 0 ? 'profit-positive' : 'profit-negative';
const maxGainClass = maxGain > 0 ? 'profit-positive' : '';
htmlRows += `
<tr>
<td style="font-weight:500;">${escapeHtml(stock.code)}</td>
<td>${escapeHtml(stock.name)}</td>
<td>${escapeHtml(stock.entryDateStr)}</td>
<td>${stock.entryPrice.toFixed(2)}</td>
<td>${stock.currentPrice.toFixed(2)}</td>
<td class="${earningClass}">${(currentEarning > 0 ? '+' : '')}${currentEarning.toFixed(2)}%</td>
<td class="${returnClass}">${(currentReturn > 0 ? '+' : '')}${currentReturn.toFixed(2)}%</td>
<td class="${maxGainClass}">${maxGain.toFixed(2)}%</td>
<td>${holdDays} 天</td>
<td><button class="delete-btn" data-id="${stock.id}" data-type="adv">删除</button></td>
</tr>
`;
}
tbody.innerHTML = htmlRows;
const totalCount = filtered.length;
const winRate = totalCount === 0 ? 0 : (winCount / totalCount) * 100;
const avgEarning = totalCount === 0 ? 0 : (totalEarningSum / totalCount);
document.getElementById('advTotalCount').innerText = totalCount;
document.getElementById('advWinRate').innerText = winRate.toFixed(2) + '%';
document.getElementById('advAvgReturn').innerText = (avgEarning > 0 ? '+' : '') + avgEarning.toFixed(2) + '%';
document.querySelectorAll('#advTbody .delete-btn').forEach(btn => {
btn.removeEventListener('click', handleDelete);
btn.addEventListener('click', handleDelete);
});
}
// 删除逻辑
function handleDelete(e) {
const btn = e.currentTarget;
const id = parseInt(btn.dataset.id);
const type = btn.dataset.type;
if (type === 'base') {
baseStocks = baseStocks.filter(s => s.id !== id);
saveBaseStocks(baseStocks);
renderBase();
} else if (type === 'adv') {
advancedStocks = advancedStocks.filter(s => s.id !== id);
saveAdvStocks(advancedStocks);
renderAdvanced();
}
}
// 添加基础版股票
function addBaseStock(code, name, price, entryDate) {
if (!code.trim() || !name.trim() || isNaN(price) || price <= 0) {
alert("请完整填写:代码/名称/有效价格");
return false;
}
if (!entryDate) {
entryDate = new Date().toISOString().slice(0,10);
}
// 组合入池时间字符串:日期 + 当前时间(时分秒)
const now = new Date();
const timePart = `${now.getHours().toString().padStart(2,'0')}:${now.getMinutes().toString().padStart(2,'0')}:${now.getSeconds().toString().padStart(2,'0')}`;
const entryDateTime = `${entryDate} ${timePart}`;
const entryPriceVal = parseFloat(price);
const newId = Date.now() + Math.random() * 10000;
const newStock = {
id: newId,
code: code.trim(),
name: name.trim(),
entryDateStr: entryDateTime,
entryPrice: entryPriceVal,
currentPrice: entryPriceVal,
highPrice: entryPriceVal
};
baseStocks.push(newStock);
saveBaseStocks(baseStocks);
renderBase();
return true;
}
function addAdvancedStock(code, name, price, entryDate) {
if (!code.trim() || !name.trim() || isNaN(price) || price <= 0) {
alert("请完整填写:代码/名称/有效价格");
return false;
}
if (!entryDate) {
entryDate = new Date().toISOString().slice(0,10);
}
const now = new Date();
const timePart = `${now.getHours().toString().padStart(2,'0')}:${now.getMinutes().toString().padStart(2,'0')}:${now.getSeconds().toString().padStart(2,'0')}`;
const entryDateTime = `${entryDate} ${timePart}`;
const entryPriceVal = parseFloat(price);
const newId = Date.now() + Math.random() * 10000;
const newStock = {
id: newId,
code: code.trim(),
name: name.trim(),
entryDateStr: entryDateTime,
entryPrice: entryPriceVal,
currentPrice: entryPriceVal,
highPrice: entryPriceVal
};
advancedStocks.push(newStock);
saveAdvStocks(advancedStocks);
renderAdvanced();
return true;
}
// 管理面板显示/隐藏
function bindManageButtons() {
const baseToggle = document.querySelector('.manage-toggle[data-target="base"]');
const advToggle = document.querySelector('.manage-toggle[data-target="adv"]');
const basePanel = document.getElementById('baseManagePanel');
const advPanel = document.getElementById('advManagePanel');
baseToggle.addEventListener('click', () => {
const isVisible = basePanel.style.display === 'flex';
basePanel.style.display = isVisible ? 'none' : 'flex';
baseToggle.classList.toggle('active', !isVisible);
});
advToggle.addEventListener('click', () => {
const isVisible = advPanel.style.display === 'flex';
advPanel.style.display = isVisible ? 'none' : 'flex';
advToggle.classList.toggle('active', !isVisible);
});
}
// 自动筛选监听
function bindAutoFilter() {
const baseDateInput = document.getElementById('baseSingleDate');
const advDateInput = document.getElementById('advSingleDate');
baseDateInput.addEventListener('change', () => renderBase());
advDateInput.addEventListener('change', () => renderAdvanced());
}
// 添加按钮事件
function bindAddButtons() {
document.getElementById('addBaseStockBtn').addEventListener('click', () => {
const code = document.getElementById('baseCode').value;
const name = document.getElementById('baseName').value;
const price = document.getElementById('basePrice').value;
const entryDate = document.getElementById('baseEntryDate').value;
if (addBaseStock(code, name, price, entryDate)) {
document.getElementById('baseCode').value = '';
document.getElementById('baseName').value = '';
document.getElementById('basePrice').value = '';
// 不清空日期,保留用户习惯
}
});
document.getElementById('addAdvStockBtn').addEventListener('click', () => {
const code = document.getElementById('advCode').value;
const name = document.getElementById('advName').value;
const price = document.getElementById('advPrice').value;
const entryDate = document.getElementById('advEntryDate').value;
if (addAdvancedStock(code, name, price, entryDate)) {
document.getElementById('advCode').value = '';
document.getElementById('advName').value = '';
document.getElementById('advPrice').value = '';
}
});
}
function escapeHtml(str) {
if (!str) return '';
return str.replace(/[&<>]/g, function(m) {
if (m === '&') return '&';
if (m === '<') return '<';
if (m === '>') return '>';
return m;
});
}
// 定时刷新持仓天数
function initAutoRefresh() {
setInterval(() => {
renderBase();
renderAdvanced();
}, 60000);
}
function init() {
renderBase();
renderAdvanced();
bindManageButtons();
bindAutoFilter();
bindAddButtons();
initAutoRefresh();
}
init();
</script>
</body>
</html>