diff --git a/vscode/a-shares/images/dark/add.svg b/vscode/a-shares/images/dark/add.svg new file mode 100644 index 0000000..9418975 --- /dev/null +++ b/vscode/a-shares/images/dark/add.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vscode/a-shares/images/dark/amount-sort.svg b/vscode/a-shares/images/dark/amount-sort.svg new file mode 100644 index 0000000..b1be8d4 --- /dev/null +++ b/vscode/a-shares/images/dark/amount-sort.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vscode/a-shares/images/dark/amount.svg b/vscode/a-shares/images/dark/amount.svg new file mode 100644 index 0000000..4a06823 --- /dev/null +++ b/vscode/a-shares/images/dark/amount.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vscode/a-shares/images/dark/flow.svg b/vscode/a-shares/images/dark/flow.svg new file mode 100644 index 0000000..b0533b9 --- /dev/null +++ b/vscode/a-shares/images/dark/flow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vscode/a-shares/images/dark/news.svg b/vscode/a-shares/images/dark/news.svg new file mode 100644 index 0000000..dc8a26f --- /dev/null +++ b/vscode/a-shares/images/dark/news.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vscode/a-shares/images/dark/rank.svg b/vscode/a-shares/images/dark/rank.svg new file mode 100644 index 0000000..75ef581 --- /dev/null +++ b/vscode/a-shares/images/dark/rank.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vscode/a-shares/images/dark/remind.svg b/vscode/a-shares/images/dark/remind.svg new file mode 100644 index 0000000..190e5a8 --- /dev/null +++ b/vscode/a-shares/images/dark/remind.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vscode/a-shares/images/dark/sort.svg b/vscode/a-shares/images/dark/sort.svg new file mode 100644 index 0000000..a8ed735 --- /dev/null +++ b/vscode/a-shares/images/dark/sort.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vscode/a-shares/images/dark/top.svg b/vscode/a-shares/images/dark/top.svg new file mode 100644 index 0000000..38c718a --- /dev/null +++ b/vscode/a-shares/images/dark/top.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vscode/a-shares/images/dark/trend.svg b/vscode/a-shares/images/dark/trend.svg new file mode 100644 index 0000000..597be37 --- /dev/null +++ b/vscode/a-shares/images/dark/trend.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vscode/a-shares/images/dark/view.svg b/vscode/a-shares/images/dark/view.svg new file mode 100644 index 0000000..cf14752 --- /dev/null +++ b/vscode/a-shares/images/dark/view.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vscode/a-shares/images/down.svg b/vscode/a-shares/images/down.svg new file mode 100644 index 0000000..45aa11d --- /dev/null +++ b/vscode/a-shares/images/down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vscode/a-shares/images/down1.svg b/vscode/a-shares/images/down1.svg new file mode 100644 index 0000000..8d4f14d --- /dev/null +++ b/vscode/a-shares/images/down1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vscode/a-shares/images/down2.svg b/vscode/a-shares/images/down2.svg new file mode 100644 index 0000000..5764e14 --- /dev/null +++ b/vscode/a-shares/images/down2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vscode/a-shares/images/down3.svg b/vscode/a-shares/images/down3.svg new file mode 100644 index 0000000..af7fc63 --- /dev/null +++ b/vscode/a-shares/images/down3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vscode/a-shares/images/light/add.svg b/vscode/a-shares/images/light/add.svg new file mode 100644 index 0000000..4b1e62e --- /dev/null +++ b/vscode/a-shares/images/light/add.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vscode/a-shares/images/light/amount-sort.svg b/vscode/a-shares/images/light/amount-sort.svg new file mode 100644 index 0000000..89934f8 --- /dev/null +++ b/vscode/a-shares/images/light/amount-sort.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vscode/a-shares/images/light/amount.svg b/vscode/a-shares/images/light/amount.svg new file mode 100644 index 0000000..dc001e2 --- /dev/null +++ b/vscode/a-shares/images/light/amount.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vscode/a-shares/images/light/flow.svg b/vscode/a-shares/images/light/flow.svg new file mode 100644 index 0000000..8d66d89 --- /dev/null +++ b/vscode/a-shares/images/light/flow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vscode/a-shares/images/light/news.svg b/vscode/a-shares/images/light/news.svg new file mode 100644 index 0000000..57d989c --- /dev/null +++ b/vscode/a-shares/images/light/news.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vscode/a-shares/images/light/rank.svg b/vscode/a-shares/images/light/rank.svg new file mode 100644 index 0000000..803e5de --- /dev/null +++ b/vscode/a-shares/images/light/rank.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vscode/a-shares/images/light/remind.svg b/vscode/a-shares/images/light/remind.svg new file mode 100644 index 0000000..18aba32 --- /dev/null +++ b/vscode/a-shares/images/light/remind.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vscode/a-shares/images/light/sort.svg b/vscode/a-shares/images/light/sort.svg new file mode 100644 index 0000000..f1eb61b --- /dev/null +++ b/vscode/a-shares/images/light/sort.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vscode/a-shares/images/light/top.svg b/vscode/a-shares/images/light/top.svg new file mode 100644 index 0000000..d2b4b97 --- /dev/null +++ b/vscode/a-shares/images/light/top.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vscode/a-shares/images/light/trend.svg b/vscode/a-shares/images/light/trend.svg new file mode 100644 index 0000000..d2a501c --- /dev/null +++ b/vscode/a-shares/images/light/trend.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vscode/a-shares/images/light/view.svg b/vscode/a-shares/images/light/view.svg new file mode 100644 index 0000000..d738f35 --- /dev/null +++ b/vscode/a-shares/images/light/view.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vscode/a-shares/images/logo.png b/vscode/a-shares/images/logo.png new file mode 100644 index 0000000..edd37dd Binary files /dev/null and b/vscode/a-shares/images/logo.png differ diff --git a/vscode/a-shares/images/logo.svg b/vscode/a-shares/images/logo.svg new file mode 100644 index 0000000..6a859ab --- /dev/null +++ b/vscode/a-shares/images/logo.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/vscode/a-shares/images/logo1.svg b/vscode/a-shares/images/logo1.svg new file mode 100644 index 0000000..34bfb0d --- /dev/null +++ b/vscode/a-shares/images/logo1.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/vscode/a-shares/images/tag.svg b/vscode/a-shares/images/tag.svg new file mode 100644 index 0000000..3256687 --- /dev/null +++ b/vscode/a-shares/images/tag.svg @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vscode/a-shares/images/up.svg b/vscode/a-shares/images/up.svg new file mode 100644 index 0000000..a3a7d8c --- /dev/null +++ b/vscode/a-shares/images/up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vscode/a-shares/images/up1.svg b/vscode/a-shares/images/up1.svg new file mode 100644 index 0000000..01f43e1 --- /dev/null +++ b/vscode/a-shares/images/up1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vscode/a-shares/images/up2.svg b/vscode/a-shares/images/up2.svg new file mode 100644 index 0000000..acea7d5 --- /dev/null +++ b/vscode/a-shares/images/up2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vscode/a-shares/images/up3.svg b/vscode/a-shares/images/up3.svg new file mode 100644 index 0000000..b378767 --- /dev/null +++ b/vscode/a-shares/images/up3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vscode/a-shares/src/explorer/chatService.ts b/vscode/a-shares/src/explorer/chatService.ts new file mode 100644 index 0000000..057d96b --- /dev/null +++ b/vscode/a-shares/src/explorer/chatService.ts @@ -0,0 +1,202 @@ +import * as vscode from 'vscode'; +import globalState from '../globalState'; +import Axios from 'axios'; +import { LeekFundConfig } from '../shared/leekConfig'; +import { StockProvider } from '../explorer/stockProvider'; +import stockTrend from '../webview/stockTrend'; +import homeTrend from '../webview/homeTrend'; + +export default class ChatViewProvider implements vscode.WebviewViewProvider { + + public static readonly viewType = 'asharesFundView.chat'; + + private _view?: vscode.WebviewView; + private interval?: NodeJS.Timeout; + constructor( + private readonly _extensionUri: vscode.Uri, + private readonly _extensionContext: any, + + ) { } + + public resolveWebviewView( + webviewView: vscode.WebviewView, + context: vscode.WebviewViewResolveContext, + _token: vscode.CancellationToken, + ) { + this._view = webviewView; + + webviewView.webview.options = { + // Allow scripts in the webview + enableScripts: true, + + localResourceRoots: [ + this._extensionUri, + this._extensionContext + ] + }; + // const userid = globalState.userId; + // if (userid.length === 0) {// 未登录 + // this.updateUser(); + // // this.interval = setInterval(this.updateUser, 3000); + // } + + webviewView.webview.html = this._getHtmlForWebview(); + + + // Reset when the current panel is closed + webviewView.onDidDispose( + () => { + // When the panel is closed, cancel any future updates to the webview content + clearInterval(this.interval); + }, + null, + this._extensionContext + ); + + webviewView.webview.onDidReceiveMessage(data => { + switch (data.type) { + case 'code': + { + stockTrend(data.code, data.name, data.code); + break + } + case 'home': + { + homeTrend(data.url); + break + } + case 'user': + { + this.updateUser(); + break + } + } + }); + } + + public async clearUser() { + const deviceId = globalState.deviceId; + const res = await Axios.post('https://www.xxxxxx.cn/shares/api/v1/weixin.clear_login', { code: deviceId }); + console.log(res) + + + globalState.userId = ""; + LeekFundConfig.setConfig('a-shares.userid', globalState.userId); + globalState.userName = ""; + LeekFundConfig.setConfig('a-shares.username', globalState.userName); + globalState.userUrl = ""; + LeekFundConfig.setConfig('a-shares.userurl', globalState.userUrl); + if (this._view) { + this._view.webview.html = this._getHtmlForWebview();// 更新页面 + // this.updateUser(); + } + } + public async updateCode(stockProvider: StockProvider) {// + const userId = globalState.userId; + if (userId.length === 0) { + vscode.window.showErrorMessage("请先扫码登录登录"); + }else if (this._view ) { + this._view.webview.html = this._getHtmlForWebview(); + } + } + + public async updateUser() { + const deviceId = globalState.deviceId; + const res = await Axios.post('https://www.xxxxxx.cn/shares/api/v1/weixin.h5_login', { code: deviceId }); + if (res.status === 200) { + const data = res.data; + if (data.state === true) { + const data1 = data.data; + if (data1.status === 1) {// 已经登陆了 + globalState.userId = data1.userId; + LeekFundConfig.setConfig('a-shares.userid', globalState.userId); + globalState.userName = data1.userName; + LeekFundConfig.setConfig('a-shares.username', globalState.userName); + globalState.userUrl = data1.avatarUrl; + LeekFundConfig.setConfig('a-shares.userurl', globalState.userUrl); + if (data1.rg) { + LeekFundConfig.setConfig('a-shares.riseColor', "#ff785d"); + LeekFundConfig.setConfig('a-shares.fallColor', "#95ec69"); + } else { + LeekFundConfig.setConfig('a-shares.riseColor', "#95ec69"); + LeekFundConfig.setConfig('a-shares.fallColor', "#ff785d"); + } + globalState.rg = data1.rg; + if (this._view) { + this._view.webview.html = this._getHtmlForWebview();// 更新页面 + } + } + // else { + // setTimeout(() => this.updateUser(), 3000) + // } + } + } + console.log('🐥>>>updateUser: ', res); + } + + private _getHtmlForWebview() { + + const userid = globalState.userId; + if (userid.length > 0) {// 已经登陆了 + const url = `http://localhost:8080/pc/#/chat?username=${userid}`; + return ` + + + + + 复利Chat + + +
+ +
+ + + `; + } else { + const url = `https://www.xxxxxx.cn/webshares/#/pages/mine/login?deviceid=${globalState.deviceId}`; + return ` + + + + + 登录 + + +
+ +
+ + + `; + } + + } +} + diff --git a/vscode/a-shares/src/explorer/leekService.ts b/vscode/a-shares/src/explorer/leekService.ts new file mode 100644 index 0000000..baa7235 --- /dev/null +++ b/vscode/a-shares/src/explorer/leekService.ts @@ -0,0 +1,11 @@ +import { LeekTreeItem } from '../shared/leekTreeItem'; + +export abstract class LeekService { + public showLabel: boolean = true; + + public toggleLabel(): void { + this.showLabel = !this.showLabel; + } + + abstract getData(code: Array, order: number, group: string): Promise>; +} diff --git a/vscode/a-shares/src/explorer/megaService.ts b/vscode/a-shares/src/explorer/megaService.ts new file mode 100644 index 0000000..4f3bb19 --- /dev/null +++ b/vscode/a-shares/src/explorer/megaService.ts @@ -0,0 +1,93 @@ +import * as vscode from 'vscode'; + +import ReusedWebviewPanel from '../webview/ReusedWebviewPanel'; +import globalState from '../globalState'; + +export class MegaProvider implements vscode.TreeDataProvider { + private _onDidChangeTreeData: vscode.EventEmitter<(Node | undefined)[] | undefined> = new vscode.EventEmitter(); + + readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; + + + // Keep track of any nodes we create so that we can re-use the same objects. + private nodes: any = {}; + + constructor(element: Node[]) { + this.nodes = element; + } + + refresh(): any { + this._onDidChangeTreeData.fire(undefined); + } + + public getChildren(element?: Node): Node[] { + if (!element) { + return this.nodes; + } + return []; + } + + getParent(element: Node): Node|undefined { + return undefined; + } + + getTreeItem(element: Node): vscode.TreeItem { + const tooltip = new vscode.MarkdownString(`$(zap) Tooltip for ${element.key}`, true); + return { + command: { + command: 'mega.itemClick', + title: 'Open', + arguments: [element], + }, + label: { label: element.key }, + tooltip, + resourceUri: vscode.Uri.parse(`${element.key}`), + }; + } + + // 处理点击事件 + async onDidClickTreeItem(item: Node) { + // 这里可以添加你想要的点击事件逻辑 + //vscode.window.showInformationMessage(`You clicked on ${item}`); + const panel = ReusedWebviewPanel.create('stockTrendWebview', item.key, vscode.ViewColumn.One, { + enableScripts: true, + }); + let url = item.value + const userid = globalState.userId; + if (userid?.length > 0) { + url += `&username=${userid}` + }else { + url += `&logout=true` + vscode.window.showInformationMessage(`请先点击下方'ACCOUNT'扫码登录,效果更佳.`); + } + + panel.webview.html = panel.webview.html = ` + + + + + + ${item.key} + + + +
+ +
+ + + + `; + } + +} + +type Node = { key: string; value: string;}; \ No newline at end of file diff --git a/vscode/a-shares/src/explorer/stockProvider.ts b/vscode/a-shares/src/explorer/stockProvider.ts new file mode 100644 index 0000000..6dcc674 --- /dev/null +++ b/vscode/a-shares/src/explorer/stockProvider.ts @@ -0,0 +1,180 @@ +import { Event, EventEmitter, TreeDataProvider, TreeItem, TreeItemCollapsibleState } from 'vscode'; +import globalState from '../globalState'; +import { LeekTreeItem } from '../shared/leekTreeItem'; +import { defaultFundInfo, SortType, StockCategory } from '../shared/typed'; +import { LeekFundConfig } from '../shared/leekConfig'; +import StockService from './stockService'; + +export class StockProvider implements TreeDataProvider { + private _onDidChangeTreeData: EventEmitter = new EventEmitter(); + + readonly onDidChangeTreeData: Event = this._onDidChangeTreeData.event; + + private service: StockService; + private order: SortType; + private expandAStock: boolean; + private expandHKStock: boolean; + private expandUSStock: boolean; + private expandOverseaFuture: boolean; + + constructor(service: StockService) { + this.service = service; + this.order = LeekFundConfig.getConfig('a-shares.stockSort') || SortType.NORMAL; + this.expandAStock = LeekFundConfig.getConfig('a-shares.expandAStock', true); + this.expandHKStock = LeekFundConfig.getConfig('a-shares.expandHKStock', false); + this.expandUSStock = LeekFundConfig.getConfig('a-shares.expandUSStock', false); + this.expandOverseaFuture = LeekFundConfig.getConfig('a-shares.expandOverseaFuture', false); + } + + refresh(): any { + this._onDidChangeTreeData.fire(undefined); + } + + getChildren(element?: LeekTreeItem | undefined): LeekTreeItem[] | Thenable { + if (!element) { + // Root view + const stockCodes = LeekFundConfig.getConfig('a-shares.stocks') || []; + return this.service.getData(stockCodes, this.order).then(() => { + return this.getRootNodes(); + }); + } else { + const resultPromise = Promise.resolve(this.service.stockList || []); + switch ( element.id ) { + case StockCategory.A: + return this.getAStockNodes(resultPromise); + case StockCategory.HK: + return this.getHkStockNodes(resultPromise); + case StockCategory.US: + return this.getUsStockNodes(resultPromise); + case StockCategory.NODATA: + return this.getNoDataStockNodes(resultPromise); + default: + return []; + // return this.getChildrenNodesById(element.id); + } + } + } + + getParent(): LeekTreeItem | undefined { + return undefined; + } + + getTreeItem(element: LeekTreeItem): TreeItem { + if (!element.isCategory) { + return element; + } else { + return { + id: element.id, + label: element.info.name, + // tooltip: this.getSubCategoryTooltip(element), + collapsibleState: + (element.id === StockCategory.A && this.expandAStock) || + (element.id === StockCategory.HK && this.expandHKStock) || + (element.id === StockCategory.US && this.expandUSStock) + ? TreeItemCollapsibleState.Expanded + : TreeItemCollapsibleState.Collapsed, + // iconPath: this.parseIconPathFromProblemState(element), + command: undefined, + contextValue: element.contextValue, + }; + } + } + + getRootNodes(): LeekTreeItem[] { + const nodes = [ + new LeekTreeItem( + Object.assign({ contextValue: 'category' }, defaultFundInfo, { + id: StockCategory.A, + name: `${StockCategory.A}${ + globalState.aStockCount > 0 ? `(${globalState.aStockCount})` : '' + }`, + }), + undefined, + true + ), + new LeekTreeItem( + Object.assign({ contextValue: 'category' }, defaultFundInfo, { + id: StockCategory.HK, + name: `${StockCategory.HK}${ + globalState.hkStockCount > 0 ? `(${globalState.hkStockCount})` : '' + }`, + }), + undefined, + true + ), + new LeekTreeItem( + Object.assign({ contextValue: 'category' }, defaultFundInfo, { + id: StockCategory.US, + name: `${StockCategory.US}${ + globalState.usStockCount > 0 ? `(${globalState.usStockCount})` : '' + }`, + }), + undefined, + true + ), + ]; + // 显示接口不支持的股票,避免用户老问为什么添加了股票没反应 + if (globalState.noDataStockCount) { + nodes.push( + new LeekTreeItem( + Object.assign({ contextValue: 'category' }, defaultFundInfo, { + id: StockCategory.NODATA, + name: `${StockCategory.NODATA}(${globalState.noDataStockCount})`, + }), + undefined, + true + ) + ); + } + return nodes; + } + getAStockNodes(stocks: Promise): Promise { + const aStocks: Promise = stocks.then((res: LeekTreeItem[]) => { + const arr = res.filter((item: LeekTreeItem) => /^(sh|sz|bj)/.test(item.type || '')); + return arr; + }); + + return aStocks; + } + getHkStockNodes(stocks: Promise): Promise { + return stocks.then((res: LeekTreeItem[]) => + res.filter((item: LeekTreeItem) => /^(hk)/.test(item.type || '')) + ); + } + getUsStockNodes(stocks: Promise): Promise { + return stocks.then((res: LeekTreeItem[]) => + res.filter((item: LeekTreeItem) => /^(usr_)/.test(item.type || '')) + ); + } + getFutureStockNodes(stocks: Promise): Promise { + return stocks.then((res: LeekTreeItem[]) => + res.filter((item: LeekTreeItem) => /^(nf_)/.test(item.type || '')) + ); + } + getOverseaFutureStockNodes(stocks: Promise): Promise { + return stocks.then((res: LeekTreeItem[]) => + res.filter((item: LeekTreeItem) => /^(hf_)/.test(item.type || '')) + ); + } + getNoDataStockNodes(stocks: Promise): Promise { + return stocks.then((res: LeekTreeItem[]) => { + return res.filter((item: LeekTreeItem) => { + return /^(nodata)/.test(item.type || ''); + }); + }); + } + + changeOrder(): void { + let order = this.order as number; + order += 1; + if (order > 1) { + this.order = SortType.DESC; + } else if (order === 1) { + this.order = SortType.ASC; + } else if (order === 0) { + this.order = SortType.NORMAL; + } + LeekFundConfig.setConfig('a-shares.stockSort', this.order); + this.refresh(); + } +} diff --git a/vscode/a-shares/src/explorer/stockService.ts b/vscode/a-shares/src/explorer/stockService.ts new file mode 100644 index 0000000..d4ffdb4 --- /dev/null +++ b/vscode/a-shares/src/explorer/stockService.ts @@ -0,0 +1,651 @@ +import { decode } from 'iconv-lite'; +import Axios from 'axios'; +import { ExtensionContext, QuickPickItem, window } from 'vscode'; +import globalState from '../globalState'; +import { LeekTreeItem } from '../shared/leekTreeItem'; +import { HeldData } from '../shared/typed'; +import { calcFixedPriceNumber, events, formatNumber, randHeader, sortData } from '../shared/utils'; +import { LeekService } from './leekService'; +import moment = require('moment'); + +export default class StockService extends LeekService { + public stockList: Array = []; + private context: ExtensionContext; + private token: string = ''; + + constructor(context: ExtensionContext) { + super(); + this.context = context; + } + /** + * 获取自选,去掉大盘数据 + * @returns + */ + getSelfSelected() { + const s = + 'sh000001,sh000300,sh000016,sh000688'; + const maps = s.split(','); + return this.stockList.filter((item) => !maps.includes(item.info.code)); + } + async getToken(): Promise { + if (this.token !== '') return this.token; + + try { + const res = await Axios.get('https://xueqiu.com/hq/detail',{ + headers: { + ...randHeader(), + Referer: 'https://xueqiu.com/', + }, + }); + const cookies: string[] = res.headers['set-cookie']; + const param: string = cookies.filter((key) => key.includes('xq_a_token'))[0] || ''; + this.token = param.split(';')[0] || ''; + }catch (err) { + const res = await Axios.get('https://xueqiu.com/about',{ + headers: { + ...randHeader(), + Referer: 'https://xueqiu.com/', + }, + }); + const cookies: string[] = res.headers['set-cookie']; + + const param: string = cookies.filter((key) => key.includes('xq_a_token'))[0] || ''; + this.token = param.split(';')[0] || ''; + } + + if (this.token === '') { + return "xq_a_token=49c5e355d2fc1b871fde601c659cf9ae1457a8891"; + } + + return this.token; + } + + async getData(codes: Array, order: number): Promise> { + // console.log('fetching stock data…'); + if ((codes && codes.length === 0) || !codes) { + return []; + } + + // 兼容2.1-2.5版本中以大写开头及cnf_开头的期货代码 + const transFuture = (code: string) => { + if (/^[A-Z]+/.test(code)) { + return code.replace(/^[A-Z]+/, (it: string) => `nf_${it}`); + } else if (/cnf_/.test(code)) { + return code.replace('cnf_', 'nf_'); + } + return code; + }; + + let stockCodes = codes.map(transFuture); + const hkCodes: Array = []; // 港股单独请求雪球数据源 + stockCodes = stockCodes.filter((code) => { + if (code.startsWith('hk')) { + const _code = code.startsWith('hk0') ? code.replace('hk', '') : code.toUpperCase(); // 个股去掉'hk', 指数保留'hk'并转为大写 + hkCodes.push(_code); + return false; + } else { + return true; + } + }); + + let stockList: Array = []; + const result = await Promise.allSettled([ + this.getStockData(stockCodes), + this.getHKStockData(hkCodes), + ]); + result.forEach((item) => { + if (item.status === 'fulfilled') { + stockList = stockList.concat(item.value); + } + }); + + const res = sortData(stockList, order); + // executeStocksRemind(res, this.stockList); + const oldStockList = this.stockList; + this.stockList = res; + events.emit('updateBar:stock-profit-refresh', this); + events.emit('stockListUpdate', this.stockList, oldStockList); + return res; + } + + async getStockData(codes: Array): Promise> { + if ((codes && codes.length === 0) || !codes) { + return []; + } + + let aStockCount = 0; + let usStockCount = 0; + // let cnfStockCount = 0; + // let hfStockCount = 0; + let noDataStockCount = 0; + let stockList: Array = []; + + const url = `https://hq.sinajs.cn/list=${codes.join(',')}`; + try { + const resp = await Axios.get(url, { + // axios 乱码解决 + responseType: 'arraybuffer', + transformResponse: [ + (data) => { + const body = decode(data, 'GB18030'); + return body; + }, + ], + headers: { + ...randHeader(), + Referer: 'http://finance.sina.com.cn/', + }, + }); + if (/FAILED/.test(resp.data)) { + if (codes.length === 1) { + window.showErrorMessage( + `fail: error Stock code in ${codes}, please delete error Stock code.` + ); + return []; + } + for (const code of codes) { + stockList = stockList.concat(await this.getStockData(new Array(code))); + } + } else { + const splitData = resp.data.split(';\n'); + const stockPrice: { + [key: string]: { + amount: number; + earnings: number; + name: string; + price: string; + unitPrice: number; + }; + } = globalState.stockPrice; + + for (let i = 0; i < splitData.length - 1; i++) { + const code = splitData[i].split('="')[0].split('var hq_str_')[1]; + const params = splitData[i].split('="')[1].split(','); + let type = code.substr(0, 2) || 'sh'; + let symbol = code.substr(2); + let stockItem: any; + let fixedNumber = 2; + if (params.length > 1) { + if (/^(sh|sz|bj)/.test(code)) { + // A股 + let open = params[1]; + let yestclose = params[2]; + let price = params[3]; + let high = params[4]; + let low = params[5]; + fixedNumber = calcFixedPriceNumber(open, yestclose, price, high, low); + const profitData = stockPrice[code] || {}; + const heldData: HeldData = {}; + if (profitData.amount) { + // 表示是持仓股 + heldData.heldAmount = profitData.amount; + heldData.heldPrice = profitData.unitPrice; + } + stockItem = { + code, + name: params[0], + open: formatNumber(open, fixedNumber, false), + yestclose: formatNumber(yestclose, fixedNumber, false), + price: formatNumber(price, fixedNumber, false), + low: formatNumber(low, fixedNumber, false), + high: formatNumber(high, fixedNumber, false), + volume: formatNumber(params[8], 2), + amount: formatNumber(params[9], 2), + time: `${params[30]} ${params[31]}`, + percent: '', + ...heldData, + }; + aStockCount += 1; + } else if (/^gb_/.test(code)) { + symbol = code.substr(3); + let open = params[5]; + let yestclose = params[26]; + let price = params[1]; + let high = params[6]; + let low = params[7]; + fixedNumber = calcFixedPriceNumber(open, yestclose, price, high, low); + stockItem = { + code, + name: params[0], + open: formatNumber(open, fixedNumber, false), + yestclose: formatNumber(yestclose, fixedNumber, false), + price: formatNumber(price, fixedNumber, false), + low: formatNumber(low, fixedNumber, false), + high: formatNumber(high, fixedNumber, false), + volume: formatNumber(params[10], 2), + amount: '接口无数据', + percent: '', + }; + type = code.substr(0, 3); + noDataStockCount += 1; + } else if (/^usr_/.test(code)) { + symbol = code.substr(4); + let open = params[5]; + let yestclose = params[26]; + let price = params[1]; + let high = params[6]; + let low = params[7]; + fixedNumber = calcFixedPriceNumber(open, yestclose, price, high, low); + stockItem = { + code, + name: params[0], + open: formatNumber(open, fixedNumber, false), + yestclose: formatNumber(yestclose, fixedNumber, false), + price: formatNumber(price, fixedNumber, false), + low: formatNumber(low, fixedNumber, false), + high: formatNumber(high, fixedNumber, false), + volume: formatNumber(params[10], 2), + amount: '接口无数据', + percent: '', + }; + type = code.substr(0, 4); + usStockCount += 1; + } else if (/nf_/.test(code)) { + /* 解析格式,与股票略有不同 + var hq_str_V2201="PVC2201,230000, + 8585.00, 8692.00, 8467.00, 8641.00, // params[2,3,4,5] 开,高,低,昨收 + 8673.00, 8674.00, // params[6, 7] 买一、卖一价 + 8675.00, // 现价 params[8] + 8630.00, // 均价 + 8821.00, // 昨日结算价【一般软件的行情涨跌幅按这个价格显示涨跌幅】(后续考虑配置项,设置按收盘价还是结算价显示涨跌幅) + 109, // 买一量 + 2, // 卖一量 + 289274, // 持仓量 + 230643, //总量 + 连, // params[8 + 7] 交易所名称 ["连","沪", "郑"] + PVC,2021-11-26,1,9243.000,8611.000,9243.000,8251.000,9435.000,8108.000,13380.000,8108.000,445.541"; + */ + let name = params[0]; + let open = params[2]; + let high = params[3]; + let low = params[4]; + // let yestclose = params[5]; // 昨收盘。但是这个字段不返回数据。 + let price = params[8]; + let yestCallPrice = params[8 + 2]; // 结算价 + /* + 由于期货默认采用结算价计算涨跌幅。本项目的涨跌幅使用【昨收盘】进行计算, + 新浪接口对于商品期货的 昨收盘返回 0.0,导致无法计算【昨收盘涨跌幅】,只能计算【结算涨跌幅】。 + 使用期货的结算价对应 股票通用的 【昨收盘 yestclose】字段以方便计算涨跌幅的显示。 + */ + let yestclose = params[8 + 2]; + let volume = params[8 + 6]; // 成交量 + //股指期货 + const stockIndexFuture = + /nf_IC/.test(code) || // 中证500 + /nf_IF/.test(code) || // 沪深300 + /nf_IH/.test(code) || // 上证50 + /nf_IM/.test(code) || // 中证 1000 + /nf_TF/.test(code) || // 五债 + /nf_TS/.test(code) || // 二债 + /nf_T\d+/.test(code) || // 十债 + /nf_TL/.test(code); // 三十年国债 + if (stockIndexFuture) { + // 0 开盘 1 最高 2 最低 3 收盘 + // ['5372.000', '5585.000', '5343.000', '5581.600', + // 4 成交量 6 持仓量 + // '47855', '261716510.000', '124729.000', '5581.600', + // '0.000', '5849.800', '4786.200', '0.000', '0.000', + // 13 昨收盘 14 昨天结算 + // '5342.800', '5318.000', '126776.000', '5581.600', + // '4', '0.000', '0', '0.000', '0', '0.000', '0', '0.000', '0', '5582.000', '2', '0.000', '0', '0.000', '0', '0.000', '0', '0.000', '0', '2022-04-29', '15:00:00', '300', '0', '', '', '', '', '', '', '', '', + // 48 49 名称 + // '5468.948', '中证500指数期货2206"'] + + name = params[49].slice(0, -1); // 最后一位去掉 " + open = params[0]; + high = params[1]; + low = params[2]; + price = params[3]; + volume = params[4]; + yestclose = params[13]; + yestCallPrice = params[14]; + } + fixedNumber = calcFixedPriceNumber(open, yestclose, price, high, low); + stockItem = { + code: code, + name: name, + open: formatNumber(open, fixedNumber, false), + yestclose: formatNumber(yestclose, fixedNumber, false), + yestcallprice: formatNumber(yestCallPrice, fixedNumber, false), + price: formatNumber(price, fixedNumber, false), + low: formatNumber(low, fixedNumber, false), + high: formatNumber(high, fixedNumber, false), + volume: formatNumber(volume, 2), + amount: '接口无数据', + percent: '', + }; + type = 'nf_'; + } else if (/hf_/.test(code)) { + // 海外期货格式 + // 0 当前价格 + // ['105.306', '', + // 2 买一价 3 卖一价 4 最高价 5 最低价 + // '105.270', '105.290', '105.540', '102.950', + // 6 时间 7 昨日结算价 8 开盘价 9 持仓量 + // '15:51:34', '102.410', '103.500', '250168.000', + // 10 买 11 卖 12 日期 13 名称 14 成交量 + // '5', '2', '2022-05-04', 'WTI纽约原油2206', '28346"'] + // 当前价格 + let price = params[0]; + // 名称 + let name = params[13]; + let open = params[8]; + let high = params[4]; + let low = params[5]; + let yestclose = params[7]; // 昨收盘 + let yestCallPrice = params[7]; // 昨结算 + let volume = params[14].slice(0, -1); // 成交量。slice 去掉最后一位 " + fixedNumber = calcFixedPriceNumber(open, yestclose, price, high, low); + + stockItem = { + code: code, + name: name, + open: formatNumber(open, fixedNumber, false), + yestclose: formatNumber(yestclose, fixedNumber, false), + yestcallprice: formatNumber(yestCallPrice, fixedNumber, false), + price: formatNumber(price, fixedNumber, false), + low: formatNumber(low, fixedNumber, false), + high: formatNumber(high, fixedNumber, false), + volume: formatNumber(volume, 2), + amount: '接口无数据', + percent: '', + }; + type = 'hf_'; + } + if (stockItem) { + const { yestclose, open } = stockItem; + let { price } = stockItem; + /* if (open === price && price === '0.00') { + stockItem.isStop = true; + } */ + + // 竞价阶段部分开盘和价格为0.00导致显示 -100% + try { + if (Number(open) <= 0) { + price = yestclose; + } + } catch (err) { + console.error(err); + } + stockItem.showLabel = this.showLabel; + stockItem.isStock = true; + stockItem.type = type; + stockItem.symbol = symbol; + stockItem.updown = formatNumber(+price - +yestclose, fixedNumber, false); + stockItem.percent = + (stockItem.updown >= 0 ? '+' : '-') + + formatNumber((Math.abs(stockItem.updown) / +yestclose) * 100, 2, false); + + const treeItem = new LeekTreeItem(stockItem, this.context); + stockList.push(treeItem); + } + } else { + // 接口不支持的 + noDataStockCount += 1; + stockItem = { + id: code, + name: `接口不支持该股票 ${code}`, + showLabel: this.showLabel, + isStock: true, + percent: '', + type: 'nodata', + contextValue: 'nodata', + }; + const treeItem = new LeekTreeItem(stockItem, this.context); + stockList.push(treeItem); + } + } + } + } catch (err) { + console.info(url); + console.error(err); + if (globalState.showStockErrorInfo) { + window.showErrorMessage(`fail: Stock error ` + url); + globalState.showStockErrorInfo = false; + globalState.telemetry.sendEvent('error: stockService', { + url, + error: err, + }); + } + } + + globalState.aStockCount = aStockCount; + globalState.usStockCount = usStockCount; + // globalState.cnfStockCount = cnfStockCount; + // globalState.hfStockCount = hfStockCount; + globalState.noDataStockCount = noDataStockCount; + return stockList; + } + + async getHKStockData(codes: Array): Promise> { + if ((codes && codes.length === 0) || !codes) { + return []; + } + + let hkStockCount = 0; + let stockList: Array = []; + + const url = `https://stock.xueqiu.com/v5/stock/batch/quote.json?symbol=${codes.join(',')}`; + try { + const resp = await Axios.get(url, { + responseType: 'text', + transformResponse: [ + (data) => { + const body = JSON.parse(data); + return body; + }, + ], + headers: { + ...randHeader(), + Referer: 'https://stock.xueqiu.com/', + Cookie: await this.getToken(), + }, + }); + const { data, error_code, error_description } = resp.data; + if (error_code) { + window.showErrorMessage( + `fail: a HK Stock request error has occured. (${error_code}, ${error_description})` + ); + return []; + } else { + const stocks = data.items || []; + stocks.forEach((item: any, index: number) => { + if (item.quote) { + const quote = item.quote; + let open = quote.open?.toString() || '0'; + let yestclose = quote.last_close?.toString() || '0'; + let price = quote.current?.toString() || '0'; + let high = quote.high?.toString() || '0'; + let low = quote.low?.toString() || '0'; + const fixedNumber = calcFixedPriceNumber(open, yestclose, price, high, low); + const stockItem: any = { + code: quote.symbol.startsWith('HK') + ? quote.symbol.replace('HK', 'hk') + : 'hk' + quote.symbol, + name: quote.name, + open: formatNumber(open, fixedNumber, false), + yestclose: formatNumber(yestclose, fixedNumber, false), + price: formatNumber(price, fixedNumber, false), + low: formatNumber(low, fixedNumber, false), + high: formatNumber(high, fixedNumber, false), + volume: formatNumber(quote.volume || 0, 2), + amount: formatNumber(quote.amount || 0, 2), + percent: '', + time: `${moment(quote.time).format('YYYY-MM-DD HH:mm:ss')}`, + }; + hkStockCount += 1; + if (stockItem) { + const { yestclose, open } = stockItem; + let { price } = stockItem; + // 竞价阶段部分开盘和价格为0.00导致显示 -100% + if (Number(open) <= 0) { + price = yestclose; + } + stockItem.showLabel = this.showLabel; + stockItem.isStock = true; + stockItem.type = 'hk'; + stockItem.symbol = quote.code; + stockItem.updown = formatNumber(+price - +yestclose, fixedNumber, false); + stockItem.percent = + (stockItem.updown >= 0 ? '+' : '-') + + formatNumber((Math.abs(stockItem.updown) / +yestclose) * 100, 2, false); + + const treeItem = new LeekTreeItem(stockItem, this.context); + stockList.push(treeItem); + } + } else { + window.showErrorMessage( + `fail: error Stock code in ${codes[index]}, please delete error Stock code.` + ); + } + }); + } + } catch (err) { + console.info(url); + console.error(err); + if (globalState.showStockErrorInfo) { + window.showErrorMessage(`fail: HK Stock error ` + url); + globalState.showStockErrorInfo = false; + globalState.telemetry.sendEvent('error: stockService', { + url, + error: err, + }); + } + } + + globalState.hkStockCount = hkStockCount; + return stockList; + } + + // https://github.com/LeekHub/a-shares/issues/266 + async getStockSuggestList(searchText = ''): Promise { + if (!searchText) { + return [{ label: '请输入关键词查询,如:0000001 或 上证指数' }]; + } + + const result: QuickPickItem[] = []; + + // 期货大写字母开头 + const isFuture = + /^[A-Z]/.test(searchText.charAt(0)) || + /nf_/.test(searchText) || + /hf_/.test(searchText) || + /fx_/.test(searchText); + if (isFuture) { + //期货使用新浪数据源 + const type = '85,86,88'; + const futureUrl = `http://suggest3.sinajs.cn/suggest/type=${type}&key=${encodeURIComponent( + searchText + )}`; + try { + console.log('getFutureSuggestList: getting...'); + const futureResponse = await Axios.get(futureUrl, { + responseType: 'arraybuffer', + transformResponse: [ + (data) => { + const body = decode(data, 'GB18030'); + return body; + }, + ], + headers: randHeader(), + }); + const text = futureResponse.data.slice(18, -2); + if (text === '') { + return result; + } + const tempArr = text.split(';'); + console.log(tempArr); + + tempArr.forEach((item: string) => { + const arr = item.split(','); + let code = arr[3]; + let market = arr[1]; + code = code.toUpperCase(); + // 国内交易所 + if (market === '85' || market === '88') { + code = 'nf_' + code; + } else if (market === '86') { + // 海外交易所 + code = 'hf_' + code; + } + // if (code.substr(0, 2) === 'of') { + // 修改lof以及etf的前缀,防止被过滤 + // http://www.csisc.cn/zbscbzw/cpbmjj/201212/f3263ab61f7c4dba8461ebbd9d0c6755.shtml + // 在上海证券交易所挂牌的证券投资基金使用50~59开头6位数字编码,在深圳证券交易所挂牌的证券投资基金使用15~19开头6位数字编码。 + // code = code.replace(/^(of)(5[0-9])/g, 'sh$2').replace(/^(of)(1[5-9])/g, 'sz$2'); + // } + + // 期货 suggest 请求返回的 code 小写开头改为大写 + + // if (code === 'hkhsi' || code === 'hkhscei' || isFuture) { + // code = code.toUpperCase().replace('HK', 'hk'); + // } + + // 过滤多余的 us. 开头的股干扰 + // if ((STOCK_TYPE.includes(code.substr(0, 2)) && !code.startsWith('us.')) || isFuture) { + result.push({ + label: `${code} | ${arr[4]}`, + description: arr[7] && arr[7].replace(/"/g, ''), + }); + // } + }); + return result; + } catch (err) { + console.log(futureUrl); + console.error(err); + return [{ label: '期货查询失败,请重试' }]; + } + } else { + //股票使用雪球数据源 + const stockUrl = `https://xueqiu.com/query/v1/suggest_stock.json?q=${encodeURIComponent( + searchText + )}`; + try { + console.log('getStockSuggestList: getting...'); + const stockResponse = await Axios.get(stockUrl, { + responseType: 'text', + transformResponse: [ + (data) => { + const body = JSON.parse(data); + return body; + }, + ], + headers: { + ...randHeader(), + Referer: 'https://xueqiu.com/', + Cookie: await this.getToken(), + }, + }); + const stocks = stockResponse.data.data || []; + stocks.forEach((item: any) => { + const { code, query } = item; + if (code.startsWith('SH') || code.startsWith('SZ') || code.startsWith('BJ')) { + const _code = code.toLowerCase(); + result.push({ + label: `${_code} | ${query} | ${searchText}`, + description: `A股`, + }); + } else if (/^0\d{4}$/.test(code) || /^HK[A-Z].*/.test(code)) { + // 港股个股 || 港股指数 + const _code = code.startsWith('HK') ? code.replace('HK', 'hk') : 'hk' + code; + result.push({ + label: `${_code} | ${query}`, + description: `港股`, + }); + } else if (/\.?[A-Z]*[A-Z]$/.test(code)) { + const _code = 'us' + code.toLowerCase().replace('.', ''); // 去除美股指数前面的'.' + result.push({ + label: `${_code} | ${query}`, + description: `美股`, + }); + } + }); + return result; + } catch (err) { + console.log(stockUrl); + console.error(err); + return [{ label: '股票查询失败,请重试' }]; + } + } + } +} diff --git a/vscode/a-shares/src/explorer/userService.ts b/vscode/a-shares/src/explorer/userService.ts new file mode 100644 index 0000000..e39067e --- /dev/null +++ b/vscode/a-shares/src/explorer/userService.ts @@ -0,0 +1,217 @@ +import * as vscode from 'vscode'; +import globalState from '../globalState'; +import Axios from 'axios'; +import { LeekFundConfig } from '../shared/leekConfig'; +import { StockProvider } from '../explorer/stockProvider'; +import stockTrend from '../webview/stockTrend'; + +export default class UserViewProvider implements vscode.WebviewViewProvider { + + public static readonly viewType = 'asharesFundView.account'; + + private _view?: vscode.WebviewView; + private interval?: NodeJS.Timeout; + constructor( + private readonly _extensionUri: vscode.Uri, + private readonly _extensionContext: any, + + ) { } + + public resolveWebviewView( + webviewView: vscode.WebviewView, + context: vscode.WebviewViewResolveContext, + _token: vscode.CancellationToken, + ) { + this._view = webviewView; + + webviewView.webview.options = { + // Allow scripts in the webview + enableScripts: true, + + localResourceRoots: [ + this._extensionUri, + this._extensionContext + ] + }; + // const userid = globalState.userId; + // if (userid.length === 0) {// 已经登陆了 + // this.updateUser(); + // // this.interval = setInterval(this.updateUser, 3000); + // } + + webviewView.webview.html = this._getHtmlForWebview(); + // Reset when the current panel is closed + webviewView.onDidDispose( + () => { + // When the panel is closed, cancel any future updates to the webview content + clearInterval(this.interval); + }, + null, + this._extensionContext + ); + + webviewView.webview.onDidReceiveMessage(data => { + switch (data.type) { + case 'user': + { + this.updateUser(); + break + } + } + }); + } + + public async clearUser() { + const deviceId = globalState.deviceId; + const res = await Axios.post('https://www.xxxxxx.cn/shares/api/v1/weixin.clear_login',{code: deviceId}); + console.log(res) + + globalState.userId = ""; + LeekFundConfig.setConfig('a-shares.userid', globalState.userId); + globalState.userName = ""; + LeekFundConfig.setConfig('a-shares.username', globalState.userName); + globalState.userUrl = ""; + LeekFundConfig.setConfig('a-shares.userurl', globalState.userUrl); + if (this._view ) { + this._view.webview.html = this._getHtmlForWebview();// 更新页面 + // this.updateUser(); + } + } + public async updateCode(stockProvider: StockProvider) {// + const userId = globalState.userId; + if(userId.length === 0) { + vscode.window.showErrorMessage("请先扫码登录登录"); + }else if (this._view ) { + this._view.webview.html = this._getHtmlForWebview(); + } + + const res = await Axios.post('https://www.xxxxxx.cn/shares/api/v1/shares.get_my_code',{yjzd: false}, + { + headers : { + 'Content-Type': 'application/json', + 'user-token': userId + } + } + ); + if (res.status === 200) { + const data = res.data; + if (data.state === true) { + const data1 = data.data; + if (data1.rg) { + LeekFundConfig.setConfig('a-shares.riseColor', "#ff785d"); + LeekFundConfig.setConfig('a-shares.fallColor', "#95ec69"); + }else{ + LeekFundConfig.setConfig('a-shares.riseColor', "#95ec69"); + LeekFundConfig.setConfig('a-shares.fallColor', "#ff785d"); + } + globalState.rg = data1.rg; + + let coodes = "" + for(let i=0;i { + stockProvider.refresh(); + }); + } + } + } + + public async updateUser() { + const deviceId = globalState.deviceId; + const res = await Axios.post('https://www.xxxxxx.cn/shares/api/v1/weixin.h5_login', { code: deviceId }); + if (res.status === 200) { + const data = res.data; + if (data.state === true) { + const data1 = data.data; + if (data1.status === 1) {// 已经登陆了 + globalState.userId = data1.userId; + LeekFundConfig.setConfig('a-shares.userid', globalState.userId); + globalState.userName = data1.userName; + LeekFundConfig.setConfig('a-shares.username', globalState.userName); + globalState.userUrl = data1.avatarUrl; + LeekFundConfig.setConfig('a-shares.userurl', globalState.userUrl); + if (data1.rg) { + LeekFundConfig.setConfig('a-shares.riseColor', "#ff785d"); + LeekFundConfig.setConfig('a-shares.fallColor', "#95ec69"); + }else{ + LeekFundConfig.setConfig('a-shares.riseColor', "#95ec69"); + LeekFundConfig.setConfig('a-shares.fallColor', "#ff785d"); + } + globalState.rg = data1.rg; + if (this._view) { + this._view.webview.html = this._getHtmlForWebview();// 更新页面 + } + } + // else { + // setTimeout(() => this.updateUser(), 3000) + // } + } + } + console.log('🐥>>>updateUser: ', res); + } + + private _getHtmlForWebview() { + const userid = globalState.userId; + if (userid.length > 0) {// 已经登陆了 + const userName = globalState.userName; + const userUrl = globalState.userUrl; + + return ` + + + + + + + + User Info + + + +
+
+ Google
+
+
${userName}
+
+
+ + + `; + }else { + const url = `https://www.xxxxxx.cn/webshares/#/pages/mine/login?deviceid=${globalState.deviceId}`; + return ` + + + + + 登录 + + +
+ +
+ + + `; + } + + } +} + diff --git a/vscode/a-shares/src/extension.ts b/vscode/a-shares/src/extension.ts new file mode 100644 index 0000000..2513011 --- /dev/null +++ b/vscode/a-shares/src/extension.ts @@ -0,0 +1,175 @@ +// The module 'vscode' contains the VS Code extensibility API +// Import the module and reference it with the alias vscode in your code below +import { ConfigurationChangeEvent, ExtensionContext, TreeView, window, workspace } from 'vscode'; +import { registerCommandPaletteEvent, registerViewEvent } from './registerCommand';// 注册事件 +import { Telemetry } from './shared/telemetry'; +import globalState from './globalState'; +import StockService from './explorer/stockService'; +import { StockProvider } from './explorer/stockProvider'; +import UserViewProvider from './explorer/userService'; +import { LeekFundConfig } from './shared/leekConfig'; +import { SortType } from './shared/typed'; +import { events, isStockTime } from './shared/utils'; +import { MegaProvider } from './explorer/megaService'; +import Axios from 'axios'; +import { StatusBar } from './statusbar/statusBar'; +import ChatViewProvider from './explorer/chatService'; + +let loopTimer: NodeJS.Timeout | undefined; +let stockTreeView: TreeView | null = null; + +// This method is called when your extension is activated +// Your extension is activated the very first time the command is executed +export async function activate(context: ExtensionContext) { + // Use the console to output diagnostic information (console.log) and errors (console.error) + // This line of code will only be executed once when your extension is activated + console.log('Congratulations, your extension "a-shares" is now active!'); + + globalState.context = context; + + const telemetry = new Telemetry(); + globalState.telemetry = telemetry; + + let intervalTimeConfig = LeekFundConfig.getConfig('a-shares.interval', 5000); + let intervalTime = intervalTimeConfig; + + + setGlobalVariable(); + + const stockService = new StockService(context); + const nodeStockProvider = new StockProvider(stockService); + + stockTreeView = window.createTreeView('asharesFundView.stock', { + treeDataProvider: nodeStockProvider, + }); + + + + const userService = new UserViewProvider(context.extensionUri,context.subscriptions); + context.subscriptions.push(window.registerWebviewViewProvider(UserViewProvider.viewType, userService)); + + const chatService = new ChatViewProvider(context.extensionUri,context.subscriptions); + context.subscriptions.push(window.registerWebviewViewProvider(ChatViewProvider.viewType, chatService)); + + const statusBar = new StatusBar(stockService); + + var nodes = []; + const deviceId = globalState.deviceId; + const res = await Axios.post('https://www.xxxxxx.cn/shares/api/v1/weixin.get_maga', { code: deviceId }); + if (res.status === 200) { + const data = res.data; + if (data.state === true) { + const data1 = data.data; + nodes = []; + for (var i = 0; i < data1.maga.length; i++) { + nodes.push({ key: data1.maga[i].name, value: data1.maga[i].url }); + } + } + } + const megaTreeProvider = new MegaProvider(nodes) + context.subscriptions.push(window.registerTreeDataProvider('asharesFundView.mega', megaTreeProvider)); + + + + console.log('🐥>>>deviceid: ', globalState.deviceId);// VSCODE_lx0gd8fd_tqupgbgod + + // fix when TreeView collapse https://github.com/giscafer/a-shares/issues/31 + const manualRequest = () => { + stockService.getData(LeekFundConfig.getConfig('a-shares.stocks'), SortType.NORMAL); + }; + + manualRequest(); + + // loop + const loopCallback = () => { + if (isStockTime()) { + // 重置定时器 + if (intervalTime !== intervalTimeConfig) { + intervalTime = intervalTimeConfig; + setIntervalTime(); + return; + } + + if (stockTreeView?.visible ) { + nodeStockProvider.refresh(); + } else { + manualRequest(); + } + } else { + console.log('StockMarket Closed! Polling closed!'); + // 闭市时增加轮询间隔时长 + if (intervalTime === intervalTimeConfig) { + intervalTime = intervalTimeConfig * 100; + setIntervalTime(); + } + } + }; + + const setIntervalTime = () => { + // prevent qps + if (intervalTime < 3000) { + intervalTime = 3000; + } + if (loopTimer) { + clearInterval(loopTimer); + loopTimer = undefined; + } + + loopTimer = setInterval(loopCallback, intervalTime); + + }; + + setIntervalTime(); + + workspace.onDidChangeConfiguration((e: ConfigurationChangeEvent) => { + console.log('🐥>>>Configuration changed', e); + intervalTimeConfig = LeekFundConfig.getConfig('a-shares.interval'); + setIntervalTime(); + setGlobalVariable(); + nodeStockProvider.refresh(); + events.emit('onDidChangeConfiguration'); + }); + + // register event + registerViewEvent( + context, + stockService, + userService, + chatService, + nodeStockProvider, + megaTreeProvider, + ); + + // register command + registerCommandPaletteEvent(context, statusBar); + + // Telemetry Event + telemetry.sendEvent('activate'); + +} + +function setGlobalVariable() { + globalState.iconType = LeekFundConfig.getConfig('a-shares.iconType') || 'arrow'; + globalState.rg = LeekFundConfig.getConfig('a-shares.riseColor') == "#ff785d"; + globalState.labelFormat = LeekFundConfig.getConfig('a-shares.labelFormat'); + globalState.deviceId = LeekFundConfig.getConfig('a-shares.deviceid', ""); + if (!globalState.deviceId) { + globalState.deviceId = 'VSCODE_' + Date.now().toString(36) + '_' + Math.random().toString(36).substr(2,9) + LeekFundConfig.setConfig('a-shares.deviceid', globalState.deviceId); + } + globalState.userId = LeekFundConfig.getConfig('a-shares.userid', ""); + globalState.userName = LeekFundConfig.getConfig('a-shares.username', ""); + globalState.userUrl = LeekFundConfig.getConfig('a-shares.userurl', ""); +} + +// This method is called when your extension is deactivated +export function deactivate() { + console.log('🐥deactivate'); + if (loopTimer) { + clearInterval(loopTimer); + loopTimer = undefined; + } +} + + +type Node = { key: string; value: string;}; \ No newline at end of file diff --git a/vscode/a-shares/src/globalState.ts b/vscode/a-shares/src/globalState.ts new file mode 100644 index 0000000..69971dd --- /dev/null +++ b/vscode/a-shares/src/globalState.ts @@ -0,0 +1,63 @@ +import { ExtensionContext } from 'vscode'; +import { DEFAULT_LABEL_FORMAT } from './shared/constant'; +import { Telemetry } from './shared/telemetry'; + +let deviceId : string = ''; +let userId : string = ''; +let userName : string = ''; +let userUrl : string = ''; + +let context: ExtensionContext = undefined as unknown as ExtensionContext; + +let telemetry: Telemetry | any = null; +let rg = true; + +let iconType = 'arrow'; + +let stocksRemind: Record = {}; +let newsIntervalTime = 20000; // 新闻刷新频率(毫秒) +let newsIntervalTimer: NodeJS.Timer | any = null; // 计算器控制 +let labelFormat = DEFAULT_LABEL_FORMAT; + + +let aStockCount = 0; +let usStockCount = 0; +let hkStockCount = 0; +let noDataStockCount = 0; +let isHolidayChina = false; // 初始化状态,默认是false,免得API有问题,就当它不是好了,可以继续运行 + +let showStockErrorInfo = true; // 控制只显示一次错误弹窗(临时处理) + +let isDevelopment = false; // 是否开发环境 + + +let stockPrice = {}; // 缓存数据 +let stockPriceCacheDate = '2020-10-30'; +export default { + context, + telemetry, + iconType, + deviceId, + userId, + userName, + userUrl, + newsIntervalTime, + newsIntervalTimer, + aStockCount, + usStockCount, + hkStockCount, + noDataStockCount, + /** + * 当天是否中国节假日(在插件启动时获取) + */ + isHolidayChina, + stocksRemind, + labelFormat, + showStockErrorInfo, + isDevelopment, + + stockPrice, + stockPriceCacheDate, + rg, + +}; diff --git a/vscode/a-shares/src/registerCommand.ts b/vscode/a-shares/src/registerCommand.ts new file mode 100644 index 0000000..d389d2f --- /dev/null +++ b/vscode/a-shares/src/registerCommand.ts @@ -0,0 +1,298 @@ +import { commands, ExtensionContext, window } from 'vscode'; +// import { FundProvider } from './explorer/fundProvider'; +import { StockProvider } from './explorer/stockProvider'; +import StockService from './explorer/stockService'; +// import globalState from './globalState'; +import { LeekFundConfig } from './shared/leekConfig'; +// import { LeekTreeItem } from './shared/leekTreeItem'; +// import checkForUpdate from './shared/update'; +// import { colorOptionList, randomColor } from './shared/utils'; +// import donate from './webview/donate'; +// import leekCenterView from './webview/leekCenterView'; + +import stockTrend, { RegisterCallFunc } from './webview/stockTrend'; +import UserViewProvider from './explorer/userService'; +import { MegaProvider } from './explorer/megaService'; +import globalState from './globalState'; +import Axios from 'axios'; +import { StatusBar } from './statusbar/statusBar'; +import { LeekTreeItem } from './shared/leekTreeItem'; +import ChatViewProvider from './explorer/chatService'; +// import tucaoForum from './webview/tucaoForum'; +// import { StatusBar } from './statusbar/statusBar'; +let codes = ""; +let yjzd = false; + + +export function registerViewEvent( + context: ExtensionContext, + stockService: StockService, + userService: UserViewProvider, + chatService: ChatViewProvider, + stockProvider: StockProvider, + megaTreeProvider: MegaProvider, +) { + + // Stock operation + commands.registerCommand('a-shares.clearStock', () => { + LeekFundConfig.clearStock('sh000001', () => { + stockProvider.refresh(); + + }); + }); + + // Stock operation + commands.registerCommand('a-shares.refreshStock', () => { + stockProvider.refresh(); + const handler = window.setStatusBarMessage(`股票数据已刷新`); + setTimeout(() => { + handler.dispose(); + }, 1000); + }); + + + // 一键诊断 + commands.registerCommand('a-shares.yjzd', async () => { + const userId = globalState.userId; + if(userId.length === 0) { + window.setStatusBarMessage(`股票数据已刷新`); + return; + } + yjzd = !yjzd; + if (yjzd) { + codes = LeekFundConfig.getConfig('a-shares.stocks'); + const res = await Axios.post('https://www.xxxxxx.cn/shares/api/v1/shares.yjzd_code',{codes: codes}, + { + headers : { + 'Content-Type': 'application/json', + 'user-token': userId + } + } + ); + if (res.status === 200) { + const data = res.data; + if (data.state === true) { + const data1 = data.data; + let coodes = "" + for(let i=0;i { + stockProvider.refresh(); + }); + const handler = window.setStatusBarMessage(`一键诊断已完成`); + setTimeout(() => { + handler.dispose(); + }, 1000); + }else { + const handler = window.setStatusBarMessage(data.error); + setTimeout(() => { + handler.dispose(); + }, 1000); + } + } + + }else { + let coodes = "" + for(let i=0;i { + stockProvider.refresh(); + }); + } + }); + + // 删除股票 + commands.registerCommand('a-shares.deleteStock', (target) => { + LeekFundConfig.removeStockCfg(target.id, () => { + stockProvider.refresh(); + }); + }); + // 删除股票 + commands.registerCommand('a-shares.deleteStockOnline', (target) => { + LeekFundConfig.removeStockCfg(target.id, () => { + stockProvider.refresh(); + const userId = globalState.userId; + if(userId.length === 0) { + window.showErrorMessage("请先扫码登录登录"); + return; + } + Axios.post('https://www.xxxxxx.cn/shares/api/v1/shares.delete_my_code',{ code: target.id}, + { + headers : { + 'Content-Type': 'application/json', + 'user-token': userId + } + } + ); + }); + }); + commands.registerCommand('a-shares.addStockToBar', (target) => { + LeekFundConfig.addStockToBarCfg(target.id, () => { + stockProvider.refresh(); + }); + }); + commands.registerCommand('a-shares.leekCenterView', () => { + if (stockService.stockList.length === 0 ) { + window.showWarningMessage('数据刷新中,请稍候!'); + return; + } + //leekCenterView(stockService); + }); + + commands.registerCommand('a-shares.addStock', () => { + // vscode QuickPick 不支持动态查询,只能用此方式解决 + // https://github.com/microsoft/vscode/issues/23633 + const qp = window.createQuickPick(); + qp.items = [{ label: '请输入关键词查询,如:0000001 或 上证指数 或者拼音首字母' }]; + let code: string | undefined; + let timer: NodeJS.Timeout | undefined; + qp.onDidChangeValue((value) => { + qp.busy = true; + if (timer) { + clearTimeout(timer); + timer = undefined; + } + timer = setTimeout(async () => { + const res = await stockService.getStockSuggestList(value); + qp.items = res; + qp.busy = false; + }, 100); // 简单防抖 + }); + qp.onDidChangeSelection((e) => { + if (e[0].description) { + code = e[0].label && e[0].label.split(' | ')[0]; + } + }); + qp.show(); + qp.onDidAccept(() => { + if (!code) { + return; + } + // 存储到配置的时候是接口的参数格式,接口请求时不需要再转换 + const newCode = code.replace('gb', 'gb_').replace('us', 'usr_'); + LeekFundConfig.updateStockCfg(newCode, () => { + stockProvider.refresh(); + }); + + + qp.hide(); + qp.dispose(); + }); + }); + + commands.registerCommand('a-shares.sortStock', () => { + stockProvider.changeOrder(); + stockProvider.refresh(); + }); + + /** + * WebView + */ + // 股票点击 + context.subscriptions.push( + commands.registerCommand('a-shares.stockItemClick', (code, name, text, stockCode) => + stockTrend(code, name, stockCode) + ) + ); + // 状态栏 + context.subscriptions.push( + commands.registerCommand('a-shares.changeStatusBarItem', (stockId) => { + const stockList = stockService.stockList; + const stockNameList = stockList + .filter((stock) => stock.id !== stockId) + .map((item: LeekTreeItem) => { + return { + label: `${item.info.name}`, + description: `${item.info.code}`, + }; + }); + stockNameList.unshift({ + label: `删除`, + description: `-1`, + }); + window + .showQuickPick(stockNameList, { + placeHolder: '更换状态栏个股', + }) + .then((res) => { + if (!res) return; + const statusBarStocks = LeekFundConfig.getConfig('a-shares.statusBarStock'); + const newCfg = [...statusBarStocks]; + const newStockId = res.description; + const index = newCfg.indexOf(stockId); + if (newStockId === '-1') { + if (index > -1) { + newCfg.splice(index, 1); + } + } else { + if (statusBarStocks.includes(newStockId)) { + window.showWarningMessage(`「${res.label}」已在状态栏`); + return; + } + if (index > -1) { + newCfg[index] = res.description; + } + } + LeekFundConfig.updateStatusBarStockCfg(newCfg, () => { + const handler = window.setStatusBarMessage(`下次数据刷新见效`); + setTimeout(() => { + handler.dispose(); + }, 1500); + }); + }); + }) + ); + + // 股票置顶 + commands.registerCommand('a-shares.setStockTop', (target) => { + LeekFundConfig.setStockTopCfg(target.id, () => { + // fundProvider.refresh(); + //stockProvider.refresh() + }); + }); + // 股票上移 + commands.registerCommand('a-shares.setStockUp', (target) => { + LeekFundConfig.setStockUpCfg(target.id, () => { + // fundProvider.refresh(); + // stockProvider.refresh() + }); + }); + // 股票下移 + commands.registerCommand('a-shares.setStockDown', (target) => { + LeekFundConfig.setStockDownCfg(target.id, () => { + // fundProvider.refresh(); + //stockProvider.refresh() + }); + }); + + context.subscriptions.push( + commands.registerCommand('a-shares.clearUser', () => { + userService.clearUser(); + chatService.clearUser(); + })); + + context.subscriptions.push( + commands.registerCommand('a-shares.updateCode', () => { + userService.updateCode(stockProvider); + chatService.updateCode(stockProvider); + })); + RegisterCallFunc(()=>{ + userService.updateCode(stockProvider); + chatService.updateCode(stockProvider); + }); + // 监听节点被点击的事件 + context.subscriptions.push(commands.registerCommand('mega.itemClick', (item) => { + megaTreeProvider.onDidClickTreeItem(item); + })); +} + +export function registerCommandPaletteEvent(context: ExtensionContext, statusbar: StatusBar) { + context.subscriptions.push( + commands.registerCommand('a-shares.toggleStatusBarVisibility', () => { + statusbar.toggleVisibility(); + }) + ); +} diff --git a/vscode/a-shares/src/shared/constant.ts b/vscode/a-shares/src/shared/constant.ts new file mode 100644 index 0000000..59965f9 --- /dev/null +++ b/vscode/a-shares/src/shared/constant.ts @@ -0,0 +1,24 @@ +/** + * 默认模板格式 + */ +export const DEFAULT_LABEL_FORMAT = { + statusBarLabelFormat: '「${name}」${price} ${icon}(${percent})', + sidebarStockLabelFormat: + '${icon|padRight|0}${percent|padRight|8} ${price|padRight|8} ${name}', + sidebarFundLabelFormat: '${icon|padRight|4}${percent|padRight}「${name}」${earnings} ${time}', + sidebarForexLabelFormat: + '「${name}」 现汇:${spotBuyPrice|padRight|6} / ${spotSellPrice|padRight|6} 现钞:${cashBuyPrice|padRight|6} / ${cashSellPrice|padRight|6}', +}; + +// /** +// * 提示语 +// * TODO: 丰富模板 +// */ +// export const TIPS_LOSE = [ +// '今晚吃面🍜!', +// '关灯吃面🍜!', +// '稳住,我们能赢!', +// '在A股,稳住才会有收益!', +// '投资其实就是一次心态修炼,稳住心态长期投资都会有收益的!', +// ]; +// export const TIPS_WIN = ['喝汤吃肉!', '吃鸡腿🍗!', '好起来了!', '祝老板吃肉!']; diff --git a/vscode/a-shares/src/shared/leekConfig.ts b/vscode/a-shares/src/shared/leekConfig.ts new file mode 100644 index 0000000..363344a --- /dev/null +++ b/vscode/a-shares/src/shared/leekConfig.ts @@ -0,0 +1,296 @@ +/*-------------------------------------------------------------- + * Copyright (c) Nickbing Lao. All rights reserved. + * Licensed under the MIT License. + * Github: https://github.com/giscafer + *-------------------------------------------------------------*/ + +import { window, workspace } from 'vscode'; +import globalState from '../globalState'; +import { clean, uniq, events } from './utils'; + +export class BaseConfig { + static getConfig(key: string, defaultValue?: any): any { + const config = workspace.getConfiguration(); + const value = config.get(key); + return value === undefined ? defaultValue : value; + } + + static setConfig(cfgKey: string, cfgValue: Array | string | number | Object) { + events.emit('updateConfig:' + cfgKey, cfgValue); + const config = workspace.getConfiguration(); + return config.update(cfgKey, cfgValue, true); + } + + static updateConfig(cfgKey: string, codes: Array) { + const config = workspace.getConfiguration(); + const sourceCfg = config.get(cfgKey, []); + + const newCfg = sourceCfg.filter((item) => !codes.includes(item)); + + const updatedCfg = [...newCfg, ...codes]; + let newCodes = clean(updatedCfg); + newCodes = uniq(newCodes); + return config.update(cfgKey, newCodes, true); + } + + static clearConfig(cfgKey: string, codes: Array) { + const config = workspace.getConfiguration(); + + return config.update(cfgKey, codes, true); + } + + static removeConfig(cfgKey: string, code: string) { + const config = workspace.getConfiguration(); + const sourceCfg = config.get(cfgKey, []); + const newCfg = sourceCfg.filter((item) => item !== code); + if(sourceCfg.length === newCfg.length){ + window.showInformationMessage(`删除期货不成功。请 [点击此处](https://github.com/LeekHub/a-shares/issues/281) 查看期货相关问题`); + } + return config.update(cfgKey, newCfg, true); + } +} + +export class LeekFundConfig extends BaseConfig { + constructor() { + super(); + } + + // Stock Begin + static clearStock(codes: string, cb?: Function) { + this.clearConfig('a-shares.stocks',codes.split(',')).then(() => { + window.showInformationMessage(`数据已清空.`); + if (cb && typeof cb === 'function') { + cb(codes); + } + }); + } + + // Stock Begin + static updateStockCfg(codes: string, cb?: Function) { + this.updateConfig('a-shares.stocks', codes.split(',')).then(() => { + window.showInformationMessage(`Stock Successfully add.`); + if (cb && typeof cb === 'function') { + cb(codes); + } + }); + } + + // Stock Begin + static replaceStockCfg(codes: string, cb?: Function) { + this.clearConfig('a-shares.stocks', codes.split(',')).then(() => { + window.showInformationMessage(`Stock Successfully add.`); + if (cb && typeof cb === 'function') { + cb(codes); + } + }); + } + + static removeStockCfg(code: string, cb?: Function) { + this.removeConfig('a-shares.stocks', code).then(() => { + window.showInformationMessage(`Stock Successfully delete.`); + if (cb && typeof cb === 'function') { + cb(code); + } + }); + } + + static addStockToBarCfg(code: string, cb?: Function) { + const addStockToBar = () => { + let configArr: string[] = this.getConfig('a-shares.statusBarStock'); + if (configArr.length >= 4) { + window.showInformationMessage(`StatusBar Exceeding Length.`); + if (cb && typeof cb === 'function') { + cb(code); + } + } else if (configArr.includes(code)) { + window.showInformationMessage(`StatusBar Already Have.`); + if (cb && typeof cb === 'function') { + cb(code); + } + } else { + configArr.push(code); + this.setConfig('a-shares.statusBarStock', configArr).then(() => { + window.showInformationMessage(`Stock Successfully add to statusBar.`); + if (cb && typeof cb === 'function') { + cb(code); + } + }); + } + }; + + if (this.getConfig('a-shares.hideStatusBarStock')) { + this.setConfig('a-shares.hideStatusBarStock', false).then(() => { + addStockToBar(); + }); + } else { + addStockToBar(); + } + } + + static setStockTopCfg(code: string, cb?: Function) { + let configArr: string[] = this.getConfig('a-shares.stocks'); + + configArr = [code, ...configArr.filter((item) => item !== code)]; + + this.setConfig('a-shares.stocks', configArr).then(() => { + window.showInformationMessage(`Stock successfully set to top.`); + if (cb && typeof cb === 'function') { + cb(code); + } + }); + } + + static setStockUpCfg(code: string, cb?: Function) { + const callback = () => { + window.showInformationMessage(`Stock successfully move up.`); + if (cb && typeof cb === 'function') { + cb(code); + } + }; + + let configArr: string[] = this.getConfig('a-shares.stocks'); + const currentIndex = configArr.indexOf(code); + let previousIndex = currentIndex - 1; + // 找到前一个同市场的股票 + for (let index = currentIndex - 1; index >= 0; index--) { + const previousCode = configArr[index]; + if (/^(sh|sz|bj)/.test(code) && /^(sh|sz|bj)/.test(previousCode)) { + previousIndex = index; + break; + } + if (/^(hk)/.test(code) && /^(hk)/.test(previousCode)) { + previousIndex = index; + break; + } + if (/^(usr_)/.test(code) && /^(usr_)/.test(previousCode)) { + previousIndex = index; + break; + } + if (/^(nf_)/.test(code) && /^(nf_)/.test(previousCode)) { + previousIndex = index; + break; + } + if (/^(hf_)/.test(code) && /^(hf_)/.test(previousCode)) { + previousIndex = index; + break; + } + } + if (previousIndex < 0) { + callback(); + } else { + // 交换位置 + configArr[currentIndex] = configArr.splice(previousIndex, 1, configArr[currentIndex])[0]; + this.setConfig('a-shares.stocks', configArr).then(() => { + callback(); + }); + } + } + + static setStockDownCfg(code: string, cb?: Function) { + const callback = () => { + window.showInformationMessage(`Stock successfully move down.`); + if (cb && typeof cb === 'function') { + cb(code); + } + }; + + let configArr: string[] = this.getConfig('a-shares.stocks'); + const currentIndex = configArr.indexOf(code); + let nextIndex = currentIndex + 1; + //找到后一个同市场的股票 + for (let index = currentIndex + 1; index < configArr.length; index++) { + const nextCode = configArr[index]; + if (/^(sh|sz|bj)/.test(code) && /^(sh|sz|bj)/.test(nextCode)) { + nextIndex = index; + break; + } + if (/^(hk)/.test(code) && /^(hk)/.test(nextCode)) { + nextIndex = index; + break; + } + if (/^(usr_)/.test(code) && /^(usr_)/.test(nextCode)) { + nextIndex = index; + break; + } + if (/^(nf_)/.test(code) && /^(nf_)/.test(nextCode)) { + nextIndex = index; + break; + } + if (/^(hf_)/.test(code) && /^(hf_)/.test(nextCode)) { + nextIndex = index; + break; + } + } + if (nextIndex >= configArr.length) { + callback(); + } else { + // 交换位置 + configArr[currentIndex] = configArr.splice(nextIndex, 1, configArr[currentIndex])[0]; + this.setConfig('a-shares.stocks', configArr).then(() => { + callback(); + }); + } + } + + // Stock End + + // Binance Begin + static updateBinanceCfg(codes: string, cb?: Function) { + this.updateConfig('a-shares.binance', codes.split(',')).then(() => { + window.showInformationMessage(`Pair Successfully add.`); + if (cb && typeof cb === 'function') { + cb(codes); + } + }); + } + static removeBinanceCfg(code: string, cb?: Function) { + this.removeConfig('a-shares.binance', code).then(() => { + window.showInformationMessage(`Pair Successfully delete.`); + if (cb && typeof cb === 'function') { + cb(code); + } + }); + } + static setBinanceTopCfg(code: string, cb?: Function) { + let configArr: string[] = this.getConfig('a-shares.binance'); + configArr = [code, ...configArr.filter((item) => item !== code)]; + this.setConfig('a-shares.binance', configArr).then(() => { + window.showInformationMessage(`Pair successfully set to top.`); + if (cb && typeof cb === 'function') { + cb(code); + } + }); + } + // Binance end + + // StatusBar Begin + static updateStatusBarStockCfg(codes: Array, cb?: Function) { + const updateStatusBarStock = () => { + this.setConfig('a-shares.statusBarStock', codes).then(() => { + window.showInformationMessage(`Status Bar Stock Successfully update.`); + if (cb && typeof cb === 'function') { + cb(codes); + } + }); + }; + + if (codes.length) { + if (this.getConfig('a-shares.hideStatusBarStock')) { + this.setConfig('a-shares.hideStatusBarStock', false).then(() => { + updateStatusBarStock(); + }); + } else { + updateStatusBarStock(); + } + } else { + if (!this.getConfig('a-shares.hideStatusBarStock')) { + this.setConfig('a-shares.hideStatusBarStock', true).then(() => { + updateStatusBarStock(); + }); + } else { + updateStatusBarStock(); + } + } + } + // StatusBar End +} diff --git a/vscode/a-shares/src/shared/leekTreeItem.ts b/vscode/a-shares/src/shared/leekTreeItem.ts new file mode 100644 index 0000000..65c0d97 --- /dev/null +++ b/vscode/a-shares/src/shared/leekTreeItem.ts @@ -0,0 +1,219 @@ +import { join } from 'path'; +import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; +import globalState from '../globalState'; +import { DEFAULT_LABEL_FORMAT } from './constant'; +import { FundInfo, IconType, TreeItemType } from './typed'; +import { formatLabelString, formatTreeText } from './utils'; + +export class LeekTreeItem extends TreeItem { + info: FundInfo; + type: string | undefined; + isCategory: boolean; + contextValue: string | undefined; + _itemType?: TreeItemType; + + constructor(info: FundInfo, context: ExtensionContext | undefined, isCategory = false) { + super('', TreeItemCollapsibleState.None); + this.info = info; + this.isCategory = isCategory; + const { + showLabel, + isStock, + name, + code, + type, + symbol, + percent, + price, + open, + yestclose, + high, + low, + updown, + volume, + amount = 0, + earnings, + // priceDate, + time, + isStop, + t2, + contextValue, + _itemType, + spotBuyPrice = 0, + spotSellPrice = 0, + cashBuyPrice = 0, + cashSellPrice = 0, + conversionPrice = 0, + publishDateTime = '', + heldAmount = 0, + heldPrice = 0, + } = info; + + if (_itemType) { + this._itemType = _itemType; + } else { + this._itemType = isStock ? TreeItemType.STOCK : TreeItemType.FUND; + } + + this.type = type; + this.contextValue = contextValue; + let _percent: number | string = Math.abs(percent); + if (isNaN(_percent)) { + _percent = '--'; + } else { + _percent = _percent.toFixed(2); + } + let icon = 'up'; + const grow = percent?.indexOf('-') === 0 ? false : true; + const val = Math.abs(percent); + if (grow) {// > 0 + if (globalState.rg) { + icon = val >= 2 ? 'up' : 'up1'; + } else { + icon = val >= 2 ? 'up2' : 'up3'; + } + _percent = '+' + _percent; + } else { + if (globalState.rg) { + icon = val >= 2 ? 'down' : 'down1'; + } else { + icon = val >= 2 ? 'down2' : 'down3'; + } + _percent = '-' + _percent; + } + if (isStop) { + icon = 'stop'; + } + let iconPath: string | undefined = ''; + if (showLabel) { + iconPath = + globalState.iconType !== IconType.ICON_FOOD && globalState.iconType !== IconType.NONE + ? context?.asAbsolutePath(join('images', `${icon}.svg`)) + : icon; + } + const isIconPath = iconPath?.lastIndexOf('.svg') !== -1; + if (isIconPath && type !== 'nodata') { + this.iconPath = iconPath; + } + let text = ''; + + if (showLabel) { + /* `showLabel: true` */ + if (this._itemType === TreeItemType.STOCK) { + const risePercent = isStop ? '停牌' : `${_percent}%`; + if (type === 'nodata') { + text = info.name; + } else { + /* text = `${!isIconPath ? iconPath : ''}${risePercent}${formatTreeText( + price, + 15 + )}「${name}」`; */ + text = formatLabelString( + globalState.labelFormat?.['sidebarStockLabelFormat'] ?? + DEFAULT_LABEL_FORMAT.sidebarStockLabelFormat, + { + ...info, + icon: !isIconPath ? iconPath : '', + percent: risePercent, + } + ); + } + } else if (this._itemType === TreeItemType.FUND) { + /* text = + `${!isIconPath ? iconPath : ''}${formatTreeText(`${_percent}%`)}「${name}」${ + t2 || !(globalState.showEarnings && amount > 0) + ? '' + : `(${grow ? '盈' : '亏'}:${grow ? '+' : ''}${earnings})` + }` + `${t2 ? `(${time})` : ''}`; */ + text = formatLabelString( + globalState.labelFormat?.['sidebarFundLabelFormat'] ?? + DEFAULT_LABEL_FORMAT.sidebarFundLabelFormat, + { + ...info, + icon: !isIconPath ? iconPath : '', + percent: `${_percent}%`, + earnings: + t2 || !(Number(amount) > 0) + ? '' + : `(${grow ? '盈' : '亏'}:${grow ? '+' : ''}${earnings})`, + time: t2 ? `(${time})` : '', + } + ); + // ${earningPercent !== 0 ? ',率:' + earningPercent + '%' : ''} + } else if (this._itemType === TreeItemType.FOREX) { + text = formatLabelString( + globalState.labelFormat?.['sidebarForexLabelFormat'] ?? + DEFAULT_LABEL_FORMAT.sidebarForexLabelFormat, + { + ...info, + } + ); + } + } else { + /* `showLabel: false` */ + text = (this._itemType === TreeItemType.STOCK) + ? `${formatTreeText(`${_percent}%`, 11)}${formatTreeText(price, 15)} 「${code}」` + : `${formatTreeText(`${_percent}%`)}「${code}」`; + } + if (heldAmount ) { + this.label = { + label: text, + highlights: [[0, text.length]], + }; + this.description = '(持仓)'; + } else { + this.label = text; + } + this.id = info.id || code; + + if (this._itemType === TreeItemType.STOCK || this._itemType === TreeItemType.FUND) { + let typeAndSymbol = `${type}${symbol}`; + const isFuture = /nf_/.test(code) || /hf_/.test(code); + if (isFuture) { + typeAndSymbol = code; + } + this.command = { + title: name, // 标题 + command: + this._itemType === TreeItemType.STOCK + ? 'a-shares.stockItemClick' + : 'a-shares.fundItemClick', // 命令 ID + arguments: [ + this._itemType === TreeItemType.STOCK ? '0' + symbol : code, // 基金/股票编码 + name, // 基金/股票名称 + text, + typeAndSymbol, + ], + }; + if (type === 'nodata') { + this.command.command = ''; + } + } + + if (this._itemType === TreeItemType.STOCK) { + const labelText = !showLabel ? name : ''; + + const isFuture = /nf_/.test(code) || /hf_/.test(code); + + // type字段:国内期货前缀 `nf_` 。股票的 type 是交易所 (sz,sh,bj) + const typeText = type; + const symbolText = isFuture ? name : symbol; + + if (type === 'nodata') { + this.tooltip = '接口不支持,右键删除关注'; + } else if (isFuture) { + this.tooltip = `【今日行情】${name} ${code}\n 涨跌:${updown} 百分比:${_percent}%\n 最高:${high} 最低:${low}\n 今开:${open} 昨结:${yestclose}\n 成交量:${volume} 成交额:${amount}`; + } else { + this.tooltip = `【今日行情】${labelText}${typeText}${symbolText}\n 涨跌:${updown} 百分比:${_percent}%\n 最高:${high} 最低:${low}\n 今开:${open} 昨收:${yestclose}\n 成交量:${volume} 成交额:${amount}\n ${ + heldAmount ? `持仓数:${volume} 持仓价:${heldPrice}` : '' + }`; + } + } else if (this._itemType === TreeItemType.BINANCE) { + this.tooltip = `【今日行情】${name}\n 涨跌:${updown} 百分比:${_percent}%\n 最高:${high} 最低:${low}\n 今开:${open} 昨收:${yestclose}\n 成交量:${volume} 成交额:${amount}`; + } else if (this._itemType === TreeItemType.FOREX) { + this.tooltip = `现汇买入价:${spotBuyPrice}\n现钞买入价:${cashBuyPrice}\n现汇卖出价:${spotSellPrice}\n现钞卖出价:${cashSellPrice}\n中行折算价:${conversionPrice}\n发布日期:${publishDateTime}`; + } else { + this.tooltip = `「${name}」(${code})`; + } + } +} diff --git a/vscode/a-shares/src/shared/telemetry.ts b/vscode/a-shares/src/shared/telemetry.ts new file mode 100644 index 0000000..5935405 --- /dev/null +++ b/vscode/a-shares/src/shared/telemetry.ts @@ -0,0 +1,88 @@ +import * as vscode from 'vscode'; +const os = require('os'); +const publicIp = require('public-ip'); +const Amplitude = require('amplitude'); + +export class Telemetry { + amplitude: any; + userId: string; + ip: string; + isTelemetryEnabled: boolean; + + constructor() { + this.userId = vscode.env.machineId; + this.isTelemetryEnabled = false; + this.ip = ''; + + this.getSettingFromConfig(); + this.setup(); + vscode.workspace.onDidChangeConfiguration(this.configurationChanged, this); + } + + async setup() { + if (!this.isTelemetryEnabled) { + return; + } + + if (this.amplitude) { + return; + } + + this.amplitude = new Amplitude('d3c2366d3c3e0712bdf2efdb3dd498c2'); + + let extension = vscode.extensions.getExtension('giscafer.a-shares'); + let extensionVersion = extension ? extension.packageJSON.version : ''; + + // Store + this.ip = await publicIp.v4(); + + // Amplitude + this.amplitude.identify({ + user_id: this.userId, + language: vscode.env.language, + platform: os.platform(), + app_version: extensionVersion, + ip: this.ip, + user_properties: { + vscodeSessionId: vscode.env.sessionId, + vscodeVersion: vscode.version, + }, + }); + } + + sendEvent(eventName: string, params?: any) { + if (!this.isTelemetryEnabled) { + return; + } + + /* let data = { + ...params, + distinct_id: this.userId, + ip: this.ip, + }; */ + + // Amplitude + this.amplitude.track({ + event_type: eventName, + event_properties: params, + user_id: this.userId, + ip: this.ip, + }); + } + + configurationChanged() { + // vscode.window.showInformationMessage('Updated'); + this.getSettingFromConfig(); + } + + private getSettingFromConfig() { + let config = vscode.workspace.getConfiguration('telemetry'); + if (config) { + let enableTelemetry = config.get('enableTelemetry'); + this.isTelemetryEnabled = !!enableTelemetry; + } + if (this.isTelemetryEnabled) { + this.setup(); + } + } +} diff --git a/vscode/a-shares/src/shared/typed.ts b/vscode/a-shares/src/shared/typed.ts new file mode 100644 index 0000000..523a199 --- /dev/null +++ b/vscode/a-shares/src/shared/typed.ts @@ -0,0 +1,112 @@ +// 支持的股票类型 +export const STOCK_TYPE = ['sh', 'sz', 'bj', 'hk', 'gb', 'us']; + +export enum SortType { + NORMAL = 0, // 基金默认顺序 + ASC = 1, // 涨跌升序 + DESC = -1, // 涨跌降序 + AMOUNTASC = 2, // 持仓金额升序 + AMOUNTDESC = -2, // 持仓金额降序 +} + +export enum IconType { + ARROW = 'arrow', + ARROW1 = 'arrow1', + FOOD1 = 'food1', + FOOD2 = 'food2', + FOOD3 = 'food3', + ICON_FOOD = 'iconfood', + NONE = 'none', +} + +/** Tree Item Type */ +export enum TreeItemType { + /** 基金 */ + FUND = 'fund', + /** 股票 */ + STOCK = 'stock', + /** 币安 */ + BINANCE = 'binance', + /** 外汇 */ + FOREX = 'forex', +} +export interface IAmount { + name: string; + price: number | string; + amount: number; + priceDate: string; + earnings: number; + unitPrice: number; + earningPercent: number; + yestEarnings?: number; +} + +export interface FundInfo { + percent: any; + yestpercent?: string; // 净值涨跌幅度 + name: string; + code: string; + showLabel?: boolean; + id?: string; + contextValue?: string; + symbol?: string; + type?: string; + yestclose?: string | number; // 昨日净值 + open?: string | number; + highStop?: string | number; + high?: string | number; + lowStop?: string | number; + low?: string | number; + time?: string; + updown?: string; // 涨跌值 price-yestclose + unitPrice?: number; // 成本价格 + priceDate?: string; // 价格日期 + yestPriceDate?: string; // 最新净值更新日期 + price?: string; // 当前价格 + volume?: string; // 成交量 + amount?: string | number; // 成交额 + earnings?: number; // 盈亏 + earningPercent?: number; // 盈亏率 + isStop?: boolean; // 停牌 + t2?: boolean; + isUpdated?: boolean; + showEarnings?: boolean; + isStock?: boolean; + _itemType?: TreeItemType; + spotBuyPrice?: number; // 现汇买入价 + cashBuyPrice?: number; // 现钞买入价 + spotSellPrice?: number; // 现汇卖出价 + cashSellPrice?: number; // 现钞卖出价 + conversionPrice?: number; // 中行折算价 + publishDateTime?: string; // 发布日期:年月日 时分秒 + publishTime?: string; // 发布时间:时分秒 + heldAmount?: number; // 持仓数 + heldPrice?: number; // 持仓价 +} + +export const defaultFundInfo: FundInfo = { + id: '', + name: '', + percent: '', + code: '', + showLabel: true, +}; + +export enum StockCategory { + A = 'A Stock', + US = 'US Stock', + HK = 'HK Stock', + NODATA = 'Not Support Stock', +} + +export interface ProfitStatusBarInfo { + fundProfit: number; + fundProfitPercent: number; + fundAmount: number; + priceDate: string; +} + +export type HeldData = { + heldAmount?: number; + heldPrice?: number; +}; diff --git a/vscode/a-shares/src/shared/utils.ts b/vscode/a-shares/src/shared/utils.ts new file mode 100644 index 0000000..a64b152 --- /dev/null +++ b/vscode/a-shares/src/shared/utils.ts @@ -0,0 +1,537 @@ +import { EventEmitter } from 'events'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { QuickPickItem, window } from 'vscode'; +import globalState from '../globalState'; +import { LeekFundConfig } from './leekConfig'; +import { LeekTreeItem } from './leekTreeItem'; +import { SortType, StockCategory } from './typed'; + +const stockTimes = allStockTimes(); + +const formatNum = (n: number) => { + const m = n.toString(); + return m[1] ? m : '0' + m; +}; + +export const objectToQueryString = (queryParameters: Object): string => { + return queryParameters + ? Object.entries(queryParameters).reduce((queryString, [key, val]) => { + const symbol = queryString.length === 0 ? '?' : '&'; + queryString += typeof val !== 'object' ? `${symbol}${key}=${val}` : ''; + return queryString; + }, '') + : ''; +}; + +export const formatDate = (val: Date | string | undefined, seperator = '-') => { + let date = new Date(); + if (typeof val === 'object') { + date = val; + } else { + date = new Date(val || ''); + } + const year = date.getFullYear(); + const month = date.getMonth() + 1; + const day = date.getDate(); + + return [year, month, day].map(formatNum).join(seperator); +}; + +// 时间格式化 +export const formatDateTime = (date: Date) => { + const year = date.getFullYear(); + const month = date.getMonth() + 1; + const day = date.getDate(); + const hour = date.getHours(); + const minute = date.getMinutes(); + const second = date.getSeconds(); + + return ( + [year, month, day].map(formatNum).join('-') + + ' ' + + [hour, minute, second].map(formatNum).join(':') + ); +}; + +/** + * 数组去重 + */ +export const uniq = (elements: Array) => { + if (!Array.isArray(elements)) { + return []; + } + + return elements.filter((element, index) => index === elements.indexOf(element)); +}; + +/** + * 清除数组里面的非法值 + */ +export const clean = (elements: Array) => { + if (!Array.isArray(elements)) { + return []; + } + + return elements.filter((element) => !!element); +}; + +/** + * toFixed 解决js精度问题,使用方式:toFixed(value) + * @param {Number | String} value + * @param {Number} precision 精度,默认2位小数,需要取整则传0 + * @param {Number} percent 倍增 + * 该方法会处理好以下这些问题 + * 1.12*100=112.00000000000001 + * 1.13*100=112.9999999999999 + * '0.015'.toFixed(2)结果位0.01 + * 1121.1/100 = 11.210999999999999 + */ +export const toFixed = (value = 0, precision = 2, percent = 1) => { + const num = Number(value); + if (Number.isNaN(num)) { + return 0; + } + if (num < Math.pow(-2, 31) || num > Math.pow(2, 31) - 1) { + return 0; + } + let newNum = value * percent; + // console.log(num, precision) + if (precision < 0 || typeof precision !== 'number') { + return newNum * percent; + } else if (precision > 0) { + newNum = Math.round(num * Math.pow(10, precision) * percent) / Math.pow(10, precision); + return newNum; + } + newNum = Math.round(num); + + return newNum; +}; + +export const calcFixedPriceNumber = ( + open: string, + yestclose: string, + price: string, + high: string, + low: string +): number => { + let reg = /0+$/g; + open = open.replace(reg, ''); + yestclose = yestclose.replace(reg, ''); + price = price.replace(reg, ''); + high = high.replace(reg, ''); + low = low.replace(reg, ''); + let o = open.indexOf('.') === -1 ? 0 : open.length - open.indexOf('.') - 1; + let yc = yestclose.indexOf('.') === -1 ? 0 : yestclose.length - yestclose.indexOf('.') - 1; + let p = price.indexOf('.') === -1 ? 0 : price.length - price.indexOf('.') - 1; + let h = high.indexOf('.') === -1 ? 0 : high.length - high.indexOf('.') - 1; + let l = low.indexOf('.') === -1 ? 0 : low.length - low.indexOf('.') - 1; + let max = Math.max(o, yc, p, h, l); + if (max > 3) { + max = 2; // 接口返回的指数数值的小数位为4,但习惯两位小数 + } + return max; +}; + +export const formatNumber = (val: number = 0, fixed: number = 2, format = true): string => { + const num = +val; + if (format) { + if (num > 1000 * 10000) { + return (num / (10000 * 10000)).toFixed(fixed) + '亿'; + } else if (num > 1000) { + return (num / 10000).toFixed(fixed) + '万'; + } + } + return `${num.toFixed(fixed)}`; +}; + +export const sortData = (data: LeekTreeItem[] = [], order = SortType.NORMAL) => { + if (order === SortType.ASC || order === SortType.DESC) { + return data.sort((a: any, b: any) => { + const aValue = +a.info.percent; + const bValue = +b.info.percent; + if (order === SortType.DESC) { + return aValue > bValue ? -1 : 1; + } else { + return aValue > bValue ? 1 : -1; + } + }); + } else if (order === SortType.AMOUNTASC || order === SortType.AMOUNTDESC) { + return data.sort((a: any, b: any) => { + const aValue = a.info.amount - 0; + const bValue = b.info.amount - 0; + if (order === SortType.AMOUNTDESC) { + return aValue > bValue ? -1 : 1; + } else { + return aValue > bValue ? 1 : -1; + } + }); + } else { + return data; + } +}; + +export const formatTreeText = (text = '', num = 10): string => { + const str = text + ''; + const lenx = Math.max(num - str.length, 0); + return str + ' '.repeat(lenx); +}; + +export const caculateEarnings = (money: number, price: number, currentPrice: number): number => { + if (Number(currentPrice) > 0) { + return (money / price) * currentPrice - money; + } else { + return 0; + } +}; + +export const colorOptionList = (): QuickPickItem[] => { + const list = [ + { + label: '🔴Red Color', + description: 'red', + }, + { + label: '💹Green Color', + description: 'green', + }, + { + label: '⚪White Color', + description: 'white', + }, + { + label: '⚫Black Color', + description: 'black', + }, + { + label: '🌕Yellow Color', + description: 'black', + }, + { + label: '🔵Blue Color', + description: 'blue', + }, + { + label: 'Gray Color', + description: '#888888', + }, + { + label: 'Random Color', + description: 'random', + }, + ]; + return list; +}; + +export const randomColor = (): string => { + const colors = [ + '#E74B84', + '#11FB23', + '#F79ADA', + '#C9AD06', + '#82D3A6', + '#C6320D', + '#83C06A', + '#54A0EB', + '#85AB66', + '#53192F', + '#6CD2D7', + '#6C6725', + '#7B208B', + '#B832A5', + '#C1FDCD', + ]; + + const num = Math.ceil(Math.random() * 10); + return colors[num]; +}; + +export const randHeader = () => { + const head_connection = ['Keep-Alive', 'close']; + const head_accept = ['text/html, application/xhtml+xml, */*']; + const head_accept_language = [ + 'zh-CN,fr-FR;q=0.5', + 'en-US,en;q=0.8,zh-Hans-CN;q=0.5,zh-Hans;q=0.3', + ]; + const head_user_agent = [ + 'Opera/8.0 (Macintosh; PPC Mac OS X; U; en)', + 'Opera/9.27 (Windows NT 5.2; U; zh-cn)', + 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Win64; x64; Trident/4.0)', + 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0)', + 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; InfoPath.2; .NET4.0C; .NET4.0E)', + 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; InfoPath.2; .NET4.0C; .NET4.0E; QQBrowser/7.3.9825.400)', + 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0; BIDUBrowser 2.x)', + 'Mozilla/5.0 (Windows; U; Windows NT 5.1) Gecko/20070309 Firefox/2.0.0.3', + 'Mozilla/5.0 (Windows; U; Windows NT 5.1) Gecko/20070803 Firefox/1.5.0.12', + 'Mozilla/5.0 (Windows; U; Windows NT 5.2) Gecko/2008070208 Firefox/3.0.1', + 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.12) Gecko/20080219 Firefox/2.0.0.12 Navigator/9.0.0.6', + 'Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.95 Safari/537.36', + 'Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; rv:11.0) like Gecko)', + 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20100101 Firefox/21.0 ', + 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Maxthon/4.0.6.2000 Chrome/26.0.1410.43 Safari/537.1 ', + 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.92 Safari/537.1 LBBROWSER', + 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36', + 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.11 TaoBrowser/3.0 Safari/536.11', + 'Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; rv:11.0) like Gecko', + 'Mozilla/5.0 (Macintosh; PPC Mac OS X; U; en) Opera 8.0', + ]; + const result = { + Connection: head_connection[0], + Accept: head_accept[0], + 'Accept-Language': head_accept_language[1], + 'User-Agent': head_user_agent[Math.floor(Math.random() * 10)], + }; + return result; +}; + +/** + * 判断是否周未的方法 + * @param {*} date 参与判断的日期,默认今天 + */ +export const isWeekend = (date: Date = new Date()) => { + let tof = false; + let dayOfWeek = date.getDay(); + + tof = dayOfWeek === 6 || dayOfWeek === 0; + + return tof; +}; + +export const isStockTime = () => { + const markets = allMarkets(); + const date = new Date(); + const hours = date.getHours(); + const minus = date.getMinutes(); + const delay = 5; + for (let i = 0; i < markets.length; i++) { + let stockTime = stockTimes.get(markets[i]); + if (!stockTime || stockTime.length < 2 || isHoliday(markets[i])) { + continue; + } + // 针对美股交易时间跨越北京时间0点 + if (stockTime[0] > stockTime[1]) { + if ( + hours >= stockTime[0] || + hours < stockTime[1] || + (hours === stockTime[1] && minus <= delay) + ) { + return true; + } + } else { + if ( + (hours >= stockTime[0] && hours < stockTime[1]) || + (hours === stockTime[1] && minus <= delay) + ) { + return true; + } + } + } + return false; +}; + +export function allMarkets(): Array { + let result: Array = []; + const funds: Array = LeekFundConfig.getConfig('a-shares.funds'); + if (funds && funds.length > 0) { + // 针对只配置基金的用户,默认增加A股交易时间 + result.push(StockCategory.A); + } + + const stocks: Array = LeekFundConfig.getConfig('a-shares.stocks'); + stocks.forEach((item: string) => { + let market = StockCategory.NODATA; + if (/^(sh|sz|bj)/.test(item)) { + market = StockCategory.A; + } else if (/^(hk)/.test(item)) { + market = StockCategory.HK; + } else if (/^(usr_)/.test(item)) { + market = StockCategory.US; + } + if (!result.includes(market)) { + result.push(market); + } + }); + return result; +} + +export function allStockTimes(): Map> { + let stocks = new Map>(); + stocks.set(StockCategory.A, [9, 15]); + stocks.set(StockCategory.HK, [9, 16]); + // TODO: 判断夏令时,夏令时交易时间为[21, 4],非夏令时交易时间为[22, 5] + stocks.set(StockCategory.US, [21, 5]); + return stocks; +} + +export function allHolidays(): Map> { + // https://websys.fsit.com.tw/FubonETF/Top/Holiday.aspx + // 假日日期格式为yyyyMMdd + let days = new Map>(); + const A = []; + if (globalState.isHolidayChina) { + A.push(formatDate(new Date(), '')); + } + // https://www.hkex.com.hk/-/media/HKEX-Market/Services/Circulars-and-Notices/Participant-and-Members-Circulars/SEHK/2020/ce_SEHK_CT_038_2020.pdf + const HK = [ + '20201225', + '20210101', + '20210212', + '20210215', + '20210402', + '20210405', + '20210406', + '20210519', + '20210614', + '20210701', + '20210922', + '20211001', + '20211014', + '20211227', + ]; + // https://www.nyse.com/markets/hours-calendars + const US = [ + '20201225', + '20210101', + '20210118', + '20210215', + '20210402', + '20210531', + '20210705', + '20210906', + '20211125', + '20211224', + '20220117', + '20220221', + '20220415', + '20220530', + '20220704', + '20220905', + '20221124', + '20221226', + ]; + days.set(StockCategory.A, A); + days.set(StockCategory.HK, HK); + days.set(StockCategory.US, US); + return days; +} + +export function timezoneDate(timezone: number): Date { + const date = new Date(); + const diff = date.getTimezoneOffset(); // 分钟差 + const gmt = date.getTime() + diff * 60 * 1000; + let nydate = new Date(gmt + timezone * 60 * 60 * 1000); + return nydate; +} + +export function isHoliday(market: string): boolean { + let date = new Date(); + if (market === StockCategory.US) { + date = timezoneDate(-5); + } + + const holidays = allHolidays(); + if (isWeekend(date) || holidays.get(market)?.includes(formatDate(date, ''))) { + return true; + } + return false; +} + +function isRemoteLink(link: string) { + return /^(https?|vscode-webview-resource|javascript):/.test(link); +} + +export function formatHTMLWebviewResourcesUrl( + html: string, + conversionUrlFn: (link: string) => string +) { + const LinkRegexp = /\s?(?:src|href)=('|")(.*?)\1/gi; + let matcher = LinkRegexp.exec(html); + + while (matcher) { + const origin = matcher[0]; + const originLen = origin.length; + const link = matcher[2]; + if (!isRemoteLink(link)) { + let resourceLink = link; + try { + resourceLink = conversionUrlFn(link); + html = + html.substring(0, matcher.index) + + origin.replace(link, resourceLink) + + html.substring(matcher.index + originLen); + } catch (err) { + console.error(err); + } + } + matcher = LinkRegexp.exec(html); + } + return html; +} + +export function getTemplateFileContent(tplPaths: string | string[], webview: vscode.Webview) { + if (!Array.isArray(tplPaths)) { + tplPaths = [tplPaths]; + } + const tplPath = path.join(globalState.context.extensionPath, 'template', ...tplPaths); + const html = fs.readFileSync(tplPath, 'utf-8'); + const extensionUri = globalState.context.extensionUri; + const dirUri = tplPaths.slice(0, -1).join('/'); + return formatHTMLWebviewResourcesUrl(html, (link) => { + return webview + .asWebviewUri(vscode.Uri.parse([extensionUri, 'template', dirUri, link].join('/'))) + .toString(); + }); +} + +export function multi1000(n: number) { + return Math.ceil(n * 1000); +} + +export const events = new EventEmitter(); + +export function formatLabelString(str: string, params: Record) { + try { + str = str.replace(/\$\{(.*?)\}/gi, function (_, $1) { + const formatMatch = /(.*?)\s*\|\s*padRight\s*(\|\s*(\d+))?/gi.exec($1); + + if (formatMatch) { + return formatTreeText( + params[formatMatch[1]], + formatMatch[3] ? parseInt(formatMatch[3]) : undefined + ); + } else { + return String(params[$1]); + } + }); + } catch (err) { + // @ts-ignore + window.showErrorMessage(`fail: Label format Error, ${str};\n${err.message}`); + return '模板格式错误!'; + } + return str; +} + +export function getWebviewResourcesUrl( + webview: vscode.Webview, + args: string[], + _extensionUri: vscode.Uri = globalState.context.extensionUri +) { + return args.map((arg) => { + return webview.asWebviewUri( + vscode.Uri.parse([_extensionUri.toString(), 'template', arg].join('/')) + ); + }); +} + +export function getResourcesImageSrc( + webview: vscode.Webview, + args: string[], + _extensionUri: vscode.Uri = globalState.context.extensionUri +) { + return args.map((arg) => { + return webview.asWebviewUri( + vscode.Uri.parse([_extensionUri.toString(), 'images', 'images', arg].join('/')) + ); + }); +} diff --git a/vscode/a-shares/src/statusbar/statusBar.ts b/vscode/a-shares/src/statusbar/statusBar.ts new file mode 100644 index 0000000..ad8384c --- /dev/null +++ b/vscode/a-shares/src/statusbar/statusBar.ts @@ -0,0 +1,133 @@ +import { StatusBarAlignment, StatusBarItem, window } from 'vscode'; +import StockService from '../explorer/stockService'; +import globalState from '../globalState'; +import { DEFAULT_LABEL_FORMAT } from '../shared/constant'; +import { LeekFundConfig } from '../shared/leekConfig'; +import { LeekTreeItem } from '../shared/leekTreeItem'; +import { events, formatLabelString } from '../shared/utils'; + +export class StatusBar { + private stockService: StockService; + private statusBarList: StatusBarItem[] = []; + private statusBarItemLabelFormat: string = ''; + constructor(stockService: StockService) { + this.stockService = stockService; + this.statusBarList = []; + this.refreshStockStatusBar(); + this.bindEvents(); + /* events.on('updateConfig:a-shares.statusBarStock',()=>{ + + }) */ + } + + get riseColor(): string { + return LeekFundConfig.getConfig('a-shares.riseColor'); + } + + get fallColor(): string { + return LeekFundConfig.getConfig('a-shares.fallColor'); + } + + /** 隐藏股市状态栏 */ + get hideStatusBarStock(): boolean { + return LeekFundConfig.getConfig('a-shares.hideStatusBarStock'); + } + /** 隐藏状态栏 */ + get hideStatusBar(): boolean { + return LeekFundConfig.getConfig('a-shares.hideStatusBar'); + } + + bindEvents() { + events.on('stockListUpdate', () => { + this.refreshStockStatusBar(); + }); + } + + refresh() { + this.refreshStockStatusBar(); + } + + toggleVisibility() { + LeekFundConfig.setConfig('a-shares.hideStatusBar', !this.hideStatusBar); + this.refresh(); + } + + refreshStockStatusBar() { + if (this.hideStatusBar || this.hideStatusBarStock || !this.stockService.stockList.length) { + if (this.statusBarList.length) { + this.statusBarList.forEach((bar) => bar.dispose()); + this.statusBarList = []; + } + return; + } + + let sz: LeekTreeItem | null = null; + const statusBarStocks = LeekFundConfig.getConfig('a-shares.statusBarStock'); + const barStockList: Array = new Array(statusBarStocks.length); + + this.statusBarItemLabelFormat = + globalState.labelFormat?.['statusBarLabelFormat'] ?? + DEFAULT_LABEL_FORMAT.statusBarLabelFormat; + + this.stockService.stockList.forEach((stockItem) => { + const { code } = stockItem.info; + if (code === 'sh000001') { + sz = stockItem; + } + if (statusBarStocks.includes(code)) { + // barStockList.push(stockItem); + barStockList[statusBarStocks.indexOf(code)] = stockItem; + } + }); + + if (!barStockList.length) { + barStockList.push(sz || this.stockService.stockList[0]); + } + + let count = barStockList.length - this.statusBarList.length; + if (count > 0) { + while (--count >= 0) { + const stockBarItem = window.createStatusBarItem(StatusBarAlignment.Left, 3); + this.statusBarList.push(stockBarItem); + } + } else if (count < 0) { + let num = Math.abs(count); + while (--num >= 0) { + const bar = this.statusBarList.pop(); + bar?.hide(); + bar?.dispose(); + } + } + barStockList.forEach((stock, index) => { + this.updateBarInfo(this.statusBarList[index], stock); + }); + } + + updateBarInfo(stockBarItem: StatusBarItem, item: LeekTreeItem | null) { + if (!item) return; + const { code, percent, open,price, yestclose, high, low, updown, amount } = item.info; + const deLow = percent.indexOf('-') === -1; + stockBarItem.text = `「${this.stockService.showLabel ? item.info.name : item.id}」${price}(${percent}%)`; + // stockBarItem.text = formatLabelString(this.statusBarItemLabelFormat, { + // ...item.info, + // percent: `${percent}%`, + // icon: deLow ? '📈' : '📉', + // }); + + stockBarItem.tooltip = `「今日行情」 ${ + item.info?.name ?? '今日行情' + }(${code})\n涨跌:${updown} 百分:${percent}%\n最高:${high} 最低:${low}\n今开:${open} 昨收:${yestclose}\n成交额:${amount}\n更新时间:${ + item.info?.time + }`; + stockBarItem.color = deLow ? this.riseColor : this.fallColor; + stockBarItem.command = { + title: 'Change stock', + command: 'a-shares.changeStatusBarItem', + arguments: [item.id], + }; + + stockBarItem.show(); + return stockBarItem; + } + +} diff --git a/vscode/a-shares/src/test/extension.test.ts b/vscode/a-shares/src/test/extension.test.ts new file mode 100644 index 0000000..4ca0ab4 --- /dev/null +++ b/vscode/a-shares/src/test/extension.test.ts @@ -0,0 +1,15 @@ +import * as assert from 'assert'; + +// You can import and use all API from the 'vscode' module +// as well as import your extension to test it +import * as vscode from 'vscode'; +// import * as myExtension from '../../extension'; + +suite('Extension Test Suite', () => { + vscode.window.showInformationMessage('Start all tests.'); + + test('Sample test', () => { + assert.strictEqual(-1, [1, 2, 3].indexOf(5)); + assert.strictEqual(-1, [1, 2, 3].indexOf(0)); + }); +}); diff --git a/vscode/a-shares/src/webview/ReusedWebviewPanel.ts b/vscode/a-shares/src/webview/ReusedWebviewPanel.ts new file mode 100644 index 0000000..bb8cb5c --- /dev/null +++ b/vscode/a-shares/src/webview/ReusedWebviewPanel.ts @@ -0,0 +1,59 @@ +import { window, ViewColumn, WebviewPanel, WebviewPanelOptions, WebviewOptions } from 'vscode'; +import globalState from '../globalState'; + +module ReusedWebviewPanel { + const webviewPanelsPool: Map = new Map(); // webviewPanel池 + + /** + * 创建webviewPanel + * @param viewType 唯一标识 + * @param title 标题 + * @param showOptions 显示位置 + * @param options 可选选项 + */ + export function create( + viewType: string, + title: string, + showOptions = ViewColumn.Active, + options?: WebviewPanelOptions & WebviewOptions + ) { + const oldPanel = webviewPanelsPool.get(viewType); + + if (oldPanel) { + oldPanel.title = title; + oldPanel.reveal(); + return oldPanel; + } + + const newPanel = window.createWebviewPanel(viewType, title, showOptions, options); + + newPanel.onDidDispose(() => webviewPanelsPool.delete(viewType)); + webviewPanelsPool.set(viewType, newPanel); + + console.log('webviewPanelsPool.size:', webviewPanelsPool.size); + + try { + globalState.telemetry.sendEvent(viewType, { title }); + } catch (err) { + console.error(err); + } + + return newPanel; + } + + /** + * 销毁webviewPanel + * @param viewType 唯一标识 + */ + export function destroy(viewType: string) { + const target = webviewPanelsPool.get(viewType); + + if (target) { + webviewPanelsPool.delete(viewType); + // createWebviewPanel是异步的,setTimeout避免创建未完成时调用dispose报错 + setTimeout(() => target.dispose(), 0); + } + } +} + +export default ReusedWebviewPanel; diff --git a/vscode/a-shares/src/webview/homeTrend.ts b/vscode/a-shares/src/webview/homeTrend.ts new file mode 100644 index 0000000..b6b2ca8 --- /dev/null +++ b/vscode/a-shares/src/webview/homeTrend.ts @@ -0,0 +1,34 @@ +import { ViewColumn, window } from 'vscode'; +import ReusedWebviewPanel from './ReusedWebviewPanel'; + + + +function homeTrend(url: string) { + + const panel = ReusedWebviewPanel.create('stockTrendWebview', "QCloud", ViewColumn.One, { + enableScripts: true, + }); + + panel.webview.html = panel.webview.html = ` + + + + + + QCloud + + +
+ +
+ + + + `; +} + +export default homeTrend; diff --git a/vscode/a-shares/src/webview/stockTrend.ts b/vscode/a-shares/src/webview/stockTrend.ts new file mode 100644 index 0000000..7ca6873 --- /dev/null +++ b/vscode/a-shares/src/webview/stockTrend.ts @@ -0,0 +1,112 @@ +import { ViewColumn, window } from 'vscode'; +import ReusedWebviewPanel from './ReusedWebviewPanel'; +import globalState from '../globalState'; +import { LeekFundConfig } from '../shared/leekConfig'; +import { register } from 'module'; + + + +function stockTrend(code: string, name: string, stockCode: string) { + let url = `https://www.xxxxxx.cn/pc/#/add?scode=${stockCode}` + + stockCode = stockCode.toLowerCase(); + let market = '1'; + if (stockCode.indexOf('hk') === 0) {// 港股 + market = '116'; + let mcid = market + '.' + code.substr(1); + url = `https://quote.eastmoney.com/basic/full.html?mcid=${mcid}` + } if (stockCode.indexOf('usr_') === 0) {// 美股 + stockCode = stockCode.replace('usr_', ''); + market = '105'; + let mcid = market + '.' + code.substr(1); + url = `https://quote.eastmoney.com/basic/full.html?mcid=${mcid}` + } + + + const userid = globalState.userId; + if (userid?.length > 0) { + url += `&username=${userid}` + } else { + url += `&logout=true` + window.showInformationMessage(`请先点击下方'ACCOUNT'扫码登录,效果更佳.`); + } + + const panel = ReusedWebviewPanel.create('stockTrendWebview', name, ViewColumn.One, { + enableScripts: true, + }); + + panel.webview.html = ` + + + + + + 股票走势 + + + +
+ +
+ + + + `; + + panel.webview.onDidReceiveMessage(data => { + switch (data.type) { + case 'user': + { + globalState.userId = data.userId; + LeekFundConfig.setConfig('a-shares.userid', globalState.userId); + globalState.userName = data.userName; + LeekFundConfig.setConfig('a-shares.username', globalState.userName); + globalState.userUrl = data.avatarUrl; + LeekFundConfig.setConfig('a-shares.userurl', globalState.userUrl); + if (data.rg) { + LeekFundConfig.setConfig('a-shares.riseColor', "#ff785d"); + LeekFundConfig.setConfig('a-shares.fallColor', "#95ec69"); + } else { + LeekFundConfig.setConfig('a-shares.riseColor', "#95ec69"); + LeekFundConfig.setConfig('a-shares.fallColor', "#ff785d"); + } + globalState.rg = data.rg; + console.log('🐥>>>updateUser: ', data); + registerCallFunc(); + break; + } + } + }); +} + +var registerCallFunc = function () { + console.log('🐥>>>registerCallFunc'); +} + +function RegisterCallFunc(call : any) { + registerCallFunc = call; +} + + + +export default stockTrend; +export {RegisterCallFunc};