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
+
+
+
+
+
+
+
+
+
+
+ `;
+ }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};