diff --git a/src/components/StockChart/StockChartModal.js b/src/components/StockChart/StockChartModal.js.backup
similarity index 94%
rename from src/components/StockChart/StockChartModal.js
rename to src/components/StockChart/StockChartModal.js.backup
index 96d553fe..f4b0ca80 100644
--- a/src/components/StockChart/StockChartModal.js
+++ b/src/components/StockChart/StockChartModal.js.backup
@@ -7,6 +7,7 @@ import dayjs from 'dayjs';
import { stockService } from '../../services/eventService';
import { logger } from '../../utils/logger';
import RiskDisclaimer from '../RiskDisclaimer';
+import { RelationDescription } from '../StockRelation';
const StockChartModal = ({
isOpen,
@@ -24,27 +25,6 @@ const StockChartModal = ({
const [chartData, setChartData] = useState(null);
const [preloadedData, setPreloadedData] = useState({});
- // 处理关联描述(兼容对象和字符串格式)- 使用 useMemo 缓存计算结果
- const relationDesc = useMemo(() => {
- const desc = stock?.relation_desc;
-
- if (!desc) return null;
-
- if (typeof desc === 'string') {
- return desc;
- }
-
- if (typeof desc === 'object' && desc.data && Array.isArray(desc.data)) {
- // 新格式:{data: [{query_part: "...", sentences: "..."}]}
- return desc.data
- .map(item => item.query_part || item.sentences || '')
- .filter(s => s)
- .join(';') || null;
- }
-
- return null;
- }, [stock?.relation_desc]);
-
// 预加载数据
const preloadData = async (type) => {
if (!stock || preloadedData[type]) return;
@@ -563,21 +543,7 @@ const StockChartModal = ({
{/* 关联描述 */}
- {relationDesc && (
-
-
- 关联描述:
-
-
- {relationDesc}
-
-
- )}
+
{/* 风险提示 */}
diff --git a/src/components/StockChart/StockChartModal.tsx b/src/components/StockChart/StockChartModal.tsx
new file mode 100644
index 00000000..6ed58f59
--- /dev/null
+++ b/src/components/StockChart/StockChartModal.tsx
@@ -0,0 +1,689 @@
+// src/components/StockChart/StockChartModal.tsx - 统一的股票图表组件
+import React, { useState, useEffect, useRef } from 'react';
+import {
+ Modal,
+ ModalOverlay,
+ ModalContent,
+ ModalHeader,
+ ModalCloseButton,
+ ModalBody,
+ Button,
+ ButtonGroup,
+ VStack,
+ HStack,
+ Text,
+ Badge,
+ Box,
+ Flex,
+ CircularProgress,
+} from '@chakra-ui/react';
+import * as echarts from 'echarts';
+import type { EChartsOption, ECharts } from 'echarts';
+import dayjs from 'dayjs';
+import { stockService } from '../../services/eventService';
+import { logger } from '../../utils/logger';
+import RiskDisclaimer from '../RiskDisclaimer';
+import { RelationDescription } from '../StockRelation';
+import type { RelationDescType } from '../StockRelation';
+
+/**
+ * 图表类型
+ */
+type ChartType = 'timeline' | 'daily';
+
+/**
+ * K线数据项
+ */
+interface KLineDataItem {
+ time: string;
+ date?: string;
+ open: number;
+ high: number;
+ low: number;
+ close: number;
+ price?: number;
+ volume: number;
+ avg_price?: number;
+}
+
+/**
+ * 图表数据结构
+ */
+interface ChartData {
+ data: KLineDataItem[];
+ trade_date: string;
+ prev_close?: number;
+}
+
+/**
+ * 股票信息
+ */
+interface StockInfo {
+ stock_code: string;
+ stock_name?: string;
+ relation_desc?: RelationDescType;
+}
+
+/**
+ * StockChartModal 组件 Props
+ */
+export interface StockChartModalProps {
+ /** 模态框是否打开 */
+ isOpen: boolean;
+
+ /** 关闭回调 */
+ onClose: () => void;
+
+ /** 股票信息 */
+ stock: StockInfo | null;
+
+ /** 事件时间 */
+ eventTime?: string | null;
+
+ /** 是否使用 Chakra UI(保留字段,当前未使用) */
+ isChakraUI?: boolean;
+
+ /** 模态框大小 */
+ size?: string;
+
+ /** 初始图表类型 */
+ initialChartType?: ChartType;
+}
+
+/**
+ * ECharts 实例(带自定义 resizeHandler)
+ */
+interface EChartsInstance extends ECharts {
+ resizeHandler?: () => void;
+}
+
+const StockChartModal: React.FC = ({
+ isOpen,
+ onClose,
+ stock,
+ eventTime,
+ isChakraUI = true,
+ size = '6xl',
+ initialChartType = 'timeline',
+}) => {
+ const chartRef = useRef(null);
+ const chartInstanceRef = useRef(null);
+ const [chartType, setChartType] = useState(initialChartType);
+ const [loading, setLoading] = useState(false);
+ const [chartData, setChartData] = useState(null);
+ const [preloadedData, setPreloadedData] = useState>({
+ timeline: undefined,
+ daily: undefined,
+ });
+
+ // 预加载数据
+ const preloadData = async (type: ChartType): Promise => {
+ if (!stock || preloadedData[type]) return;
+
+ try {
+ let adjustedEventTime = eventTime;
+ if (eventTime) {
+ try {
+ const eventMoment = dayjs(eventTime);
+ if (eventMoment.isValid() && eventMoment.hour() >= 15) {
+ const nextDay = eventMoment.clone().add(1, 'day');
+ nextDay.hour(9).minute(30).second(0).millisecond(0);
+ adjustedEventTime = nextDay.format('YYYY-MM-DD HH:mm');
+ }
+ } catch (e) {
+ logger.warn('StockChartModal', '事件时间解析失败', {
+ eventTime,
+ error: (e as Error).message,
+ });
+ }
+ }
+
+ const response = await stockService.getKlineData(stock.stock_code, type, adjustedEventTime);
+ setPreloadedData((prev) => ({ ...prev, [type]: response }));
+ } catch (err) {
+ logger.error('StockChartModal', 'preloadData', err, {
+ stockCode: stock?.stock_code,
+ type,
+ });
+ }
+ };
+
+ useEffect(() => {
+ if (isOpen && stock) {
+ // 预加载两种图表类型的数据
+ preloadData('timeline');
+ preloadData('daily');
+
+ // 清理图表实例
+ return () => {
+ if (chartInstanceRef.current) {
+ window.removeEventListener('resize', chartInstanceRef.current.resizeHandler!);
+ chartInstanceRef.current.dispose();
+ chartInstanceRef.current = null;
+ }
+ };
+ }
+ }, [isOpen, stock, eventTime]);
+
+ useEffect(() => {
+ if (isOpen && stock) {
+ loadChartData(chartType);
+ }
+ }, [chartType, isOpen, stock]);
+
+ const loadChartData = async (type: ChartType): Promise => {
+ if (!stock) return;
+
+ try {
+ setLoading(true);
+
+ // 先尝试使用预加载的数据
+ let response = preloadedData[type];
+
+ if (!response) {
+ // 如果预加载数据不存在,则立即请求
+ let adjustedEventTime = eventTime;
+ if (eventTime) {
+ try {
+ const eventMoment = dayjs(eventTime);
+ if (eventMoment.isValid() && eventMoment.hour() >= 15) {
+ const nextDay = eventMoment.clone().add(1, 'day');
+ nextDay.hour(9).minute(30).second(0).millisecond(0);
+ adjustedEventTime = nextDay.format('YYYY-MM-DD HH:mm');
+ }
+ } catch (e) {
+ logger.warn('StockChartModal', '事件时间解析失败', {
+ eventTime,
+ error: (e as Error).message,
+ });
+ }
+ }
+
+ response = await stockService.getKlineData(stock.stock_code, type, adjustedEventTime);
+ }
+
+ setChartData(response);
+
+ // 初始化图表
+ if (chartRef.current && !chartInstanceRef.current) {
+ const chart = echarts.init(chartRef.current) as EChartsInstance;
+ chart.resizeHandler = () => chart.resize();
+ window.addEventListener('resize', chart.resizeHandler);
+ chartInstanceRef.current = chart;
+ }
+
+ if (chartInstanceRef.current) {
+ const option = generateChartOption(response, type, eventTime);
+ chartInstanceRef.current.setOption(option, true);
+ }
+ } catch (err) {
+ logger.error('StockChartModal', 'loadChartData', err, {
+ stockCode: stock?.stock_code,
+ chartType: type,
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const generateChartOption = (
+ data: ChartData,
+ type: ChartType,
+ originalEventTime?: string | null
+ ): EChartsOption => {
+ if (!data || !data.data || data.data.length === 0) {
+ return {
+ title: {
+ text: '暂无数据',
+ left: 'center',
+ top: 'center',
+ textStyle: { color: '#999', fontSize: 16 },
+ },
+ };
+ }
+
+ const stockData = data.data;
+ const tradeDate = data.trade_date;
+
+ // 分时图
+ if (type === 'timeline') {
+ const times = stockData.map((item) => item.time);
+ const prices = stockData.map((item) => item.close || item.price || 0);
+ const avgPrices = stockData.map((item) => item.avg_price || 0);
+ const volumes = stockData.map((item) => item.volume);
+
+ // 获取昨收盘价作为基准
+ const prevClose = data.prev_close || (prices.length > 0 ? prices[0] : 0);
+
+ // 计算涨跌幅数据
+ const changePercentData = prices.map((price) => ((price - prevClose) / prevClose) * 100);
+ const avgChangePercentData = avgPrices.map(
+ (avgPrice) => ((avgPrice - prevClose) / prevClose) * 100
+ );
+
+ const currentPrice = prices[prices.length - 1];
+ const currentChange = ((currentPrice - prevClose) / prevClose) * 100;
+ const isUp = currentChange >= 0;
+ const lineColor = isUp ? '#ef5350' : '#26a69a';
+
+ // 计算事件标记线位置
+ const eventMarkLineData: any[] = [];
+ if (originalEventTime && times.length > 0) {
+ const eventMoment = dayjs(originalEventTime);
+ const eventDate = eventMoment.format('YYYY-MM-DD');
+
+ if (eventDate === tradeDate) {
+ // 找到最接近的时间点
+ let nearestIdx = 0;
+ const eventMinutes = eventMoment.hour() * 60 + eventMoment.minute();
+
+ for (let i = 0; i < times.length; i++) {
+ const [h, m] = times[i].split(':').map(Number);
+ const timeMinutes = h * 60 + m;
+ const currentDiff = Math.abs(timeMinutes - eventMinutes);
+ const nearestDiff = Math.abs(
+ times[nearestIdx].split(':').map(Number)[0] * 60 +
+ times[nearestIdx].split(':').map(Number)[1] -
+ eventMinutes
+ );
+ if (currentDiff < nearestDiff) {
+ nearestIdx = i;
+ }
+ }
+
+ eventMarkLineData.push({
+ name: '事件发生',
+ xAxis: nearestIdx,
+ label: {
+ formatter: '事件发生',
+ position: 'middle',
+ color: '#FFD700',
+ fontSize: 12,
+ },
+ lineStyle: {
+ color: '#FFD700',
+ type: 'solid',
+ width: 2,
+ },
+ });
+ }
+ }
+
+ return {
+ title: {
+ text: `${stock!.stock_name || stock!.stock_code} - 分时图`,
+ left: 'center',
+ textStyle: { fontSize: 16, fontWeight: 'bold' },
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: { type: 'cross' },
+ formatter: function (params: any) {
+ if (!params || params.length === 0) return '';
+ const point = params[0];
+ const idx = point.dataIndex;
+ const priceChangePercent = ((prices[idx] - prevClose) / prevClose) * 100;
+ const avgChangePercent = ((avgPrices[idx] - prevClose) / prevClose) * 100;
+ const priceColor = priceChangePercent >= 0 ? '#ef5350' : '#26a69a';
+ const avgColor = avgChangePercent >= 0 ? '#ef5350' : '#26a69a';
+
+ return `时间:${times[idx]}
现价:¥${prices[
+ idx
+ ]?.toFixed(2)} (${priceChangePercent >= 0 ? '+' : ''}${priceChangePercent.toFixed(
+ 2
+ )}%)
均价:¥${avgPrices[idx]?.toFixed(
+ 2
+ )} (${avgChangePercent >= 0 ? '+' : ''}${avgChangePercent.toFixed(
+ 2
+ )}%)
昨收:¥${prevClose?.toFixed(2)}
成交量:${Math.round(
+ volumes[idx] / 100
+ )}手`;
+ },
+ },
+ grid: [
+ { left: '10%', right: '10%', height: '60%', top: '15%' },
+ { left: '10%', right: '10%', top: '80%', height: '15%' },
+ ],
+ xAxis: [
+ { type: 'category', data: times, gridIndex: 0, boundaryGap: false },
+ { type: 'category', data: times, gridIndex: 1, axisLabel: { show: false } },
+ ],
+ yAxis: [
+ {
+ type: 'value',
+ gridIndex: 0,
+ scale: false,
+ position: 'left',
+ axisLabel: {
+ formatter: function (value: number) {
+ return (value >= 0 ? '+' : '') + value.toFixed(2) + '%';
+ },
+ },
+ splitLine: {
+ show: true,
+ lineStyle: {
+ color: '#f0f0f0',
+ },
+ },
+ },
+ {
+ type: 'value',
+ gridIndex: 0,
+ scale: false,
+ position: 'right',
+ axisLabel: {
+ formatter: function (value: number) {
+ return (value >= 0 ? '+' : '') + value.toFixed(2) + '%';
+ },
+ },
+ },
+ {
+ type: 'value',
+ gridIndex: 1,
+ scale: true,
+ axisLabel: { formatter: (v: number) => Math.round(v / 100) + '手' },
+ },
+ ],
+ dataZoom: [
+ { type: 'inside', xAxisIndex: [0, 1], start: 0, end: 100 },
+ { show: true, xAxisIndex: [0, 1], type: 'slider', bottom: '0%' },
+ ],
+ series: [
+ {
+ name: '分时价',
+ type: 'line',
+ xAxisIndex: 0,
+ yAxisIndex: 0,
+ data: changePercentData,
+ smooth: true,
+ showSymbol: false,
+ lineStyle: { color: lineColor, width: 2 },
+ areaStyle: {
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+ {
+ offset: 0,
+ color: isUp ? 'rgba(239, 83, 80, 0.3)' : 'rgba(38, 166, 154, 0.3)',
+ },
+ {
+ offset: 1,
+ color: isUp ? 'rgba(239, 83, 80, 0.05)' : 'rgba(38, 166, 154, 0.05)',
+ },
+ ]),
+ },
+ markLine: {
+ symbol: 'none',
+ data: [
+ // 昨收盘价基准线 (0%)
+ {
+ yAxis: 0,
+ lineStyle: {
+ color: '#666',
+ type: 'dashed',
+ width: 1.5,
+ opacity: 0.8,
+ },
+ label: {
+ show: true,
+ formatter: '昨收盘价',
+ position: 'insideEndTop',
+ color: '#666',
+ fontSize: 12,
+ },
+ },
+ ...eventMarkLineData,
+ ],
+ animation: false,
+ },
+ },
+ {
+ name: '均价线',
+ type: 'line',
+ xAxisIndex: 0,
+ yAxisIndex: 1,
+ data: avgChangePercentData,
+ smooth: true,
+ showSymbol: false,
+ lineStyle: { color: '#FFA500', width: 1 },
+ },
+ {
+ name: '成交量',
+ type: 'bar',
+ xAxisIndex: 1,
+ yAxisIndex: 2,
+ data: volumes,
+ itemStyle: { color: '#b0c4de', opacity: 0.6 },
+ },
+ ],
+ };
+ }
+
+ // 日K线
+ if (type === 'daily') {
+ const dates = stockData.map((item) => item.time || item.date || '');
+ const klineData = stockData.map((item) => [item.open, item.close, item.low, item.high]);
+ const volumes = stockData.map((item) => item.volume);
+
+ // 计算事件标记线位置
+ const eventMarkLineData: any[] = [];
+ if (originalEventTime && dates.length > 0) {
+ const eventMoment = dayjs(originalEventTime);
+ const eventDate = eventMoment.format('YYYY-MM-DD');
+
+ // 找到事件发生日期或最接近的交易日
+ let targetIndex = -1;
+
+ // 1. 先尝试找到完全匹配的日期
+ targetIndex = dates.findIndex((date) => date === eventDate);
+
+ // 2. 如果没有完全匹配,找到第一个大于等于事件日期的交易日
+ if (targetIndex === -1) {
+ for (let i = 0; i < dates.length; i++) {
+ if (dates[i] >= eventDate) {
+ targetIndex = i;
+ break;
+ }
+ }
+ }
+
+ // 3. 如果事件日期晚于所有交易日,则标记在最后一个交易日
+ if (targetIndex === -1 && eventDate > dates[dates.length - 1]) {
+ targetIndex = dates.length - 1;
+ }
+
+ // 4. 如果事件日期早于所有交易日,则标记在第一个交易日
+ if (targetIndex === -1 && eventDate < dates[0]) {
+ targetIndex = 0;
+ }
+
+ if (targetIndex >= 0) {
+ let labelText = '事件发生';
+ let labelPosition: any = 'middle';
+
+ // 根据事件时间和交易日的关系调整标签
+ if (eventDate === dates[targetIndex]) {
+ if (eventMoment.hour() >= 15) {
+ labelText = '事件发生\n(盘后)';
+ } else if (eventMoment.hour() < 9 || (eventMoment.hour() === 9 && eventMoment.minute() < 30)) {
+ labelText = '事件发生\n(盘前)';
+ }
+ } else if (eventDate < dates[targetIndex]) {
+ labelText = '事件发生\n(前一日)';
+ labelPosition = 'start';
+ } else {
+ labelText = '事件发生\n(影响日)';
+ labelPosition = 'end';
+ }
+
+ eventMarkLineData.push({
+ name: '事件发生',
+ xAxis: targetIndex,
+ label: {
+ formatter: labelText,
+ position: labelPosition,
+ color: '#FFD700',
+ fontSize: 12,
+ backgroundColor: 'rgba(0,0,0,0.5)',
+ padding: [4, 8],
+ borderRadius: 4,
+ },
+ lineStyle: {
+ color: '#FFD700',
+ type: 'solid',
+ width: 2,
+ },
+ });
+ }
+ }
+
+ return {
+ title: {
+ text: `${stock!.stock_name || stock!.stock_code} - 日K线`,
+ left: 'center',
+ textStyle: { fontSize: 16, fontWeight: 'bold' },
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: { type: 'cross' },
+ formatter: function (params: any) {
+ if (!params || params.length === 0) return '';
+ const kline = params[0];
+ const volume = params[1];
+ if (!kline || !kline.data) return '';
+
+ let tooltipHtml = `日期: ${kline.axisValue}
开盘: ¥${kline.data[0]}
收盘: ¥${kline.data[1]}
最低: ¥${kline.data[2]}
最高: ¥${kline.data[3]}`;
+
+ if (volume && volume.data) {
+ tooltipHtml += `
成交量: ${Math.round(volume.data / 100)}手`;
+ }
+
+ return tooltipHtml;
+ },
+ },
+ grid: [
+ { left: '10%', right: '10%', height: '60%' },
+ { left: '10%', right: '10%', top: '75%', height: '20%' },
+ ],
+ xAxis: [
+ { type: 'category', data: dates, boundaryGap: true, gridIndex: 0 },
+ { type: 'category', data: dates, gridIndex: 1, axisLabel: { show: false } },
+ ],
+ yAxis: [
+ { type: 'value', scale: true, splitArea: { show: true }, gridIndex: 0 },
+ {
+ scale: true,
+ gridIndex: 1,
+ axisLabel: { formatter: (value: number) => Math.round(value / 100) + '手' },
+ },
+ ],
+ dataZoom: [
+ { type: 'inside', xAxisIndex: [0, 1], start: 70, end: 100 },
+ { show: true, xAxisIndex: [0, 1], type: 'slider', bottom: '0%', start: 70, end: 100 },
+ ],
+ series: [
+ {
+ name: 'K线',
+ type: 'candlestick',
+ yAxisIndex: 0,
+ data: klineData,
+ markLine: {
+ symbol: 'none',
+ data: eventMarkLineData,
+ animation: false,
+ },
+ itemStyle: {
+ color: '#ef5350',
+ color0: '#26a69a',
+ borderColor: '#ef5350',
+ borderColor0: '#26a69a',
+ },
+ },
+ {
+ name: '成交量',
+ type: 'bar',
+ xAxisIndex: 1,
+ yAxisIndex: 1,
+ data: volumes.map((volume, index) => ({
+ value: volume,
+ itemStyle: {
+ color: stockData[index].close >= stockData[index].open ? '#ef5350' : '#26a69a',
+ },
+ })),
+ },
+ ],
+ };
+ }
+
+ return {};
+ };
+
+ if (!stock) return null;
+
+ return (
+
+
+
+
+
+
+
+ {stock.stock_name || stock.stock_code} ({stock.stock_code}) - 股票详情
+
+ {chartData && {chartData.trade_date}}
+
+
+
+
+
+
+
+
+
+ {/* 图表区域 */}
+
+ {loading && (
+
+
+
+ 加载图表数据...
+
+
+ )}
+
+
+
+ {/* 关联描述 */}
+
+
+ {/* 风险提示 */}
+
+
+
+
+
+
+ );
+};
+
+export default StockChartModal;
diff --git a/src/components/StockRelation/RelationDescription.tsx b/src/components/StockRelation/RelationDescription.tsx
new file mode 100644
index 00000000..10f0f35e
--- /dev/null
+++ b/src/components/StockRelation/RelationDescription.tsx
@@ -0,0 +1,121 @@
+/**
+ * 关联描述组件
+ *
+ * 用于显示股票与事件的关联描述信息
+ * 固定标题为"关联描述:"
+ * 自动处理多种数据格式(字符串、对象数组)
+ *
+ * @example
+ * ```tsx
+ * // 基础使用 - 传入原始 relation_desc 数据
+ *
+ *
+ * // 自定义样式
+ *
+ * ```
+ */
+
+import React, { useMemo } from 'react';
+import { Box, Text, BoxProps } from '@chakra-ui/react';
+
+/**
+ * 关联描述数据类型
+ * - 字符串格式:直接的描述文本
+ * - 对象格式:包含多个句子的数组
+ */
+export type RelationDescType =
+ | string
+ | {
+ data: Array<{
+ query_part?: string;
+ sentences?: string;
+ }>;
+ }
+ | null
+ | undefined;
+
+export interface RelationDescriptionProps {
+ /** 原始关联描述数据(支持字符串或对象格式) */
+ relationDesc: RelationDescType;
+
+ /** 字体大小,默认 'sm' */
+ fontSize?: string;
+
+ /** 标题颜色,默认 'gray.700' */
+ titleColor?: string;
+
+ /** 文本颜色,默认 'gray.600' */
+ textColor?: string;
+
+ /** 行高,默认 '1.7' */
+ lineHeight?: string;
+
+ /** 容器额外属性 */
+ containerProps?: BoxProps;
+}
+
+export const RelationDescription: React.FC = ({
+ relationDesc,
+ fontSize = 'sm',
+ titleColor = 'gray.700',
+ textColor = 'gray.600',
+ lineHeight = '1.7',
+ containerProps = {}
+}) => {
+ // 处理关联描述(兼容对象和字符串格式)
+ const processedDesc = useMemo(() => {
+ if (!relationDesc) return null;
+
+ // 字符串格式:直接返回
+ if (typeof relationDesc === 'string') {
+ return relationDesc;
+ }
+
+ // 对象格式:提取并拼接文本
+ if (typeof relationDesc === 'object' && relationDesc.data && Array.isArray(relationDesc.data)) {
+ return (
+ relationDesc.data
+ .map((item) => item.query_part || item.sentences || '')
+ .filter((s) => s)
+ .join(';') || null
+ );
+ }
+
+ return null;
+ }, [relationDesc]);
+
+ // 如果没有有效的描述内容,不渲染组件
+ if (!processedDesc) {
+ return null;
+ }
+
+ return (
+
+
+ 关联描述:
+
+
+ {processedDesc}
+
+
+ );
+};
diff --git a/src/components/StockRelation/index.ts b/src/components/StockRelation/index.ts
new file mode 100644
index 00000000..890b0970
--- /dev/null
+++ b/src/components/StockRelation/index.ts
@@ -0,0 +1,6 @@
+/**
+ * StockRelation 组件导出入口
+ */
+
+export { RelationDescription } from './RelationDescription';
+export type { RelationDescriptionProps, RelationDescType } from './RelationDescription';