├── .gitignore ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs └── compatibility.md ├── manifest-chrome.json ├── manifest-firefox.json ├── package.json ├── src ├── background.ts ├── content.ts ├── global.d.ts ├── images │ ├── gzh.jpg │ ├── hb-grid.png │ ├── logo │ │ ├── 1024x1024.png │ │ ├── 128x128.png │ │ ├── 16x16.png │ │ ├── 32x32.png │ │ ├── 48x48.png │ │ ├── icon1024.png │ │ ├── icon128.png │ │ ├── icon16.png │ │ ├── icon32.png │ │ └── icon48.png │ └── wx.jpg ├── index.html ├── options │ ├── index.scss │ └── index.tsx ├── popup │ ├── .DS_Store │ ├── grid │ │ ├── index.scss │ │ └── index.tsx │ ├── hb │ │ ├── index.scss │ │ └── index.tsx │ ├── index.scss │ ├── index.tsx │ └── points │ │ └── index.tsx ├── styles │ ├── global.scss │ └── variable.scss └── utils │ └── index.ts ├── tsconfig.json ├── webpack.config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # production 5 | /dist 6 | *.zip 7 | 8 | .DS_Store -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.md -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "printWidth": 80, 6 | "tabWidth": 2 7 | } 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## [0.4.0] - 2025-07-21 4 | 5 | - 新增生成自定义网格策略功能 6 | - 网格策略遵循E大的网格三篇,为了增加网格利润有三个策略,分别是:`逐级加码`、`一网打尽`、`留利润` 7 | - 显示网格策略的压力测试和盈利测算 8 | 9 | ## [0.3.2] - 2025-06-22 10 | 11 | - 修复一个bug。 12 | 13 | ## [0.3.1] - 2025-06-22 14 | 15 | - 插件顶部增加新版本提醒。 16 | - E大关键点位改为表格展示。 17 | - 关键点位标线根据该点位的发布日期开始画线。 18 | - 点击关键点位日期可将 K 线图的范围转换为该日期到当前日期。 19 | - 增加显示该关键点位发布日期的收盘价。 20 | 21 | ## [0.3.0] - 2025-06-16 22 | 23 | - 新增E大关键点位数据,使用 K 线图可视化展示。 24 | - 华宝条件单延期判断条件更改为 60 天。 25 | 26 | ## [0.2.0] - 2025-05-23 27 | 28 | - 没有符合延期条件的订单时弹窗提醒,避免点击延期按钮无反馈 29 | - Options 页面增加更新日志链接 30 | - Popup 页面增加网格策略最近更新日期及显示策略网页二维码 31 | 32 | ## [0.1.0] - 2025-05-11 33 | 34 | - 快捷打开华宝智投条件单网页 35 | - 一键延期所有条件单 36 | - 停止正在进行的延期 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Max 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ETF投资助手 2 | 3 | 一键延期华宝条件单;利用 K 线图显示 E大(ETF拯救世界)关键点位;自定义网格策略 4 | 5 | ## 下载 6 | 7 | 1. [Chrome 浏览器](https://chromewebstore.google.com/detail/%E5%8D%8E%E5%AE%9D%E6%9D%A1%E4%BB%B6%E5%8D%95%E5%8A%A9%E6%89%8B/peeidnfboehmmandoghpbhgmadegikaf) 8 | 2. [Edge 浏览器]()(审核中) 9 | 3. [其他浏览器](https://www.maxmeng.top/files/hb-helper.crx.zip)(参考离线包安装教程) 10 | 11 | ## 离线包安装教程 12 | 13 | 1. 下载安装文件,点击[下载](https://www.maxmeng.top/files/hb-helper.crx.zip)离线包。 14 | 2. 打开浏览器插件管理页面,地址为 `chrome://extensions/`。 15 | 3. 开启“开发者模式”。 16 | 4. 重启浏览器。 17 | 5. 安装插件。拖拽第一步中预先下载的浏览器插件安装包至 `chrome://extensions` 页面即可。 18 | 19 | ## 功能 20 | 21 | 1. 一键延期华宝条件单 22 | 2. 利用 K 线图显示 E大(ETF拯救世界)关键点位 23 | 3. 自定义网格策略 24 | 25 | ## 华宝条件单自动延期 26 | 27 | 1. 点击插件小图标,显示插件弹窗。 28 | 2. 点击“查看条件单”,自动打开“华宝智投条件单”网页。 29 | 3. 点击“自动延期(单个)”,自动延期第一个符合条件的条件单。 30 | 4. 点击“自动延期(多个)”,自动延期所有符合条件的条件单。如果想提前结束,则点击“停止延期”。 31 | 32 | ## 其他 33 | 34 | 1. [web-ext](https://extensionworkshop.com/documentation/develop/getting-started-with-web-ext/) 35 | 2. [chrome-extensions](https://developer.chrome.com/docs/extensions/mv3/architecture-overview/) 36 | -------------------------------------------------------------------------------- /docs/compatibility.md: -------------------------------------------------------------------------------- 1 | ## manifest.json 文件差异 2 | 3 | ### 1. `background` 字段 4 | 5 | - firefox 6 | 7 | ```json 8 | { 9 | "background": { 10 | "scripts": ["src/background.js"] 11 | } 12 | } 13 | ``` 14 | 15 | - chrome 16 | 17 | ```json 18 | { 19 | "background": { 20 | "service_worker": "src/background.js", 21 | "type": "module" 22 | } 23 | } 24 | ``` 25 | -------------------------------------------------------------------------------- /manifest-chrome.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "ETF投资助手", 4 | "version": "0.4.2", 5 | "description": "一键延期华宝条件单;利用K线图显示E大(ETF拯救世界)关键点位;自定义网格策略。", 6 | "action": { 7 | "default_popup": "popup.html", 8 | "default_icon": { 9 | "16": "images/logo/16x16.png", 10 | "32": "images/logo/32x32.png", 11 | "48": "images/logo/icon48.png", 12 | "128": "images/logo/icon128.png" 13 | } 14 | }, 15 | "content_scripts": [ 16 | { 17 | "matches": ["https://*.touker.com/*"], 18 | "js": ["content.js"] 19 | } 20 | ], 21 | "options_page": "options.html", 22 | "permissions": ["activeTab", "storage"], 23 | "optional_permissions": [], 24 | "background": { 25 | "service_worker": "background.js", 26 | "type": "module" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /manifest-firefox.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "ETF投资助手", 4 | "version": "0.4.2", 5 | "description": "一键延期华宝条件单;利用K线图显示E大(ETF拯救世界)关键点位;自定义网格策略。", 6 | "action": { 7 | "default_popup": "popup.html", 8 | "default_icon": { 9 | "16": "images/logo/16x16.png", 10 | "32": "images/logo/32x32.png", 11 | "48": "images/logo/icon48.png", 12 | "128": "images/logo/icon128.png" 13 | } 14 | }, 15 | "content_scripts": [ 16 | { 17 | "matches": ["https://*.touker.com/*"], 18 | "js": ["content.js"] 19 | } 20 | ], 21 | "options_page": "options.html", 22 | "permissions": ["activeTab", "storage"], 23 | "optional_permissions": [], 24 | "background": { 25 | "scripts": ["background.js"] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "etf-helper", 3 | "version": "0.4.2", 4 | "description": "ETF投资助手。一键延期华宝条件单;利用K线图显示E大(ETF拯救世界)关键点位;自定义网格策略。", 5 | "scripts": { 6 | "build": "webpack --env production", 7 | "watch": "webpack --watch", 8 | "reload:firfox": "web-ext run --source-dir=dist" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/maxmeng93/hb-helper.git" 13 | }, 14 | "keywords": [], 15 | "author": "maxmeng93", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/maxmeng93/hb-helper/issues" 19 | }, 20 | "homepage": "https://github.com/maxmeng93/hb-helper#readme", 21 | "devDependencies": { 22 | "@babel/core": "^7.24.5", 23 | "@babel/preset-env": "^7.24.5", 24 | "@types/chrome": "^0.0.268", 25 | "babel-loader": "^9.1.3", 26 | "clean-webpack-plugin": "^4.0.0", 27 | "copy-webpack-plugin": "^12.0.2", 28 | "css-loader": "^7.1.2", 29 | "html-webpack-plugin": "^5.6.0", 30 | "sass": "^1.77.2", 31 | "sass-loader": "^14.2.1", 32 | "style-loader": "^4.0.0", 33 | "ts-loader": "^9.5.1", 34 | "typescript": "^5.4.5", 35 | "web-ext": "^7.11.0", 36 | "webpack": "^5.91.0", 37 | "webpack-cli": "^5.1.4", 38 | "webpack-dev-server": "^5.0.4" 39 | }, 40 | "dependencies": { 41 | "@ant-design/icons": "^5.3.7", 42 | "@types/react": "^18.3.3", 43 | "@types/react-dom": "^18.3.0", 44 | "antd": "^5.17.4", 45 | "classnames": "^2.5.1", 46 | "echarts": "^5.5.0", 47 | "mathjs": "^13.0.2", 48 | "react": "^18.3.1", 49 | "react-dom": "^18.3.1" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/background.ts: -------------------------------------------------------------------------------- 1 | // 监听安装事件 2 | chrome.runtime.onInstalled.addListener(() => { 3 | console.log("扩展程序已安装"); 4 | 5 | // 插件上显示徽章 6 | // chrome.action.setBadgeText({ 7 | // text: "ON", 8 | // }); 9 | }); 10 | 11 | // 监听来自 content script 的消息 12 | // chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 13 | // if (request.action === 'getBookmarks') { 14 | // chrome.bookmarks.getTree((bookmarkTreeNodes) => { 15 | // sendResponse(bookmarkTreeNodes); 16 | // }); 17 | // return true; 18 | // } 19 | // }); 20 | -------------------------------------------------------------------------------- /src/content.ts: -------------------------------------------------------------------------------- 1 | // 缓存前缀 2 | const PREFIX = "hb-helper"; 3 | // 检查截止日期 4 | const CHECK_DEADLINE = "CHECK_DEADLINE"; 5 | // 修改截止日期 6 | const CHANGE_DEADLINE = "CHANGE_DEADLINE"; 7 | // 提交订单 8 | const SUBMIT_ORDER = "SUBMIT_ORDER"; 9 | 10 | const single = "postpone-single"; 11 | const multiple = "postpone-multiple"; 12 | const postponeStop = "postpone-stop"; 13 | 14 | const stopCache = "stop"; 15 | const stateCache = "state"; 16 | const postponeTypeCache = "postpone-type"; 17 | 18 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 19 | const action = request.action; 20 | 21 | switch (action) { 22 | case single: 23 | case multiple: 24 | const orders = getOrders(); 25 | if (orders.length === 0) { 26 | alert( 27 | "没有符合延期条件的的订单。\n本插件暂时只延期截止日期在20天内的条件单。" 28 | ); 29 | return; 30 | } 31 | setLocalStorage(postponeTypeCache, action); 32 | checkDeadline(orders); 33 | break; 34 | case postponeStop: 35 | setLocalStorage(stopCache, "true"); 36 | break; 37 | default: 38 | break; 39 | } 40 | }); 41 | 42 | window.addEventListener("load", function () { 43 | checkState(); 44 | }); 45 | 46 | function delay(time: number) { 47 | return new Promise((resolve) => { 48 | setTimeout(() => { 49 | resolve(true); 50 | }, time); 51 | }); 52 | } 53 | 54 | function setLocalStorage(key: string, value: string) { 55 | const dataKey = `${PREFIX}-${key}`; 56 | chrome.storage.local.set({ [dataKey]: value }); 57 | } 58 | 59 | function getLocalStorage(key: string) { 60 | const dataKey = `${PREFIX}-${key}`; 61 | return new Promise((resolve, reject) => { 62 | chrome.storage.local.get(dataKey, function (data) { 63 | resolve(data[dataKey]); 64 | }); 65 | }); 66 | } 67 | 68 | async function checkState() { 69 | try { 70 | const isStop = await getLocalStorage(stopCache); 71 | if (isStop) { 72 | setLocalStorage(stopCache, ""); 73 | setLocalStorage(stateCache, ""); 74 | return; 75 | } 76 | 77 | const state = await getLocalStorage(stateCache); 78 | 79 | // 检查截止日期 80 | if (state === CHECK_DEADLINE) { 81 | const postponeType = await getLocalStorage(postponeTypeCache); 82 | if (postponeType === single) return; 83 | await delay(1000); 84 | const orders = getOrders(); 85 | checkDeadline(orders); 86 | } 87 | 88 | // 修改截止日期 89 | if (state === CHANGE_DEADLINE) { 90 | await delay(1000); 91 | let eles = document.querySelectorAll(".deadline-card .quick-label"); 92 | const last = eles[eles.length - 1].querySelector(".time-select-btn"); 93 | // @ts-ignore 94 | last.click(); 95 | 96 | const submit = document.querySelector(".submit-condition>input"); 97 | // @ts-ignore 98 | submit.click(); 99 | setLocalStorage(stateCache, SUBMIT_ORDER); 100 | } 101 | 102 | // 提交订单 103 | if (state === SUBMIT_ORDER) { 104 | await delay(1000); 105 | const checkbox = document.querySelector("#onlyTg"); 106 | if (!checkbox) return; 107 | // @ts-ignore 108 | if (!checkbox.checked) checkbox.click(); 109 | 110 | // @ts-ignore 111 | document.querySelector("#btnSubmit").click(); 112 | setLocalStorage(stateCache, CHECK_DEADLINE); 113 | } 114 | } catch (error) { 115 | setLocalStorage(stateCache, ""); 116 | } 117 | } 118 | 119 | function checkDeadline(orders: any[]) { 120 | for (let order of orders) { 121 | let ele = document.querySelectorAll(".monitor-item")[order.index]; 122 | 123 | // @ts-ignore 124 | const options = ele.querySelector(".opr").querySelectorAll("i") || []; 125 | // @ts-ignore 126 | for (let option of options) { 127 | if (option.textContent === "延期") { 128 | option.click(); 129 | 130 | setLocalStorage(stateCache, CHANGE_DEADLINE); 131 | break; 132 | } 133 | } 134 | return; 135 | } 136 | 137 | setLocalStorage(stateCache, ""); 138 | } 139 | 140 | function getOrders() { 141 | const list: { date: string; index: number }[] = []; 142 | let elements = document.querySelectorAll(".monitor-item"); 143 | 144 | for (let i = 0; i < elements.length; i++) { 145 | const element = elements[i]; 146 | let expireDate = element.querySelector(".expire-date"); 147 | // @ts-ignore 148 | const date = expireDate.textContent.replace("截止日期:", ""); 149 | if (isMatch(date)) list.push({ date, index: i }); 150 | } 151 | 152 | return list; 153 | } 154 | 155 | function isMatch(date: string) { 156 | const now = new Date(); 157 | const expireDate = new Date(date); 158 | const diff = expireDate.getTime() - now.getTime(); 159 | const days = Math.floor(diff / (24 * 3600 * 1000)); 160 | return days <= 61; 161 | } 162 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.scss" { 2 | const content: { [className: string]: string }; 3 | export default content; 4 | } 5 | -------------------------------------------------------------------------------- /src/images/gzh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxmeng93/hb-helper/8a8e5bcf9884b4c8eedaa5b2505885ebe7d8f5a9/src/images/gzh.jpg -------------------------------------------------------------------------------- /src/images/hb-grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxmeng93/hb-helper/8a8e5bcf9884b4c8eedaa5b2505885ebe7d8f5a9/src/images/hb-grid.png -------------------------------------------------------------------------------- /src/images/logo/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxmeng93/hb-helper/8a8e5bcf9884b4c8eedaa5b2505885ebe7d8f5a9/src/images/logo/1024x1024.png -------------------------------------------------------------------------------- /src/images/logo/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxmeng93/hb-helper/8a8e5bcf9884b4c8eedaa5b2505885ebe7d8f5a9/src/images/logo/128x128.png -------------------------------------------------------------------------------- /src/images/logo/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxmeng93/hb-helper/8a8e5bcf9884b4c8eedaa5b2505885ebe7d8f5a9/src/images/logo/16x16.png -------------------------------------------------------------------------------- /src/images/logo/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxmeng93/hb-helper/8a8e5bcf9884b4c8eedaa5b2505885ebe7d8f5a9/src/images/logo/32x32.png -------------------------------------------------------------------------------- /src/images/logo/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxmeng93/hb-helper/8a8e5bcf9884b4c8eedaa5b2505885ebe7d8f5a9/src/images/logo/48x48.png -------------------------------------------------------------------------------- /src/images/logo/icon1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxmeng93/hb-helper/8a8e5bcf9884b4c8eedaa5b2505885ebe7d8f5a9/src/images/logo/icon1024.png -------------------------------------------------------------------------------- /src/images/logo/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxmeng93/hb-helper/8a8e5bcf9884b4c8eedaa5b2505885ebe7d8f5a9/src/images/logo/icon128.png -------------------------------------------------------------------------------- /src/images/logo/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxmeng93/hb-helper/8a8e5bcf9884b4c8eedaa5b2505885ebe7d8f5a9/src/images/logo/icon16.png -------------------------------------------------------------------------------- /src/images/logo/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxmeng93/hb-helper/8a8e5bcf9884b4c8eedaa5b2505885ebe7d8f5a9/src/images/logo/icon32.png -------------------------------------------------------------------------------- /src/images/logo/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxmeng93/hb-helper/8a8e5bcf9884b4c8eedaa5b2505885ebe7d8f5a9/src/images/logo/icon48.png -------------------------------------------------------------------------------- /src/images/wx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxmeng93/hb-helper/8a8e5bcf9884b4c8eedaa5b2505885ebe7d8f5a9/src/images/wx.jpg -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ETF投资助手 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /src/options/index.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --color: #dd3432; 3 | --color-primary: #1677ff; 4 | --color-error: #ff4d4f; 5 | --color-disabled: #c2c2c2; 6 | --color-text-disabled: rgba(0, 0, 0, 0.25); 7 | --color-bg-disabled: rgba(0, 0, 0, 0.04); 8 | --color-border-disabled: #d9d9d9; 9 | --color-link-hover: #69b1ff; 10 | --color-link-active: #0958d9; 11 | } 12 | 13 | a { 14 | color: var(--color-primary); 15 | text-decoration: none; 16 | } 17 | 18 | a:hover { 19 | color: var(--color-link-hover); 20 | } 21 | 22 | a:active { 23 | color: var(--color-link-active); 24 | } 25 | 26 | #main { 27 | margin: 0 auto; 28 | width: 800px; 29 | } 30 | 31 | a { 32 | font-size: 18px; 33 | } 34 | 35 | img { 36 | width: 200px; 37 | } 38 | 39 | .version { 40 | font-size: 18px; 41 | } 42 | -------------------------------------------------------------------------------- /src/options/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from "react-dom/client"; 2 | import React, { useEffect, useState } from "react"; 3 | import "./index.scss"; 4 | 5 | const Options: React.FC = () => { 6 | const [version, setVersion] = useState(""); 7 | const [curVersion, setCurVersion] = useState(""); 8 | 9 | useEffect(() => { 10 | getConfig(); 11 | getCurVersion(); 12 | }, []); 13 | 14 | function getConfig() { 15 | const url = "https://www.maxmeng.top/data/hb-helper.json?t=" + Date.now(); 16 | 17 | fetch(url) 18 | .then((res) => res.json()) 19 | .then((data) => { 20 | const version = data?.version; 21 | if (version) { 22 | setVersion(version); 23 | } 24 | }) 25 | .catch((err) => { 26 | console.log(err); 27 | }); 28 | } 29 | 30 | function getCurVersion() { 31 | let manifestData = chrome.runtime.getManifest(); 32 | let version = manifestData.version; 33 | setCurVersion(version); 34 | } 35 | 36 | return ( 37 |
38 |
39 |

最新版本:

40 |
{version}
41 |
42 |
43 |

当前版本:

44 |
{curVersion}
45 |
46 |
47 |

更新日志:

48 | 52 | 查看 53 | 54 |
55 |
56 |

使用教程:

57 | 75 |
76 | 77 |
78 |

联系方式:

79 |
80 | 微信 81 | 公众号 82 |
83 |
84 |
85 | ); 86 | }; 87 | 88 | const container = document.getElementById("root"); 89 | const root = createRoot(container!); 90 | root.render(); 91 | -------------------------------------------------------------------------------- /src/popup/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxmeng93/hb-helper/8a8e5bcf9884b4c8eedaa5b2505885ebe7d8f5a9/src/popup/.DS_Store -------------------------------------------------------------------------------- /src/popup/grid/index.scss: -------------------------------------------------------------------------------- 1 | .result { 2 | p { 3 | margin: 0 0; 4 | } 5 | .icon { 6 | color: rgba(0, 0, 0, 0.45); 7 | cursor: help; 8 | } 9 | } 10 | 11 | .article { 12 | ul { 13 | margin: 0; 14 | padding: 0; 15 | li { 16 | list-style: none; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/popup/grid/index.tsx: -------------------------------------------------------------------------------- 1 | import * as math from 'mathjs'; 2 | import React, { useState, useEffect, useRef, useMemo } from 'react'; 3 | import { 4 | Button, 5 | Form, 6 | InputNumber, 7 | Table, 8 | Tooltip, 9 | Row, 10 | Col, 11 | message, 12 | } from 'antd'; 13 | import { QuestionCircleOutlined } from '@ant-design/icons'; 14 | import './index.scss'; 15 | 16 | const { create, all, add, pow, divide, number, round } = math; 17 | 18 | const { evaluate } = create(all, { 19 | number: 'BigNumber', 20 | }); 21 | 22 | interface Grid { 23 | key: string; 24 | // 网格种类 25 | type: string; 26 | // 档位 27 | level: number; 28 | // 买入触发价 29 | buyTriggerPrice: number; 30 | // 买入价 31 | buyPrice: number; 32 | // 买入金额 33 | buyAmount: number; 34 | // 入股数 35 | buyStock: number; 36 | // 卖出触发价 37 | sellTriggerPrice: number; 38 | // 卖出价 39 | sellPrice: number; 40 | // 出股数 41 | sellStock: number; 42 | // 卖出金额 43 | sellAmount: number; 44 | } 45 | 46 | /** 47 | * 数学计算 48 | * @param str 表达式 49 | * @param roundN 四舍五入的位数 50 | * @returns 计算结果 51 | */ 52 | const calc = (str: string, roundN?: number): number => { 53 | let d = evaluate(str); 54 | if (roundN) d = round(d, roundN); 55 | return number(d); 56 | }; 57 | 58 | const Grid: React.FC = () => { 59 | const [form] = Form.useForm(); 60 | const [data, setData] = useState([]); 61 | const mergeData = useRef({}); 62 | 63 | useEffect(() => { 64 | form.submit(); 65 | }, []); 66 | 67 | const d = useMemo(() => { 68 | const { basePrice } = form.getFieldsValue(); 69 | if (data.length === 0) return null; 70 | let buyAmount = 0; 71 | let buyStock = 0; 72 | let sellAmount = 0; 73 | let sellStock = 0; 74 | 75 | data.forEach((item) => { 76 | // buyAmount += item.buyAmount; 77 | buyAmount = calc(`${buyAmount} + ${item.buyAmount}`); 78 | buyStock += item.buyStock; 79 | // sellAmount += item.sellAmount; 80 | sellAmount = calc(`${sellAmount} + ${item.sellAmount}`); 81 | sellStock += item.sellStock; 82 | }); 83 | const surplusStock = calc(`${buyStock} - ${sellStock}`); 84 | const profit = calc( 85 | `${sellAmount} - ${buyAmount} + ${surplusStock} * ${basePrice}`, 86 | ); 87 | // 利润率 88 | const profitRate = calc(`${profit} / ${buyAmount} * 100`, 2); 89 | 90 | return { 91 | buyAmount, 92 | buyStock, 93 | sellAmount, 94 | sellStock, 95 | surplusStock, 96 | profit, 97 | profitRate, 98 | }; 99 | }, [data]); 100 | 101 | const columns = [ 102 | { 103 | title: '网格种类', 104 | dataIndex: 'type', 105 | key: 'type', 106 | onCell: (_, index) => { 107 | const len = mergeData.current[index]; 108 | if (len) return { rowSpan: len }; 109 | return { rowSpan: 0 }; 110 | }, 111 | }, 112 | { 113 | title: '档位', 114 | dataIndex: 'level', 115 | key: 'level', 116 | render: (v) => v.toFixed(2), 117 | }, 118 | { 119 | title: '买入触发价', 120 | dataIndex: 'buyTriggerPrice', 121 | key: 'buyTriggerPrice', 122 | render: (v) => v.toFixed(3), 123 | }, 124 | { 125 | title: '买入价', 126 | dataIndex: 'buyPrice', 127 | key: 'buyPrice', 128 | render: (v) => v.toFixed(3), 129 | }, 130 | { 131 | title: '买入金额', 132 | dataIndex: 'buyAmount', 133 | key: 'buyAmount', 134 | }, 135 | { 136 | title: '入股数', 137 | dataIndex: 'buyStock', 138 | key: 'buyStock', 139 | }, 140 | { 141 | title: '卖出触发价', 142 | dataIndex: 'sellTriggerPrice', 143 | key: 'sellTriggerPrice', 144 | render: (v) => v.toFixed(3), 145 | }, 146 | { 147 | title: '卖出价', 148 | dataIndex: 'sellPrice', 149 | key: 'sellPrice', 150 | render: (v) => v.toFixed(3), 151 | }, 152 | { 153 | title: '出股数', 154 | dataIndex: 'sellStock', 155 | key: 'sellStock', 156 | }, 157 | { 158 | title: '卖出金额', 159 | dataIndex: 'sellAmount', 160 | key: 'sellAmount', 161 | }, 162 | ]; 163 | 164 | const percentProps = { 165 | min: 0, 166 | max: 100, 167 | formatter: (value) => `${value}%`, 168 | parser: (value) => value?.replace('%', ''), 169 | }; 170 | 171 | /** 172 | * 计算系数 173 | * @param {number} index - 输入的索引值,从0开始 174 | * @param {number} k - 控制增长的宽度参数 175 | * @param {number} a - 控制增长的速率参数 176 | * @returns {number} - 计算出的系数 177 | * 178 | * 调节参数以实现不同的效果: 179 | * 1. 开始增长更慢,后期加速更快: 180 | * 增大 k 的值。例如,将 k 设置为 30 或更大。 181 | * 增大 a 的值。例如,将 a 设置为 2.5 或更大。 182 | * 2. 整体增长平缓: 183 | * 减小 k 的值。例如,将 k 设置为 10 或更小。 184 | * 减小 a 的值。例如,将 a 设置为 1.5 或更小。 185 | */ 186 | const calcCoefficient = (index, k = 3.5, a = 2) => { 187 | // 确保参数为大于0的数值 188 | if (k <= 0 || a <= 0) { 189 | throw new Error('参数 k 和 a 必须大于0'); 190 | } 191 | 192 | // 使用指数增长公式计算系数 193 | const coefficient = add(1, pow(divide(index, k), a)) as number; 194 | 195 | // 将结果保留2位小数并转换为数字 196 | return number(round(coefficient, 2)); 197 | }; 198 | 199 | const getLevels = (step, type: string) => { 200 | if (step === 0) return []; 201 | 202 | const { basePrice, minPrice, baseAmount } = form.getFieldsValue(); 203 | // 最大跌幅 = 最低价 / 基准价 204 | const decline = calc(`${minPrice} / ${basePrice}`); 205 | 206 | let index = 0; 207 | let level = 1; 208 | const levels: number[] = []; 209 | 210 | while (level >= decline) { 211 | levels.push(level); 212 | level = calc( 213 | `${level} - ${calc(`${step} / 100`)} * ${calcCoefficient(index)}`, 214 | 2, 215 | ); 216 | index++; 217 | } 218 | 219 | return levels.map((level, i) => { 220 | const preLevel = levels[i - 1] || level + step / 100; 221 | 222 | // 买入价、卖出价、买入触发价、卖出触发价 223 | const price = calcGridItemByLevel(basePrice, level, preLevel); 224 | const { buyPrice, sellPrice } = price; 225 | // 买入金额、买入股数、卖出金额、卖出股数 226 | const buySell = calcBuySell(baseAmount, level, buyPrice, sellPrice); 227 | 228 | return { 229 | key: `${type}-${level}`, 230 | type, 231 | level, 232 | ...buySell, 233 | ...price, 234 | }; 235 | }); 236 | }; 237 | 238 | // 计算买入金额、入股数、卖出金额、出股数 239 | const calcBuySell = (baseAmount, level, buyPrice, sellPrice) => { 240 | // 最大买入金额 241 | const maxAmount = calc(`${baseAmount} * (1 - ${level} + 1)`); 242 | // 最大买入股数 243 | const maxStock = calc(`${maxAmount} / ${buyPrice}`); 244 | // 买入股数,向下取整到100的整数倍 245 | const buyStock = Math.floor(maxStock / 100) * 100; 246 | // 买入金额 247 | const buyAmount = calc(`${buyStock} * ${buyPrice}`); 248 | 249 | // 卖出股数 250 | let sellStock = calc( 251 | `${buyStock} * (1 - (${sellPrice} - ${buyPrice}) / ${sellPrice})`, 252 | ); 253 | sellStock = Math.floor(sellStock / 100) * 100; 254 | sellStock = Math.max(sellStock, 100); 255 | // 卖出金额 256 | const sellAmount = calc(`${sellStock} * ${sellPrice}`); 257 | 258 | return { 259 | buyAmount, 260 | buyStock, 261 | sellAmount, 262 | sellStock, 263 | }; 264 | }; 265 | 266 | // 根据基准价和档位计算网格每档的数据 267 | const calcGridItemByLevel = (basePrice, level, preLevel) => { 268 | // 买入价 = 基准价 * 档位,保留3位小数 269 | const buyPrice = calc(`${basePrice} * ${level}`, 3); 270 | 271 | // 卖出价 = 基准价 * (前一个档位),保留3位小数 272 | const sellPrice = calc(`${basePrice} * ${preLevel}`, 3); 273 | 274 | return { 275 | buyPrice, 276 | buyTriggerPrice: getTriggerPrice(buyPrice, '+'), 277 | sellPrice, 278 | sellTriggerPrice: getTriggerPrice(sellPrice, '-'), 279 | }; 280 | }; 281 | 282 | const getTriggerPrice = (price: number, fun: '+' | '-', step = 0.005) => { 283 | return calc(`${price} ${fun} ${step}`, 3); 284 | }; 285 | 286 | const genGrid = (values) => { 287 | const { step, middleStep, bigStep, basePrice, baseAmount } = values; 288 | if (basePrice <= 0 || baseAmount <= 0) return; 289 | if (baseAmount / basePrice < 100) { 290 | message.warning('每份金额不够买入1手(100股)'); 291 | return; 292 | } 293 | 294 | // 中网、大网过滤1档 295 | const fbp = (e) => e.level != 1; 296 | const sPrice = getLevels(step, '小网'); 297 | const mPrice = getLevels(middleStep, '中网').filter(fbp); 298 | const bPrice = getLevels(bigStep, '大网').filter(fbp); 299 | 300 | const list = [sPrice, mPrice, bPrice].flat(); 301 | 302 | mergeData.current = { 303 | 0: sPrice.length, 304 | [sPrice.length]: mPrice.length, 305 | [sPrice.length + mPrice.length]: bPrice.length, 306 | }; 307 | 308 | setData(list); 309 | }; 310 | 311 | return ( 312 |
313 |
327 | 333 | 334 | 335 | 341 | 342 | 343 | 349 | 350 | 351 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 368 |
369 |
375 | {d === null ? null : ( 376 |
377 |

压力测试与盈利测算

378 | 379 | 380 |

买入金额:{d?.buyAmount}

381 | 382 | 383 |

买入股数:{d?.buyStock}

384 | 385 | 386 |

卖出金额:{d?.sellAmount}

387 | 388 | 389 |

卖出股数:{d?.sellStock}

390 | 391 | 392 |

剩余股数:{d?.surplusStock}

393 | 394 | 395 |

396 | 利润  397 | 398 | 399 | 400 | :{d?.profit} 401 |

402 | 403 | 404 |

利润率:{d?.profitRate}%

405 | 406 |
407 |
408 | )} 409 |
410 |

E大网格三篇

411 | 437 |
438 |
439 | ); 440 | }; 441 | 442 | export default Grid; 443 | -------------------------------------------------------------------------------- /src/popup/hb/index.scss: -------------------------------------------------------------------------------- 1 | .hb { 2 | .btn-wrap { 3 | button { 4 | width: 100%; 5 | } 6 | } 7 | 8 | .info { 9 | margin: 14px 0 0 0; 10 | padding: 0; 11 | list-style: none; 12 | .time { 13 | margin-right: 4px; 14 | } 15 | } 16 | 17 | #hb-grid-wrap { 18 | display: block; 19 | position: absolute; 20 | z-index: 999; 21 | top: 0; 22 | left: 0; 23 | width: 100%; 24 | height: 100%; 25 | background-color: rgba(0, 0, 0, 0.5); 26 | } 27 | 28 | #hb-grid-wrap > img { 29 | position: absolute; 30 | top: 50%; 31 | left: 50%; 32 | 33 | transform: translate(-50%, -50%); 34 | width: 150px; 35 | height: 150px; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/popup/hb/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Button, Row, Col, Modal } from "antd"; 3 | import "./index.scss"; 4 | 5 | const hb = "https://m.touker.com"; 6 | const url = "https://m.touker.com/fd/conditions/monitoring"; 7 | 8 | const Hb: React.FC = () => { 9 | const [time, setTime] = useState(""); 10 | const [show, setShow] = useState(false); 11 | const [disabled, setDisabled] = useState(true); 12 | const [stopDisabled, setStopDisabled] = useState(true); 13 | 14 | useEffect(() => { 15 | getConfig(); 16 | checkIsHb(); 17 | }, []); 18 | 19 | const checkIsHb = () => { 20 | chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { 21 | if (tabs.length === 0) return; 22 | const tab = tabs[0]; 23 | const curUrl = tab?.url || ""; 24 | 25 | if (curUrl.startsWith(url)) { 26 | setDisabled(false); 27 | } 28 | 29 | if (curUrl.startsWith(hb)) { 30 | setStopDisabled(false); 31 | } 32 | }); 33 | }; 34 | 35 | const getConfig = () => { 36 | const url = "https://www.maxmeng.top/data/hb-helper.json?t=" + Date.now(); 37 | 38 | fetch(url) 39 | .then((res) => res.json()) 40 | .then((data) => { 41 | const time = data?.grid_update_time; 42 | if (time) setTime(time); 43 | }) 44 | .catch((err) => { 45 | console.log(err); 46 | }); 47 | }; 48 | 49 | const openHbPage = () => { 50 | chrome.tabs.create({ url }); 51 | }; 52 | 53 | const postpone = (action: string) => { 54 | chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { 55 | const [tab] = tabs as chrome.tabs.Tab[]; 56 | if (!tab) return; 57 | chrome.tabs.sendMessage(tab.id as number, { action: action }); 58 | }); 59 | }; 60 | 61 | return ( 62 |
63 | 64 | 65 | 68 | 69 | 70 | 78 | 79 | 80 | 88 | 89 | 90 | 99 | 100 | 101 |
    102 |
  • 103 | 华宝网格策略更新时间: 104 | {time} 105 | setShow(true)}> 106 | 查看 107 | 108 | {show ? ( 109 |
    setShow(false)}> 110 | 华宝网格策略 111 |
    112 | ) : null} 113 |
  • 114 |
115 |
116 | ); 117 | }; 118 | 119 | export default Hb; 120 | -------------------------------------------------------------------------------- /src/popup/index.scss: -------------------------------------------------------------------------------- 1 | @import url("../styles/global.scss"); 2 | 3 | body { 4 | min-width: 400px; 5 | margin: 0; 6 | background-color: #fff; 7 | } 8 | 9 | .header { 10 | padding: 16px; 11 | border-bottom: 1px solid #e8e8e8; 12 | display: flex; 13 | justify-content: space-between; 14 | align-items: center; 15 | .left { 16 | display: flex; 17 | align-items: center; 18 | .logo { 19 | width: 30px; 20 | margin-right: 6px; 21 | } 22 | .title { 23 | margin-right: 6px; 24 | font-size: 20px; 25 | font-weight: 500; 26 | line-height: 1.5; 27 | color: #424242; 28 | } 29 | .version { 30 | font-size: 16px; 31 | color: #afafaf; 32 | &.old { 33 | margin-right: 6px; 34 | text-decoration: line-through; 35 | } 36 | } 37 | } 38 | 39 | .go-to-options { 40 | cursor: pointer; 41 | color: #666666; 42 | } 43 | } 44 | 45 | .main { 46 | margin: 12px 24px 12px 24px; 47 | .type-list { 48 | margin-bottom: 20px; 49 | display: flex; 50 | justify-content: space-between; 51 | padding: 10px; 52 | border-radius: 25px; 53 | border: 1px solid #e8e8e8; 54 | & > div { 55 | height: 30px; 56 | line-height: 30px; 57 | text-align: center; 58 | border-radius: 15px; 59 | width: 45%; 60 | cursor: pointer; 61 | &.active { 62 | background-color: var(--color); 63 | color: #fff; 64 | } 65 | } 66 | } 67 | } 68 | 69 | .footer { 70 | padding: 4px 24px; 71 | background: #e8eaeb; 72 | ul, 73 | li { 74 | padding: 0; 75 | list-style: none; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/popup/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client'; 2 | import classnames from 'classnames'; 3 | import React, { useEffect, useState, useMemo } from 'react'; 4 | import Hb from './hb'; 5 | // import Grid from './grid'; 6 | // import Points from './points'; 7 | import { compareVersionLatest } from '../utils'; 8 | import './index.scss'; 9 | 10 | const types = [ 11 | { 12 | label: '华宝条件单', 13 | value: 'hb', 14 | component: Hb, 15 | }, 16 | // { 17 | // label: '关键点位', 18 | // value: 'points', 19 | // component: Points, 20 | // }, 21 | // { 22 | // label: '网格策略', 23 | // value: 'gird', 24 | // component: Grid, 25 | // }, 26 | ]; 27 | 28 | interface Notice { 29 | content: string; 30 | url?: string; 31 | } 32 | 33 | const Popup: React.FC = () => { 34 | const [version, setVersion] = useState(''); 35 | const [lastVersion, setLastVersion] = useState(''); 36 | const [notices, setNotices] = useState([]); 37 | const [type, setType] = useState('hb'); 38 | 39 | const isOld = useMemo(() => { 40 | if (!lastVersion || !version) return false; 41 | return compareVersionLatest(lastVersion, version) === 1; 42 | }, [lastVersion, version]); 43 | 44 | useEffect(() => { 45 | getVersion(); 46 | getConfig(); 47 | }, []); 48 | 49 | const getVersion = () => { 50 | let manifestData = chrome.runtime.getManifest(); 51 | let version = manifestData.version; 52 | if (version) setVersion(version); 53 | }; 54 | 55 | function getConfig() { 56 | const url = 'https://www.maxmeng.top/data/hb-helper.json?t=' + Date.now(); 57 | 58 | fetch(url) 59 | .then((res) => res.json()) 60 | .then((data) => { 61 | const version = data?.version; 62 | const notices = data?.notices || []; 63 | if (version) { 64 | setLastVersion(version); 65 | } 66 | if (notices) { 67 | setNotices(notices); 68 | } 69 | }) 70 | .catch((err) => { 71 | console.log(err); 72 | }); 73 | } 74 | 75 | const openOptionsPage = () => { 76 | if (chrome.runtime.openOptionsPage) { 77 | chrome.runtime.openOptionsPage(); 78 | } else { 79 | window.open(chrome.runtime.getURL('options.html')); 80 | } 81 | }; 82 | 83 | return ( 84 | <> 85 |
86 |
87 | LOGO 88 | ETF投资助手 89 | 94 | {version} 95 | 96 | {isOld && ( 97 | 98 | 获取最新版 99 | 100 | )} 101 |
102 |
103 | 104 | 更多 105 | 106 |
107 |
108 | 109 |
110 |
111 | {types.map((item) => { 112 | return ( 113 |
setType(item.value)} 119 | > 120 | {item.label} 121 |
122 | ); 123 | })} 124 |
125 | {types.map((item) => { 126 | return type === item.value ? ( 127 | 128 | ) : null; 129 | })} 130 |
131 | {notices.length > 0 ? ( 132 |
133 |
    134 | {notices.map((tip, index) => { 135 | return ( 136 |
  • 137 | {`${index + 1}. ${tip.content} `} 138 | {tip.url ? ( 139 | 140 | [查看] 141 | 142 | ) : null} 143 |
  • 144 | ); 145 | })} 146 |
147 |
148 | ) : null} 149 | 150 | ); 151 | }; 152 | 153 | const container = document.getElementById('root'); 154 | const root = createRoot(container!); 155 | root.render(); 156 | -------------------------------------------------------------------------------- /src/popup/points/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | useEffect, 3 | useMemo, 4 | useCallback, 5 | useRef, 6 | useState, 7 | } from 'react'; 8 | import { 9 | LeftOutlined, 10 | RightOutlined, 11 | QuestionCircleOutlined, 12 | } from '@ant-design/icons'; 13 | import * as echarts from 'echarts'; 14 | import { Spin, Tooltip, Table } from 'antd'; 15 | 16 | // 基金 17 | interface Item { 18 | code: string; 19 | name: string; 20 | points: Point[]; 21 | } 22 | 23 | // 点位 24 | interface Point { 25 | type: PointType; 26 | min: number; 27 | max: number; 28 | remark: string; 29 | date: string; 30 | url: string; 31 | } 32 | 33 | // 点位类型 34 | enum PointType { 35 | // 正常 36 | normal = 0, 37 | // 支撑位 38 | support = 1, 39 | // 压力位 40 | pressure = 2, 41 | } 42 | 43 | const pointColor = { 44 | [PointType.normal]: '#91d5ff', 45 | [PointType.support]: '#73d13d', 46 | [PointType.pressure]: '#ff4d4f', 47 | }; 48 | 49 | const pointText = { 50 | [PointType.normal]: '正常', 51 | [PointType.support]: '支撑', 52 | [PointType.pressure]: '压力', 53 | }; 54 | 55 | interface IData { 56 | 日期: string; 57 | 开盘: number; 58 | 收盘: number; 59 | 最高: number; 60 | 最低: number; 61 | 成交量: number; 62 | 成交额: number; 63 | 振幅: number; 64 | 涨跌幅: number; 65 | 涨跌额: number; 66 | 换手率: number; 67 | } 68 | 69 | const upColor = '#ec0000'; 70 | const upBorderColor = '#8A0000'; 71 | const downColor = '#00da3c'; 72 | const downBorderColor = '#008F28'; 73 | 74 | const KLineChart = () => { 75 | const [loading, setLoading] = useState(false); 76 | const [list, setList] = useState([]); 77 | const [index, setIndex] = useState(); 78 | const [current, setCurrent] = useState(); 79 | const [data, setData] = useState([]); 80 | const [start, setStart] = useState(0); 81 | const chartRef = useRef(null); 82 | const title = useMemo(() => { 83 | return current ? `${current.name} - ${current.code}` : ''; 84 | }, [current]); 85 | 86 | useEffect(() => { 87 | getList(); 88 | }, []); 89 | 90 | useEffect(() => { 91 | if (list.length && index !== undefined) { 92 | setCurrent(list[index]); 93 | } 94 | }, [list, index]); 95 | 96 | useEffect(() => { 97 | setStart(0); 98 | }, [index]); 99 | 100 | useEffect(() => { 101 | if (current) { 102 | getData(current.code); 103 | } 104 | }, [current]); 105 | 106 | const getList = () => { 107 | const url = 108 | `https://www.maxmeng.top/data/index_zh_a_hist/points.json?t=` + 109 | Date.now(); 110 | fetch(url) 111 | .then((res) => res.json()) 112 | .then((data) => { 113 | setList(data); 114 | if (data?.length) { 115 | setIndex(0); 116 | } 117 | }); 118 | }; 119 | 120 | const getData = (code) => { 121 | setLoading(true); 122 | const url = 123 | `https://www.maxmeng.top/data/index_zh_a_hist/converted_${code}.json?t=` + 124 | Date.now(); 125 | 126 | fetch(url) 127 | .then((res) => res.json()) 128 | .then((data) => { 129 | const newData = data.map((item: string) => { 130 | // 日期,开盘,收盘,最高,最低,成交量,成交额,振幅,涨跌幅,涨跌额,换手率 131 | const [ 132 | 日期, 133 | 开盘, 134 | 收盘, 135 | 最高, 136 | 最低, 137 | 成交量, 138 | 成交额, 139 | 振幅, 140 | 涨跌幅, 141 | 涨跌额, 142 | 换手率, 143 | ] = item.split(','); 144 | return { 145 | 日期, 146 | 开盘: parseFloat(开盘), 147 | 收盘: parseFloat(收盘), 148 | 最高: parseFloat(最高), 149 | 最低: parseFloat(最低), 150 | 成交量: parseFloat(成交量), 151 | 成交额: parseFloat(成交额), 152 | 振幅: parseFloat(振幅), 153 | 涨跌幅: parseFloat(涨跌幅), 154 | 涨跌额: parseFloat(涨跌额), 155 | 换手率: parseFloat(换手率), 156 | }; 157 | }); 158 | setData(newData); 159 | }) 160 | .catch(() => { 161 | console.log(`查询指数历史数据出错: ${code}`); 162 | setData([]); 163 | }) 164 | .finally(() => { 165 | setLoading(false); 166 | }); 167 | }; 168 | 169 | const processData = (data: IData[]) => { 170 | const categoryData: string[] = []; 171 | const values: any[][] = []; 172 | data.forEach((item: IData) => { 173 | categoryData.push(item.日期); 174 | values.push([item.开盘, item.收盘, item.最低, item.最高]); 175 | }); 176 | return { categoryData, values }; 177 | }; 178 | 179 | useEffect(() => { 180 | const lastDate = data[data.length - 1]?.日期; 181 | 182 | const chart = echarts.init(chartRef.current); 183 | const { categoryData, values } = processData(data); 184 | 185 | const markLine: any[] = []; 186 | const markArea: any[] = []; 187 | if (current) { 188 | current.points?.forEach((point) => { 189 | const { date, min, max, type } = point; 190 | const color = pointColor[type]; 191 | 192 | if (min === null && max === null) return; 193 | 194 | if (min === max) { 195 | const other = { 196 | label: { 197 | formatter: `${min}`, 198 | }, 199 | lineStyle: { 200 | type: 'solid', 201 | color: color, 202 | width: 2, 203 | }, 204 | }; 205 | markLine.push([ 206 | { ...other, coord: [date, min] }, 207 | { ...other, coord: [lastDate, min] }, 208 | ]); 209 | } else { 210 | const other = { 211 | label: { 212 | formatter: `${min}-${max}`, 213 | }, 214 | itemStyle: { 215 | color: color, 216 | }, 217 | }; 218 | 219 | markArea.push([ 220 | { ...other, coord: [date, min] }, 221 | { ...other, coord: [lastDate, max] }, 222 | ]); 223 | } 224 | }); 225 | } 226 | 227 | const option = { 228 | grid: { 229 | top: '5%', 230 | }, 231 | xAxis: { 232 | type: 'category', 233 | data: categoryData, 234 | scale: true, 235 | boundaryGap: false, 236 | axisLine: { onZero: false }, 237 | splitLine: { show: false }, 238 | min: 'dataMin', 239 | max: 'dataMax', 240 | }, 241 | yAxis: { 242 | scale: true, 243 | splitArea: { 244 | show: true, 245 | }, 246 | }, 247 | dataZoom: [ 248 | { 249 | type: 'inside', 250 | start: start, 251 | end: 100, 252 | }, 253 | { 254 | show: true, 255 | type: 'slider', 256 | top: '90%', 257 | start: start, 258 | end: 100, 259 | }, 260 | ], 261 | tooltip: { 262 | trigger: 'axis', 263 | axisPointer: { 264 | type: 'cross', 265 | }, 266 | }, 267 | series: [ 268 | { 269 | type: 'candlestick', 270 | name: '日K', 271 | data: values, 272 | itemStyle: { 273 | color: upColor, 274 | color0: downColor, 275 | borderColor: upBorderColor, 276 | borderColor0: downBorderColor, 277 | }, 278 | markLine: { 279 | animation: false, 280 | symbol: ['none', 'none'], 281 | data: markLine, 282 | }, 283 | markArea: { 284 | data: markArea, 285 | }, 286 | }, 287 | ], 288 | }; 289 | 290 | chart.setOption(option); 291 | 292 | const handleResize = () => { 293 | chart.resize(); 294 | }; 295 | 296 | window.addEventListener('resize', handleResize); 297 | 298 | return () => { 299 | window.removeEventListener('resize', handleResize); 300 | chart.dispose(); 301 | }; 302 | }, [start, current, data]); 303 | 304 | const pre = useCallback(() => { 305 | if (index === undefined) return; 306 | if (index === 0) { 307 | setIndex(list.length - 1); 308 | } else { 309 | setIndex(index - 1); 310 | } 311 | }, [index]); 312 | 313 | const next = useCallback(() => { 314 | if (index === undefined) return; 315 | if (index === list.length - 1) { 316 | setIndex(0); 317 | } else { 318 | setIndex(index + 1); 319 | } 320 | }, [index]); 321 | 322 | const calcStart = useCallback( 323 | (time: string) => { 324 | if (data.length === 0) return; 325 | const start = data[0].日期; 326 | const end = data[data.length - 1].日期; 327 | const diff = new Date(end).getTime() - new Date(start).getTime(); 328 | const current = new Date(time).getTime() - new Date(start).getTime(); 329 | const s = (current / diff) * 100; 330 | setStart(s); 331 | }, 332 | [data], 333 | ); 334 | 335 | const columns = [ 336 | { 337 | title: '日期', 338 | dataIndex: 'date', 339 | key: 'date', 340 | width: 100, 341 | render: (date) => { 342 | return calcStart(date)}>{date}; 343 | }, 344 | }, 345 | { 346 | title: '当日收盘', 347 | dataIndex: 'close', 348 | key: 'close', 349 | width: 90, 350 | render: (_, record) => { 351 | const { date } = record; 352 | const curDate = new Date(date); 353 | let print = 0; 354 | for (let i = 0; i < data.length; i++) { 355 | if (new Date(data[i].日期) <= curDate) { 356 | print = data[i].收盘; 357 | } 358 | } 359 | return print; 360 | }, 361 | }, 362 | { 363 | title: '关键点位', 364 | dataIndex: 'min', 365 | key: 'min', 366 | width: 100, 367 | render: (_, record) => { 368 | const { min, max } = record; 369 | if (!min && !max) return null; 370 | if (min === max) { 371 | return min; 372 | } else { 373 | return `${min}-${max}`; 374 | } 375 | }, 376 | }, 377 | { 378 | title: '类型', 379 | dataIndex: 'type', 380 | key: 'type', 381 | width: 50, 382 | render: (type) => { 383 | return ( 384 | {pointText[type]} 385 | ); 386 | }, 387 | }, 388 | { 389 | title: '原文', 390 | dataIndex: 'remark', 391 | key: 'remark', 392 | render: (_, record) => { 393 | const { remark, url } = record; 394 | return ( 395 | <> 396 | {remark} 397 | {url ? ( 398 | 399 | 来源 400 | 401 | ) : null} 402 | 403 | ); 404 | }, 405 | }, 406 | ]; 407 | 408 | return ( 409 |
410 |
417 | 418 |

{title}

419 | 420 |
421 | 422 |
423 | 424 |
425 | E大关键点位 426 | 431 | 补充点位数据 432 | 433 | } 434 | > 435 | 436 | 437 |
438 | `${date}-$${min}-${Math.random() * 1000}`} 443 | dataSource={current?.points?.sort((a, b) => { 444 | const { date: dateA } = a; 445 | const { date: dateB } = b; 446 | return dateA > dateB ? -1 : 1; 447 | })} 448 | >
449 |
450 | ); 451 | }; 452 | 453 | export default KLineChart; 454 | -------------------------------------------------------------------------------- /src/styles/global.scss: -------------------------------------------------------------------------------- 1 | @import url('./variable.scss'); -------------------------------------------------------------------------------- /src/styles/variable.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --color: #dd3432; 3 | --color-primary: #1677ff; 4 | --color-error: #ff4d4f; 5 | --color-disabled: #c2c2c2; 6 | --color-text-disabled: rgba(0, 0, 0, 0.25); 7 | --color-bg-disabled: rgba(0, 0, 0, 0.04); 8 | --color-border-disabled: #d9d9d9; 9 | --color-link-hover: #69b1ff; 10 | --color-link-active: #0958d9; 11 | } 12 | 13 | a { 14 | color: var(--color-primary); 15 | text-decoration: none; 16 | } 17 | 18 | a:hover { 19 | color: var(--color-link-hover); 20 | } 21 | 22 | a:active { 23 | color: var(--color-link-active); 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 比较插件的两个版本v1,v2大小 3 | * .e.g '3.1.7-alpha.19' 4 | * @param v1 { string } {major}.{minor}.{patch}-{pre-release} 5 | * @param v2 { string } {major}.{minor}.{patch}-{pre-release} 6 | * @return {number} 1大 0相等 -1小 7 | */ 8 | export const compareVersionLatest = (v1: string, v2: string): number => { 9 | const [v1Main, v1PreRelease] = v1.split("-"); 10 | const [v2Main, v2PreRelease] = v2.split("-"); 11 | 12 | // 比较版本主体的大小 13 | const v1List = v1Main.split("."); 14 | const v2List = v2Main.split("."); 15 | const len1 = v1List.length; 16 | const len2 = v2List.length; 17 | const minLen = Math.min(len1, len2); 18 | let curIdx = 0; 19 | for (curIdx; curIdx < minLen; curIdx += 1) { 20 | const v1CurNum = parseInt(v1List[curIdx]); 21 | const v2CurNum = parseInt(v2List[curIdx]); 22 | if (v1CurNum > v2CurNum) { 23 | return 1; 24 | } else if (v1CurNum < v2CurNum) { 25 | return -1; 26 | } 27 | } 28 | if (len1 > len2) { 29 | for (let lastIdx = curIdx; lastIdx < len1; lastIdx++) { 30 | if (parseInt(v1List[lastIdx]) != 0) { 31 | return 1; 32 | } 33 | } 34 | return 0; 35 | } else if (len1 < len2) { 36 | for (let lastIdx = curIdx; lastIdx < len2; lastIdx += 1) { 37 | if (parseInt(v2List[lastIdx]) != 0) { 38 | return -1; 39 | } 40 | } 41 | return 0; 42 | } 43 | 44 | // 如果存在先行版本,还需要比较先行版本的大小 45 | if (v1PreRelease && !v2PreRelease) { 46 | return 1; 47 | } else if (!v1PreRelease && v2PreRelease) { 48 | return -1; 49 | } else if (v1PreRelease && v2PreRelease) { 50 | const [gama1, time1] = v1PreRelease.split("."); 51 | const [gama2, time2] = v2PreRelease.split("."); 52 | if (gama1 > gama2) return 1; 53 | if (gama2 > gama1) return -1; 54 | if (parseInt(time1) > parseInt(time2)) return 1; 55 | if (parseInt(time2) > parseInt(time1)) return -1; 56 | } 57 | return 0; 58 | }; 59 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "sourceMap": true, 5 | "strict": true, 6 | "module": "esnext", 7 | "target": "es6", 8 | "jsx": "react", 9 | "moduleResolution": "node", 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "resolveJsonModule": true, 14 | "noImplicitAny": false, 15 | }, 16 | "include": ["src/**/*"] 17 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 4 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 5 | 6 | module.exports = (env) => { 7 | const isProduction = env && env.production; 8 | 9 | return { 10 | mode: 'development', 11 | devtool: isProduction ? false : 'source-map', 12 | entry: { 13 | background: './src/background.ts', 14 | content: './src/content.ts', 15 | popup: './src/popup/index.tsx', 16 | options: './src/options/index.tsx', 17 | }, 18 | output: { 19 | path: path.resolve(__dirname, 'dist'), 20 | filename: '[name].js', 21 | }, 22 | resolve: { 23 | extensions: ['.ts', '.tsx', '.js'], 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.tsx?$/, 29 | use: 'ts-loader', 30 | exclude: /node_modules/, 31 | }, 32 | { 33 | test: /\.js$/, 34 | exclude: /node_modules/, 35 | use: { 36 | loader: 'babel-loader', 37 | options: { 38 | presets: ['@babel/preset-env'], 39 | }, 40 | }, 41 | }, 42 | { 43 | test: /\.scss$/, 44 | use: [ 45 | 'style-loader', 46 | { 47 | loader: 'css-loader', 48 | // options: { 49 | // modules: true, 50 | // }, 51 | }, 52 | 'sass-loader', 53 | ], 54 | }, 55 | { 56 | test: /\.css$/, 57 | use: ['style-loader', 'css-loader'], 58 | }, 59 | ], 60 | }, 61 | plugins: [ 62 | new CleanWebpackPlugin(), 63 | new CopyWebpackPlugin({ 64 | patterns: [ 65 | { from: 'manifest-chrome.json', to: 'manifest.json' }, 66 | // { from: "manifest-firefox.json", to: "manifest.json" }, 67 | { from: 'src/images', to: 'images' }, 68 | ], 69 | }), 70 | new HtmlWebpackPlugin({ 71 | filename: 'popup.html', 72 | template: './src/index.html', 73 | chunks: ['popup'], 74 | }), 75 | new HtmlWebpackPlugin({ 76 | filename: 'options.html', 77 | template: './src/index.html', 78 | chunks: ['options'], 79 | }), 80 | ], 81 | }; 82 | }; 83 | --------------------------------------------------------------------------------