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 = {}) => { const theme = 'antd' const locale = 'zh-CN' return ( <> {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 = [], extraFooters: Array = []) { 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>) { let mapData: Record = { '*': `\${${field}}`, } mapping.forEach(item => { mapData[item['value']] = `${item['label']}` }) return { type: 'mapping', value: `\${${field}}`, map: mapData, } } export function filterableField(mapping: Array>, 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}
${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', 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 `
${param.name}
开盘: ${open}
收盘: ${close}
最低: ${lowest}
最高: ${highest}
` }, }, 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, }, }, ], }, }, ], }, }, ], }, ] }