1
0
Files
leopard/leopard-web/src/util/amis.tsx

944 lines
29 KiB
TypeScript

import {AlertComponent, attachmentAdpator, makeTranslator, render, type Schema, ToastComponent} from 'amis'
import 'amis/lib/themes/antd.css'
import 'amis/lib/helper.css'
import 'amis/sdk/iconfont.css'
import '@fortawesome/fontawesome-free/css/all.min.css'
import axios from 'axios'
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'),
baseUrl: isEqual(import.meta.env.MODE, 'development') ? 'http://localhost:9786' : '',
}
const __ = makeTranslator('zh')
const responseAdaptor = () => (response: any) => {
let payload = response.data || {} // blob 下可能会返回内容为空?
if (payload.hasOwnProperty('errno')) {
payload.status = payload.errno
payload.msg = payload.errmsg
} else if (payload.hasOwnProperty('no')) {
payload.status = payload.no
payload.msg = payload.error
}
return {
...response,
data: payload,
}
}
export const amisRender = (schema: Schema, data: Record<any, any> = {}) => {
const theme = 'antd'
const locale = 'zh-CN'
return (
<>
<ToastComponent
theme={theme}
key="toast"
position={'top-right'}
locale={locale}
/>
<AlertComponent theme={theme} key="alert" locale={locale}/>
{render(
schema,
{
data: {
...commonInfo,
...data,
},
theme: theme,
},
{
enableAMISDebug: commonInfo.debug,
fetcher: async (api: any) => {
let {url, method, data, responseType, config, headers} = api
config = config || {}
config.url = url
config.withCredentials = true
responseType && (config.responseType = responseType)
if (config.cancelExecutor) {
config.cancelToken = new (axios as any).CancelToken(
config.cancelExecutor,
)
}
config.headers = headers || {}
config.method = method
config.data = data
if (method === 'get' && data) {
config.params = data
} else if (data && data instanceof FormData) {
// config.headers['Content-Type'] = 'multipart/form-data';
} else if (
data &&
typeof data !== 'string' &&
!(data instanceof Blob) &&
!(data instanceof ArrayBuffer)
) {
data = JSON.stringify(data)
config.headers['Content-Type'] = 'application/json'
}
// 支持返回各种报错信息
config.validateStatus = function () {
return true
}
let response = await axios(config)
response = await attachmentAdpator(response, __, api)
response = responseAdaptor()(response)
if (response.status >= 400) {
if (response.data) {
// 主要用于 raw: 模式下,后端自己校验登录,
if (
response.status === 401 &&
response.data.location &&
response.data.location.startsWith('http')
) {
location.href = response.data.location.replace(
'{{redirect}}',
encodeURIComponent(location.href),
)
return new Promise(() => {
})
} else if (response.data.msg) {
throw new Error(response.data.msg)
} else {
throw new Error(
'System.requestError' + JSON.stringify(response.data, null, 2),
)
}
} else {
throw new Error(
`${'System.requestErrorStatus'} ${response.status}`,
)
}
}
return response
},
isCancel: (value: any) => (axios as any).isCancel(value),
},
)}
</>
)
}
export function horizontalFormOptions() {
return {
mode: 'horizontal',
horizontal: {
leftFixed: 'sm',
},
}
}
export function crudCommonOptions() {
return {
affixHeader: false,
stopAutoRefreshWhenModalIsOpen: true,
resizable: false,
syncLocation: false,
silentPolling: true,
columnsTogglable: false,
}
}
export function readOnlyDialogOptions() {
return {
actions: [],
showCloseButton: false,
closeOnEsc: true,
closeOnOutside: true,
disabled: true,
}
}
export function paginationCommonOptions(perPage = true, maxButtons = 5) {
let option = {
type: 'pagination',
layout: [
'pager',
],
maxButtons: maxButtons,
showPageInput: false,
perPageAvailable: [10, 15, 20, 50, 100, 200],
}
if (perPage) {
option.layout.push('perPage')
}
return option
}
export function paginationTemplate(perPage = 20, maxButtons = 5, extraHeaders: Array<Schema | string> = [], extraFooters: Array<Schema | string> = []) {
return {
perPage: perPage,
headerToolbar: [
'reload',
paginationCommonOptions(true, maxButtons),
...extraHeaders,
],
footerToolbar: [
'statistics',
paginationCommonOptions(true, maxButtons),
...extraFooters,
],
}
}
export function copyField(field: string, tips = '复制', ignoreLength = 0) {
let tpl = ignoreLength === 0 ? `\${${field}}` : `\${TRUNCATE(${field}, ${ignoreLength})}`
return {
type: 'wrapper',
size: 'none',
body: [
{
type: 'tpl',
className: 'mr-1',
tpl: tpl,
},
{
type: 'action',
level: 'link',
label: '',
icon: 'fa fa-copy',
size: 'xs',
actionType: 'copy',
content: `\$${field}`,
tooltip: `${tips}`,
},
],
}
}
export function mappingItem(label: string, value: string, color = 'bg-info') {
return {
label: label,
value: value,
color: color,
}
}
export function mappingField(field: string, mapping: Array<Record<string, string>>) {
let mapData: Record<string, string> = {
'*': `<span class='label bg-gray-300'>\${${field}}</span>`,
}
mapping.forEach(item => {
mapData[item['value']] = `<span class='label ${item['color']}'>${item['label']}</span>`
})
return {
type: 'mapping',
value: `\${${field}}`,
map: mapData,
}
}
export function filterableField(mapping: Array<Record<string, any>>, multiple = false) {
return {
multiple: multiple,
options: [
...mapping,
],
}
}
export function time(field: string) {
return {
type: 'tpl',
tpl: `\${IF(${field}, DATETOSTR(${field}, 'YYYY-MM-DD HH:mm:ss'), '/')}`,
}
}
export function date(field: string) {
return {
type: 'tpl',
tpl: `\${IF(${field}, DATETOSTR(${field}, 'YYYY-MM-DD'), '/')}`,
}
}
export function pictureFromIds(field: string) {
return `\${ARRAYMAP(${field},id => '${commonInfo.baseUrl}/upload/download/' + id)}`
}
export const formInputFileStaticColumns = [
{
name: 'filename',
label: '文件名',
},
{
type: 'operation',
label: '操作',
width: 140,
buttons: [
{
type: 'action',
label: '预览',
level: 'link',
icon: 'fas fa-eye',
},
{
type: 'action',
label: '下载',
level: 'link',
icon: 'fa fa-download',
actionType: 'ajax',
// api: {
// ...apiGet('${base}/upload/download/${id}'),
// responseType: 'blob',
// }
},
],
},
]
export function formInputSingleFileStatic(field: string, label: string) {
return {
visibleOn: '${static}',
type: 'control',
label: label,
required: true,
body: {
type: 'table',
source: `\${${field}|asArray}`,
columns: formInputFileStaticColumns,
},
}
}
export function formInputMultiFileStatic(field: string, label: string) {
return {
visibleOn: '${static}',
type: 'input-table',
label: label,
name: field,
required: true,
resizable: false,
columns: formInputFileStaticColumns,
}
}
export function remoteOptions(type: string = 'select', name: string) {
return {
type: type,
source: `get:${commonInfo.baseUrl}/constants/options/${name}`,
}
}
export function remoteMappings(name: string, field: string) {
return {
type: 'mapping',
source: `get:${commonInfo.baseUrl}/constants/mappings/${name}/${field}`,
}
}
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}<br/>${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<ColumnSchema> = []) {
return [
{
name: 'code',
label: '编号',
width: 150,
},
{
name: 'name',
label: '简称',
width: 100,
},
{
name: 'fullname',
label: '全名',
},
{
name: 'market',
label: '市场',
width: 100,
align: 'center',
...remoteMappings('stock_market', 'market'),
},
{
name: 'industry',
label: '行业',
width: 80,
},
{
label: '上市日期',
width: 100,
align: 'center',
...date('listedDate'),
},
...extraColumns,
{
type: 'operation',
label: '操作',
width: 100,
buttons: [
{
type: 'action',
label: '详情',
level: 'link',
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'},
{
type: 'service',
api: `get:${commonInfo.baseUrl}/stock/daily/current/\${${idField}}`,
body: [
"现价 (${date})",
{
className: 'my-2',
type: 'property',
column: 4,
items: [
{label: '开盘价', content: '${open}'},
{label: '收盘价', content: '${close}'},
{label: '最高价', content: '${high}'},
{label: '最低价', content: '${low}'},
],
},
],
},
{
type: 'chart',
title: '100日线数据',
height: 500,
api: `get:${commonInfo.baseUrl}/stock/daily/\${${idField}}`,
config: {
title: {
text: '100日线数据',
subtext: '后复权数据',
},
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 `<div class="text-center font-bold mb-2">${param.name}</div>
<div class="text-center">
<span>开盘:</span>
<span class="font-bold ml-4">${open}</span>
</div>
<div class="text-center">
<span>收盘:</span>
<span class="font-bold ml-4">${close}</span>
</div>
<div class="text-center">
<span>最低:</span>
<span class="font-bold ml-4">${lowest}</span>
</div>
<div class="text-center">
<span>最高:</span>
<span class="font-bold ml-4">${highest}</span>
</div>`
},
},
grid: {
left: '2%',
right: '2%',
top: '15%',
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,
},
},
],
},
},
],
},
},
],
},
]
}