├── .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 |
55 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------