@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.
Files changed (3) hide show
  1. package/README.md +23 -7
  2. package/dist/fund.js +364 -87
  3. 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 ct}from"commander";import{readFileSync as k,writeFileSync as V,existsSync as D}from"fs";import{homedir as W}from"os";import{join as X,resolve as Z}from"path";var v=X(W(),".fundrc.json");function S(){return D(v)&&JSON.parse(k(v,"utf-8")).configPath||null}function j(t){let e=Z(t);D(e)||(console.error(`\u914D\u7F6E\u6587\u4EF6\u4E0D\u5B58\u5728: ${e}`),process.exit(1));try{let n=JSON.parse(k(e,"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)}V(v,JSON.stringify({configPath:e},null,2)),console.log(`\u5DF2\u8BBE\u7F6E\u914D\u7F6E\u6587\u4EF6: ${e}`)}function N(){let t=S();return 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)),D(t)||(console.error(`\u914D\u7F6E\u6587\u4EF6\u4E0D\u5B58\u5728: ${t}\uFF0C\u8BF7\u91CD\u65B0\u8BBE\u7F6E: fund config <\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84>`),process.exit(1)),JSON.parse(k(t,"utf-8"))}import b from"axios";import*as I from"cheerio";function B(t){let e=I.load(t),n=[];return e("table").first().find("tbody tr, tr").each((o,r)=>{let c=e(r).find("td");if(c.length<7)return;let s=c.eq(1).find("a"),d=s.text().trim();if(!d)return;let i=(s.attr("href")||"").match(/\/r\/(\d+)\.(\w+)/),u=i?parseInt(i[1]):d.startsWith("6")?1:0,l=u>=100,g=c.eq(2).find("a").text().trim()||c.eq(2).text().trim(),h=c.eq(6).text().trim().replace("%",""),O=parseFloat(h);isNaN(O)||n.push({stockCode:d,stockName:g,holdingRatio:O,market:u,isOverseas:l})}),n}var tt={"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/"};async function z(t){let e=`https://fundf10.eastmoney.com/FundArchivesDatas.aspx?type=jjcc&code=${t}&topline=50`,{data:n}=await b.get(e,{responseType:"text",headers:tt}),a=n.match(/content:"([\s\S]*?)",arryear:/);return a?B(a[1]):[]}async function T(t){try{let e=`http://fundgz.1234567.com.cn/js/${t}.js`,{data:n}=await b.get(e,{responseType:"text"}),a=n.match(/jsonpgz\((.+)\)/);if(!a)return null;let o=JSON.parse(a[1]);return!o.gszzl||!o.gztime?null:{change:parseFloat(o.gszzl),time:o.gztime}}catch{return null}}async function R(t){let e=new Map;return t.length===0||await Promise.all(t.map(async({code:n,market:a})=>{try{let o=`https://push2.eastmoney.com/api/qt/stock/get?secid=${a}.${n}&fields=f170`,{data:r}=await b.get(o,{timeout:5e3});r.data&&r.data.f170!==void 0&&e.set(n,{changePercent:r.data.f170/100})}catch{}})),e}function $(t){return[t.getFullYear(),String(t.getMonth()+1).padStart(2,"0"),String(t.getDate()).padStart(2,"0")].join("")}async function P(t){let e=new Map;if(t.length===0)return e;let n=new Date,a=new Date(n);return a.setDate(a.getDate()-14),await Promise.all(t.map(async({code:o,market:r})=>{try{let c=`https://push2his.eastmoney.com/api/qt/stock/kline/get?secid=${r}.${o}&fields1=f1,f2,f3,f4,f5,f6&fields2=f51,f52,f53,f54,f55,f56,f57,f58,f59,f60,f61&klt=101&fqt=1&beg=${$(a)}&end=${$(n)}`,{data:s}=await b.get(c,{timeout:5e3}),d=s?.data?.klines;if(!Array.isArray(d)||d.length===0){e.set(o,{latestDate:null,changesByDate:new Map});return}let f=new Map;for(let u of d){let l=u.split(",");if(l.length<9)continue;let g=l[0],h=parseFloat(l[8]);!g||Number.isNaN(h)||f.set(g,h)}let i=d[d.length-1]?.split(",")[0]||null;e.set(o,{latestDate:i,changesByDate:f})}catch{e.set(o,{latestDate:null,changesByDate:new Map})}})),e}async function K(t){let e=new Map;if(t.length===0)return e;let n=t.map(({code:a,market:o})=>o===1?`s_sh${a}`:`s_sz${a}`);try{let a=`https://qt.gtimg.cn/q=${n.join(",")}`,{data:o}=await b.get(a,{responseType:"arraybuffer"}),c=new TextDecoder("gbk").decode(o).trim().split(`
3
- `);for(let s of c){let d=s.match(/^v_[^=]+="(.+)";?$/);if(!d)continue;let f=d[1].split("~");if(f.length<6)continue;let i=f[2],u=f[1],l=parseFloat(f[5]);i&&e.set(i,{name:u,changePercent:l})}}catch{}return e}function H(t,e){return e.some(a=>a.isOverseas)||/全球|QDII|美元|港|海外|纳斯达克|标普|道琼斯/.test(t)?"\u5168\u7403":/ETF|指数/.test(t)?"\u6307\u6570":/债/.test(t)?"\u504F\u503A":"\u504F\u80A1"}function et(){let t=new Date;return[t.getFullYear(),String(t.getMonth()+1).padStart(2,"0"),String(t.getDate()).padStart(2,"0")].join("-")}function p(t){return!t||t.length<10?"":t.slice(5)}function x(t){return typeof t=="number"&&!Number.isNaN(t)}function F(t){let e=new Date(t.replace(" ","T"));return e.setDate(e.getDate()-1),p([e.getFullYear(),String(e.getMonth()+1).padStart(2,"0"),String(e.getDate()).padStart(2,"0")].join("-"))}function q(t){return t.map(e=>({code:e.stockCode,market:e.market}))}function A(t,e,n,a){let o=et(),r=[];for(let i of t){let u=a.get(i.stockCode);if(u)for(let l of u.changesByDate.keys())l<o&&r.push(l)}r.sort();let c=r[r.length-1]||null,s=t.map(i=>{let u=a.get(i.stockCode),l=c&&u?u.changesByDate.get(c)??null:null,g=i.isOverseas?u?.latestDate===o?n.get(i.stockCode)?.changePercent??null:null:e.get(i.stockCode)?.changePercent??null;return{stockCode:i.stockCode,stockName:i.stockName,holdingRatio:i.holdingRatio,todayChange:g,yesterdayChange:l,stockChange:l,contribution:x(l)?i.holdingRatio*l/100:0,isOverseas:i.isOverseas}});return{estimatedChange:s.filter(i=>x(i.yesterdayChange)).length>0?s.reduce((i,u)=>i+u.contribution,0):null,contributions:s,todayDate:o,yesterdayDate:c}}function J(t,e){let n=0,a=0,o=[];for(let r of t){let c=e.get(r.stockCode),s=c?c.changePercent:null,d=x(s)?r.holdingRatio*s/100:0;x(s)&&(n+=d,a+=1),o.push({stockCode:r.stockCode,stockName:r.stockName,holdingRatio:r.holdingRatio,stockChange:s,contribution:d})}return{estimatedChange:a>0?n:null,contributions:o}}async function M(t){let e=new Map;return await Promise.all(Object.keys(t).map(async n=>{try{e.set(n,await z(n))}catch{e.set(n,[])}})),e}function nt(t){let e=new Map;for(let[,n]of t)for(let a of n)!a.isOverseas&&!e.has(a.stockCode)&&e.set(a.stockCode,{code:a.stockCode,market:a.market});return[...e.values()]}async function L(t){return K(nt(t))}async function E(t,e,n,a){let{estimatedChange:o,contributions:r}=J(n,a),c=H(e,n);if(c==="\u5168\u7403"){let d=await T(t),f=n.filter(h=>h.isOverseas).map(h=>({code:h.stockCode,market:h.market})),[i,u]=await Promise.all([R(f),P(q(n))]),l=A(n,a,i,u),g=d?F(d.time):p(l.yesterdayDate);return{code:t,name:e,estimatedChange:d?d.change:l.estimatedChange,contributions:l.contributions,category:c,canDetail:n.length>0,detailMode:"global",todayLabel:p(l.todayDate),yesterdayLabel:p(l.yesterdayDate),estimateDateLabel:g,estimateLabel:g?` (${g})`:"",estimateSource:d?"fund-estimate":"holdings-yesterday"}}let s=await T(t);if(s!==null){let d=new Date(s.time.replace(" ","T")),i=Date.now()-d>7200*1e3?F(s.time):"";return{code:t,name:e,estimatedChange:s.change,contributions:r,category:c,canDetail:n.length>0,estimateLabel:i?` (${i})`:"",estimateDateLabel:i}}return{code:t,name:e,estimatedChange:o,contributions:r,category:c,canDetail:n.length>0}}async function C(t){let e=await M(t),n=await L(e);return Promise.all(Object.entries(t).map(([a,o])=>E(a,o,e.get(a)||[],n)))}import w from"chalk";import U from"cli-table3";function y(t){if(t==null||Number.isNaN(t))return"-";let e=parseFloat(t);return e>0?w.red(`+${e.toFixed(2)}%`):e<0?w.green(`${e.toFixed(2)}%`):`${e.toFixed(2)}%`}function Q(t){let{code:e,name:n,estimatedChange:a,contributions:o}=t,r=new U({head:t.detailMode==="global"?["\u6301\u4ED3","\u5360\u6BD4",`\u4ECA\u65E5
4
- ${t.todayLabel||""}`,`\u6628\u65E5
5
- ${t.yesterdayLabel||""}`]:["\u6301\u4ED3","\u5360\u6BD4","\u6DA8\u8DCC"],style:{head:["cyan"]}});for(let s of o){if(t.detailMode==="global"){r.push([`${s.stockCode} ${s.stockName}`,`${s.holdingRatio.toFixed(2)}%`,s.todayChange===null||s.todayChange===void 0?"-":y(s.todayChange),s.yesterdayChange===null||s.yesterdayChange===void 0?"-":y(s.yesterdayChange)]);continue}r.push([`${s.stockCode} ${s.stockName}`,`${s.holdingRatio.toFixed(2)}%`,y(s.stockChange)])}console.log(`
6
- ${w.bold(e)} ${w.bold(n)}`),console.log(r.toString());let c=y(a);console.log(`\u9884\u4F30\u6DA8\u5E45: ${c}${t.estimateLabel||""}
7
- `)}function _(t){let e=new U({head:["\u57FA\u91D1\u4EE3\u7801","\u57FA\u91D1\u540D\u79F0","\u9884\u4F30\u6DA8\u5E45"],style:{head:["cyan"]}});for(let n of t)e.push([n.code,n.name,y(n.estimatedChange)+(n.estimateLabel||"")]);console.log(e.toString())}import G from"express";async function at(t){return C(t)}function ot(){return`
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: 24px; font-size: 13px; }
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
- .estimate-label { display: block; font-size: 11px; color: #b0a8a0; margin-top: 2px; }
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; margin-bottom: 20px; }
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 { content: ' \u21C5'; font-size: 11px; opacity: 0.4; }
65
- th.sortable.asc::after { content: ' \u2191'; opacity: 1; }
66
- th.sortable.desc::after { content: ' \u2193'; opacity: 1; }
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
- `}function rt(){return`
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="estimatedChange">\u9884\u4F30\u6DA8\u5E45</th></tr></thead>
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 st(){return`
150
- function formatDateLabel(timeStr) {
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: 'estimatedChange',
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
- function hasDisplayableContributions(fund) {
226
- if (!Array.isArray(fund.contributions) || fund.contributions.length === 0) {
227
- return false;
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('&', '&amp;')
337
+ .replaceAll('<', '&lt;')
338
+ .replaceAll('>', '&gt;')
339
+ .replaceAll('"', '&quot;')
340
+ .replaceAll("'", '&#39;');
341
+ }
229
342
 
230
- return fund.detailMode !== 'global' || fund.contributions.some(c =>
231
- (c.todayChange !== null && c.todayChange !== undefined) ||
232
- (c.yesterdayChange !== null && c.yesterdayChange !== undefined)
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
- function getEstimateLabelText(fund) {
237
- if (fund.estimateDateLabel) return fund.estimateDateLabel;
238
- if (fund.estimateLabel) return formatDateLabel(fund.estimateLabel);
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
- const canDetail = fund.canDetail && hasDisplayableContributions(fund);
264
- const nameClass = canDetail ? 'fund-name' : 'fund-name disabled';
265
- const nameAttr = canDetail ? ' data-code="' + fund.code + '"' : '';
266
- return '<td class="' + nameClass + '"' + nameAttr + '>' + fund.name + '</td>';
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
- function buildEstimateCell(fund) {
270
- const estimateLabelText = getEstimateLabelText(fund);
271
- return '<td class="' + colorClass(fund.estimatedChange) + '">' +
272
- fmtPercent(fund.estimatedChange) +
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
- buildEstimateCell(fund) +
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 === 'stockChange') return compareNullableNumbers(a.stockChange, b.stockChange, state.drawerSortDir);
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
- function buildGlobalDrawerTable(fund, sorted) {
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
- '<th class="sortable' + (state.drawerSortKey === 'holdingRatio' ? ' ' + state.drawerSortDir : '') + '" data-dkey="holdingRatio">\u5360\u6BD4</th>' +
322
- '<th class="sortable' + (state.drawerSortKey === 'todayChange' ? ' ' + state.drawerSortDir : '') + '" data-dkey="todayChange">\u4ECA\u65E5<br>' + (fund.todayLabel || '') + '</th>' +
323
- '<th class="sortable' + (state.drawerSortKey === 'stockChange' ? ' ' + state.drawerSortDir : '') + '" data-dkey="stockChange">\u6628\u65E5<br>' + (fund.yesterdayLabel || '') + '</th>' +
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.todayChange ?? 0) + '">' + fmtNullablePercent(c.todayChange) + '</td>' +
329
- '<td class="' + colorClass(c.yesterdayChange ?? 0) + '">' + fmtNullablePercent(c.yesterdayChange) + '</td></tr>';
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
- function buildDefaultDrawerTable(sorted) {
335
- let html = '<table><thead><tr>' +
336
- '<th>\u6301\u4ED3</th>' +
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
- let html = '<div class="drawer-title">' + fund.name + '</div>';
352
- html += '<div class="drawer-subtitle">' + fund.code + ' \u6301\u4ED3\u660E\u7EC6</div>';
353
- html += fund.detailMode === 'global'
354
- ? buildGlobalDrawerTable(fund, sorted)
355
- : buildDefaultDrawerTable(sorted);
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
- function setCurrentData(data) {
386
- state.currentData = data;
387
- buildMap(data);
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\u6DFB\u52A0"\u5207\u6362\u914D\u7F6E"\u6309\u94AE
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
- // \u6BCF30\u79D2\u81EA\u52A8\u5237\u65B0
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 it(){return`<!DOCTYPE html>
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>${ot()}</style>
815
+ <style>${Ge()}</style>
539
816
  </head>
540
817
  <body>
541
- ${rt()}
542
- <script>${st()}</script>
818
+ ${Ze()}
819
+ <script>${Xe()}</script>
543
820
  </body>
544
- </html>`}async function Y(t){let e=G();e.use(G.json()),e.get("/",(a,o)=>{o.type("html").send(it())}),e.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 c=await at(r);o.json(c)}catch(r){o.status(500).json({error:r.message})}});let n=e.listen(t,()=>{console.log(`\u670D\u52A1\u5DF2\u542F\u52A8: http://localhost:${t}`)})}function dt(t,e){let n=t.estimatedChange===null||t.estimatedChange===void 0||Number.isNaN(t.estimatedChange),a=e.estimatedChange===null||e.estimatedChange===void 0||Number.isNaN(e.estimatedChange);return n&&a?0:n?1:a?-1:e.estimatedChange-t.estimatedChange}var m=new ct;m.name("fund").description("\u57FA\u91D1\u4F30\u503C\u67E5\u8BE2\u5DE5\u5177").version("1.0.0","-v, -V, --version");m.action(()=>{m.help()});m.command("config [path]").description("\u8BBE\u7F6E\u6216\u67E5\u770B\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84").action(t=>{if(t)j(t);else{let e=S();e?console.log(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))}});m.command("list").description("\u5217\u51FA\u6240\u6709\u914D\u7F6E\u7684\u57FA\u91D1").action(async()=>{let t=N(),e=await C(t);e.sort(dt),_(e)});m.command("detail <code>").description("\u67E5\u770B\u5355\u53EA\u57FA\u91D1\u6301\u4ED3\u8BE6\u60C5").action(async t=>{let n=N()[t];n||(console.error(`\u57FA\u91D1 ${t} \u672A\u5728\u914D\u7F6E\u6587\u4EF6\u4E2D\u914D\u7F6E`),process.exit(1));let a=await M({[t]:n}),o=await L(a),r=await E(t,n,a.get(t)||[],o);Q(r)});m.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 t=>{let e=parseInt(t.port,10);await Y(e)});m.command("*",null,{noHelp:!0}).action(()=>{console.error(`\u672A\u77E5\u547D\u4EE4
545
- `),m.help()});m.parse();
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cxxgo/fund-valuation-query",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "fund": "./dist/fund.js"