diff --git a/package.json b/package.json index 13c39b6b..fc502052 100644 --- a/package.json +++ b/package.json @@ -211,6 +211,10 @@ "command": "leek-fund.deleteStock", "title": "删除股票" }, + { + "command": "leek-fund.addMyStock", + "title": "加入我的自选" + }, { "command": "leek-fund.addStockToBar", "title": "添加至状态栏" @@ -439,42 +443,47 @@ }, { "command": "leek-fund.setStockTop", - "when": "view == leekFundView.stock && viewItem != category && viewItem!=nodata", + "when": "view == leekFundView.stock && viewItem != category && viewItem!=nodata && viewItem != limit", "group": "group1" }, { "command": "leek-fund.setStockUp", - "when": "view == leekFundView.stock && viewItem != category && viewItem!=nodata", + "when": "view == leekFundView.stock && viewItem != category && viewItem!=nodata && viewItem != limit", "group": "group1" }, { "command": "leek-fund.setStockDown", - "when": "view == leekFundView.stock && viewItem != category && viewItem!=nodata", + "when": "view == leekFundView.stock && viewItem != category && viewItem!=nodata && viewItem != limit", "group": "group1" }, { "command": "leek-fund.stockTrendPic", - "when": "view == leekFundView.stock && viewItem != category && viewItem!=nodata", + "when": "view == leekFundView.stock && viewItem != category && viewItem!=nodata && viewItem != limit", "group": "group2" }, { "command": "leek-fund.setStockTop", - "when": "view == leekFundView.stock && viewItem != category && viewItem!=nodata", + "when": "view == leekFundView.stock && viewItem != category && viewItem != limit && viewItem != limit", "group": "inline" }, { "command": "leek-fund.setStockUp", - "when": "view == leekFundView.stock && viewItem != category && viewItem!=nodata", + "when": "view == leekFundView.stock && viewItem != category && viewItem!=nodata && viewItem != limit", "group": "inline" }, { "command": "leek-fund.setStockDown", - "when": "view == leekFundView.stock && viewItem != category && viewItem!=nodata", + "when": "view == leekFundView.stock && viewItem != category && viewItem!=nodata && viewItem != limit", "group": "inline" }, { "command": "leek-fund.deleteStock", - "when": "view == leekFundView.stock && viewItem != category", + "when": "view == leekFundView.stock && viewItem != category && viewItem != limit", + "group": "group5" + }, + { + "command": "leek-fund.addMyStock", + "when": "view == leekFundView.stock && viewItem == limit", "group": "group5" }, { @@ -688,6 +697,16 @@ "default": true, "description": "默认展开A股" }, + "leek-fund.expandAUpStock": { + "type": "boolean", + "default": false, + "description": "默认展开涨停股" + }, + "leek-fund.expandADownStock": { + "type": "boolean", + "default": false, + "description": "默认展开跌停股" + }, "leek-fund.expandHKStock": { "type": "boolean", "default": false, @@ -803,6 +822,7 @@ "lodash": "4.17.21", "moment": "^2.29.4", "public-ip": "^4.0.3", + "puppeteer": "^23.10.4", "ws": "^7.4.1" }, "extensionKind": [ diff --git a/src/explorer/stockProvider.ts b/src/explorer/stockProvider.ts index 30083642..7dcbc2aa 100644 --- a/src/explorer/stockProvider.ts +++ b/src/explorer/stockProvider.ts @@ -14,15 +14,19 @@ export class StockProvider implements TreeDataProvider { private service: StockService; private order: SortType; private expandAStock: boolean; - private expandHKStock: boolean; - private expandUSStock: boolean; - private expandCNFuture: boolean; - private expandOverseaFuture: boolean; + private expandAUpStock: boolean = false; + private expandADownStock: boolean = false; + private expandHKStock: boolean = false; + private expandUSStock: boolean = false; + private expandCNFuture: boolean = false; + private expandOverseaFuture: boolean = false; constructor(service: StockService) { this.service = service; this.order = LeekFundConfig.getConfig('leek-fund.stockSort') || SortType.NORMAL; this.expandAStock = LeekFundConfig.getConfig('leek-fund.expandAStock', true); + this.expandAUpStock = LeekFundConfig.getConfig('leek-fund.expandAUpStock', false); + this.expandADownStock = LeekFundConfig.getConfig('leek-fund.expandADownStock', false); this.expandHKStock = LeekFundConfig.getConfig('leek-fund.expandHKStock', false); this.expandUSStock = LeekFundConfig.getConfig('leek-fund.expandUSStock', false); this.expandCNFuture = LeekFundConfig.getConfig('leek-fund.expandCNFuture', false); @@ -48,6 +52,10 @@ export class StockProvider implements TreeDataProvider { ) { case StockCategory.A: return this.getAStockNodes(resultPromise); + case StockCategory.UP: + return this.service.uplimitStockList; + case StockCategory.DOWN: + return this.service.downlimitStockList; case StockCategory.HK: return this.getHkStockNodes(resultPromise); case StockCategory.US: @@ -78,6 +86,9 @@ export class StockProvider implements TreeDataProvider { label: element.info.name, // tooltip: this.getSubCategoryTooltip(element), collapsibleState: + (element.id === StockCategory.A && this.expandAStock) || + (element.id === StockCategory.UP && this.expandAUpStock) || + (element.id === StockCategory.DOWN && this.expandADownStock) || (element.id === StockCategory.A && this.expandAStock) || (element.id === StockCategory.HK && this.expandHKStock) || (element.id === StockCategory.US && this.expandUSStock) || @@ -104,6 +115,26 @@ export class StockProvider implements TreeDataProvider { undefined, true ), + new LeekTreeItem( + Object.assign({ contextValue: 'category' }, defaultFundInfo, { + id: StockCategory.UP, + name: `${StockCategory.UP}${ + globalState.aLmitUpStockCount > 0 ? `(${globalState.aLmitUpStockCount})` : '' + }`, + }), + undefined, + true + ), + new LeekTreeItem( + Object.assign({ contextValue: 'category' }, defaultFundInfo, { + id: StockCategory.DOWN, + name: `${StockCategory.DOWN}${ + globalState.aLmitDownStockCount > 0 ? `(${globalState.aLmitDownStockCount})` : '' + }`, + }), + undefined, + true + ), new LeekTreeItem( Object.assign({ contextValue: 'category' }, defaultFundInfo, { id: StockCategory.HK, diff --git a/src/explorer/stockService.ts b/src/explorer/stockService.ts index bf0c1dcb..3b4435da 100644 --- a/src/explorer/stockService.ts +++ b/src/explorer/stockService.ts @@ -4,16 +4,21 @@ import { ExtensionContext, QuickPickItem, window } from 'vscode'; import globalState from '../globalState'; import { LeekTreeItem } from '../shared/leekTreeItem'; import { executeStocksRemind } from '../shared/remindNotification'; -import { HeldData } from '../shared/typed'; -import { calcFixedPriceNumber, events, formatNumber, randHeader, sortData } from '../shared/utils'; +import { FundInfo, HeldData } from '../shared/typed'; +import { calcFixedPriceNumber, events, formatNumber, formatLimitTime, randHeader, sortData } from '../shared/utils'; import { getXueQiuToken } from '../shared/xueqiu-helper'; import { LeekService } from './leekService'; import moment = require('moment'); import Log from '../shared/log'; import { getTencentHKStockData, searchStockList } from '../shared/tencentStock'; +import puppeteer from 'puppeteer'; +import { URL } from 'url'; +const querystring = require('querystring'); export default class StockService extends LeekService { public stockList: Array = []; + public uplimitStockList: Array = []; + public downlimitStockList: Array = []; private context: ExtensionContext; private token: string = ''; @@ -77,6 +82,12 @@ export default class StockService extends LeekService { stockList = stockList.concat(item.value); } }); + // 自选 + const customIds = stockList.map((item) => item.id); + const upList = await this.getLimitData('ztgc'); + this.uplimitStockList = upList.filter(item => !customIds.includes(item.id)); + const downList = await this.getLimitData('dtgc'); + this.downlimitStockList = downList.filter(item => !customIds.includes(item.id)); const res = sortData(stockList, order); executeStocksRemind(res, this.stockList); @@ -87,6 +98,78 @@ export default class StockService extends LeekService { return res; } + async getLimitData(limitType = 'ztgc') { + const requestKey = limitType === 'ztgc' ? 'getTopicZTPool' : 'getTopicDTPool'; + const stocks: Array = []; + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.setRequestInterception(true); + page.on('request', request => { + // 只拦截 GET 请求 + if (request.url().includes(requestKey)) { + const url = new URL(request.url()); + // 修改查询参数 + url.searchParams.set('pagesize', '500'); // 改变 pagesize 参数 + // 继续请求,传入修改后的 URL + request.continue({ url: url.toString() }); + } else { + // 对其他请求不做修改,直接继续 + request.continue(); + } + }); + // 监听网络请求 + page.on('response', async (response) => { + // 检查请求 URL 或请求的类型(可根据需要进行过滤) + const url = response.url(); + // 假设我们要获取某个特定 API 请求的内容 + if (url.includes(requestKey)) { + const paramsObject = querystring.parse(url.split('?')[1]); + const data = await response.text(); + const regex = new RegExp(`^${paramsObject.cb}\\((.*)\\);$`); + const matchResult = data.match(regex); + if (matchResult && matchResult[1]) { + const resData = JSON.parse(matchResult[1]); + if (limitType === 'ztgc') { + globalState.aLmitUpStockCount = resData.data.tc; + } else { + globalState.aLmitDownStockCount = resData.data.tc; + } + resData.data.pool.forEach((item: any) => { + const type = item.m === 0 ? 'sz' : item.m === 1 ? 'sh' : 'nodata'; + const stockItem: FundInfo = { + name: item.n, + code: type + item.c, + symbol: item.c, + isStock: true, + showLabel: true, + hybk: item.hybk, + type, + lbc: item.lbc, + contextValue: 'limit', + updown: `${item.zdp.toFixed(2)}%`, + percent: item.zdp > 0 ? `+${item.zdp}` : `${item.zdp}`, + price: `${item.p / 1000}`, + time: formatLimitTime(`${limitType === 'ztgc' ? item.fbt : item.lbt}`), + fbt: formatLimitTime(`${item.fbt}`), + lbt: formatLimitTime(`${item.lbt}`), + }; + if (limitType === 'ztgc') { + stockItem.zttj = `${item.zttj.ct}/${item.zttj.days}`; + stockItem.fbzz = formatNumber(item.fund, 2, true); + } + + const treeItem = new LeekTreeItem(stockItem, this.context); + stocks.push(treeItem); + }); + } + await browser.close(); + return stocks; + } + }); + await page.goto(`https://quote.eastmoney.com/ztb/detail#type=${limitType}`); + return stocks; + } + async getStockData(codes: Array): Promise> { if ((codes && codes.length === 0) || !codes) { return []; @@ -348,6 +431,7 @@ export default class StockService extends LeekService { } catch (err) { console.error(err); } + stockItem.contextValue = 'FavoriteStock'; stockItem.showLabel = this.showLabel; stockItem.isStock = true; stockItem.type = type; @@ -372,6 +456,7 @@ export default class StockService extends LeekService { type: 'nodata', contextValue: 'nodata', }; + stockItem.contextValue = 'FavoriteStock'; const treeItem = new LeekTreeItem(stockItem, this.context); stockList.push(treeItem); } @@ -435,6 +520,7 @@ export default class StockService extends LeekService { if (Number(open) <= 0) { price = yestclose; } + stockItem.contextValue = 'FavoriteStock'; stockItem.showLabel = this.showLabel; stockItem.isStock = true; stockItem.type = 'hk'; diff --git a/src/globalState.ts b/src/globalState.ts index c48d15d7..17247d7d 100644 --- a/src/globalState.ts +++ b/src/globalState.ts @@ -23,6 +23,8 @@ let labelFormat = DEFAULT_LABEL_FORMAT; let stockHeldTipShow = true; // 是否开启股票持仓提示 let aStockCount = 0; +let aLmitUpStockCount = 0; // 涨停家数 +let aLmitDownStockCount = 0; // 跌停家数 let usStockCount = 0; let hkStockCount = 0; let cnfStockCount = 0; // 期货数量 @@ -51,6 +53,8 @@ export default { newsIntervalTime, newsIntervalTimer, aStockCount, + aLmitUpStockCount, + aLmitDownStockCount, usStockCount, hkStockCount, cnfStockCount, // 期货 diff --git a/src/registerCommand.ts b/src/registerCommand.ts index fac13940..d05966e9 100644 --- a/src/registerCommand.ts +++ b/src/registerCommand.ts @@ -139,11 +139,19 @@ export function registerViewEvent( }, 1000); }); commands.registerCommand('leek-fund.deleteStock', (target) => { + if (!target) return; LeekFundConfig.removeStockCfg(target.id, () => { stockProvider.refresh(); }); }); + commands.registerCommand('leek-fund.addMyStock', (target) => { + if (!target) return; + LeekFundConfig.updateStockCfg(target.id, () => { + stockProvider.refresh(); + }); + }); commands.registerCommand('leek-fund.addStockToBar', (target) => { + if (!target) return; LeekFundConfig.addStockToBarCfg(target.id, () => { stockProvider.refresh(); }); diff --git a/src/shared/leekTreeItem.ts b/src/shared/leekTreeItem.ts index d0ea9b11..449db628 100644 --- a/src/shared/leekTreeItem.ts +++ b/src/shared/leekTreeItem.ts @@ -47,6 +47,12 @@ export class LeekTreeItem extends TreeItem { publishDateTime = '', heldAmount = 0, heldPrice = 0, + hybk = '', + lbc = 1, + fbt = '', + lbt = '', + zttj = '', + fbzz = '', } = info; if (_itemType) { @@ -200,6 +206,9 @@ export class LeekTreeItem extends TreeItem { } else { this.label = text; } + if (hybk) { + this.label = `${text} ${lbc} ${zttj} [${hybk}]`; + } this.id = info.id || code; if (isStockItem || isFundItem || isBinanceItem) { let typeAndSymbol = `${type}${symbol}`; @@ -231,6 +240,8 @@ export class LeekTreeItem extends TreeItem { const isFuture = /nf_/.test(code) || /hf_/.test(code); + const isZt = !!hybk; + // type字段:国内期货前缀 `nf_` 。股票的 type 是交易所 (sz,sh,bj) const typeText = type; const symbolText = isFuture ? name : symbol; @@ -239,6 +250,8 @@ export class LeekTreeItem extends TreeItem { this.tooltip = '接口不支持,右键删除关注'; } else if (isFuture) { this.tooltip = `【今日行情】${name} ${code}\n 涨跌:${updown} 百分比:${_percent}%\n 最高:${high} 最低:${low}\n 今开:${open} 昨结:${yestclose}\n 成交量:${volume} 成交额:${amount}`; + } else if (isZt) { + this.tooltip = `首封时间:${fbt} 最后封板时间:${lbt}\n 涨停统计:${zttj} 连板次数:${lbc}\n 封板资金:${fbzz}`; } else { this.tooltip = `【今日行情】${labelText}${typeText}${symbolText}\n 涨跌:${updown} 百分比:${_percent}%\n 最高:${high} 最低:${low}\n 今开:${open} 昨收:${yestclose}\n 成交量:${volume} 成交额:${amount}\n ${heldAmount ? `持仓数:${volume} 持仓价:${heldPrice}` : '' }`; diff --git a/src/shared/typed.ts b/src/shared/typed.ts index f0513938..ee8412e0 100644 --- a/src/shared/typed.ts +++ b/src/shared/typed.ts @@ -82,6 +82,12 @@ export interface FundInfo { publishTime?: string; // 发布时间:时分秒 heldAmount?: number; // 持仓数 heldPrice?: number; // 持仓价 + hybk?: string; // 行业板块 + lbc?: number // 连板次数 + fbt?: string; // 首次封板时间 + lbt?: string; // 最后封板时间 + zttj?: string; // 涨停统计 + fbzz?: string; // 封板资金 } export const defaultFundInfo: FundInfo = { @@ -94,6 +100,8 @@ export const defaultFundInfo: FundInfo = { export enum StockCategory { A = 'A Stock', + UP = '涨停盯盘', + DOWN = '跌停盯盘', US = 'US Stock', HK = 'HK Stock', Future = 'CN Future', diff --git a/src/shared/utils.ts b/src/shared/utils.ts index b590e3bd..a1be8d92 100644 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@ -543,3 +543,14 @@ export function getResourcesImageSrc( ); }); } + +export function formatLimitTime(time: string = '') { + return time.padStart(6, '0').split('').reduce((acc, curr, index) => { + // 每两个数字之间加上 ':' + if (index === 2 || index === 4) { + acc += ':'; + } + acc += curr; + return acc; + }, ''); +}