diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..3544777 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,21 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2020, + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint" + ], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "prettier" + ], + "ignorePatterns": [ + "out", + "dist", + "**/*.d.ts" + ] +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..62cb3f7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +out +dist +node_modules +.vscode-test/ +*.vsix +.DS_Store +package-lock.json +demo/donate.html +demo/api.html +yarn-error.log + +template/leek-center/build +template-packages/leek-center/.eslintcache +!template-packages/leek-center/yarn.lock + +.idea diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..b31310e --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "singleQuote": true, + "trailingComma": "es5", + "printWidth": 80, + "tabWidth": 2, + "semi": true, + "bracketSpacing": true, + "arrowParens": "always" +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..8b29ce4 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,25 @@ +// A launch configuration that compiles the extension and then opens it inside a new window +// Use IntelliSense to learn about possible attributes. +// Hover to view descriptions of existing attributes. +// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ], + "preLaunchTask": "${defaultBuildTask}", + "env": { + "NODE_ENV": "development" + } + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..30bf8c2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +// Place your settings in this file to overwrite default and user settings. +{ + "files.exclude": { + "out": false // set this to true to hide the "out" folder with the compiled JS files + }, + "search.exclude": { + "out": true // set this to false to include "out" folder in search results + }, + // Turn off tsc task auto detection since we have the necessary tasks as npm scripts + "typescript.tsc.autoDetect": "off" +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..3b17e53 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,20 @@ +// See https://go.microsoft.com/fwlink/?LinkId=733558 +// for the documentation about the tasks.json format +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "watch", + "problemMatcher": "$tsc-watch", + "isBackground": true, + "presentation": { + "reveal": "never" + }, + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} diff --git a/.vscodeignore b/.vscodeignore new file mode 100644 index 0000000..1a1a299 --- /dev/null +++ b/.vscodeignore @@ -0,0 +1,9 @@ +.vscode/** +.vscode-test/** +src/** +.gitignore +vsc-extension-quickstart.md +**/tsconfig.json +**/.eslintrc.json +**/*.map +**/*.ts \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..95d2157 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# Change Log + +## 1.0.0 (2025-01-25) + +Initial release of leek-fund-lite: + +- Support fund data display +- Support stock data display +- Auto refresh data +- Manual refresh data diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c13f991 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b85c249 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# Leek Fund Lite + +A simple VSCode extension for viewing fund and stock data, inspired by [LeekHub/leek-fund](https://github.com/LeekHub/leek-fund). + +## Features + +- Real-time fund data display +- Real-time stock data display +- Auto refresh data +- Manual refresh data + +## Usage + +1. Configure fund codes +2. Configure stock codes +3. View data in sidebar diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..a2ccbb8 Binary files /dev/null and b/logo.png differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..6cc4280 --- /dev/null +++ b/package.json @@ -0,0 +1,159 @@ +{ + "name": "leek-fund-lite", + "displayName": "Leek Fund Lite", + "description": "A VSCode extension for viewing fund and stock data", + "version": "1.0.0", + "license": "MIT", + "engines": { + "vscode": "^1.52.0" + }, + "categories": [ + "Other" + ], + "repository": { + "type": "git", + "url": "https://github.com/zomixi/leek-fund-lite.git" + }, + "bugs": { + "url": "https://github.com/zomixi/leek-fund-lite/issues" + }, + "icon": "logo.png", + "publisher": "zomixi", + "main": "./dist/extension.js", + "activationEvents": [ + "onStartupFinished" + ], + "contributes": { + "viewsContainers": { + "activitybar": [ + { + "id": "leekFundLite", + "title": "Leek Fund Lite", + "icon": "$(graph)" + } + ] + }, + "views": { + "leekFundLite": [ + { + "id": "leekFundLite.fund", + "name": "Funds" + }, + { + "id": "leekFundLite.stock", + "name": "Stocks" + } + ] + }, + "commands": [ + { + "command": "leek-fund-lite.refreshFund", + "title": "Refresh Fund Data", + "icon": "$(refresh)" + }, + { + "command": "leek-fund-lite.refreshStock", + "title": "Refresh Stock Data", + "icon": "$(refresh)" + }, + { + "command": "leek-fund-lite.addFund", + "title": "Add Fund", + "icon": "$(add)" + }, + { + "command": "leek-fund-lite.addStock", + "title": "Add Stock", + "icon": "$(add)" + }, + { + "command": "leek-fund-lite.deleteFund", + "title": "Delete Fund", + "icon": "$(trash)" + }, + { + "command": "leek-fund-lite.deleteStock", + "title": "Delete Stock", + "icon": "$(trash)" + } + ], + "configuration": { + "title": "Leek Fund Lite", + "properties": { + "leek-fund-lite.funds": { + "type": "array", + "default": [], + "description": "Fund code list" + }, + "leek-fund-lite.stocks": { + "type": "array", + "default": [], + "description": "Stock code list" + }, + "leek-fund-lite.interval": { + "type": "number", + "default": 10000, + "description": "Refresh interval (ms)" + } + } + }, + "menus": { + "view/title": [ + { + "command": "leek-fund-lite.addFund", + "when": "view == leekFundLite.fund", + "group": "navigation" + }, + { + "command": "leek-fund-lite.refreshFund", + "when": "view == leekFundLite.fund", + "group": "navigation" + }, + { + "command": "leek-fund-lite.addStock", + "when": "view == leekFundLite.stock", + "group": "navigation" + }, + { + "command": "leek-fund-lite.refreshStock", + "when": "view == leekFundLite.stock", + "group": "navigation" + } + ], + "view/item/context": [ + { + "command": "leek-fund-lite.deleteFund", + "when": "view == leekFundLite.fund && viewItem == fundItem", + "group": "inline" + }, + { + "command": "leek-fund-lite.deleteStock", + "when": "view == leekFundLite.stock && viewItem == stockItem", + "group": "inline" + } + ] + } + }, + "scripts": { + "vscode:prepublish": "npm run compile", + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./", + "lint": "eslint src --ext ts", + "format": "prettier --write \"src/**/*.ts\"" + }, + "dependencies": { + "node-fetch": "^2.6.7", + "iconv-lite": "^0.6.3" + }, + "devDependencies": { + "@types/node": "^14.14.7", + "@types/vscode": "^1.52.0", + "@typescript-eslint/eslint-plugin": "^5.59.0", + "@typescript-eslint/parser": "^5.59.0", + "eslint": "^8.38.0", + "eslint-config-prettier": "^8.8.0", + "prettier": "^2.8.7", + "typescript": "^4.1.2", + "@types/node-fetch": "^2.6.3" + } +} diff --git a/src/extension.ts b/src/extension.ts new file mode 100644 index 0000000..ae2a96d --- /dev/null +++ b/src/extension.ts @@ -0,0 +1,186 @@ +import * as vscode from 'vscode'; +import { FundService } from './service/fundService'; +import { StockService } from './service/stockService'; +import { LeekTreeItem } from './utils/leekTreeItem'; + +export function activate(context: vscode.ExtensionContext) { + const fundService = new FundService(); + const stockService = new StockService(); + + let fundTreeView: vscode.TreeView | null = null; + let stockTreeView: vscode.TreeView | null = null; + let loopTimer: NodeJS.Timer | null = null; + let refreshTimer: NodeJS.Timeout | null = null; + let isViewVisible = false; + + const refresh = () => { + if (refreshTimer) { + clearTimeout(refreshTimer); + refreshTimer = null; + } + + refreshTimer = setTimeout(() => { + fundService.refresh(); + stockService.refresh(); + refreshTimer = null; + }, 100); + }; + + const updatePolling = () => { + const interval = vscode.workspace + .getConfiguration() + .get('leek-fund-lite.interval', 10000); + + if (isViewVisible) { + if (!loopTimer) { + loopTimer = setInterval(refresh, interval); + console.log('[LEEK_FUND_LITE] Started polling'); + } + } else { + if (loopTimer) { + clearInterval(loopTimer); + loopTimer = null; + console.log('[LEEK_FUND_LITE] Stopped polling'); + } + } + }; + + fundTreeView = vscode.window.createTreeView('leekFundLite.fund', { + treeDataProvider: fundService, + }); + + stockTreeView = vscode.window.createTreeView('leekFundLite.stock', { + treeDataProvider: stockService, + }); + + // Watch for view visibility changes + context.subscriptions.push( + fundTreeView.onDidChangeVisibility(() => { + isViewVisible = fundTreeView?.visible || stockTreeView?.visible || false; + if (isViewVisible) { + refresh(); + } + updatePolling(); + }), + stockTreeView.onDidChangeVisibility(() => { + isViewVisible = fundTreeView?.visible || stockTreeView?.visible || false; + updatePolling(); + }) + ); + + // Initial visibility check + isViewVisible = fundTreeView?.visible || stockTreeView?.visible || false; + if (isViewVisible) { + refresh(); + } + updatePolling(); + + // Register commands + context.subscriptions.push( + vscode.commands.registerCommand('leek-fund-lite.refreshFund', async () => { + console.log('[LEEK_FUND_LITE] Refreshing fund data...'); + await fundService.refresh(); + console.log('[LEEK_FUND_LITE] Fund data refreshed'); + }), + vscode.commands.registerCommand('leek-fund-lite.refreshStock', async () => { + console.log('[LEEK_FUND_LITE] Refreshing stock data...'); + await stockService.refresh(); + console.log('[LEEK_FUND_LITE] Stock data refreshed'); + }), + vscode.commands.registerCommand('leek-fund-lite.addFund', async () => { + console.log('[LEEK_FUND_LITE] Adding fund...'); + const code = await vscode.window.showInputBox({ + prompt: 'Please input fund code', + placeHolder: 'e.g. 000001', + }); + if (code) { + const config = vscode.workspace.getConfiguration(); + const funds = config.get('leek-fund-lite.funds', []); + if (!funds.includes(code)) { + await config.update('leek-fund-lite.funds', [...funds, code], true); + console.log(`[LEEK_FUND_LITE] Fund ${code} added`); + fundService.refresh(); + } + } + }), + vscode.commands.registerCommand('leek-fund-lite.addStock', async () => { + console.log('[LEEK_FUND_LITE] Adding stock...'); + const code = await vscode.window.showInputBox({ + prompt: 'Please input stock code', + placeHolder: 'e.g. sh000001', + }); + if (code) { + const config = vscode.workspace.getConfiguration(); + const stocks = config.get('leek-fund-lite.stocks', []); + if (!stocks.includes(code)) { + await config.update('leek-fund-lite.stocks', [...stocks, code], true); + console.log(`[LEEK_FUND_LITE] Stock ${code} added`); + stockService.refresh(); + } + } + }), + vscode.commands.registerCommand( + 'leek-fund-lite.deleteFund', + async (item) => { + if (item) { + console.log('[LEEK_FUND_LITE] Deleting fund...'); + const config = vscode.workspace.getConfiguration(); + const funds = config.get('leek-fund-lite.funds', []); + await config.update( + 'leek-fund-lite.funds', + funds.filter((code) => code !== item.code), + true + ); + console.log(`[LEEK_FUND_LITE] Fund ${item.code} deleted`); + fundService.refresh(); + } + } + ), + vscode.commands.registerCommand( + 'leek-fund-lite.deleteStock', + async (item) => { + if (item) { + console.log('[LEEK_FUND_LITE] Deleting stock...'); + const config = vscode.workspace.getConfiguration(); + const stocks = config.get('leek-fund-lite.stocks', []); + await config.update( + 'leek-fund-lite.stocks', + stocks.filter((code) => code !== item.code), + true + ); + console.log(`[LEEK_FUND_LITE] Stock ${item.code} deleted`); + stockService.refresh(); + } + } + ) + ); + + // Cleanup function + context.subscriptions.push({ + dispose: () => { + if (refreshTimer) { + clearTimeout(refreshTimer); + refreshTimer = null; + } + if (loopTimer) { + clearInterval(loopTimer); + loopTimer = null; + } + if (fundTreeView) { + fundTreeView.dispose(); + fundTreeView = null; + } + if (stockTreeView) { + stockTreeView.dispose(); + stockTreeView = null; + } + console.log('[LEEK_FUND_LITE] Extension is now deactivated'); + }, + }); + + console.log('[LEEK_FUND_LITE] Extension is now active!'); +} + +export function deactivate() { + // Cleanup is handled by the dispose function registered in subscriptions +} diff --git a/src/service/fundService.ts b/src/service/fundService.ts new file mode 100644 index 0000000..c432be3 --- /dev/null +++ b/src/service/fundService.ts @@ -0,0 +1,65 @@ +import * as vscode from 'vscode'; +import { fetchFundData } from '../utils/fetch'; +import type { FundData } from '../utils/fetch'; +import { LeekTreeItem } from '../utils/leekTreeItem'; + +export class FundService implements vscode.TreeDataProvider { + private _onDidChangeTreeData: vscode.EventEmitter< + LeekTreeItem | undefined | null | void + > = new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event< + LeekTreeItem | undefined | null | void + > = this._onDidChangeTreeData.event; + + private fundCodes: string[] = []; + private fundData: FundData[] = []; + + constructor() { + this.fundCodes = vscode.workspace + .getConfiguration() + .get('leek-fund-lite.funds', []); + } + + async refresh(): Promise { + this.fundCodes = vscode.workspace + .getConfiguration() + .get('leek-fund-lite.funds', []); + try { + this.fundData = await fetchFundData(this.fundCodes); + this._onDidChangeTreeData.fire(); + } catch (error) { + console.error(`[LEEK_FUND_LITE] Failed to refresh fund data:`, error); + vscode.window.showErrorMessage('Failed to refresh fund data'); + this._onDidChangeTreeData.fire(); + } + } + + getTreeItem(element: LeekTreeItem): vscode.TreeItem { + return element; + } + + getChildren(): Thenable { + if (!this.fundCodes.length) { + return Promise.resolve([new LeekTreeItem('0', '', '', '0', 'fund')]); + } + + const fundMap = new Map(this.fundData.map((fund) => [fund.code, fund])); + + return Promise.resolve( + this.fundCodes.map((code) => { + const fund = fundMap.get(code); + if (!fund) { + return new LeekTreeItem(code, code, '-', '0', 'fund'); + } + + return new LeekTreeItem( + fund.code, + fund.name, + fund.estimatedWorth, + fund.estimatedWorthPercent, + 'fund' + ); + }) + ); + } +} diff --git a/src/service/stockService.ts b/src/service/stockService.ts new file mode 100644 index 0000000..985d7b2 --- /dev/null +++ b/src/service/stockService.ts @@ -0,0 +1,74 @@ +import * as vscode from 'vscode'; +import type { StockData } from '../utils/fetch'; +import { fetchStockData } from '../utils/fetch'; +import { LeekTreeItem } from '../utils/leekTreeItem'; + +export class StockService implements vscode.TreeDataProvider { + private _onDidChangeTreeData: vscode.EventEmitter< + LeekTreeItem | undefined | null | void + > = new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event< + LeekTreeItem | undefined | null | void + > = this._onDidChangeTreeData.event; + + private stockCodes: string[] = []; + private stockData: StockData[] = []; + + constructor() { + this.stockCodes = vscode.workspace + .getConfiguration() + .get('leek-fund-lite.stocks', []); + } + + async refresh(): Promise { + this.stockCodes = vscode.workspace + .getConfiguration() + .get('leek-fund-lite.stocks', []); + + try { + this.stockData = await fetchStockData(this.stockCodes); + this._onDidChangeTreeData.fire(); + } catch (error) { + console.error(`[LEEK_FUND_LITE] Failed to refresh stock data:`, error); + vscode.window.showErrorMessage('Failed to refresh stock data'); + this._onDidChangeTreeData.fire(); + } + } + + getTreeItem(element: LeekTreeItem): vscode.TreeItem { + return element; + } + + getChildren(): Thenable { + if (!this.stockCodes.length) { + return Promise.resolve([new LeekTreeItem('0', '', '', '0', 'stock')]); + } + + const stockMap = new Map( + this.stockData.map((stock) => [stock.code, stock]) + ); + + return Promise.resolve( + this.stockCodes.map((code) => { + const stock = stockMap.get(code); + if (!stock) { + return new LeekTreeItem(code, code, '-', '0', 'stock'); + } + + const percent = ( + ((Number(stock.price) - Number(stock.yestclose)) / + Number(stock.yestclose)) * + 100 + ).toFixed(2); + + return new LeekTreeItem( + code, + stock.name, + stock.price, + percent, + 'stock' + ); + }) + ); + } +} diff --git a/src/utils/fetch.ts b/src/utils/fetch.ts new file mode 100644 index 0000000..df4a7cd --- /dev/null +++ b/src/utils/fetch.ts @@ -0,0 +1,238 @@ +import * as iconv from 'iconv-lite'; +import fetch from 'node-fetch'; + +export interface FundData { + code: string; + name: string; + netWorth: string; + netWorthDate: string; + estimatedWorth: string; + estimatedWorthPercent: string; + estimatedWorthTime: string; +} + +export interface StockData { + code: string; + name: string; + open: string; + yestclose: string; + price: string; + high: string; + low: string; + volume: string; + amount: string; + time: string; +} + +async function fetchSingleFund(code: string): Promise { + try { + const url = `https://fundgz.1234567.com.cn/js/${code}.js?rt=${new Date().getTime()}`; + + const response = await fetch(url, { + headers: { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36', + Referer: 'http://fund.eastmoney.com/', + }, + timeout: 10000, + }); + + const data = await response.text(); + + if (data && data.startsWith('jsonpgz')) { + const jsonStr = data.slice(8, -2); + const rawData = JSON.parse(jsonStr); + const fundData: FundData = { + code: rawData.fundcode, + name: rawData.name, + netWorth: rawData.dwjz, + netWorthDate: rawData.jzrq, + estimatedWorth: rawData.gsz, + estimatedWorthPercent: rawData.gszzl, + estimatedWorthTime: rawData.gztime, + }; + return fundData; + } else { + console.error( + `[LEEK_FUND_LITE] Failed to fetch fund ${code} data:`, + data + ); + } + } catch (e) { + console.error(`[LEEK_FUND_LITE] Failed to fetch fund ${code} data:`, e); + } + return null; +} + +export async function fetchFundData(codes: string[]): Promise { + if (!codes || !Array.isArray(codes)) { + console.error('[LEEK_FUND_LITE] Invalid fund codes:', codes); + return []; + } + + console.log('[LEEK_FUND_LITE] Fetching fund data...'); + const results: FundData[] = []; + const batchSize = 10; + + try { + for (let i = 0; i < codes.length; i += batchSize) { + const batch = codes.slice(i, i + batchSize); + const batchResults = await Promise.allSettled(batch.map(fetchSingleFund)); + results.push( + ...batchResults + .filter( + (result): result is PromiseFulfilledResult => + result.status === 'fulfilled' + ) + .map((result) => result.value) + .filter((data): data is FundData => data !== null) + ); + } + } catch (error) { + console.error(`[LEEK_FUND_LITE] Failed to fetch fund data:`, error); + } + + console.log('[LEEK_FUND_LITE] Fund data fetched successfully'); + return results; +} + +export async function fetchStockData(codes: string[]): Promise { + if (!codes || !Array.isArray(codes)) { + console.error('[LEEK_FUND_LITE] Invalid stock codes:', codes); + return []; + } + + console.log('[LEEK_FUND_LITE] Fetching stock data...'); + const results: StockData[] = []; + + try { + const url = `http://hq.sinajs.cn/list=${codes.join(',')}`; + + const response = await fetch(url, { + headers: { + Referer: 'http://finance.sina.com.cn', + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36', + }, + }); + + const arrayBuffer = await response.arrayBuffer(); + const text = iconv.decode(Buffer.from(arrayBuffer), 'GB18030'); + + if (text) { + const stockList = text.split(';\n').filter(Boolean); + + for (const stock of stockList) { + const [code, data] = stock.split('='); + if (data) { + const values = data.replace(/^"|"$/g, '').split(','); + const stockCode = code.replace('var hq_str_', ''); + + if (values.length > 1) { + if (/^(sh|sz|bj)/.test(stockCode)) { + // A-Shares + results.push({ + code: stockCode, + name: values[0], + open: values[1], + yestclose: values[2], + price: values[3], + high: values[4], + low: values[5], + volume: values[8], + amount: values[9], + time: `${values[30]} ${values[31]}`, + }); + } else if (/^gb_/.test(stockCode)) { + // Hong Kong Stocks + results.push({ + code: stockCode, + name: values[0], + open: values[5], + yestclose: values[26], + price: values[1], + high: values[6], + low: values[7], + volume: values[10], + amount: 'No Data', + time: values[3], + }); + } else if (/^usr_/.test(stockCode)) { + // US Stocks + results.push({ + code: stockCode, + name: values[0], + open: values[5], + yestclose: values[26], + price: values[1], + high: values[6], + low: values[7], + volume: values[10], + amount: 'No Data', + time: values[3], + }); + } else if (/^nf_/.test(stockCode)) { + // CN Futures + const isStockIndexFuture = /nf_(IC|IF|IH|IM|TF|TS|T\d+|TL)/.test( + stockCode + ); + + if (isStockIndexFuture) { + // Stock Index Futures + results.push({ + code: stockCode, + name: values[49].slice(0, -1), + open: values[0], + yestclose: values[13], + price: values[3], + high: values[1], + low: values[2], + volume: values[4], + amount: 'No Data', + time: `${values[values.length - 2]} ${ + values[values.length - 1] + }`, + }); + } else { + // Commodity Futures + results.push({ + code: stockCode, + name: values[0], + open: values[2], + yestclose: values[8 + 2], + price: values[8], + high: values[3], + low: values[4], + volume: values[8 + 6], + amount: 'No Data', + time: values[values.length - 2], + }); + } + } else if (/^hf_/.test(stockCode)) { + // International Futures + results.push({ + code: stockCode, + name: values[13], + open: values[8], + yestclose: values[7], + price: values[0], + high: values[4], + low: values[5], + volume: values[14].slice(0, -1), + amount: 'No Data', + time: values[6], + }); + } + } + } + } + } else { + console.error('[LEEK_FUND_LITE] Failed to fetch stock data:', text); + } + } catch (e) { + console.error('[LEEK_FUND_LITE] Failed to fetch stock data:', e); + } + + console.log('[LEEK_FUND_LITE] Stock data fetched successfully'); + return results; +} diff --git a/src/utils/leekTreeItem.ts b/src/utils/leekTreeItem.ts new file mode 100644 index 0000000..54242e6 --- /dev/null +++ b/src/utils/leekTreeItem.ts @@ -0,0 +1,33 @@ +import * as vscode from 'vscode'; + +function padRight(text: string, length: number): string { + return text + ' '.repeat(Math.max(length - text?.length || 0, 0)); +} + +export class LeekTreeItem extends vscode.TreeItem { + constructor( + public readonly code: string, + public readonly name: string, + public readonly price: string, + public readonly percent: string, + public readonly type: 'fund' | 'stock' + ) { + super(name); + + const percentNum = parseFloat(percent); + const icon = percentNum >= 0 ? '📈' : '📉'; + + if (code === '0') { + this.label = 'No ' + type + ' code configured'; + this.contextValue = undefined; + return; + } + + this.label = `${padRight(icon, 4)}${padRight( + percentNum + '%', + 11 + )}${padRight(String(price || '-'), 15)}「${name || '-'}」`; + this.description = ''; + this.contextValue = type === 'fund' ? 'fundItem' : 'stockItem'; + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..f4a89cf --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2020", + "outDir": "dist", + "lib": [ + "ES2020" + ], + "sourceMap": true, + "rootDir": "src", + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUnusedParameters": true, + "noImplicitAny": true, + "noImplicitThis": true, + "noUnusedLocals": true + }, + "exclude": [ + "node_modules", + ".vscode-test" + ] +} \ No newline at end of file