From 151723f79c47708b7aa8b273566b299b338b837a Mon Sep 17 00:00:00 2001 From: mars Date: Sun, 26 Jan 2025 14:36:19 +0800 Subject: [PATCH] chore: initial release of leek-fund-lite --- .eslintrc.json | 21 ++++ .gitignore | 16 +++ .prettierrc | 9 ++ .vscode/launch.json | 25 ++++ .vscode/settings.json | 11 ++ .vscode/tasks.json | 20 +++ .vscodeignore | 9 ++ CHANGELOG.md | 10 ++ LICENSE | 21 ++++ README.md | 16 +++ logo.png | Bin 0 -> 9923 bytes package.json | 159 ++++++++++++++++++++++++ src/extension.ts | 186 ++++++++++++++++++++++++++++ src/service/fundService.ts | 65 ++++++++++ src/service/stockService.ts | 74 +++++++++++ src/utils/fetch.ts | 238 ++++++++++++++++++++++++++++++++++++ src/utils/leekTreeItem.ts | 33 +++++ tsconfig.json | 23 ++++ 18 files changed, 936 insertions(+) create mode 100644 .eslintrc.json create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 .vscodeignore create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 logo.png create mode 100644 package.json create mode 100644 src/extension.ts create mode 100644 src/service/fundService.ts create mode 100644 src/service/stockService.ts create mode 100644 src/utils/fetch.ts create mode 100644 src/utils/leekTreeItem.ts create mode 100644 tsconfig.json 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 0000000000000000000000000000000000000000..a2ccbb81d4aa838b3af4afc44d1313e7c65ddfe9 GIT binary patch literal 9923 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 | 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