@cxxgo/fund-valuation-query 1.0.4
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 +88 -0
- package/dist/fund.js +545 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# fund-valuation-query
|
|
2
|
+
|
|
3
|
+
基金实时估值查询 CLI 工具。根据基金持仓股票的实时行情,估算基金当日的涨跌幅。
|
|
4
|
+
|
|
5
|
+
## 原理
|
|
6
|
+
|
|
7
|
+
1. 从东方财富获取基金的十大持仓数据(股票代码、名称、持仓占比)
|
|
8
|
+
2. 从腾讯行情 API 获取持仓股票的实时涨跌幅
|
|
9
|
+
3. 用持仓占比加权计算基金的预估涨跌幅
|
|
10
|
+
|
|
11
|
+
## 安装
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm i @cxxgo/fund-valuation-query -g
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 使用
|
|
18
|
+
|
|
19
|
+
### 列出所有基金估值
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
fund list
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
按预估涨幅降序展示。
|
|
26
|
+
|
|
27
|
+
### 查看单只基金持仓详情
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
fund detail <基金代码>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
展示该基金的十大持仓股票及各自涨跌幅、对基金的贡献。
|
|
34
|
+
|
|
35
|
+
### Web 服务
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# 启动 Web 服务,默认端口 8888
|
|
39
|
+
fund serve
|
|
40
|
+
|
|
41
|
+
# 指定端口
|
|
42
|
+
fund serve -p 8080
|
|
43
|
+
|
|
44
|
+
# 或通过环境变量
|
|
45
|
+
PORT=8080 fund serve
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
启动后访问 `http://localhost:8888` 即可使用可视化界面,功能包括:
|
|
49
|
+
|
|
50
|
+
- 首次访问需选择配置文件(上传 JSON 文件或粘贴内容),配置保存在浏览器 localStorage
|
|
51
|
+
- 点击表头按基金代码、名称、预估涨幅排序
|
|
52
|
+
- 点击基金名称,右侧抽屉展开持仓明细
|
|
53
|
+
- 抽屉内支持按占比、涨跌排序
|
|
54
|
+
- 手动刷新按钮 + 30 秒自动刷新
|
|
55
|
+
- 切换配置按钮可重新选择配置文件
|
|
56
|
+
|
|
57
|
+
### 配置文件
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# 设置配置文件路径
|
|
61
|
+
fund config /path/to/fund.config.json
|
|
62
|
+
|
|
63
|
+
# 查看当前配置文件路径
|
|
64
|
+
fund config
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
配置文件为 JSON 格式,key 为基金代码,value 为基金名称:
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"000001": "示例基金A",
|
|
72
|
+
"000002": "示例基金B"
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
配置文件路径保存在 `~/.fundrc.json` 中。
|
|
77
|
+
|
|
78
|
+
## 技术栈
|
|
79
|
+
|
|
80
|
+
- **数据源**:东方财富(持仓)、腾讯行情(实时股价)
|
|
81
|
+
- **CLI**:Commander.js + cli-table3 + chalk
|
|
82
|
+
- **Web**:Express 内嵌 HTML/JS/CSS
|
|
83
|
+
- **HTML 解析**:cheerio
|
|
84
|
+
- **HTTP**:axios
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
|
|
88
|
+
ISC
|
package/dist/fund.js
ADDED
|
@@ -0,0 +1,545 @@
|
|
|
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`
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
body { background: #f5f0eb; color: #4a4a4a; font-family: -apple-system, "Microsoft YaHei", sans-serif; padding: 24px; }
|
|
10
|
+
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
|
+
table { border-collapse: collapse; width: 100%; max-width: 700px; margin: 0 auto 16px; }
|
|
13
|
+
th, td { padding: 10px 14px; text-align: center; border-bottom: 1px solid #e8e0d8; }
|
|
14
|
+
th { background: #ede6df; color: #8a7e74; font-weight: 600; }
|
|
15
|
+
tr:hover { background: #efe8e1; }
|
|
16
|
+
.up { color: #d4756b; }
|
|
17
|
+
.down { color: #6b9e78; }
|
|
18
|
+
.flat { color: #a09890; }
|
|
19
|
+
.estimate-label { display: block; font-size: 11px; color: #b0a8a0; margin-top: 2px; }
|
|
20
|
+
.fund-name { cursor: pointer; color: #6a9ec5; }
|
|
21
|
+
.fund-name:hover { text-decoration: underline; }
|
|
22
|
+
.fund-name.disabled { cursor: default; color: #4a4a4a; }
|
|
23
|
+
.fund-name.disabled:hover { text-decoration: none; }
|
|
24
|
+
.drawer-overlay {
|
|
25
|
+
display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
|
26
|
+
background: rgba(0,0,0,0.15); z-index: 999;
|
|
27
|
+
}
|
|
28
|
+
.drawer-overlay.open { display: block; animation: fadeIn 0.2s ease; }
|
|
29
|
+
.drawer {
|
|
30
|
+
position: fixed; top: 0; right: -460px; width: 460px; height: 100%;
|
|
31
|
+
background: #faf7f4; z-index: 1000;
|
|
32
|
+
box-shadow: -2px 0 16px rgba(0,0,0,0.08);
|
|
33
|
+
transition: right 0.25s ease;
|
|
34
|
+
overflow-y: auto; padding: 32px 24px;
|
|
35
|
+
}
|
|
36
|
+
.drawer.open { right: 0; }
|
|
37
|
+
.drawer-close {
|
|
38
|
+
position: absolute; top: 16px; right: 18px; font-size: 20px;
|
|
39
|
+
color: #b0a8a0; cursor: pointer; border: none; background: none;
|
|
40
|
+
line-height: 1;
|
|
41
|
+
}
|
|
42
|
+
.drawer-close:hover { color: #8a7e74; }
|
|
43
|
+
.config-close {
|
|
44
|
+
position: absolute; top: 12px; right: 14px; font-size: 20px;
|
|
45
|
+
color: #b0a8a0; cursor: pointer; border: none; background: none;
|
|
46
|
+
line-height: 1;
|
|
47
|
+
}
|
|
48
|
+
.config-close:hover { color: #8a7e74; }
|
|
49
|
+
.drawer-title { color: #6a5e54; font-size: 16px; font-weight: 600; margin-bottom: 6px; }
|
|
50
|
+
.drawer-subtitle { color: #b0a8a0; font-size: 13px; margin-bottom: 20px; }
|
|
51
|
+
.drawer table { max-width: 100%; }
|
|
52
|
+
.drawer th { background: #f0ebe5; color: #8a7e74; font-size: 13px; padding: 8px 10px; min-width: 80px; }
|
|
53
|
+
.drawer td { font-size: 13px; padding: 8px 10px; border-bottom: 1px solid #ece4dc; }
|
|
54
|
+
.refresh-btn {
|
|
55
|
+
display: inline-block; margin-left: 8px; padding: 3px 12px;
|
|
56
|
+
font-size: 13px; color: #8a7e74; background: #ede6df; border: none;
|
|
57
|
+
border-radius: 12px; cursor: pointer; vertical-align: middle;
|
|
58
|
+
}
|
|
59
|
+
.refresh-btn:hover { background: #e0d6cc; color: #6a5e54; }
|
|
60
|
+
.refresh-btn.spinning svg { animation: spin 0.6s linear infinite; }
|
|
61
|
+
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
|
62
|
+
th.sortable { cursor: pointer; user-select: none; vertical-align: middle; }
|
|
63
|
+
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; }
|
|
67
|
+
.config-overlay {
|
|
68
|
+
display: flex; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
|
69
|
+
background: rgba(0,0,0,0.15); z-index: 2000;
|
|
70
|
+
justify-content: center; align-items: center;
|
|
71
|
+
}
|
|
72
|
+
.config-overlay.hidden { display: none; }
|
|
73
|
+
.config-card {
|
|
74
|
+
background: #faf7f4; border-radius: 12px; padding: 32px 28px;
|
|
75
|
+
box-shadow: 0 4px 24px rgba(0,0,0,0.1); width: 400px; max-width: 90vw;
|
|
76
|
+
text-align: center; position: relative;
|
|
77
|
+
}
|
|
78
|
+
.config-card h2 { color: #6a5e54; font-size: 18px; margin-bottom: 8px; }
|
|
79
|
+
.config-card p { color: #b0a8a0; font-size: 13px; margin-bottom: 20px; }
|
|
80
|
+
.config-card textarea {
|
|
81
|
+
width: 100%; height: 120px; border: 1px solid #d8d0c8; border-radius: 8px;
|
|
82
|
+
padding: 10px; font-size: 13px; resize: vertical; margin-bottom: 12px;
|
|
83
|
+
font-family: monospace; background: #fff;
|
|
84
|
+
}
|
|
85
|
+
.config-card textarea:focus { outline: none; border-color: #6a9ec5; }
|
|
86
|
+
.file-btn {
|
|
87
|
+
display: inline-block; padding: 8px 20px; background: #ede6df; color: #6a5e54;
|
|
88
|
+
border: none; border-radius: 8px; cursor: pointer; font-size: 13px; margin-bottom: 12px;
|
|
89
|
+
}
|
|
90
|
+
.file-btn:hover { background: #e0d6cc; }
|
|
91
|
+
.config-card input[type="file"] { display: none; }
|
|
92
|
+
.config-card .confirm-btn {
|
|
93
|
+
display: inline-block; padding: 8px 28px; background: #6a9ec5; color: #fff;
|
|
94
|
+
border: none; border-radius: 8px; cursor: pointer; font-size: 14px; margin-top: 4px;
|
|
95
|
+
}
|
|
96
|
+
.config-card .confirm-btn:hover { background: #5a8eb5; }
|
|
97
|
+
.config-card .confirm-btn:disabled { background: #ccc; cursor: not-allowed; }
|
|
98
|
+
.config-card .error-msg { color: #d4756b; font-size: 12px; margin-top: 8px; min-height: 18px; }
|
|
99
|
+
.config-hint { color: #b0a8a0; font-size: 11px; margin-bottom: 16px; }
|
|
100
|
+
.change-config-btn {
|
|
101
|
+
display: inline-block; margin-left: 8px; padding: 2px 8px;
|
|
102
|
+
font-size: 11px; color: #8a7e74; background: #ede6df; border: none;
|
|
103
|
+
border-radius: 8px; cursor: pointer;
|
|
104
|
+
}
|
|
105
|
+
.change-config-btn:hover { background: #e0d6cc; }
|
|
106
|
+
.filter-tabs { display: flex; gap: 6px; justify-content: center; max-width: 700px; margin: 8px auto 12px; }
|
|
107
|
+
.filter-tab {
|
|
108
|
+
padding: 3px 14px; border: 1px solid #d8d0c8; border-radius: 20px;
|
|
109
|
+
background: #f5f0eb; color: #8a7e74; cursor: pointer; font-size: 12px;
|
|
110
|
+
line-height: 1.4;
|
|
111
|
+
}
|
|
112
|
+
.filter-tab:hover { background: #ede6df; }
|
|
113
|
+
.filter-tab.active { background: #6a9ec5; color: #fff; border-color: #6a9ec5; }
|
|
114
|
+
`}function rt(){return`
|
|
115
|
+
<h1>\u57FA\u91D1\u4F30\u503C\u67E5\u8BE2</h1>
|
|
116
|
+
<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>
|
|
117
|
+
<div class="filter-tabs" id="filterTabs">
|
|
118
|
+
<button class="filter-tab active" data-cat="" data-label="\u5168\u90E8">\u5168\u90E8 0</button>
|
|
119
|
+
<button class="filter-tab" data-cat="\u504F\u80A1" data-label="\u504F\u80A1">\u504F\u80A1 0</button>
|
|
120
|
+
<button class="filter-tab" data-cat="\u504F\u503A" data-label="\u504F\u503A">\u504F\u503A 0</button>
|
|
121
|
+
<button class="filter-tab" data-cat="\u6307\u6570" data-label="\u6307\u6570">\u6307\u6570 0</button>
|
|
122
|
+
<button class="filter-tab" data-cat="\u5168\u7403" data-label="\u5168\u7403">\u5168\u7403 0</button>
|
|
123
|
+
</div>
|
|
124
|
+
<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>
|
|
126
|
+
<tbody></tbody>
|
|
127
|
+
</table>
|
|
128
|
+
<div class="drawer-overlay" id="drawerOverlay"></div>
|
|
129
|
+
<div class="drawer" id="drawer">
|
|
130
|
+
<button class="drawer-close" id="drawerClose">×</button>
|
|
131
|
+
<div id="drawerContent"></div>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<div class="config-overlay" id="configOverlay">
|
|
135
|
+
<div class="config-card">
|
|
136
|
+
<button class="config-close" id="configClose">×</button>
|
|
137
|
+
<h2>\u9009\u62E9\u914D\u7F6E\u6587\u4EF6</h2>
|
|
138
|
+
<p>\u8BF7\u4E0A\u4F20\u6216\u7C98\u8D34\u57FA\u91D1\u914D\u7F6E JSON \u6587\u4EF6</p>
|
|
139
|
+
<label class="file-btn">
|
|
140
|
+
\u9009\u62E9\u6587\u4EF6
|
|
141
|
+
<input type="file" id="fileInput" accept=".json">
|
|
142
|
+
</label>
|
|
143
|
+
<div class="config-hint">\u6216\u76F4\u63A5\u7C98\u8D34 JSON \u5185\u5BB9\uFF1A</div>
|
|
144
|
+
<textarea id="configText" placeholder='{"000001":"\u793A\u4F8B\u57FA\u91D1"}'></textarea>
|
|
145
|
+
<button class="confirm-btn" id="confirmBtn">\u786E\u8BA4</button>
|
|
146
|
+
<div class="error-msg" id="errorMsg"></div>
|
|
147
|
+
</div>
|
|
148
|
+
</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
|
+
|
|
157
|
+
const state = {
|
|
158
|
+
sortKey: 'estimatedChange',
|
|
159
|
+
sortDir: 'desc',
|
|
160
|
+
currentData: [],
|
|
161
|
+
activeCategory: '',
|
|
162
|
+
drawerSortKey: '',
|
|
163
|
+
drawerSortDir: 'desc',
|
|
164
|
+
drawerFund: null,
|
|
165
|
+
fundMap: {},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const dom = {
|
|
169
|
+
filterTabs: document.getElementById('filterTabs'),
|
|
170
|
+
filterTabItems: () => document.querySelectorAll('.filter-tab'),
|
|
171
|
+
mainTable: document.getElementById('mainTable'),
|
|
172
|
+
mainTableHead: document.getElementById('mainTable').querySelector('thead'),
|
|
173
|
+
mainTableBody: document.querySelector('#mainTable tbody'),
|
|
174
|
+
timeText: document.getElementById('timeText'),
|
|
175
|
+
drawer: document.getElementById('drawer'),
|
|
176
|
+
drawerOverlay: document.getElementById('drawerOverlay'),
|
|
177
|
+
drawerClose: document.getElementById('drawerClose'),
|
|
178
|
+
drawerContent: document.getElementById('drawerContent'),
|
|
179
|
+
configOverlay: document.getElementById('configOverlay'),
|
|
180
|
+
configClose: document.getElementById('configClose'),
|
|
181
|
+
fileInput: document.getElementById('fileInput'),
|
|
182
|
+
configText: document.getElementById('configText'),
|
|
183
|
+
confirmBtn: document.getElementById('confirmBtn'),
|
|
184
|
+
errorMsg: document.getElementById('errorMsg'),
|
|
185
|
+
refreshBtn: document.getElementById('refreshBtn'),
|
|
186
|
+
updateTime: document.getElementById('updateTime'),
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
function renderFilteredTable() {
|
|
190
|
+
const filtered = getFilteredFunds();
|
|
191
|
+
sortData();
|
|
192
|
+
renderFilterCounts();
|
|
193
|
+
renderTable(filtered);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function renderFilterCounts() {
|
|
197
|
+
const counts = state.currentData.reduce((acc, fund) => {
|
|
198
|
+
acc[fund.category] = (acc[fund.category] || 0) + 1;
|
|
199
|
+
return acc;
|
|
200
|
+
}, { \u5168\u90E8: state.currentData.length });
|
|
201
|
+
|
|
202
|
+
dom.filterTabItems().forEach(tab => {
|
|
203
|
+
const cat = tab.dataset.cat;
|
|
204
|
+
const label = tab.dataset.label;
|
|
205
|
+
const count = cat ? (counts[cat] || 0) : counts['\u5168\u90E8'];
|
|
206
|
+
tab.textContent = label + ' ' + count;
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function colorClass(val) {
|
|
211
|
+
if (val === null || val === undefined || Number.isNaN(val)) return 'flat';
|
|
212
|
+
return val > 0 ? 'up' : val < 0 ? 'down' : 'flat';
|
|
213
|
+
}
|
|
214
|
+
function fmtPercent(val) {
|
|
215
|
+
if (val === null || val === undefined || Number.isNaN(val)) return '-';
|
|
216
|
+
const sign = val > 0 ? '+' : '';
|
|
217
|
+
return sign + val.toFixed(2) + '%';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function fmtNullablePercent(val) {
|
|
221
|
+
if (val === null || val === undefined || Number.isNaN(val)) return '-';
|
|
222
|
+
return fmtPercent(val);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function hasDisplayableContributions(fund) {
|
|
226
|
+
if (!Array.isArray(fund.contributions) || fund.contributions.length === 0) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return fund.detailMode !== 'global' || fund.contributions.some(c =>
|
|
231
|
+
(c.todayChange !== null && c.todayChange !== undefined) ||
|
|
232
|
+
(c.yesterdayChange !== null && c.yesterdayChange !== undefined)
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function getEstimateLabelText(fund) {
|
|
237
|
+
if (fund.estimateDateLabel) return fund.estimateDateLabel;
|
|
238
|
+
if (fund.estimateLabel) return formatDateLabel(fund.estimateLabel);
|
|
239
|
+
return '';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function getFilteredFunds() {
|
|
243
|
+
return state.activeCategory
|
|
244
|
+
? state.currentData.filter(f => f.category === state.activeCategory)
|
|
245
|
+
: state.currentData;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function sortData() {
|
|
249
|
+
state.currentData.sort((a, b) => {
|
|
250
|
+
let va = a[state.sortKey], vb = b[state.sortKey];
|
|
251
|
+
if (typeof va === 'string') {
|
|
252
|
+
va = va.localeCompare ? va : '';
|
|
253
|
+
vb = vb.localeCompare ? vb : '';
|
|
254
|
+
return state.sortDir === 'asc' ? va.localeCompare(vb) : vb.localeCompare(va);
|
|
255
|
+
}
|
|
256
|
+
if (va === null || va === undefined || Number.isNaN(va)) return 1;
|
|
257
|
+
if (vb === null || vb === undefined || Number.isNaN(vb)) return -1;
|
|
258
|
+
return state.sortDir === 'asc' ? va - vb : vb - va;
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
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>';
|
|
267
|
+
}
|
|
268
|
+
|
|
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>' : '') +
|
|
274
|
+
'</td>';
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function renderTable(data) {
|
|
278
|
+
dom.mainTableBody.innerHTML = data.map(fund =>
|
|
279
|
+
'<tr><td>' + fund.code + '</td>' +
|
|
280
|
+
buildFundNameCell(fund) +
|
|
281
|
+
buildEstimateCell(fund) +
|
|
282
|
+
'</tr>'
|
|
283
|
+
).join('');
|
|
284
|
+
dom.timeText.textContent = '\u66F4\u65B0\u65F6\u95F4: ' + new Date().toLocaleString('zh-CN');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function showDrawer(fund) {
|
|
288
|
+
state.drawerFund = fund;
|
|
289
|
+
state.drawerSortKey = '';
|
|
290
|
+
state.drawerSortDir = 'desc';
|
|
291
|
+
renderDrawer();
|
|
292
|
+
dom.drawer.classList.add('open');
|
|
293
|
+
dom.drawerOverlay.classList.add('open');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function getSortedDrawerContributions(fund) {
|
|
297
|
+
const sorted = [...fund.contributions];
|
|
298
|
+
if (state.drawerSortKey) {
|
|
299
|
+
sorted.sort((a, b) => {
|
|
300
|
+
if (state.drawerSortKey === 'holdingRatio') return state.drawerSortDir === 'asc' ? a.holdingRatio - b.holdingRatio : b.holdingRatio - a.holdingRatio;
|
|
301
|
+
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);
|
|
303
|
+
return 0;
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
return sorted;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function compareNullableNumbers(a, b, dir) {
|
|
310
|
+
const aMissing = a === null || a === undefined || Number.isNaN(a);
|
|
311
|
+
const bMissing = b === null || b === undefined || Number.isNaN(b);
|
|
312
|
+
if (aMissing && bMissing) return 0;
|
|
313
|
+
if (aMissing) return 1;
|
|
314
|
+
if (bMissing) return -1;
|
|
315
|
+
return dir === 'asc' ? a - b : b - a;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function buildGlobalDrawerTable(fund, sorted) {
|
|
319
|
+
let html = '<table><thead><tr>' +
|
|
320
|
+
'<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>' +
|
|
324
|
+
'</tr></thead><tbody>';
|
|
325
|
+
for (const c of sorted) {
|
|
326
|
+
html += '<tr><td>' + c.stockCode + ' ' + c.stockName + '</td>' +
|
|
327
|
+
'<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>';
|
|
330
|
+
}
|
|
331
|
+
return html + '</tbody></table>';
|
|
332
|
+
}
|
|
333
|
+
|
|
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>';
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function renderDrawer() {
|
|
349
|
+
const fund = state.drawerFund;
|
|
350
|
+
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);
|
|
356
|
+
dom.drawerContent.innerHTML = html;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function hideDrawer() {
|
|
360
|
+
dom.drawer.classList.remove('open');
|
|
361
|
+
dom.drawerOverlay.classList.remove('open');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function buildMap(data) {
|
|
365
|
+
state.fundMap = {};
|
|
366
|
+
for (const f of data) state.fundMap[f.code] = f;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function updateMainSort(key) {
|
|
370
|
+
if (state.sortKey === key) state.sortDir = state.sortDir === 'desc' ? 'asc' : 'desc';
|
|
371
|
+
else {
|
|
372
|
+
state.sortKey = key;
|
|
373
|
+
state.sortDir = 'desc';
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function updateDrawerSort(key) {
|
|
378
|
+
if (state.drawerSortKey === key) state.drawerSortDir = state.drawerSortDir === 'desc' ? 'asc' : 'desc';
|
|
379
|
+
else {
|
|
380
|
+
state.drawerSortKey = key;
|
|
381
|
+
state.drawerSortDir = 'desc';
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function setCurrentData(data) {
|
|
386
|
+
state.currentData = data;
|
|
387
|
+
buildMap(data);
|
|
388
|
+
sortData();
|
|
389
|
+
renderFilteredTable();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function setActiveCategory(tab) {
|
|
393
|
+
dom.filterTabItems().forEach(t => t.classList.remove('active'));
|
|
394
|
+
tab.classList.add('active');
|
|
395
|
+
state.activeCategory = tab.dataset.cat;
|
|
396
|
+
renderFilteredTable();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function handleMainSortClick(e) {
|
|
400
|
+
const th = e.target.closest('th.sortable');
|
|
401
|
+
if (!th) return;
|
|
402
|
+
updateMainSort(th.dataset.key);
|
|
403
|
+
document.querySelectorAll('#mainTable th.sortable').forEach(t => t.classList.remove('asc', 'desc'));
|
|
404
|
+
th.classList.add(state.sortDir);
|
|
405
|
+
sortData();
|
|
406
|
+
renderFilteredTable();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function handleFundClick(e) {
|
|
410
|
+
const el = e.target.closest('.fund-name');
|
|
411
|
+
if (!el) return;
|
|
412
|
+
const code = el.dataset.code;
|
|
413
|
+
if (!code) return;
|
|
414
|
+
if (state.fundMap[code]) showDrawer(state.fundMap[code]);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function handleDrawerSortClick(e) {
|
|
418
|
+
const th = e.target.closest('th.sortable');
|
|
419
|
+
if (!th) return;
|
|
420
|
+
updateDrawerSort(th.dataset.dkey);
|
|
421
|
+
renderDrawer();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function getStoredConfig() {
|
|
425
|
+
try {
|
|
426
|
+
const raw = localStorage.getItem('fundConfig');
|
|
427
|
+
if (!raw) return null;
|
|
428
|
+
const obj = JSON.parse(raw);
|
|
429
|
+
if (obj && typeof obj === 'object' && Object.keys(obj).length > 0) return obj;
|
|
430
|
+
return null;
|
|
431
|
+
} catch { return null; }
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function showConfigUI() {
|
|
435
|
+
dom.configOverlay.classList.remove('hidden');
|
|
436
|
+
}
|
|
437
|
+
function hideConfigUI() {
|
|
438
|
+
dom.configOverlay.classList.add('hidden');
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function validateConfig(text) {
|
|
442
|
+
let obj;
|
|
443
|
+
try { obj = JSON.parse(text); } catch { throw new Error('JSON \u683C\u5F0F\u9519\u8BEF'); }
|
|
444
|
+
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) throw new Error('\u914D\u7F6E\u683C\u5F0F\u9519\u8BEF\uFF0C\u9700\u8981\u57FA\u91D1\u4EE3\u7801\u5230\u540D\u79F0\u7684\u6620\u5C04');
|
|
445
|
+
if (Object.keys(obj).length === 0) throw new Error('\u914D\u7F6E\u4E0D\u80FD\u4E3A\u7A7A');
|
|
446
|
+
return obj;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function handleFileInputChange(e) {
|
|
450
|
+
const file = e.target.files[0];
|
|
451
|
+
if (!file) return;
|
|
452
|
+
const reader = new FileReader();
|
|
453
|
+
reader.onload = function(ev) {
|
|
454
|
+
dom.configText.value = ev.target.result;
|
|
455
|
+
};
|
|
456
|
+
reader.readAsText(file);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function handleConfirmConfig() {
|
|
460
|
+
const text = dom.configText.value.trim();
|
|
461
|
+
const errEl = dom.errorMsg;
|
|
462
|
+
if (!text) { errEl.textContent = '\u8BF7\u9009\u62E9\u6587\u4EF6\u6216\u7C98\u8D34 JSON \u5185\u5BB9'; return; }
|
|
463
|
+
try {
|
|
464
|
+
const config = validateConfig(text);
|
|
465
|
+
localStorage.setItem('fundConfig', JSON.stringify(config));
|
|
466
|
+
errEl.textContent = '';
|
|
467
|
+
hideConfigUI();
|
|
468
|
+
doRefresh();
|
|
469
|
+
} catch (e) {
|
|
470
|
+
errEl.textContent = e.message;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// \u5728\u66F4\u65B0\u65F6\u95F4\u533A\u57DF\u6DFB\u52A0"\u5207\u6362\u914D\u7F6E"\u6309\u94AE
|
|
475
|
+
const changeBtn = document.createElement('button');
|
|
476
|
+
changeBtn.className = 'change-config-btn';
|
|
477
|
+
changeBtn.textContent = '\u5207\u6362\u914D\u7F6E';
|
|
478
|
+
changeBtn.addEventListener('click', showConfigUI);
|
|
479
|
+
dom.updateTime.appendChild(changeBtn);
|
|
480
|
+
|
|
481
|
+
async function doRefresh() {
|
|
482
|
+
dom.refreshBtn.classList.add('spinning');
|
|
483
|
+
try {
|
|
484
|
+
const config = getStoredConfig();
|
|
485
|
+
if (!config) { showConfigUI(); return; }
|
|
486
|
+
const res = await fetch('/api/funds', {
|
|
487
|
+
method: 'POST',
|
|
488
|
+
headers: { 'Content-Type': 'application/json' },
|
|
489
|
+
body: JSON.stringify({ funds: config })
|
|
490
|
+
});
|
|
491
|
+
if (!res.ok) throw new Error('\u8BF7\u6C42\u5931\u8D25');
|
|
492
|
+
const data = await res.json();
|
|
493
|
+
setCurrentData(data);
|
|
494
|
+
} catch {
|
|
495
|
+
state.currentData = [];
|
|
496
|
+
renderTable([]);
|
|
497
|
+
}
|
|
498
|
+
dom.refreshBtn.classList.remove('spinning');
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function bindEvents() {
|
|
502
|
+
dom.filterTabs.addEventListener('click', function(e) {
|
|
503
|
+
const tab = e.target.closest('.filter-tab');
|
|
504
|
+
if (!tab) return;
|
|
505
|
+
setActiveCategory(tab);
|
|
506
|
+
});
|
|
507
|
+
dom.mainTableHead.addEventListener('click', handleMainSortClick);
|
|
508
|
+
dom.mainTable.addEventListener('click', handleFundClick);
|
|
509
|
+
dom.drawerOverlay.addEventListener('click', hideDrawer);
|
|
510
|
+
dom.drawerClose.addEventListener('click', hideDrawer);
|
|
511
|
+
dom.configClose.addEventListener('click', hideConfigUI);
|
|
512
|
+
dom.drawerContent.addEventListener('click', handleDrawerSortClick);
|
|
513
|
+
dom.fileInput.addEventListener('change', handleFileInputChange);
|
|
514
|
+
dom.confirmBtn.addEventListener('click', handleConfirmConfig);
|
|
515
|
+
dom.refreshBtn.addEventListener('click', doRefresh);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// \u521D\u59CB\u5316\uFF1A\u68C0\u67E5 localStorage\uFF0C\u6709\u914D\u7F6E\u5219\u76F4\u63A5\u52A0\u8F7D
|
|
519
|
+
(function init() {
|
|
520
|
+
bindEvents();
|
|
521
|
+
const config = getStoredConfig();
|
|
522
|
+
if (config) {
|
|
523
|
+
hideConfigUI();
|
|
524
|
+
doRefresh();
|
|
525
|
+
} else {
|
|
526
|
+
showConfigUI();
|
|
527
|
+
}
|
|
528
|
+
})();
|
|
529
|
+
|
|
530
|
+
// \u6BCF30\u79D2\u81EA\u52A8\u5237\u65B0
|
|
531
|
+
setInterval(doRefresh, 30000);
|
|
532
|
+
`}function it(){return`<!DOCTYPE html>
|
|
533
|
+
<html lang="zh-CN">
|
|
534
|
+
<head>
|
|
535
|
+
<meta charset="UTF-8">
|
|
536
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
537
|
+
<title>\u57FA\u91D1\u4F30\u503C\u67E5\u8BE2</title>
|
|
538
|
+
<style>${ot()}</style>
|
|
539
|
+
</head>
|
|
540
|
+
<body>
|
|
541
|
+
${rt()}
|
|
542
|
+
<script>${st()}</script>
|
|
543
|
+
</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();
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cxxgo/fund-valuation-query",
|
|
3
|
+
"version": "1.0.4",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"fund": "./dist/fund.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"start": "node dist/fund.js serve",
|
|
10
|
+
"build": "node scripts/build.js",
|
|
11
|
+
"prepublishOnly": "npm run build",
|
|
12
|
+
"publish": "npm publish",
|
|
13
|
+
"release:patch": "npm version patch && git push && git push --tags && npm publish",
|
|
14
|
+
"release:minor": "npm version minor && git push && git push --tags && npm publish",
|
|
15
|
+
"release:major": "npm version major && git push && git push --tags && npm publish"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/cxxgo/fund-valuation-query.git"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [],
|
|
22
|
+
"author": "",
|
|
23
|
+
"license": "ISC",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/cxxgo/fund-valuation-query/issues"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/cxxgo/fund-valuation-query#readme",
|
|
28
|
+
"description": "基金实时估值查询 CLI 工具,根据持仓股票行情估算基金涨跌幅",
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public",
|
|
31
|
+
"registry": "https://registry.npmjs.org/"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist"
|
|
35
|
+
],
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"esbuild": "^0.25.12"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"axios": "^1.9.0",
|
|
41
|
+
"chalk": "^5.3.0",
|
|
42
|
+
"cheerio": "^1.0.0",
|
|
43
|
+
"cli-table3": "^0.6.5",
|
|
44
|
+
"commander": "^12.1.0",
|
|
45
|
+
"express": "^4.21.0"
|
|
46
|
+
}
|
|
47
|
+
}
|