diff --git a/leopard-web/src/pages/stock/StockCollectionDetail.tsx b/leopard-web/src/pages/stock/StockCollectionDetail.tsx
index 0143d1e..3f9f94e 100644
--- a/leopard-web/src/pages/stock/StockCollectionDetail.tsx
+++ b/leopard-web/src/pages/stock/StockCollectionDetail.tsx
@@ -1,9 +1,8 @@
import React from "react"
import {amisRender, commonInfo, crudCommonOptions, paginationTemplate, stockListColumns} from '../../util/amis.tsx'
-import {useNavigate, useParams} from 'react-router'
+import {useParams} from 'react-router'
function StockCollectionDetail() {
- const navigate = useNavigate()
const {id} = useParams()
return (
@@ -19,19 +18,19 @@ function StockCollectionDetail() {
...crudCommonOptions(),
...paginationTemplate(100, undefined, ['filter-toggler']),
columns: stockListColumns(
- navigate,
+ undefined,
[
{
name: 'score',
label: '得分',
width: 50,
align: 'center',
- }
- ]
+ },
+ ],
),
- }
- ]
- }
+ },
+ ],
+ },
)}
)
diff --git a/leopard-web/src/pages/stock/StockList.tsx b/leopard-web/src/pages/stock/StockList.tsx
index 20d386a..070047f 100644
--- a/leopard-web/src/pages/stock/StockList.tsx
+++ b/leopard-web/src/pages/stock/StockList.tsx
@@ -7,10 +7,8 @@ import {
remoteOptions,
stockListColumns,
} from '../../util/amis.tsx'
-import {useNavigate} from 'react-router'
function StockList() {
- const navigate = useNavigate()
return (
{amisRender(
@@ -96,7 +94,7 @@ function StockList() {
},
],
},
- columns: stockListColumns(navigate),
+ columns: stockListColumns(),
},
],
},
diff --git a/leopard-web/src/util/amis.tsx b/leopard-web/src/util/amis.tsx
index 7306e62..32a0a20 100644
--- a/leopard-web/src/util/amis.tsx
+++ b/leopard-web/src/util/amis.tsx
@@ -4,9 +4,10 @@ import 'amis/lib/helper.css'
import 'amis/sdk/iconfont.css'
import '@fortawesome/fontawesome-free/css/all.min.css'
import axios from 'axios'
-import {isEqual} from 'es-toolkit'
-import type {NavigateFunction} from 'react-router'
+import {isEqual, isNil} from 'es-toolkit'
+// @ts-ignore
import type {ColumnSchema} from 'amis/lib/renderers/Table2'
+import {toNumber} from 'es-toolkit/compat'
export const commonInfo = {
debug: isEqual(import.meta.env.MODE, 'development'),
@@ -337,7 +338,230 @@ export function remoteMappings(name: string, field: string) {
}
}
-export function stockListColumns(navigate: NavigateFunction, extraColumns: Array
= []) {
+const formatFinanceNumber = (value: number): string => {
+ if (isNil(value)) {
+ return '-'
+ }
+
+ const isNegative = value < 0
+ const absoluteValue = Math.abs(value)
+
+ let formatted: string
+ if (absoluteValue >= 100000000) {
+ formatted = (absoluteValue / 100000000).toFixed(2) + '亿'
+ } else if (absoluteValue >= 10000) {
+ formatted = (absoluteValue / 10000).toFixed(2) + '万'
+ } else {
+ formatted = absoluteValue.toLocaleString()
+ }
+
+ return isNegative ? `-${formatted}` : formatted
+}
+
+const formatDaysNumber = (value: number): string => {
+ if (isNil(value)) {
+ return '-'
+ }
+ return `${value.toFixed(0)}天`
+}
+
+const formatPercentageNumber = (value: number): string => {
+ if (isNil(value)) {
+ return '-'
+ }
+ return `${(value * 100).toFixed(2)}%`
+}
+
+type FinanceType = 'PERCENTAGE' | 'FINANCE' | 'DAYS'
+
+const financePropertyLabel = (idField: string, label: string, type: FinanceType, field: string): Schema => {
+ let formatter: (value: number) => string
+ switch (type) {
+ case 'PERCENTAGE':
+ formatter = formatPercentageNumber
+ break
+ case 'FINANCE':
+ formatter = formatFinanceNumber
+ break
+ case 'DAYS':
+ formatter = formatDaysNumber
+ break
+ default:
+ formatter = (v: number) => v.toFixed(2)
+ }
+ return {
+ type: 'wrapper',
+ size: 'none',
+ body: [
+ {
+ visibleOn: `\${!${idField}}`,
+ type: 'tpl',
+ tpl: label,
+ },
+ {
+ visibleOn: `\${${idField}}`,
+ className: 'text-current font-bold',
+ type: 'action',
+ label: label,
+ level: 'link',
+ tooltip: '这是什么?',
+ tooltipPlacement: 'top',
+ actionType: 'dialog',
+ dialog: {
+ title: '',
+ size: 'lg',
+ ...readOnlyDialogOptions(),
+ actions: [
+ {
+ type: 'action',
+ label: '新页面打开',
+ icon: 'fa fa-solid fa-arrow-up-right-from-square',
+ actionType: 'url',
+ url: `https://zh.wikipedia.org/wiki/${label}`,
+ blank: true,
+ },
+ ],
+ body: {
+ type: 'iframe',
+ src: `https://zh.wikipedia.org/wiki/${label}`,
+ height: 800,
+ },
+ },
+ },
+ {
+ className: 'text-secondary',
+ type: 'action',
+ label: '',
+ icon: 'fa fa-eye',
+ level: 'link',
+ size: 'xs',
+ tooltip: '查看五年趋势',
+ tooltipPlacement: 'top',
+ actionType: 'dialog',
+ dialog: {
+ title: `${label}五年趋势`,
+ size: 'lg',
+ bodyClassName: 'p-0',
+ ...readOnlyDialogOptions(),
+ body: {
+ type: 'chart',
+ api: `get:${commonInfo.baseUrl}/stock/finance/\${${idField}}/${field}`,
+ height: 500,
+ config: {
+ tooltip: {
+ trigger: 'axis',
+ backgroundColor: 'rgba(255, 255, 255, 0.9)',
+ borderColor: '#ccc',
+ borderWidth: 1,
+ textStyle: {
+ color: '#333',
+ },
+ padding: [10, 15],
+ formatter: (params: any) => {
+ const item = params[0]
+ return `${item.name}
${item.marker}${formatter(item.value)}`
+ },
+ },
+ grid: {
+ left: '5%',
+ right: '5%',
+ top: '10%',
+ bottom: '15%',
+ containLabel: true,
+ },
+ xAxis: {
+ type: 'category',
+ data: '${xList || []}',
+ axisLine: {
+ lineStyle: {
+ color: '#e0e0e0',
+ },
+ },
+ axisLabel: {
+ color: '#666',
+ fontWeight: 'bold',
+ },
+ axisTick: {
+ show: false,
+ },
+ },
+ yAxis: {
+ type: 'value',
+ show: true,
+ splitLine: {
+ lineStyle: {
+ type: 'dashed',
+ color: '#f0f0f0',
+ },
+ },
+ axisLine: {
+ show: false,
+ },
+ axisLabel: {
+ color: '#999',
+ fontSize: 12,
+ formatter: (value: number) => {
+ return formatter(value)
+ },
+ },
+ axisTick: {
+ show: false,
+ },
+ },
+ series: [
+ {
+ data: '${yList || []}',
+ type: 'line',
+ smooth: true,
+ showSymbol: true,
+ symbolSize: 6,
+ lineStyle: {
+ width: 3,
+ color: '#4096ff',
+ shadowColor: 'rgba(64, 150, 255, 0.3)',
+ shadowBlur: 5,
+ shadowOffsetY: 2,
+ },
+ itemStyle: {
+ color: '#4096ff',
+ borderWidth: 2,
+ borderColor: '#fff',
+ },
+ areaStyle: {
+ color: {
+ type: 'linear',
+ x: 0,
+ y: 0,
+ x2: 0,
+ y2: 1,
+ colorStops: [{
+ offset: 0, color: 'rgba(64, 150, 255, 0.2)',
+ }, {
+ offset: 1, color: 'rgba(64, 150, 255, 0.01)',
+ }],
+ },
+ },
+ label: {
+ show: true,
+ position: 'top',
+ color: '#333',
+ fontWeight: 'bold',
+ fontSize: 12,
+ formatter: (params: any) => {
+ return formatter(params.value)
+ },
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ ],
+ }
+}
+
+export function stockListColumns(idField: string = 'id', extraColumns: Array = []) {
return [
{
name: 'code',
@@ -381,18 +605,315 @@ export function stockListColumns(navigate: NavigateFunction, extraColumns: Array
type: 'action',
label: '详情',
level: 'link',
- onEvent: {
- click: {
- actions: [
- {
- actionType: 'custom',
- // @ts-ignore
- script: (context, action, event) => {
- navigate(`/stock/detail/${context.props.data['id']}`)
+ actionType: 'dialog',
+ dialog: {
+ title: '股票详情',
+ size: 'full',
+ ...readOnlyDialogOptions(),
+ body: [
+ {
+ type: 'property',
+ items: [
+ {label: '编码', content: '${code}'},
+ {label: '名称', content: '${name}'},
+ {label: '全名', content: '${fullname}'},
+ {
+ label: '市场',
+ content: {
+ ...remoteMappings('stock_market', 'market'),
+ value: '${market}',
+ },
},
- },
- ],
- },
+ {label: '行业', content: '${industry}'},
+ {label: '上市日期', content: '${listedDate}'},
+ ],
+ },
+ {type: 'divider'},
+ {
+ type: 'service',
+ api: `get:${commonInfo.baseUrl}/stock/finance/\${${idField}}`,
+ body: [
+ '资产负债表',
+ {
+ className: 'my-2',
+ type: 'property',
+ column: 4,
+ items: [
+ {
+ label: financePropertyLabel(idField, '总资产', 'FINANCE', 'totalAssets'),
+ content: '${balanceSheet.totalAssets}',
+ span: 2,
+ },
+ {
+ label: financePropertyLabel(idField, '总负债', 'FINANCE', 'totalLiabilities'),
+ content: '${balanceSheet.totalLiabilities}',
+ span: 2,
+ },
+ {
+ label: financePropertyLabel(idField, '流动资产', 'FINANCE', 'currentAssets'),
+ content: '${balanceSheet.currentAssets}',
+ },
+ {
+ label: financePropertyLabel(idField, '流动资产占比', 'PERCENTAGE', 'currentAssetsToTotalAssetsRatio'),
+ content: '${balanceSheet.currentAssetsRatio}',
+ },
+ {
+ label: financePropertyLabel(idField, '流动负债', 'FINANCE', 'currentLiabilities'),
+ content: '${balanceSheet.currentLiabilities}',
+ },
+ {
+ label: financePropertyLabel(idField, '流动负债占比', 'PERCENTAGE', 'currentLiabilitiesToTotalAssetsRatio'),
+ content: '${balanceSheet.currentLiabilitiesRatio}',
+ },
+ {
+ label: financePropertyLabel(idField, '非流动资产', 'FINANCE', 'fixedAssets'),
+ content: '${balanceSheet.fixedAssets}',
+ },
+ {
+ label: financePropertyLabel(idField, '非流动资产占比', 'PERCENTAGE', 'fixedAssetsToTotalAssetsRatio'),
+ content: '${balanceSheet.fixedAssetsRatio}',
+ },
+ {
+ label: financePropertyLabel(idField, '非流动负债', 'FINANCE', 'longTermLiabilities'),
+ content: '${balanceSheet.longTermLiabilities}',
+ },
+ {
+ label: financePropertyLabel(idField, '非流动负债占比', 'PERCENTAGE', 'longTermLiabilitiesToTotalAssetsRatio'),
+ content: '${balanceSheet.longTermLiabilitiesRatio}',
+ },
+ ],
+ },
+ '利润表',
+ {
+ className: 'my-2',
+ type: 'property',
+ items: [
+ {
+ label: financePropertyLabel(idField, '营业收入', 'FINANCE', 'operatingRevenue'),
+ content: '${income.operatingRevenue}',
+ },
+ {
+ label: financePropertyLabel(idField, '营业成本', 'FINANCE', 'operatingCost'),
+ content: '${income.operatingCost}',
+ },
+ {
+ label: financePropertyLabel(idField, '营业利润', 'FINANCE', 'operatingProfit'),
+ content: '${income.operatingProfit}',
+ },
+ ],
+ },
+ '现金流量表',
+ {
+ className: 'my-2',
+ type: 'property',
+ items: [
+ {
+ label: financePropertyLabel(idField, '净利润', 'FINANCE', 'netProfit'),
+ content: '${cashFlow.netProfit}',
+ span: 3,
+ },
+ {
+ label: financePropertyLabel(idField, '营业活动现金流量', 'FINANCE', 'cashFlowFromOperatingActivities'),
+ content: '${cashFlow.cashFlowFromOperatingActivities}',
+ },
+ {
+ label: financePropertyLabel(idField, '投资活动现金流量', 'FINANCE', 'cashFlowFromInvestingActivities'),
+ content: '${cashFlow.cashFlowFromInvestingActivities}',
+ },
+ {
+ label: financePropertyLabel(idField, '筹资活动现金流量', 'FINANCE', 'cashFlowFromFinancingActivities'),
+ content: '${cashFlow.cashFlowFromFinancingActivities}',
+ },
+ ],
+ },
+ '财务指标',
+ {
+ className: 'my-2',
+ type: 'property',
+ column: 4,
+ items: [
+ {
+ label: financePropertyLabel(idField, '流动比率', 'FINANCE', 'currentRatio'),
+ content: '${indicate.currentRatio}',
+ },
+ {
+ label: financePropertyLabel(idField, '速动比率', 'FINANCE', 'quickRatio'),
+ content: '${indicate.quickRatio}',
+ },
+ {
+ label: financePropertyLabel(idField, 'ROE', 'FINANCE', 'returnOnEquity'),
+ content: '${indicate.roe}',
+ },
+ {
+ label: financePropertyLabel(idField, 'ROA', 'FINANCE', 'returnOnAssets'),
+ content: '${indicate.roa}',
+ },
+ {
+ label: financePropertyLabel(idField, '应收账款周转率', 'FINANCE', 'accountsReceivableTurnover'),
+ content: '${indicate.accountsReceivableTurnover}',
+ },
+ {
+ label: financePropertyLabel(idField, '应收账款周转天数', 'DAYS', 'daysAccountsReceivableTurnover'),
+ content: '${indicate.daysAccountsReceivableTurnover}',
+ },
+ {
+ label: financePropertyLabel(idField, '存货周转率', 'FINANCE', 'inventoryTurnover'),
+ content: '${indicate.inventoryTurnover}',
+ },
+ {
+ label: financePropertyLabel(idField, '存货周转天数', 'DAYS', 'daysInventoryTurnover'),
+ content: '${indicate.daysInventoryTurnover}',
+ },
+ {
+ label: financePropertyLabel(idField, '固定资产周转率', 'FINANCE', 'fixedAssetsTurnover'),
+ content: '${indicate.fixedAssetsTurnover}',
+ },
+ {
+ label: financePropertyLabel(idField, '固定资产周转天数', 'DAYS', 'daysFixedAssetsTurnover'),
+ content: '${indicate.daysFixedAssetsTurnover}',
+ },
+ {
+ label: financePropertyLabel(idField, '总资产周转率', 'FINANCE', 'totalAssetsTurnover'),
+ content: '${indicate.totalAssetsTurnover}',
+ },
+ {
+ label: financePropertyLabel(idField, '总资产周转天数', 'DAYS', 'daysTotalAssetsTurnover'),
+ content: '${indicate.daysTotalAssetsTurnover}',
+ },
+ ],
+ },
+ {type: 'divider'},
+ "100日线数据",
+ {
+ type: 'chart',
+ height: 500,
+ api: `get:${commonInfo.baseUrl}/stock/daily/\${${idField}}`,
+ config: {
+ backgroundColor: '#fff',
+ animation: true,
+ animationDuration: 1000,
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'cross',
+ },
+ backgroundColor: 'rgba(0, 0, 0, 0.7)',
+ borderColor: '#333',
+ borderWidth: 1,
+ textStyle: {
+ color: '#fff',
+ fontSize: 12,
+ },
+ padding: 12,
+ formatter: function (params: any) {
+ const param = params[0]
+ const open = toNumber(param.data[1]).toFixed(2)
+ const close = toNumber(param.data[2]).toFixed(2)
+ const lowest = toNumber(param.data[3]).toFixed(2)
+ const highest = toNumber(param.data[4]).toFixed(2)
+
+ return `${param.name}
+
+ 开盘:
+ ${open}
+
+
+ 收盘:
+ ${close}
+
+
+ 最低:
+ ${lowest}
+
+
+ 最高:
+ ${highest}
+
`
+ },
+ },
+ grid: {
+ left: '10%',
+ right: '10%',
+ top: '10%',
+ bottom: '15%',
+ containLabel: true,
+ },
+ xAxis: {
+ data: '${xList || []}',
+ axisLine: {
+ lineStyle: {
+ color: '#e0e0e0',
+ },
+ },
+ axisLabel: {
+ color: '#666',
+ fontWeight: 'bold',
+ },
+ splitLine: {
+ show: false,
+ },
+ axisTick: {
+ show: false,
+ },
+ },
+ yAxis: {
+ scale: true,
+ axisLine: {
+ lineStyle: {
+ color: '#e0e0e0',
+ },
+ },
+ axisLabel: {
+ color: '#666',
+ fontWeight: 'bold',
+ formatter: function (value: number) {
+ return value.toFixed(2)
+ },
+ },
+ splitLine: {
+ lineStyle: {
+ type: 'dashed',
+ color: '#f0f0f0',
+ },
+ },
+ axisTick: {
+ show: false,
+ },
+ },
+ dataZoom: [
+ {
+ type: 'inside',
+ start: 0,
+ end: 100,
+ },
+ {
+ show: true,
+ type: 'slider',
+ top: '90%',
+ start: 0,
+ end: 100,
+ },
+ ],
+ series: [
+ {
+ type: 'candlestick',
+ data: '${yList || []}',
+ itemStyle: {
+ color: '#eb5454',
+ color0: '#4aaa93',
+ borderColor: '#eb5454',
+ borderColor0: '#4aaa93',
+ borderWidth: 1,
+ },
+
+ },
+ ],
+ },
+ },
+ "12月线数据",
+ ],
+ },
+ ],
},
},
],