@cxxgo/fund-valuation-query 1.0.4 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -7
- package/dist/fund.js +364 -87
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
# fund-valuation-query
|
|
2
2
|
|
|
3
|
-
基金实时估值查询 CLI
|
|
3
|
+
基金实时估值查询 CLI 工具。根据基金持仓股票的实时行情,展示基金最近一个收盘交易日表现和盘中估值。
|
|
4
4
|
|
|
5
5
|
## 原理
|
|
6
6
|
|
|
7
|
-
1.
|
|
7
|
+
1. 从东方财富获取基金最新报告期持仓数据(股票代码、名称、持仓占比)
|
|
8
8
|
2. 从腾讯行情 API 获取持仓股票的实时涨跌幅
|
|
9
|
-
3.
|
|
9
|
+
3. 从基金估值接口获取基金盘中估值
|
|
10
|
+
4. 从历史日线接口回溯最近一个已收盘交易日的涨跌
|
|
11
|
+
|
|
12
|
+
## 核心口径
|
|
13
|
+
|
|
14
|
+
### 主表
|
|
15
|
+
|
|
16
|
+
- `净值`:使用基金净值接口返回的最近一个净值日涨跌幅,拿不到则显示 `-`。
|
|
17
|
+
- `估值`:使用基金估值接口返回的涨跌幅。只要估值日期仍是对应市场的当天就展示,拿不到或日期不匹配时显示 `-`。
|
|
18
|
+
|
|
19
|
+
### 明细
|
|
20
|
+
|
|
21
|
+
- `收盘涨跌`:每只持仓股票“最近一个已收盘交易日”的日涨跌幅。展示规则:如果当前市场已经收盘,则展示最新收盘日;如果当前仍在交易中,则展示今天之前最近一个已收盘交易日。拿不到历史日线就直接显示 `-`。
|
|
22
|
+
- `涨跌`:表示持仓股票当前交易日的实时或最新涨跌幅。只要行情数据日期仍是对应市场的当天就展示,拿不到或日期不匹配时显示 `-`。
|
|
23
|
+
- `权重估值`:按“当前拿到盘中涨跌的持仓”做持仓占比加权汇总。
|
|
24
|
+
- `覆盖`:当前拿到有效盘中涨跌的持仓权重占比。只有覆盖率达到 `60%` 时才展示 `权重估值`,否则显示 `-`。
|
|
25
|
+
- `明细刷新`:只刷新当前基金持仓股的盘中价格,不刷新持仓结构;持仓结构按天缓存。
|
|
10
26
|
|
|
11
27
|
## 安装
|
|
12
28
|
|
|
@@ -22,7 +38,7 @@ npm i @cxxgo/fund-valuation-query -g
|
|
|
22
38
|
fund list
|
|
23
39
|
```
|
|
24
40
|
|
|
25
|
-
|
|
41
|
+
按今日涨跌降序展示。
|
|
26
42
|
|
|
27
43
|
### 查看单只基金持仓详情
|
|
28
44
|
|
|
@@ -30,7 +46,7 @@ fund list
|
|
|
30
46
|
fund detail <基金代码>
|
|
31
47
|
```
|
|
32
48
|
|
|
33
|
-
|
|
49
|
+
展示该基金最新报告期持仓股票及各自上个交易日、今日涨跌。
|
|
34
50
|
|
|
35
51
|
### Web 服务
|
|
36
52
|
|
|
@@ -48,7 +64,7 @@ PORT=8080 fund serve
|
|
|
48
64
|
启动后访问 `http://localhost:8888` 即可使用可视化界面,功能包括:
|
|
49
65
|
|
|
50
66
|
- 首次访问需选择配置文件(上传 JSON 文件或粘贴内容),配置保存在浏览器 localStorage
|
|
51
|
-
-
|
|
67
|
+
- 点击表头按基金代码、名称、上个交易日、今日涨跌排序
|
|
52
68
|
- 点击基金名称,右侧抽屉展开持仓明细
|
|
53
69
|
- 抽屉内支持按占比、涨跌排序
|
|
54
70
|
- 手动刷新按钮 + 30 秒自动刷新
|
|
@@ -77,7 +93,7 @@ fund config
|
|
|
77
93
|
|
|
78
94
|
## 技术栈
|
|
79
95
|
|
|
80
|
-
-
|
|
96
|
+
- **数据源**:东方财富(持仓/净值)、天天基金(基金估值)、腾讯行情(实时股价)、Nasdaq(美股/ADR 历史日线)
|
|
81
97
|
- **CLI**:Commander.js + cli-table3 + chalk
|
|
82
98
|
- **Web**:Express 内嵌 HTML/JS/CSS
|
|
83
99
|
- **HTML 解析**:cheerio
|
package/dist/fund.js
CHANGED
|
@@ -1,14 +1,42 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{Command as
|
|
3
|
-
`)
|
|
4
|
-
${t.todayLabel||""}
|
|
5
|
-
${t
|
|
6
|
-
${
|
|
7
|
-
|
|
2
|
+
import{Command as tt}from"commander";import{readFileSync as at}from"fs";import y from"axios";import*as G from"cheerio";function Z(e){let t=G.load(e),n=[];return t("table").first().find("tbody tr, tr").each((o,r)=>{let s=t(r).find("td");if(s.length<7)return;let d=s.eq(1).find("a"),i=d.text().trim();if(!i)return;let u=(d.attr("href")||"").match(/\/r\/(\d+)\.(\w+)/),f=u?parseInt(u[1]):i.startsWith("6")?1:0,g=f>=100,p=s.eq(2).find("a").text().trim()||s.eq(2).text().trim(),l=s.eq(6).text().trim().replace("%",""),m=parseFloat(l);isNaN(m)||n.push({stockCode:i,stockName:p,holdingRatio:m,market:f,isOverseas:g})}),n}var I={"User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",Referer:"https://fund.eastmoney.com/"},N=1e3,ke=60*N,B=60*ke,R=24*B,T=new Map;async function v(e,t,n,{force:a=!1}={}){let o=Date.now(),r=T.get(e);if(!a&&r?.value!==void 0&&r.expiresAt>o)return r.value;if(r?.promise)return r.promise;let s=(async()=>{try{let d=await n();return T.set(e,{value:d,expiresAt:o+t,promise:null}),d}catch(d){if(r?.value!==void 0&&!a)return T.set(e,r),r.value;throw T.delete(e),d}})();return T.set(e,{value:r?.value,expiresAt:r?.expiresAt||0,promise:s}),s}async function P(e){return v(`fund-holdings:${e}`,R,async()=>{let t=`https://fundf10.eastmoney.com/FundArchivesDatas.aspx?type=jjcc&code=${e}&topline=50`,{data:n}=await y.get(t,{responseType:"text",headers:I,timeout:8e3}),a=n.match(/content:"([\s\S]*?)",arryear:/);return a?Z(a[1]):[]})}async function j(e,{force:t=!1}={}){try{return await v(`fund-estimate:${e}`,30*N,async()=>{let n=`http://fundgz.1234567.com.cn/js/${e}.js`,{data:a}=await y.get(n,{responseType:"text",timeout:5e3}),o=a.match(/jsonpgz\((.+)\)/);if(!o)return null;let r=JSON.parse(o[1]);return r.gszzl==null||!r.gztime?null:{change:parseFloat(r.gszzl),time:r.gztime,navDate:F(r.jzrq)}},{force:t})}catch{return null}}function ee(e){let t=new Intl.DateTimeFormat("en-CA",{timeZone:"Asia/Shanghai",year:"numeric",month:"2-digit",day:"2-digit"}).formatToParts(new Date(e)),n=a=>t.find(o=>o.type===a)?.value||"";return`${n("year")}-${n("month")}-${n("day")}`}function X(e,t="Asia/Shanghai"){let n=new Intl.DateTimeFormat("en-CA",{timeZone:t,year:"numeric",month:"2-digit",day:"2-digit"}).formatToParts(e),a=o=>n.find(r=>r.type===o)?.value||"";return`${a("year")}-${a("month")}-${a("day")}`}async function De(e){return v(`stock-history:nasdaq:${e}`,12*B,async()=>{let t=new Date,n=new Date(t.getTime()-30*R),a=`https://api.nasdaq.com/api/quote/${encodeURIComponent(e)}/historical?assetclass=stocks&fromdate=${X(n)}&limit=12&todate=${X(t)}`,o=null;for(let i=0;i<3;i+=1)try{({data:o}=await y.get(a,{timeout:8e3,headers:{"User-Agent":I["User-Agent"],Accept:"application/json, text/plain, */*",Origin:"https://www.nasdaq.com",Referer:"https://www.nasdaq.com/"}}));break}catch(c){if(c?.response?.status!==429||i===2)throw c;await te(500*(i+1))}let r=o?.data?.tradesTable?.rows;if(!Array.isArray(r)||r.length<2)return{latestDate:null,changesByDate:new Map};let s=r.map(i=>({date:Ce(i?.date),close:Te(i?.close)})).filter(i=>i.date&&Number.isFinite(i.close)).reverse();if(s.length<2)return{latestDate:null,changesByDate:new Map};let d=new Map;for(let i=1;i<s.length;i+=1){let c=s[i-1].close,u=s[i];c!==0&&d.set(u.date,(u.close-c)/c*100)}return{latestDate:s[s.length-1]?.date||null,changesByDate:d}})}function Ce(e){if(!e)return null;let[t,n,a]=String(e).split("/");return!a||!t||!n?null:`${a}-${t.padStart(2,"0")}-${n.padStart(2,"0")}`}function Te(e){return e==null?Number.NaN:Number.parseFloat(String(e).replace(/[$,]/g,""))}function te(e){return new Promise(t=>setTimeout(t,e))}async function L(e){try{return await v(`fund-networth:${e}`,R,async()=>{let t=`https://fund.eastmoney.com/pingzhongdata/${e}.js?v=${Date.now()}`,{data:n}=await y.get(t,{responseType:"text",timeout:8e3,headers:I}),a=n.match(/var Data_netWorthTrend = (\[[\s\S]*?\]);\/\*/);if(!a)return null;let o=JSON.parse(a[1]),r=o[o.length-1];return!r||typeof r.x!="number"||typeof r.equityReturn!="number"?null:{date:ee(r.x),change:r.equityReturn}})}catch{return null}}function Ne({code:e,market:t}){return/^(8|9)\d{5}$/.test(e)?`bj${e}`:t===1?`sh${e}`:t===0?`sz${e}`:t===116?`r_hk${e}`:t>=100?`us${e}`:null}function Se({code:e,market:t}){return/^(8|9)\d{5}$/.test(e)?`bj${e}`:t===1?`sh${e}`:t===0?`sz${e}`:t===116?`hk${e}`:t>=100?`us${e}`:null}function F(e){if(!e)return null;if(/^\d{14}$/.test(e))return`${e.slice(0,4)}-${e.slice(4,6)}-${e.slice(6,8)}`;let t=e.replace(/\//g,"-");return/^\d{4}-\d{2}-\d{2}/.test(t)?t.slice(0,10):null}async function O(){try{return await v("china-market-date",30*N,async()=>{let{data:e}=await y.get("https://qt.gtimg.cn/q=sh000001",{responseType:"arraybuffer",timeout:5e3}),n=new TextDecoder("gbk").decode(e).match(/^v_[^=]+="(.+)";?$/m);if(!n)return null;let a=n[1].split("~");return F(a[30])})}catch{return null}}var Me=[{code:"sh000001",name:"\u4E0A\u8BC1\u6307\u6570"},{code:"sz399001",name:"\u6DF1\u8BC1\u6210\u6307"},{code:"sz399006",name:"\u521B\u4E1A\u677F\u6307"},{code:"bj899050",name:"\u5317\u8BC150"},{code:"sh000688",name:"\u79D1\u521B50"},{code:"sh000016",name:"\u4E0A\u8BC150"},{code:"sz399330",name:"\u6DF1\u8BC1100"},{code:"sh000300",name:"\u6CAA\u6DF1300"},{code:"sh000905",name:"\u4E2D\u8BC1500"},{code:"sh000852",name:"\u4E2D\u8BC11000"},{code:"sh000012",name:"\u56FD\u503A\u6307\u6570"},{code:"sh000013",name:"\u4F01\u503A\u6307\u6570"}];function Le(){return Me.map(e=>({...e}))}function Fe(e){let t=e.match(/^v_[^=]+="(.+)";?$/m);return t?t[1].split("~"):null}function ae(){let e=new Intl.DateTimeFormat("en-GB",{timeZone:"Asia/Shanghai",hour:"2-digit",minute:"2-digit",hour12:!1}).formatToParts(new Date),t=n=>e.find(a=>a.type===n)?.value||"";return`${t("hour")}:${t("minute")}`}function Oe(e){let t=ee(Date.now());if(!e||e!==t)return"closed";let n=ae(),a=Number.parseInt(n.slice(0,2),10)*60+Number.parseInt(n.slice(3,5),10),o=a>=570&&a<=690,r=a>=780&&a<=900;return a>690&&a<780?"midday_break":o||r?"trading":"closed"}async function ne({force:e=!1}={}){let t=Le();try{return await v("market-overview",30*N,async()=>{let n=`https://qt.gtimg.cn/q=${t.map(c=>c.code).join(",")}`,{data:a}=await y.get(n,{responseType:"arraybuffer",timeout:5e3}),o=new TextDecoder("gbk").decode(a),r=new Map;for(let c of o.trim().split(`
|
|
3
|
+
`)){let u=c.match(/^v_([^=]+)=/);if(!u)continue;let f=Fe(c);!f||f.length<33||r.set(u[1],f)}let s=r.get(t[0].code),d=F(s?.[30])||null,i=Oe(d);return{status:i,dateLabel:d?d.slice(5):"",timeLabel:i==="trading"?ae():"",items:t.map(c=>{let u=r.get(c.code),f=Number.parseFloat(u?.[3]),g=Number.parseFloat(u?.[4]),p=Number.parseFloat(u?.[32]);return{code:c.code,name:c.name,value:Number.isFinite(f)?f:null,change:Number.isFinite(f)&&Number.isFinite(g)?f-g:null,changePercent:Number.isFinite(p)?p:null}})}},{force:e})}catch{return{status:"closed",dateLabel:"",timeLabel:"",items:t.map(n=>({...n,value:null,change:null,changePercent:null}))}}}async function z(e){let t=new Map;if(e.length===0)return t;await Promise.all(e.map(async({code:a,market:o})=>{if(o>=100)return;let r=Se({code:a,market:o});if(!r){t.set(a,{latestDate:null,changesByDate:new Map});return}try{let s=await v(`stock-history:${r}`,12*B,async()=>{let d=`https://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param=${r},day,,,30,qfq`,{data:i}=await y.get(d,{timeout:5e3,headers:{Referer:"https://gu.qq.com/"}}),c=i?.data?.[r],u=c?.qfqday||c?.day;if(!Array.isArray(u)||u.length===0)return{latestDate:null,changesByDate:new Map};let f=new Map;for(let g=1;g<u.length;g+=1){let p=u[g-1],l=u[g],m=l?.[0],x=parseFloat(p?.[2]),k=parseFloat(l?.[2]);if(!m||Number.isNaN(x)||Number.isNaN(k)||x===0)continue;let D=(k-x)/x*100;f.set(m,D)}return{latestDate:u[u.length-1]?.[0]||null,changesByDate:f}});t.set(a,s)}catch{t.set(a,{latestDate:null,changesByDate:new Map})}}));let n=0;for(let{code:a,market:o}of e)if(!(o<100)){try{n>0&&await te(250);let r=await De(a);t.set(a,r)}catch{t.set(a,{latestDate:null,changesByDate:new Map})}n+=1}return t}async function re(e,{force:t=!1}={}){let n=new Map;if(e.length===0)return n;let a=e.map(({code:r,market:s})=>({code:r,symbol:Ne({code:r,market:s})})).filter(r=>r.symbol);if(a.length===0)return n;let o=new Map(a.map(r=>[r.symbol,r.code]));try{let r=a.map(d=>d.symbol).sort().join(","),s=await v(`stock-quotes:${r}`,20*N,async()=>{let d=`https://qt.gtimg.cn/q=${a.map(g=>g.symbol).join(",")}`,{data:i}=await y.get(d,{responseType:"arraybuffer",timeout:5e3}),c=new TextDecoder("gbk").decode(i),u=new Map,f=c.trim().split(`
|
|
4
|
+
`);for(let g of f){let p=g.match(/^v_([^=]+)="(.+)";?$/);if(!p)continue;let l=p[1],m=p[2].split("~");m.length<33||u.set(l,{name:m[1],changePercent:parseFloat(m[32]),quoteDate:F(m[30])})}return u},{force:t});for(let[d,i]of o.entries())s.has(d)&&n.set(i,s.get(d))}catch{}return n}import{readFileSync as H,writeFileSync as $e,existsSync as q}from"fs";import{homedir as Ee}from"os";import{join as Ie,resolve as Be}from"path";var A=Ie(Ee(),".fundrc.json");function K(){return q(A)&&JSON.parse(H(A,"utf-8")).configPath||null}function oe(e){let t=Be(e);q(t)||(console.error(`\u914D\u7F6E\u6587\u4EF6\u4E0D\u5B58\u5728: ${t}`),process.exit(1));try{let n=JSON.parse(H(t,"utf-8"));(!n||typeof n!="object"||Object.keys(n).length===0)&&(console.error("\u914D\u7F6E\u6587\u4EF6\u4E3A\u7A7A\u6216\u683C\u5F0F\u9519\u8BEF"),process.exit(1))}catch{console.error("\u914D\u7F6E\u6587\u4EF6\u683C\u5F0F\u9519\u8BEF\uFF0C\u9700\u8981\u5408\u6CD5\u7684 JSON"),process.exit(1)}$e(A,JSON.stringify({configPath:t},null,2)),console.log(`\u5DF2\u8BBE\u7F6E\u914D\u7F6E\u6587\u4EF6: ${t}`)}function U(){let e=K();return e||(console.error("\u672A\u8BBE\u7F6E\u914D\u7F6E\u6587\u4EF6\uFF0C\u8BF7\u8FD0\u884C: fund config <\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84>"),process.exit(1)),q(e)||(console.error(`\u914D\u7F6E\u6587\u4EF6\u4E0D\u5B58\u5728: ${e}\uFF0C\u8BF7\u91CD\u65B0\u8BBE\u7F6E: fund config <\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84>`),process.exit(1)),JSON.parse(H(e,"utf-8"))}function ie(e,t){return t.some(a=>a.isOverseas)||/全球|QDII|美元|港|海外|纳斯达克|标普|道琼斯/.test(e)?"\u5168\u7403":/ETF|指数/.test(e)?"\u6307\u6570":/债/.test(e)?"\u504F\u503A":"\u504F\u80A1"}var S="Asia/Shanghai";function Re(e=S,t=new Date){return new Date(t.toLocaleString("en-US",{timeZone:e}))}function Pe(e){return[e.getFullYear(),String(e.getMonth()+1).padStart(2,"0"),String(e.getDate()).padStart(2,"0")].join("-")}function je(){return Pe(Re())}function h(e,t=new Date){let n=new Intl.DateTimeFormat("en-CA",{timeZone:e,year:"numeric",month:"2-digit",day:"2-digit",weekday:"short",hour:"2-digit",minute:"2-digit",hour12:!1}).formatToParts(t),a=o=>n.find(r=>r.type===o)?.value||"";return{date:`${a("year")}-${a("month")}-${a("day")}`,weekday:a("weekday"),minutes:Number.parseInt(a("hour"),10)*60+Number.parseInt(a("minute"),10)}}function _(e){return["Mon","Tue","Wed","Thu","Fri"].includes(e)}function ze(e=new Date){let t=h(S,e);return _(t.weekday)?t.minutes>900||t.minutes<570:!0}function Ae(e=new Date){let t=h("Asia/Hong_Kong",e);return _(t.weekday)?t.minutes>960||t.minutes<570:!0}function He(e=new Date){let t=h("America/New_York",e);return _(t.weekday)?t.minutes>960||t.minutes<570:!0}function qe(e,t,n=new Date){return!t?.quoteDate||!w(t.changePercent)?!1:e.market===116?t.quoteDate===h("Asia/Hong_Kong",n).date:e.market>=100?t.quoteDate===h("America/New_York",n).date:t.quoteDate===h(S,n).date}function Ke(e,t){return!!(e&&t&&e===t)}function Ue(e,t=new Date){return e.market===116?h("Asia/Hong_Kong",t).date:e.market>=100?h("America/New_York",t).date:h(S,t).date}function _e(e,t=new Date){return e.market===116?Ae(t):e.market>=100?He(t):ze(t)}function Je(e,t,n=new Date){let a=t.get(e.stockCode);if(!a)return null;let o=[...a.changesByDate.keys()].sort();if(o.length===0)return null;let r=Ue(e,n);if(_e(e,n))return o[o.length-1]||null;let d=o.filter(i=>i<r);return d[d.length-1]||null}function se(e,t,n=new Date){if(!e?.time||!w(e.change))return!1;let a=e.time.slice(0,10);if(t==="\u5168\u7403"){let r=h("America/New_York",n),s=h("Asia/Hong_Kong",n);return a===r.date||a===s.date}let o=h(S,n).date;return a===o}function $(e){return!e||e.length<10?"":e.slice(5)}function w(e){return typeof e=="number"&&!Number.isNaN(e)}function J(e){return e.map(t=>({code:t.stockCode,market:t.market}))}function W(e,t,n,a=null){let o=je(),r=new Date,s=0,d=e.map(l=>{let m=t.get(l.stockCode),x=n.get(l.stockCode),k=Je(l,n,r),D=qe(l,m,r)&&(l.isOverseas||Ke(o,a))?m.changePercent:null,we=k,E=k&&x?x.changesByDate.get(k)??null:null,xe=w(D)?l.holdingRatio*D/100:0;return w(D)&&(s+=l.holdingRatio),{stockCode:l.stockCode,stockName:l.stockName,holdingRatio:l.holdingRatio,todayChange:D,previousTradingDayChange:E,previousTradingDayDate:we,contribution:w(E)?l.holdingRatio*E/100:0,weightedTodayContribution:xe,isOverseas:l.isOverseas}}),c=d.length>0&&d.every(l=>w(l.previousTradingDayChange))?d.reduce((l,m)=>l+m.contribution,0):null,u=s>0?s/100:0,f=u>=.6?d.reduce((l,m)=>l+m.weightedTodayContribution,0):null,g=d.map(l=>l.previousTradingDayDate).filter(Boolean).sort(),p=g.length>0?g[g.length-1]:null;return{previousTradingDayChange:c,weightedTodayChange:f,coverageRatio:u,contributions:d,todayDate:o,previousTradingDate:p}}async function Q(e){let t=new Map;return await Promise.all(Object.keys(e).map(async n=>{try{t.set(n,await P(n))}catch{t.set(n,[])}})),t}function We(e){let t=new Map;for(let[,n]of e)for(let a of n)t.has(a.stockCode)||t.set(a.stockCode,{code:a.stockCode,market:a.market});return[...t.values()]}async function Y(e,t){return re(We(e),t)}function de(e,t,n,a,o){let r=ie(t,n);return{code:e,name:t,todayChange:se(a,r)?a.change:null,previousTradingDayChange:o?.change??null,previousTradingDayLabel:$(o?.date??null),category:r,canDetail:!0}}async function ce(e,t,n,a,o=null){let[r,s,d]=await Promise.all([z(J(n)),j(e),L(e)]),i=W(n,a,r,o);return{...de(e,t,n,s,d),contributions:i.contributions,weightedTodayChange:i.weightedTodayChange,coverageRatio:i.coverageRatio,todayLabel:$(i.todayDate)}}async function V(e,{forceRefresh:t=!1}={}){let n=await Q(e);return(await Promise.all(Object.entries(e).map(async([o,r])=>{let s=n.get(o)||[],[d,i]=await Promise.all([j(o,{force:t}),L(o)]);return{code:o,name:r,holdings:s,estimate:d,latestNetWorth:i}}))).map(({code:o,name:r,holdings:s,estimate:d,latestNetWorth:i})=>de(o,r,s,d,i))}async function le(e){let[t,n]=await Promise.all([V(e,{forceRefresh:!0}),ne({force:!0})]);return{funds:t,marketOverview:n}}async function ue(e,t,{forceRefreshPrices:n=!1}={}){let a=await P(e),o=new Map([[e,a]]),[r,s,d,i]=await Promise.all([Y(o,{force:n}),O(),z(J(a)),L(e)]),c=W(a,r,d,s);return{code:e,name:t,previousTradingDayChange:i?.change??null,previousTradingDayLabel:$(i?.date??null),weightedTodayChange:w(c.weightedTodayChange)?c.weightedTodayChange:null,coverageRatio:c.coverageRatio,contributions:c.contributions}}import M from"chalk";import fe from"cli-table3";function C(e){if(e==null||Number.isNaN(e))return"-";let t=parseFloat(e);return t>0?M.red(`+${t.toFixed(2)}%`):t<0?M.green(`${t.toFixed(2)}%`):`${t.toFixed(2)}%`}function ge(e,t,n){return!t||t===n?C(e):`${C(e)}
|
|
5
|
+
${M.gray(t)}`}function Qe(e,t){return t?`${e} (${t})`:e}function me(e,t){let n="";for(let a of e){let o=t(a);o&&o>n&&(n=o)}return n}function pe(e,t){return t?`${e}
|
|
6
|
+
${t}`:e}function he(e){let{code:t,name:n,todayChange:a,previousTradingDayChange:o,contributions:r}=e,s=me(r,i=>i.previousTradingDayDate?.slice(5)||""),d=new fe({head:["\u6301\u4ED3","\u5360\u6BD4",pe("\u6536\u76D8\u6DA8\u8DCC",s),"\u6DA8\u8DCC"],style:{head:["cyan"]}});for(let i of r)d.push([`${i.stockCode} ${i.stockName}`,`${i.holdingRatio.toFixed(2)}%`,ge(i.previousTradingDayChange,i.previousTradingDayDate?.slice(5),s),C(i.todayChange)]);console.log(`
|
|
7
|
+
${M.bold(t)} ${M.bold(n)}`),console.log(d.toString()),console.log(`${Qe("\u51C0\u503C",e.previousTradingDayLabel)}: ${C(o)}`),console.log(`\u6DA8\u8DCC: ${C(a)}
|
|
8
|
+
`)}function be(e){let t=me(e,a=>a.previousTradingDayLabel||""),n=new fe({head:["\u57FA\u91D1\u4EE3\u7801","\u57FA\u91D1\u540D\u79F0",pe("\u51C0\u503C",t),"\u4F30\u503C"],style:{head:["cyan"]}});for(let a of e)n.push([a.code,a.name,ge(a.previousTradingDayChange,a.previousTradingDayLabel,t),C(a.todayChange)]);console.log(n.toString())}import ye from"express";async function Ye(e){return le(e)}async function Ve(e,t,n=!1){return ue(e,t,{forceRefreshPrices:n})}function Ge(){return`
|
|
8
9
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
10
|
body { background: #f5f0eb; color: #4a4a4a; font-family: -apple-system, "Microsoft YaHei", sans-serif; padding: 24px; }
|
|
10
11
|
h1 { text-align: center; margin-bottom: 8px; font-size: 24px; color: #5a5a5a; }
|
|
11
|
-
.update-time { text-align: center; color: #b0a8a0; margin-bottom:
|
|
12
|
+
.update-time { text-align: center; color: #b0a8a0; margin-bottom: 16px; font-size: 13px; }
|
|
13
|
+
.market-overview { max-width: 700px; margin: 0 auto 16px; }
|
|
14
|
+
.market-overview.hidden { display: none; }
|
|
15
|
+
.market-overview-header {
|
|
16
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
17
|
+
gap: 12px; color: #8a7e74; margin-bottom: 12px;
|
|
18
|
+
}
|
|
19
|
+
.market-overview-status { font-size: 14px; color: #9b9188; }
|
|
20
|
+
.market-overview-toggle {
|
|
21
|
+
border: none; background: transparent; color: #9b9188; cursor: pointer;
|
|
22
|
+
font-size: 14px; display: inline-flex; align-items: center; gap: 4px;
|
|
23
|
+
}
|
|
24
|
+
.market-overview-toggle:hover { color: #7f746b; }
|
|
25
|
+
.market-overview-grid {
|
|
26
|
+
display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px;
|
|
27
|
+
}
|
|
28
|
+
.market-overview-grid.collapsed .market-card:nth-child(n + 4) { display: none; }
|
|
29
|
+
.market-card {
|
|
30
|
+
min-height: 76px; border-radius: 12px; padding: 10px 10px;
|
|
31
|
+
background: linear-gradient(180deg, rgba(255,255,255,0.88) 0%, rgba(255,248,248,0.96) 100%);
|
|
32
|
+
box-shadow: inset 0 1px 0 rgba(255,255,255,0.55);
|
|
33
|
+
}
|
|
34
|
+
.market-card.down {
|
|
35
|
+
background: linear-gradient(180deg, rgba(241,255,252,0.96) 0%, rgba(232,248,244,0.96) 100%);
|
|
36
|
+
}
|
|
37
|
+
.market-card-name { font-size: 13px; color: #504842; margin-bottom: 6px; }
|
|
38
|
+
.market-card-value { font-size: 17px; line-height: 1.1; font-weight: 700; margin-bottom: 5px; }
|
|
39
|
+
.market-card-change { font-size: 12px; font-weight: 600; }
|
|
12
40
|
table { border-collapse: collapse; width: 100%; max-width: 700px; margin: 0 auto 16px; }
|
|
13
41
|
th, td { padding: 10px 14px; text-align: center; border-bottom: 1px solid #e8e0d8; }
|
|
14
42
|
th { background: #ede6df; color: #8a7e74; font-weight: 600; }
|
|
@@ -16,7 +44,10 @@ ${w.bold(e)} ${w.bold(n)}`),console.log(r.toString());let c=y(a);console.log(`\u
|
|
|
16
44
|
.up { color: #d4756b; }
|
|
17
45
|
.down { color: #6b9e78; }
|
|
18
46
|
.flat { color: #a09890; }
|
|
19
|
-
.
|
|
47
|
+
.header-label { display: inline-block; line-height: 1.2; white-space: nowrap; }
|
|
48
|
+
.header-date { display: block; font-size: 11px; color: #b8b0a8; margin-top: 3px; line-height: 1.1; font-weight: 500; }
|
|
49
|
+
.cell-value { display: block; line-height: 1.2; }
|
|
50
|
+
.cell-date { display: block; font-size: 11px; color: #b8b0a8; margin-top: 3px; line-height: 1.1; }
|
|
20
51
|
.fund-name { cursor: pointer; color: #6a9ec5; }
|
|
21
52
|
.fund-name:hover { text-decoration: underline; }
|
|
22
53
|
.fund-name.disabled { cursor: default; color: #4a4a4a; }
|
|
@@ -47,7 +78,35 @@ ${w.bold(e)} ${w.bold(n)}`),console.log(r.toString());let c=y(a);console.log(`\u
|
|
|
47
78
|
}
|
|
48
79
|
.config-close:hover { color: #8a7e74; }
|
|
49
80
|
.drawer-title { color: #6a5e54; font-size: 16px; font-weight: 600; margin-bottom: 6px; }
|
|
50
|
-
.drawer-subtitle { color: #b0a8a0; font-size: 13px;
|
|
81
|
+
.drawer-subtitle { color: #b0a8a0; font-size: 13px; }
|
|
82
|
+
.drawer-header {
|
|
83
|
+
display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 16px;
|
|
84
|
+
}
|
|
85
|
+
.drawer-actions {
|
|
86
|
+
display: flex; align-items: center; gap: 10px; flex-shrink: 0;
|
|
87
|
+
}
|
|
88
|
+
.drawer-estimate {
|
|
89
|
+
color: #8a7e74; font-size: 12px; white-space: nowrap;
|
|
90
|
+
}
|
|
91
|
+
.drawer-estimate strong { font-size: 14px; margin-left: 4px; }
|
|
92
|
+
.drawer-info-label {
|
|
93
|
+
display: inline-flex; align-items: center; gap: 4px; cursor: help;
|
|
94
|
+
text-decoration: underline dotted rgba(176, 168, 160, 0.85);
|
|
95
|
+
text-underline-offset: 2px;
|
|
96
|
+
}
|
|
97
|
+
.drawer-coverage {
|
|
98
|
+
color: #b0a8a0; font-size: 11px; margin-top: 2px; text-align: right;
|
|
99
|
+
}
|
|
100
|
+
.drawer-refresh-btn {
|
|
101
|
+
display: inline-flex; align-items: center; gap: 6px; padding: 5px 12px;
|
|
102
|
+
font-size: 12px; color: #8a7e74; background: #ede6df; border: none;
|
|
103
|
+
border-radius: 999px; cursor: pointer;
|
|
104
|
+
}
|
|
105
|
+
.drawer-refresh-btn:hover { background: #e0d6cc; color: #6a5e54; }
|
|
106
|
+
.drawer-refresh-btn.spinning svg { animation: spin 0.6s linear infinite; }
|
|
107
|
+
.drawer-loading, .drawer-empty {
|
|
108
|
+
color: #9b9188; font-size: 13px; padding: 24px 0;
|
|
109
|
+
}
|
|
51
110
|
.drawer table { max-width: 100%; }
|
|
52
111
|
.drawer th { background: #f0ebe5; color: #8a7e74; font-size: 13px; padding: 8px 10px; min-width: 80px; }
|
|
53
112
|
.drawer td { font-size: 13px; padding: 8px 10px; border-bottom: 1px solid #ece4dc; }
|
|
@@ -59,11 +118,21 @@ ${w.bold(e)} ${w.bold(n)}`),console.log(r.toString());let c=y(a);console.log(`\u
|
|
|
59
118
|
.refresh-btn:hover { background: #e0d6cc; color: #6a5e54; }
|
|
60
119
|
.refresh-btn.spinning svg { animation: spin 0.6s linear infinite; }
|
|
61
120
|
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
|
62
|
-
th.sortable { cursor: pointer; user-select: none; vertical-align: middle; }
|
|
121
|
+
th.sortable { position: relative; cursor: pointer; user-select: none; vertical-align: middle; padding-right: 28px; white-space: nowrap; }
|
|
63
122
|
th.sortable:hover { color: #5a5a5a; }
|
|
64
|
-
th.sortable::after {
|
|
65
|
-
|
|
66
|
-
|
|
123
|
+
th.sortable::after {
|
|
124
|
+
content: '';
|
|
125
|
+
position: absolute;
|
|
126
|
+
top: 50%;
|
|
127
|
+
right: 10px;
|
|
128
|
+
transform: translateY(-50%);
|
|
129
|
+
font-size: 11px;
|
|
130
|
+
line-height: 1;
|
|
131
|
+
opacity: 0;
|
|
132
|
+
}
|
|
133
|
+
th.sortable.is-sortable::after { content: '\u21C5'; opacity: 0.4; }
|
|
134
|
+
th.sortable.asc::after { content: '\u2191'; opacity: 1; }
|
|
135
|
+
th.sortable.desc::after { content: '\u2193'; opacity: 1; }
|
|
67
136
|
.config-overlay {
|
|
68
137
|
display: flex; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
|
69
138
|
background: rgba(0,0,0,0.15); z-index: 2000;
|
|
@@ -111,9 +180,22 @@ ${w.bold(e)} ${w.bold(n)}`),console.log(r.toString());let c=y(a);console.log(`\u
|
|
|
111
180
|
}
|
|
112
181
|
.filter-tab:hover { background: #ede6df; }
|
|
113
182
|
.filter-tab.active { background: #6a9ec5; color: #fff; border-color: #6a9ec5; }
|
|
114
|
-
|
|
183
|
+
@media (max-width: 768px) {
|
|
184
|
+
body { padding: 16px; }
|
|
185
|
+
.market-overview-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
186
|
+
.market-card { min-height: 72px; padding: 9px 9px; }
|
|
187
|
+
.drawer { width: min(460px, 100vw); }
|
|
188
|
+
}
|
|
189
|
+
`}function Ze(){return`
|
|
115
190
|
<h1>\u57FA\u91D1\u4F30\u503C\u67E5\u8BE2</h1>
|
|
116
191
|
<div class="update-time" id="updateTime"><span id="timeText"></span><button class="refresh-btn" id="refreshBtn"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 4v6h-6"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg></button></div>
|
|
192
|
+
<section class="market-overview hidden" id="marketOverview">
|
|
193
|
+
<div class="market-overview-header">
|
|
194
|
+
<div class="market-overview-status" id="marketOverviewStatus">\u4F11\u5E02</div>
|
|
195
|
+
<button class="market-overview-toggle" id="marketOverviewToggle">\u5C55\u5F00</button>
|
|
196
|
+
</div>
|
|
197
|
+
<div class="market-overview-grid collapsed" id="marketOverviewGrid"></div>
|
|
198
|
+
</section>
|
|
117
199
|
<div class="filter-tabs" id="filterTabs">
|
|
118
200
|
<button class="filter-tab active" data-cat="" data-label="\u5168\u90E8">\u5168\u90E8 0</button>
|
|
119
201
|
<button class="filter-tab" data-cat="\u504F\u80A1" data-label="\u504F\u80A1">\u504F\u80A1 0</button>
|
|
@@ -122,7 +204,7 @@ ${w.bold(e)} ${w.bold(n)}`),console.log(r.toString());let c=y(a);console.log(`\u
|
|
|
122
204
|
<button class="filter-tab" data-cat="\u5168\u7403" data-label="\u5168\u7403">\u5168\u7403 0</button>
|
|
123
205
|
</div>
|
|
124
206
|
<table id="mainTable">
|
|
125
|
-
<thead><tr><th class="sortable" data-key="code">\u57FA\u91D1\u4EE3\u7801</th><th class="sortable" data-key="name">\u57FA\u91D1\u540D\u79F0</th><th class="sortable desc" data-key="
|
|
207
|
+
<thead><tr><th class="sortable is-sortable" data-key="code">\u57FA\u91D1\u4EE3\u7801</th><th class="sortable is-sortable" data-key="name">\u57FA\u91D1\u540D\u79F0</th><th class="sortable is-sortable" data-key="previousTradingDayChange">\u51C0\u503C</th><th class="sortable is-sortable desc" data-key="todayChange">\u4F30\u503C</th></tr></thead>
|
|
126
208
|
<tbody></tbody>
|
|
127
209
|
</table>
|
|
128
210
|
<div class="drawer-overlay" id="drawerOverlay"></div>
|
|
@@ -146,25 +228,23 @@ ${w.bold(e)} ${w.bold(n)}`),console.log(r.toString());let c=y(a);console.log(`\u
|
|
|
146
228
|
<div class="error-msg" id="errorMsg"></div>
|
|
147
229
|
</div>
|
|
148
230
|
</div>
|
|
149
|
-
`}function
|
|
150
|
-
|
|
151
|
-
// "2026-05-08 04:00" -> "05-07"\uFF08\u53D6\u524D\u4E00\u5929\u65E5\u671F\uFF09
|
|
152
|
-
const d = new Date(timeStr.replace(' ', 'T'));
|
|
153
|
-
d.setDate(d.getDate() - 1);
|
|
154
|
-
return (d.getMonth() + 1).toString().padStart(2, '0') + '-' + d.getDate().toString().padStart(2, '0');
|
|
155
|
-
}
|
|
156
|
-
|
|
231
|
+
`}function Xe(){return`
|
|
232
|
+
// \u4FDD\u5B58\u5F53\u524D\u9875\u9762\u7684\u6392\u5E8F\u3001\u7B5B\u9009\u3001\u4E3B\u8868\u6570\u636E\u548C\u62BD\u5C49\u72B6\u6001\u3002
|
|
157
233
|
const state = {
|
|
158
|
-
sortKey: '
|
|
234
|
+
sortKey: 'todayChange',
|
|
159
235
|
sortDir: 'desc',
|
|
160
236
|
currentData: [],
|
|
237
|
+
marketOverview: null,
|
|
238
|
+
marketExpanded: false,
|
|
161
239
|
activeCategory: '',
|
|
162
240
|
drawerSortKey: '',
|
|
163
241
|
drawerSortDir: 'desc',
|
|
164
242
|
drawerFund: null,
|
|
243
|
+
drawerLoading: false,
|
|
165
244
|
fundMap: {},
|
|
166
245
|
};
|
|
167
246
|
|
|
247
|
+
// \u6536\u62E2\u5E38\u7528\u8282\u70B9\u67E5\u8BE2\uFF0C\u907F\u514D\u5728\u4EA4\u4E92\u6D41\u7A0B\u91CC\u53CD\u590D\u67E5 DOM\u3002
|
|
168
248
|
const dom = {
|
|
169
249
|
filterTabs: document.getElementById('filterTabs'),
|
|
170
250
|
filterTabItems: () => document.querySelectorAll('.filter-tab'),
|
|
@@ -172,6 +252,10 @@ const dom = {
|
|
|
172
252
|
mainTableHead: document.getElementById('mainTable').querySelector('thead'),
|
|
173
253
|
mainTableBody: document.querySelector('#mainTable tbody'),
|
|
174
254
|
timeText: document.getElementById('timeText'),
|
|
255
|
+
marketOverview: document.getElementById('marketOverview'),
|
|
256
|
+
marketOverviewStatus: document.getElementById('marketOverviewStatus'),
|
|
257
|
+
marketOverviewToggle: document.getElementById('marketOverviewToggle'),
|
|
258
|
+
marketOverviewGrid: document.getElementById('marketOverviewGrid'),
|
|
175
259
|
drawer: document.getElementById('drawer'),
|
|
176
260
|
drawerOverlay: document.getElementById('drawerOverlay'),
|
|
177
261
|
drawerClose: document.getElementById('drawerClose'),
|
|
@@ -186,13 +270,15 @@ const dom = {
|
|
|
186
270
|
updateTime: document.getElementById('updateTime'),
|
|
187
271
|
};
|
|
188
272
|
|
|
273
|
+
// \u4E3B\u8868\u59CB\u7EC8\u5148\u6392\u5E8F\u518D\u8FC7\u6EE4\uFF0C\u907F\u514D\u5206\u7C7B\u6807\u7B7E\u4E0B\u6392\u5E8F\u987A\u5E8F\u5F02\u5E38\u3002
|
|
189
274
|
function renderFilteredTable() {
|
|
190
|
-
const filtered = getFilteredFunds();
|
|
191
275
|
sortData();
|
|
276
|
+
const filtered = getFilteredFunds();
|
|
192
277
|
renderFilterCounts();
|
|
193
278
|
renderTable(filtered);
|
|
194
279
|
}
|
|
195
280
|
|
|
281
|
+
// \u7EDF\u8BA1\u5206\u7C7B\u6570\u91CF\uFF0C\u9A71\u52A8\u9876\u90E8\u7B5B\u9009 tab \u6587\u6848\u3002
|
|
196
282
|
function renderFilterCounts() {
|
|
197
283
|
const counts = state.currentData.reduce((acc, fund) => {
|
|
198
284
|
acc[fund.category] = (acc[fund.category] || 0) + 1;
|
|
@@ -207,44 +293,131 @@ function renderFilterCounts() {
|
|
|
207
293
|
});
|
|
208
294
|
}
|
|
209
295
|
|
|
296
|
+
// \u6839\u636E\u6570\u503C\u6B63\u8D1F\u6620\u5C04\u7EA2/\u7EFF/\u7070\u6837\u5F0F\u3002
|
|
210
297
|
function colorClass(val) {
|
|
211
298
|
if (val === null || val === undefined || Number.isNaN(val)) return 'flat';
|
|
212
299
|
return val > 0 ? 'up' : val < 0 ? 'down' : 'flat';
|
|
213
300
|
}
|
|
301
|
+
|
|
302
|
+
// \u4E3B\u8868/\u62BD\u5C49\u91CC\u767E\u5206\u6BD4\u7EDF\u4E00\u683C\u5F0F\u5316\u4E3A +1.23% / -0.56%\u3002
|
|
214
303
|
function fmtPercent(val) {
|
|
215
304
|
if (val === null || val === undefined || Number.isNaN(val)) return '-';
|
|
216
305
|
const sign = val > 0 ? '+' : '';
|
|
217
306
|
return sign + val.toFixed(2) + '%';
|
|
218
307
|
}
|
|
219
308
|
|
|
309
|
+
// \u548C fmtPercent \u4FDD\u6301\u540C\u53E3\u5F84\uFF0C\u53EA\u662F\u8BED\u4E49\u4E0A\u5F3A\u8C03\u5141\u8BB8\u7A7A\u503C\u8F93\u5165\u3002
|
|
220
310
|
function fmtNullablePercent(val) {
|
|
221
311
|
if (val === null || val === undefined || Number.isNaN(val)) return '-';
|
|
222
312
|
return fmtPercent(val);
|
|
223
313
|
}
|
|
224
314
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
315
|
+
// \u5927\u76D8\u5361\u7247\u7684\u70B9\u4F4D\u5C55\u793A\u4FDD\u7559\u4E24\u4F4D\u5C0F\u6570\u3002
|
|
316
|
+
function fmtNullableNumber(val) {
|
|
317
|
+
if (val === null || val === undefined || Number.isNaN(val)) return '-';
|
|
318
|
+
return Number(val).toFixed(2);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// \u5927\u76D8\u5361\u7247\u7684\u6DA8\u8DCC\u989D\u5C55\u793A\uFF0C\u5982 +12.34\u3002
|
|
322
|
+
function fmtSignedNumber(val) {
|
|
323
|
+
if (val === null || val === undefined || Number.isNaN(val)) return '-';
|
|
324
|
+
return (val > 0 ? '+' : '') + Number(val).toFixed(2);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// \u8986\u76D6\u7387\u5C55\u793A\u4E3A\u767E\u5206\u6BD4\uFF0C\u548C\u201C\u6743\u91CD\u4F30\u503C\u201D\u8BF4\u660E\u6587\u6848\u4FDD\u6301\u540C\u4E00\u53E3\u5F84\u3002
|
|
328
|
+
function fmtCoverage(val) {
|
|
329
|
+
if (val === null || val === undefined || Number.isNaN(val)) return '-';
|
|
330
|
+
return (val * 100).toFixed(1) + '%';
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// \u628A\u53EF\u80FD\u8FDB\u5165 innerHTML \u7684\u6587\u672C\u505A\u6700\u5C0F\u8F6C\u4E49\uFF0C\u907F\u514D\u540D\u5B57\u91CC\u5E26\u7279\u6B8A\u5B57\u7B26\u65F6\u7834\u574F DOM\u3002
|
|
334
|
+
function escapeHtml(text) {
|
|
335
|
+
return String(text)
|
|
336
|
+
.replaceAll('&', '&')
|
|
337
|
+
.replaceAll('<', '<')
|
|
338
|
+
.replaceAll('>', '>')
|
|
339
|
+
.replaceAll('"', '"')
|
|
340
|
+
.replaceAll("'", ''');
|
|
341
|
+
}
|
|
229
342
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
);
|
|
343
|
+
// \u5F53\u5355\u5143\u683C\u65E5\u671F\u548C\u8868\u5934\u65E5\u671F\u4E0D\u4E00\u81F4\u65F6\uFF0C\u989D\u5916\u5728\u5355\u5143\u683C\u91CC\u8865\u4E00\u884C\u65E5\u671F\uFF0C\u63D0\u793A\u8DE8\u65E5\u6570\u636E\u3002
|
|
344
|
+
function fmtPercentWithDate(val, dateLabel, headerDate) {
|
|
345
|
+
const value = fmtNullablePercent(val);
|
|
346
|
+
if (!dateLabel || dateLabel === headerDate) return '<span class="cell-value">' + value + '</span>';
|
|
347
|
+
return '<span class="cell-value">' + value + '</span><span class="cell-date">' + dateLabel + '</span>';
|
|
234
348
|
}
|
|
235
349
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (
|
|
239
|
-
return '';
|
|
350
|
+
// \u8868\u5934\u6807\u9898\u91CC\u5141\u8BB8\u9644\u5E26\u4E00\u884C\u5C0F\u65E5\u671F\uFF0C\u4F8B\u5982\u201C\u51C0\u503C / 05-08\u201D\u3002
|
|
351
|
+
function fmtHeaderLabel(title, dateLabel) {
|
|
352
|
+
if (!dateLabel) return '<span class="header-label">' + title + '</span>';
|
|
353
|
+
return '<span class="header-label">' + title + '<span class="header-date">' + dateLabel + '</span></span>';
|
|
240
354
|
}
|
|
241
355
|
|
|
356
|
+
// \u7EDF\u8BA1\u5217\u8868\u4E2D\u7684\u6700\u5927\u65E5\u671F\uFF0C\u4F5C\u4E3A\u8BE5\u5217\u7684\u516C\u5171\u8868\u5934\u65E5\u671F\u3002
|
|
357
|
+
function getHeaderDate(items, getLabel) {
|
|
358
|
+
let bestLabel = '';
|
|
359
|
+
for (const item of items) {
|
|
360
|
+
const label = getLabel(item);
|
|
361
|
+
if (!label) continue;
|
|
362
|
+
if (label > bestLabel) {
|
|
363
|
+
bestLabel = label;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return bestLabel;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// \u4E3B\u8868\u548C\u62BD\u5C49\u8868\u683C\u5171\u7528\u540C\u4E00\u5957\u6392\u5E8F\u8868\u5934\uFF0C\u4FDD\u8BC1\u7BAD\u5934\u903B\u8F91\u4E00\u81F4\u3002
|
|
370
|
+
function buildSortableTh(label, key, activeKey, activeDir, extraAttrs = '') {
|
|
371
|
+
const classes = ['sortable', 'is-sortable'];
|
|
372
|
+
if (activeKey === key) classes.push(activeDir);
|
|
373
|
+
return '<th class="' + classes.join(' ') + '" ' + extraAttrs + '>' + label + '</th>';
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// \u5927\u76D8\u6A21\u5757\u9876\u90E8\u72B6\u6001\u6587\u6848\uFF1A\u4EA4\u6613\u4E2D/\u4F11\u5E02 + \u65E5\u671F\u65F6\u95F4\u3002
|
|
377
|
+
function buildMarketStatusLabel(data) {
|
|
378
|
+
if (!data) return '';
|
|
379
|
+
const prefix =
|
|
380
|
+
data.status === 'trading'
|
|
381
|
+
? '\u4EA4\u6613\u4E2D'
|
|
382
|
+
: data.status === 'midday_break'
|
|
383
|
+
? '\u5348\u76D8\u4F11\u5E02'
|
|
384
|
+
: '\u4F11\u5E02';
|
|
385
|
+
const suffix = data.timeLabel ? ' ' + data.dateLabel + ' ' + data.timeLabel : (data.dateLabel ? ' ' + data.dateLabel : '');
|
|
386
|
+
return prefix + suffix;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// \u6E32\u67D3\u5927\u76D8\u5361\u7247\uFF1B\u9ED8\u8BA4\u6536\u8D77\u65F6\u53EA\u5C55\u793A\u524D\u4E09\u4E2A\u3002
|
|
390
|
+
function renderMarketOverview() {
|
|
391
|
+
const data = state.marketOverview;
|
|
392
|
+
const items = data?.items || [];
|
|
393
|
+
if (items.length === 0) {
|
|
394
|
+
dom.marketOverview.classList.add('hidden');
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
dom.marketOverview.classList.remove('hidden');
|
|
399
|
+
dom.marketOverviewStatus.textContent = buildMarketStatusLabel(data);
|
|
400
|
+
dom.marketOverviewToggle.textContent = state.marketExpanded ? '\u6536\u8D77' : '\u5C55\u5F00';
|
|
401
|
+
dom.marketOverviewToggle.style.display = items.length > 3 ? 'inline-flex' : 'none';
|
|
402
|
+
dom.marketOverviewGrid.classList.toggle('collapsed', !state.marketExpanded);
|
|
403
|
+
dom.marketOverviewGrid.innerHTML = items.map((item) => {
|
|
404
|
+
const trendClass = colorClass(item.changePercent);
|
|
405
|
+
return '<article class="market-card ' + trendClass + '">' +
|
|
406
|
+
'<div class="market-card-name">' + item.name + '</div>' +
|
|
407
|
+
'<div class="market-card-value ' + trendClass + '">' + fmtNullableNumber(item.value) + '</div>' +
|
|
408
|
+
'<div class="market-card-change ' + trendClass + '">' + fmtSignedNumber(item.change) + ' ' + fmtNullablePercent(item.changePercent) + '</div>' +
|
|
409
|
+
'</article>';
|
|
410
|
+
}).join('');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// \u6839\u636E\u5F53\u524D\u7B5B\u9009\u5206\u7C7B\u8FD4\u56DE\u4E3B\u8868\u6570\u636E\u3002
|
|
242
414
|
function getFilteredFunds() {
|
|
243
415
|
return state.activeCategory
|
|
244
416
|
? state.currentData.filter(f => f.category === state.activeCategory)
|
|
245
417
|
: state.currentData;
|
|
246
418
|
}
|
|
247
419
|
|
|
420
|
+
// \u4E3B\u8868\u6392\u5E8F\u76F4\u63A5\u4F5C\u7528\u4E8E state.currentData\uFF0C\u8FD9\u6837\u8FC7\u6EE4\u548C\u8BA1\u6570\u90FD\u5171\u7528\u540C\u4E00\u987A\u5E8F\u3002
|
|
248
421
|
function sortData() {
|
|
249
422
|
state.currentData.sort((a, b) => {
|
|
250
423
|
let va = a[state.sortKey], vb = b[state.sortKey];
|
|
@@ -259,53 +432,77 @@ function sortData() {
|
|
|
259
432
|
});
|
|
260
433
|
}
|
|
261
434
|
|
|
435
|
+
// \u57FA\u91D1\u540D\u79F0\u59CB\u7EC8\u53EF\u70B9\u51FB\uFF0C\u8BE6\u60C5\u6570\u636E\u6539\u4E3A\u6309\u9700\u52A0\u8F7D\u3002
|
|
262
436
|
function buildFundNameCell(fund) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
437
|
+
return '<td class="fund-name" data-code="' + fund.code + '">' + escapeHtml(fund.name) + '</td>';
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// \u4E3B\u8868\u201C\u51C0\u503C\u201D\u5217\u9700\u8981\u989D\u5916\u643A\u5E26\u65E5\u671F\u63D0\u793A\u3002
|
|
441
|
+
function buildPreviousTradingDayCell(fund, headerDate) {
|
|
442
|
+
return '<td class="' + colorClass(fund.previousTradingDayChange) + '">' +
|
|
443
|
+
fmtPercentWithDate(fund.previousTradingDayChange, fund.previousTradingDayLabel, headerDate) +
|
|
444
|
+
'</td>';
|
|
267
445
|
}
|
|
268
446
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
return '<td class="' + colorClass(fund.
|
|
272
|
-
|
|
273
|
-
(estimateLabelText ? '<span class="estimate-label">' + estimateLabelText + '</span>' : '') +
|
|
447
|
+
// \u4E3B\u8868\u201C\u4F30\u503C\u201D\u5217\u53EA\u5C55\u793A\u63A5\u53E3\u76F4\u8FD4\u503C\u3002
|
|
448
|
+
function buildTodayChangeCell(fund) {
|
|
449
|
+
return '<td class="' + colorClass(fund.todayChange) + '">' +
|
|
450
|
+
'<span class="cell-value">' + fmtNullablePercent(fund.todayChange) + '</span>' +
|
|
274
451
|
'</td>';
|
|
275
452
|
}
|
|
276
453
|
|
|
454
|
+
// \u4E3B\u8868\u8868\u5934\u751F\u6210\u51FD\u6570\u3002
|
|
455
|
+
function buildMainTableHead(headerDate) {
|
|
456
|
+
return '<tr>' +
|
|
457
|
+
buildSortableTh('\u57FA\u91D1\u4EE3\u7801', 'code', state.sortKey, state.sortDir, 'data-key="code"') +
|
|
458
|
+
buildSortableTh('\u57FA\u91D1\u540D\u79F0', 'name', state.sortKey, state.sortDir, 'data-key="name"') +
|
|
459
|
+
buildSortableTh(fmtHeaderLabel('\u51C0\u503C', headerDate), 'previousTradingDayChange', state.sortKey, state.sortDir, 'data-key="previousTradingDayChange"') +
|
|
460
|
+
buildSortableTh('\u4F30\u503C', 'todayChange', state.sortKey, state.sortDir, 'data-key="todayChange"') +
|
|
461
|
+
'</tr>';
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// \u91CD\u7ED8\u4E3B\u8868\u5185\u5BB9\uFF0C\u540C\u65F6\u5237\u65B0\u66F4\u65B0\u65F6\u95F4\u548C\u5927\u76D8\u6A21\u5757\u3002
|
|
277
465
|
function renderTable(data) {
|
|
466
|
+
const headerDate = getHeaderDate(data, (fund) => fund.previousTradingDayLabel || '');
|
|
467
|
+
dom.mainTableHead.innerHTML = buildMainTableHead(headerDate);
|
|
278
468
|
dom.mainTableBody.innerHTML = data.map(fund =>
|
|
279
469
|
'<tr><td>' + fund.code + '</td>' +
|
|
280
470
|
buildFundNameCell(fund) +
|
|
281
|
-
|
|
471
|
+
buildPreviousTradingDayCell(fund, headerDate) +
|
|
472
|
+
buildTodayChangeCell(fund) +
|
|
282
473
|
'</tr>'
|
|
283
474
|
).join('');
|
|
284
|
-
dom.timeText.textContent = '\u66F4\u65B0\u65F6\u95F4: ' + new Date().toLocaleString('zh-CN');
|
|
475
|
+
dom.timeText.textContent = '\u66F4\u65B0\u65F6\u95F4: ' + new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
|
|
476
|
+
renderMarketOverview();
|
|
285
477
|
}
|
|
286
478
|
|
|
479
|
+
// \u6253\u5F00\u62BD\u5C49\u65F6\u5148\u6E32\u67D3 loading\uFF0C\u518D\u5F02\u6B65\u62C9\u8BE6\u60C5\u3002
|
|
287
480
|
function showDrawer(fund) {
|
|
288
481
|
state.drawerFund = fund;
|
|
289
482
|
state.drawerSortKey = '';
|
|
290
483
|
state.drawerSortDir = 'desc';
|
|
484
|
+
state.drawerLoading = true;
|
|
291
485
|
renderDrawer();
|
|
292
486
|
dom.drawer.classList.add('open');
|
|
293
487
|
dom.drawerOverlay.classList.add('open');
|
|
488
|
+
loadDrawerDetail(fund.code, false);
|
|
294
489
|
}
|
|
295
490
|
|
|
491
|
+
// \u62BD\u5C49\u5185\u90E8\u6392\u5E8F\u53EA\u5F71\u54CD\u5F53\u524D\u57FA\u91D1\u7684\u8D21\u732E\u660E\u7EC6\u3002
|
|
296
492
|
function getSortedDrawerContributions(fund) {
|
|
297
493
|
const sorted = [...fund.contributions];
|
|
298
494
|
if (state.drawerSortKey) {
|
|
299
495
|
sorted.sort((a, b) => {
|
|
300
496
|
if (state.drawerSortKey === 'holdingRatio') return state.drawerSortDir === 'asc' ? a.holdingRatio - b.holdingRatio : b.holdingRatio - a.holdingRatio;
|
|
301
497
|
if (state.drawerSortKey === 'todayChange') return compareNullableNumbers(a.todayChange, b.todayChange, state.drawerSortDir);
|
|
302
|
-
if (state.drawerSortKey === '
|
|
498
|
+
if (state.drawerSortKey === 'previousTradingDayChange') return compareNullableNumbers(a.previousTradingDayChange, b.previousTradingDayChange, state.drawerSortDir);
|
|
303
499
|
return 0;
|
|
304
500
|
});
|
|
305
501
|
}
|
|
306
502
|
return sorted;
|
|
307
503
|
}
|
|
308
504
|
|
|
505
|
+
// \u7EDF\u4E00\u5904\u7406\u5E26\u7A7A\u503C\u7684\u6570\u5B57\u6392\u5E8F\u3002
|
|
309
506
|
function compareNullableNumbers(a, b, dir) {
|
|
310
507
|
const aMissing = a === null || a === undefined || Number.isNaN(a);
|
|
311
508
|
const bMissing = b === null || b === undefined || Number.isNaN(b);
|
|
@@ -315,57 +512,73 @@ function compareNullableNumbers(a, b, dir) {
|
|
|
315
512
|
return dir === 'asc' ? a - b : b - a;
|
|
316
513
|
}
|
|
317
514
|
|
|
318
|
-
|
|
515
|
+
// \u6784\u5EFA\u62BD\u5C49\u8868\u683C\u4E3B\u4F53\u3002
|
|
516
|
+
function buildDrawerTable(fund, sorted) {
|
|
517
|
+
const headerDate = getHeaderDate(sorted, (item) => item.previousTradingDayDate ? item.previousTradingDayDate.slice(5) : '');
|
|
319
518
|
let html = '<table><thead><tr>' +
|
|
320
519
|
'<th>\u6301\u4ED3</th>' +
|
|
321
|
-
'
|
|
322
|
-
|
|
323
|
-
'
|
|
520
|
+
buildSortableTh('\u5360\u6BD4', 'holdingRatio', state.drawerSortKey, state.drawerSortDir, 'data-dkey="holdingRatio"') +
|
|
521
|
+
buildSortableTh(fmtHeaderLabel('\u6536\u76D8\u6DA8\u8DCC', headerDate), 'previousTradingDayChange', state.drawerSortKey, state.drawerSortDir, 'data-dkey="previousTradingDayChange"') +
|
|
522
|
+
buildSortableTh('\u6DA8\u8DCC', 'todayChange', state.drawerSortKey, state.drawerSortDir, 'data-dkey="todayChange"') +
|
|
324
523
|
'</tr></thead><tbody>';
|
|
325
524
|
for (const c of sorted) {
|
|
326
|
-
html += '<tr><td>' + c.stockCode + ' ' + c.stockName + '</td>' +
|
|
525
|
+
html += '<tr><td>' + escapeHtml(c.stockCode + ' ' + c.stockName) + '</td>' +
|
|
327
526
|
'<td>' + c.holdingRatio.toFixed(2) + '%</td>' +
|
|
328
|
-
'<td class="' + colorClass(c.
|
|
329
|
-
'<td class="' + colorClass(c.
|
|
527
|
+
'<td class="' + colorClass(c.previousTradingDayChange) + '">' + fmtPercentWithDate(c.previousTradingDayChange, c.previousTradingDayDate ? c.previousTradingDayDate.slice(5) : '', headerDate) + '</td>' +
|
|
528
|
+
'<td class="' + colorClass(c.todayChange) + '"><span class="cell-value">' + fmtNullablePercent(c.todayChange) + '</span></td></tr>';
|
|
330
529
|
}
|
|
331
530
|
return html + '</tbody></table>';
|
|
332
531
|
}
|
|
333
532
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
'<th class="sortable' + (state.drawerSortKey === 'holdingRatio' ? ' ' + state.drawerSortDir : '') + '" data-dkey="holdingRatio">\u5360\u6BD4</th>' +
|
|
338
|
-
'<th class="sortable' + (state.drawerSortKey === 'stockChange' ? ' ' + state.drawerSortDir : '') + '" data-dkey="stockChange">\u6DA8\u8DCC</th>' +
|
|
339
|
-
'</tr></thead><tbody>';
|
|
340
|
-
for (const c of sorted) {
|
|
341
|
-
html += '<tr><td>' + c.stockCode + ' ' + c.stockName + '</td>' +
|
|
342
|
-
'<td>' + c.holdingRatio.toFixed(2) + '%</td>' +
|
|
343
|
-
'<td class="' + colorClass(c.stockChange) + '">' + fmtPercent(c.stockChange) + '</td></tr>';
|
|
344
|
-
}
|
|
345
|
-
return html + '</tbody></table>';
|
|
533
|
+
// \u62BD\u5C49\u9876\u90E8\u5237\u65B0\u6309\u94AE\u53EA\u5237\u65B0\u76D8\u4E2D\u4EF7\uFF0C\u4E0D\u5237\u65B0\u6301\u4ED3\u7ED3\u6784\u3002
|
|
534
|
+
function buildDrawerRefreshButton() {
|
|
535
|
+
return '<button class="drawer-refresh-btn" data-action="refresh-drawer"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 4v6h-6"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>\u5237\u65B0</button>';
|
|
346
536
|
}
|
|
347
537
|
|
|
538
|
+
// \u7EDF\u4E00\u6E32\u67D3\u62BD\u5C49\u7684\u4E09\u79CD\u72B6\u6001\uFF1Aloading / empty / table\u3002
|
|
348
539
|
function renderDrawer() {
|
|
349
540
|
const fund = state.drawerFund;
|
|
541
|
+
if (!fund) return;
|
|
542
|
+
|
|
543
|
+
let html = '<div class="drawer-header"><div>' +
|
|
544
|
+
'<div class="drawer-title">' + escapeHtml(fund.name) + '</div>' +
|
|
545
|
+
'<div class="drawer-subtitle">' + escapeHtml(fund.code) + ' \u6301\u4ED3\u660E\u7EC6</div>' +
|
|
546
|
+
'</div><div class="drawer-actions">' +
|
|
547
|
+
'<div><div class="drawer-estimate"><span class="drawer-info-label" title="\u6309\u5F53\u524D\u6709\u76D8\u4E2D\u6DA8\u8DCC\u7684\u6301\u4ED3\uFF0C\u6309\u6301\u4ED3\u5360\u6BD4\u52A0\u6743\u6C47\u603B\u5F97\u5230\u7684\u4F30\u503C\u3002\u8986\u76D6\u7387\u4E0D\u8DB3\u65F6\u4E0D\u5C55\u793A\u6570\u503C\u3002">\u6743\u91CD\u4F30\u503C</span><strong class="' + colorClass(fund.weightedTodayChange) + '">' + fmtNullablePercent(fund.weightedTodayChange) + '</strong></div>' +
|
|
548
|
+
'<div class="drawer-coverage"><span class="drawer-info-label" title="\u5F53\u524D\u62FF\u5230\u76D8\u4E2D\u6DA8\u8DCC\u7684\u6301\u4ED3\u6743\u91CD\u5360\u6BD4\u3002\u8986\u76D6\u7387\u8FBE\u5230 60% \u624D\u5C55\u793A\u6743\u91CD\u4F30\u503C\u3002">\u8986\u76D6</span> ' + fmtCoverage(fund.coverageRatio) + '</div></div>' +
|
|
549
|
+
buildDrawerRefreshButton() +
|
|
550
|
+
'</div></div>';
|
|
551
|
+
|
|
552
|
+
if (state.drawerLoading) {
|
|
553
|
+
html += '<div class="drawer-loading">\u660E\u7EC6\u52A0\u8F7D\u4E2D...</div>';
|
|
554
|
+
dom.drawerContent.innerHTML = html;
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
350
558
|
const sorted = getSortedDrawerContributions(fund);
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
559
|
+
if (!Array.isArray(sorted) || sorted.length === 0) {
|
|
560
|
+
html += '<div class="drawer-empty">\u6682\u65E0\u6301\u4ED3\u660E\u7EC6</div>';
|
|
561
|
+
dom.drawerContent.innerHTML = html;
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
html += buildDrawerTable(fund, sorted);
|
|
356
566
|
dom.drawerContent.innerHTML = html;
|
|
357
567
|
}
|
|
358
568
|
|
|
569
|
+
// \u5173\u95ED\u62BD\u5C49\u53EA\u5904\u7406 UI \u72B6\u6001\uFF0C\u4E0D\u6E05\u7A7A\u7F13\u5B58\u8FC7\u7684\u8BE6\u60C5\u6570\u636E\u3002
|
|
359
570
|
function hideDrawer() {
|
|
360
571
|
dom.drawer.classList.remove('open');
|
|
361
572
|
dom.drawerOverlay.classList.remove('open');
|
|
362
573
|
}
|
|
363
574
|
|
|
575
|
+
// \u5EFA\u7ACB code -> fund \u7684\u7D22\u5F15\uFF0C\u65B9\u4FBF\u4E3B\u8868\u70B9\u51FB\u65F6\u5FEB\u901F\u627E\u5230\u5BF9\u8C61\u3002
|
|
364
576
|
function buildMap(data) {
|
|
365
577
|
state.fundMap = {};
|
|
366
578
|
for (const f of data) state.fundMap[f.code] = f;
|
|
367
579
|
}
|
|
368
580
|
|
|
581
|
+
// \u4E3B\u8868\u6392\u5E8F\u5207\u6362\u89C4\u5219\uFF1A\u540C\u5217\u6B63\u53CD\u5207\u6362\uFF0C\u6362\u5217\u9ED8\u8BA4\u964D\u5E8F\u3002
|
|
369
582
|
function updateMainSort(key) {
|
|
370
583
|
if (state.sortKey === key) state.sortDir = state.sortDir === 'desc' ? 'asc' : 'desc';
|
|
371
584
|
else {
|
|
@@ -374,6 +587,7 @@ function updateMainSort(key) {
|
|
|
374
587
|
}
|
|
375
588
|
}
|
|
376
589
|
|
|
590
|
+
// \u62BD\u5C49\u6392\u5E8F\u5207\u6362\u89C4\u5219\u4E0E\u4E3B\u8868\u4FDD\u6301\u4E00\u81F4\u3002
|
|
377
591
|
function updateDrawerSort(key) {
|
|
378
592
|
if (state.drawerSortKey === key) state.drawerSortDir = state.drawerSortDir === 'desc' ? 'asc' : 'desc';
|
|
379
593
|
else {
|
|
@@ -382,13 +596,56 @@ function updateDrawerSort(key) {
|
|
|
382
596
|
}
|
|
383
597
|
}
|
|
384
598
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
599
|
+
// \u63A5\u6536\u4E3B\u8868\u63A5\u53E3\u8FD4\u56DE\u5E76\u5237\u65B0\u9875\u9762\u72B6\u6001\u3002
|
|
600
|
+
function setCurrentData(payload) {
|
|
601
|
+
state.currentData = payload.funds || [];
|
|
602
|
+
state.marketOverview = payload.marketOverview || null;
|
|
603
|
+
buildMap(state.currentData);
|
|
388
604
|
sortData();
|
|
389
605
|
renderFilteredTable();
|
|
390
606
|
}
|
|
391
607
|
|
|
608
|
+
// \u62C9\u62BD\u5C49\u8BE6\u60C5\u3002
|
|
609
|
+
// \u9996\u6B21\u6253\u5F00\u8D70 loading\uFF1B\u624B\u52A8\u5237\u65B0\u65F6\u53EA\u5237\u65B0\u4EF7\u683C\u5E76\u4FDD\u7559\u5F53\u524D\u62BD\u5C49\u5185\u5BB9\u3002
|
|
610
|
+
async function loadDrawerDetail(code, forceRefreshPrices) {
|
|
611
|
+
const fund = state.fundMap[code];
|
|
612
|
+
if (!fund) return;
|
|
613
|
+
|
|
614
|
+
if (forceRefreshPrices) {
|
|
615
|
+
state.drawerLoading = false;
|
|
616
|
+
renderDrawer();
|
|
617
|
+
const refreshBtn = dom.drawerContent.querySelector('[data-action="refresh-drawer"]');
|
|
618
|
+
if (refreshBtn) refreshBtn.classList.add('spinning');
|
|
619
|
+
try {
|
|
620
|
+
const detail = await fetch('/api/funds/' + encodeURIComponent(code) + '/detail?name=' + encodeURIComponent(fund.name) + '&refreshPrices=1');
|
|
621
|
+
if (!detail.ok) throw new Error('\u8BF7\u6C42\u5931\u8D25');
|
|
622
|
+
const payload = await detail.json();
|
|
623
|
+
state.fundMap[code] = { ...state.fundMap[code], ...payload };
|
|
624
|
+
state.drawerFund = state.fundMap[code];
|
|
625
|
+
renderDrawer();
|
|
626
|
+
} catch {
|
|
627
|
+
renderDrawer();
|
|
628
|
+
}
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
state.drawerLoading = true;
|
|
633
|
+
renderDrawer();
|
|
634
|
+
try {
|
|
635
|
+
const detail = await fetch('/api/funds/' + encodeURIComponent(code) + '/detail?name=' + encodeURIComponent(fund.name));
|
|
636
|
+
if (!detail.ok) throw new Error('\u8BF7\u6C42\u5931\u8D25');
|
|
637
|
+
const payload = await detail.json();
|
|
638
|
+
state.fundMap[code] = { ...state.fundMap[code], ...payload };
|
|
639
|
+
state.drawerFund = state.fundMap[code];
|
|
640
|
+
} catch {
|
|
641
|
+
state.fundMap[code] = { ...state.fundMap[code], contributions: [], weightedTodayChange: null, coverageRatio: 0 };
|
|
642
|
+
state.drawerFund = state.fundMap[code];
|
|
643
|
+
}
|
|
644
|
+
state.drawerLoading = false;
|
|
645
|
+
renderDrawer();
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// \u6FC0\u6D3B\u67D0\u4E2A\u5206\u7C7B\u7B5B\u9009\u3002
|
|
392
649
|
function setActiveCategory(tab) {
|
|
393
650
|
dom.filterTabItems().forEach(t => t.classList.remove('active'));
|
|
394
651
|
tab.classList.add('active');
|
|
@@ -396,16 +653,16 @@ function setActiveCategory(tab) {
|
|
|
396
653
|
renderFilteredTable();
|
|
397
654
|
}
|
|
398
655
|
|
|
656
|
+
// \u4E3B\u8868\u8868\u5934\u70B9\u51FB\u6392\u5E8F\u3002
|
|
399
657
|
function handleMainSortClick(e) {
|
|
400
658
|
const th = e.target.closest('th.sortable');
|
|
401
659
|
if (!th) return;
|
|
402
660
|
updateMainSort(th.dataset.key);
|
|
403
|
-
document.querySelectorAll('#mainTable th.sortable').forEach(t => t.classList.remove('asc', 'desc'));
|
|
404
|
-
th.classList.add(state.sortDir);
|
|
405
661
|
sortData();
|
|
406
662
|
renderFilteredTable();
|
|
407
663
|
}
|
|
408
664
|
|
|
665
|
+
// \u4E3B\u8868\u57FA\u91D1\u540D\u79F0\u70B9\u51FB\u6253\u5F00\u62BD\u5C49\u3002
|
|
409
666
|
function handleFundClick(e) {
|
|
410
667
|
const el = e.target.closest('.fund-name');
|
|
411
668
|
if (!el) return;
|
|
@@ -414,13 +671,20 @@ function handleFundClick(e) {
|
|
|
414
671
|
if (state.fundMap[code]) showDrawer(state.fundMap[code]);
|
|
415
672
|
}
|
|
416
673
|
|
|
674
|
+
// \u62BD\u5C49\u533A\u57DF\u540C\u65F6\u627F\u63A5\u201C\u5237\u65B0\u6309\u94AE\u201D\u548C\u201C\u8868\u5934\u6392\u5E8F\u201D\u4E24\u7C7B\u70B9\u51FB\u3002
|
|
417
675
|
function handleDrawerSortClick(e) {
|
|
676
|
+
const refreshBtn = e.target.closest('[data-action="refresh-drawer"]');
|
|
677
|
+
if (refreshBtn) {
|
|
678
|
+
if (state.drawerFund) loadDrawerDetail(state.drawerFund.code, true);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
418
681
|
const th = e.target.closest('th.sortable');
|
|
419
682
|
if (!th) return;
|
|
420
683
|
updateDrawerSort(th.dataset.dkey);
|
|
421
684
|
renderDrawer();
|
|
422
685
|
}
|
|
423
686
|
|
|
687
|
+
// \u4ECE\u6D4F\u89C8\u5668 localStorage \u8BFB\u53D6\u57FA\u91D1\u914D\u7F6E\u3002
|
|
424
688
|
function getStoredConfig() {
|
|
425
689
|
try {
|
|
426
690
|
const raw = localStorage.getItem('fundConfig');
|
|
@@ -431,13 +695,17 @@ function getStoredConfig() {
|
|
|
431
695
|
} catch { return null; }
|
|
432
696
|
}
|
|
433
697
|
|
|
698
|
+
// \u914D\u7F6E\u5F39\u5C42\u4EC5\u63A7\u5236\u663E\u793A\uFF0C\u4E0D\u4FEE\u6539\u914D\u7F6E\u5185\u5BB9\u3002
|
|
434
699
|
function showConfigUI() {
|
|
435
700
|
dom.configOverlay.classList.remove('hidden');
|
|
436
701
|
}
|
|
702
|
+
|
|
703
|
+
// \u5173\u95ED\u914D\u7F6E\u5F39\u5C42\uFF0C\u8BA9\u4E3B\u8868\u56DE\u5230\u6B63\u5E38\u4EA4\u4E92\u72B6\u6001\u3002
|
|
437
704
|
function hideConfigUI() {
|
|
438
705
|
dom.configOverlay.classList.add('hidden');
|
|
439
706
|
}
|
|
440
707
|
|
|
708
|
+
// \u6821\u9A8C\u914D\u7F6E\u6587\u4EF6\u5185\u5BB9\uFF0C\u8981\u6C42\u662F { code: name } \u7ED3\u6784\u3002
|
|
441
709
|
function validateConfig(text) {
|
|
442
710
|
let obj;
|
|
443
711
|
try { obj = JSON.parse(text); } catch { throw new Error('JSON \u683C\u5F0F\u9519\u8BEF'); }
|
|
@@ -446,6 +714,7 @@ function validateConfig(text) {
|
|
|
446
714
|
return obj;
|
|
447
715
|
}
|
|
448
716
|
|
|
717
|
+
// \u4E0A\u4F20 JSON \u6587\u4EF6\u540E\u76F4\u63A5\u628A\u5185\u5BB9\u704C\u8FDB\u6587\u672C\u6846\uFF0C\u548C\u624B\u52A8\u7C98\u8D34\u8D70\u540C\u4E00\u6761\u786E\u8BA4\u903B\u8F91\u3002
|
|
449
718
|
function handleFileInputChange(e) {
|
|
450
719
|
const file = e.target.files[0];
|
|
451
720
|
if (!file) return;
|
|
@@ -456,6 +725,7 @@ function handleFileInputChange(e) {
|
|
|
456
725
|
reader.readAsText(file);
|
|
457
726
|
}
|
|
458
727
|
|
|
728
|
+
// \u4FDD\u5B58\u914D\u7F6E\u5E76\u89E6\u53D1\u4E00\u6B21\u4E3B\u8868\u5237\u65B0\u3002
|
|
459
729
|
function handleConfirmConfig() {
|
|
460
730
|
const text = dom.configText.value.trim();
|
|
461
731
|
const errEl = dom.errorMsg;
|
|
@@ -471,13 +741,14 @@ function handleConfirmConfig() {
|
|
|
471
741
|
}
|
|
472
742
|
}
|
|
473
743
|
|
|
474
|
-
// \u5728\u66F4\u65B0\u65F6\u95F4\u533A\u57DF\
|
|
744
|
+
// \u5728\u66F4\u65B0\u65F6\u95F4\u533A\u57DF\u8FFD\u52A0\u201C\u5207\u6362\u914D\u7F6E\u201D\u6309\u94AE\uFF0C\u907F\u514D\u5355\u72EC\u5360\u4E00\u884C\u5DE5\u5177\u680F\u3002
|
|
475
745
|
const changeBtn = document.createElement('button');
|
|
476
746
|
changeBtn.className = 'change-config-btn';
|
|
477
747
|
changeBtn.textContent = '\u5207\u6362\u914D\u7F6E';
|
|
478
748
|
changeBtn.addEventListener('click', showConfigUI);
|
|
479
749
|
dom.updateTime.appendChild(changeBtn);
|
|
480
750
|
|
|
751
|
+
// \u4E3B\u5237\u65B0\uFF1A\u53EA\u5237\u65B0\u4E3B\u8868\u4F30\u503C\u548C\u5927\u76D8\uFF0C\u4E0D\u9884\u53D6\u660E\u7EC6\u3002
|
|
481
752
|
async function doRefresh() {
|
|
482
753
|
dom.refreshBtn.classList.add('spinning');
|
|
483
754
|
try {
|
|
@@ -493,11 +764,13 @@ async function doRefresh() {
|
|
|
493
764
|
setCurrentData(data);
|
|
494
765
|
} catch {
|
|
495
766
|
state.currentData = [];
|
|
767
|
+
state.marketOverview = null;
|
|
496
768
|
renderTable([]);
|
|
497
769
|
}
|
|
498
770
|
dom.refreshBtn.classList.remove('spinning');
|
|
499
771
|
}
|
|
500
772
|
|
|
773
|
+
// \u7ED1\u5B9A\u6240\u6709\u524D\u7AEF\u4E8B\u4EF6\u3002
|
|
501
774
|
function bindEvents() {
|
|
502
775
|
dom.filterTabs.addEventListener('click', function(e) {
|
|
503
776
|
const tab = e.target.closest('.filter-tab');
|
|
@@ -513,9 +786,13 @@ function bindEvents() {
|
|
|
513
786
|
dom.fileInput.addEventListener('change', handleFileInputChange);
|
|
514
787
|
dom.confirmBtn.addEventListener('click', handleConfirmConfig);
|
|
515
788
|
dom.refreshBtn.addEventListener('click', doRefresh);
|
|
789
|
+
dom.marketOverviewToggle.addEventListener('click', function() {
|
|
790
|
+
state.marketExpanded = !state.marketExpanded;
|
|
791
|
+
renderMarketOverview();
|
|
792
|
+
});
|
|
516
793
|
}
|
|
517
794
|
|
|
518
|
-
// \u521D\u59CB\u5316\uFF1A\u68C0\u67E5 localStorage\uFF0C\u6709\u914D\u7F6E\u5219\u76F4\u63A5\u52A0\u8F7D
|
|
795
|
+
// \u521D\u59CB\u5316\uFF1A\u68C0\u67E5 localStorage\uFF0C\u6709\u914D\u7F6E\u5219\u76F4\u63A5\u52A0\u8F7D\u3002
|
|
519
796
|
(function init() {
|
|
520
797
|
bindEvents();
|
|
521
798
|
const config = getStoredConfig();
|
|
@@ -527,19 +804,19 @@ function bindEvents() {
|
|
|
527
804
|
}
|
|
528
805
|
})();
|
|
529
806
|
|
|
530
|
-
// \
|
|
807
|
+
// \u6BCF 30 \u79D2\u81EA\u52A8\u5237\u65B0\u4E00\u6B21\u4E3B\u8868\u548C\u5927\u76D8\u5361\u7247\u3002
|
|
531
808
|
setInterval(doRefresh, 30000);
|
|
532
|
-
`}function
|
|
809
|
+
`}function et(){return`<!DOCTYPE html>
|
|
533
810
|
<html lang="zh-CN">
|
|
534
811
|
<head>
|
|
535
812
|
<meta charset="UTF-8">
|
|
536
813
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
537
814
|
<title>\u57FA\u91D1\u4F30\u503C\u67E5\u8BE2</title>
|
|
538
|
-
<style>${
|
|
815
|
+
<style>${Ge()}</style>
|
|
539
816
|
</head>
|
|
540
817
|
<body>
|
|
541
|
-
${
|
|
542
|
-
<script>${
|
|
818
|
+
${Ze()}
|
|
819
|
+
<script>${Xe()}</script>
|
|
543
820
|
</body>
|
|
544
|
-
</html>`}async function
|
|
545
|
-
`),
|
|
821
|
+
</html>`}async function ve(e){let t=ye();t.use(ye.json()),t.get("/",(a,o)=>{o.type("html").send(et())}),t.post("/api/funds",async(a,o)=>{try{let r=a.body?.funds;if(!r||typeof r!="object"||Object.keys(r).length===0)return o.status(400).json({error:"funds \u914D\u7F6E\u65E0\u6548"});let s=await Ye(r);o.json(s)}catch(r){o.status(500).json({error:r.message})}}),t.get("/api/funds/:code/detail",async(a,o)=>{try{let r=a.params.code,s=typeof a.query?.name=="string"?a.query.name:r,d=a.query?.refreshPrices==="1",i=await Ve(r,s,d);o.json(i)}catch(r){o.status(500).json({error:r.message})}});let n=t.listen(e,()=>{console.log(`\u670D\u52A1\u5DF2\u542F\u52A8: http://localhost:${e}`)})}var{version:nt}=JSON.parse(at(new URL("../package.json",import.meta.url),"utf8"));function rt(e,t){let n=e.todayChange===null||e.todayChange===void 0||Number.isNaN(e.todayChange),a=t.todayChange===null||t.todayChange===void 0||Number.isNaN(t.todayChange);return n&&a?0:n?1:a?-1:t.todayChange-e.todayChange}var b=new tt;b.name("fund").description("\u57FA\u91D1\u4F30\u503C\u67E5\u8BE2\u5DE5\u5177").version(nt,"-v, -V, --version");b.action(()=>{b.help()});b.command("config [path]").description("\u8BBE\u7F6E\u6216\u67E5\u770B\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84").action(e=>{if(e)oe(e);else{let t=K();t?console.log(t):(console.error("\u672A\u8BBE\u7F6E\u914D\u7F6E\u6587\u4EF6\uFF0C\u8BF7\u8FD0\u884C: fund config <\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84>"),process.exit(1))}});b.command("list").description("\u5217\u51FA\u6240\u6709\u914D\u7F6E\u7684\u57FA\u91D1").action(async()=>{let e=U(),t=await V(e);t.sort(rt),be(t)});b.command("detail <code>").description("\u67E5\u770B\u5355\u53EA\u57FA\u91D1\u6301\u4ED3\u8BE6\u60C5").action(async e=>{let n=U()[e];n||(console.error(`\u57FA\u91D1 ${e} \u672A\u5728\u914D\u7F6E\u6587\u4EF6\u4E2D\u914D\u7F6E`),process.exit(1));let a=await Q({[e]:n}),[o,r]=await Promise.all([Y(a),O()]),s=await ce(e,n,a.get(e)||[],o,r);he(s)});b.command("serve").description("\u542F\u52A8 Web \u670D\u52A1\uFF08\u7528\u4E8E\u90E8\u7F72\u5230\u670D\u52A1\u5668\uFF09").option("-p, --port <port>","\u7AEF\u53E3\u53F7",process.env.PORT||"8888").action(async e=>{let t=parseInt(e.port,10);await ve(t)});b.command("*",null,{noHelp:!0}).action(()=>{console.error(`\u672A\u77E5\u547D\u4EE4
|
|
822
|
+
`),b.help()});b.parse();
|