that wraps the table */
- position: relative;
- z-index: 1; /* container inner z-index's because
s can't do it */
- }
-.fc .fc-daygrid-day.fc-day-today {
- background-color: rgba(255, 220, 40, 0.15);
- background-color: var(--fc-today-bg-color, rgba(255, 220, 40, 0.15));
- }
-.fc .fc-daygrid-day-frame {
- position: relative;
- min-height: 100%; /* seems to work better than `height` because sets height after rows/cells naturally do it */
- }
-.fc {
-
- /* cell top */
-
-}
-.fc .fc-daygrid-day-top {
- display: flex;
- flex-direction: row-reverse;
- }
-.fc .fc-day-other .fc-daygrid-day-top {
- opacity: 0.3;
- }
-.fc {
-
- /* day number (within cell top) */
-
-}
-.fc .fc-daygrid-day-number {
- position: relative;
- z-index: 4;
- padding: 4px;
- }
-.fc {
-
- /* event container */
-
-}
-.fc .fc-daygrid-day-events {
- margin-top: 1px; /* needs to be margin, not padding, so that available cell height can be computed */
- }
-.fc {
-
- /* positioning for balanced vs natural */
-
-}
-.fc .fc-daygrid-body-balanced .fc-daygrid-day-events {
- position: absolute;
- left: 0;
- right: 0;
- }
-.fc .fc-daygrid-body-unbalanced .fc-daygrid-day-events {
- position: relative; /* for containing abs positioned event harnesses */
- min-height: 2em; /* in addition to being a min-height during natural height, equalizes the heights a little bit */
- }
-.fc .fc-daygrid-body-natural { /* can coexist with -unbalanced */
- }
-.fc .fc-daygrid-body-natural .fc-daygrid-day-events {
- margin-bottom: 1em;
- }
-.fc {
-
- /* event harness */
-
-}
-.fc .fc-daygrid-event-harness {
- position: relative;
- }
-.fc .fc-daygrid-event-harness-abs {
- position: absolute;
- top: 0; /* fallback coords for when cannot yet be computed */
- left: 0; /* */
- right: 0; /* */
- }
-.fc .fc-daygrid-bg-harness {
- position: absolute;
- top: 0;
- bottom: 0;
- }
-.fc {
-
- /* bg content */
-
-}
-.fc .fc-daygrid-day-bg .fc-non-business { z-index: 1 }
-.fc .fc-daygrid-day-bg .fc-bg-event { z-index: 2 }
-.fc .fc-daygrid-day-bg .fc-highlight { z-index: 3 }
-.fc {
-
- /* events */
-
-}
-.fc .fc-daygrid-event {
- z-index: 6;
- margin-top: 1px;
- }
-.fc .fc-daygrid-event.fc-event-mirror {
- z-index: 7;
- }
-.fc {
-
- /* cell bottom (within day-events) */
-
-}
-.fc .fc-daygrid-day-bottom {
- font-size: .85em;
- margin: 2px 3px 0;
- }
-.fc .fc-daygrid-more-link {
- position: relative;
- z-index: 4;
- cursor: pointer;
- }
-.fc {
-
- /* week number (within frame) */
-
-}
-.fc .fc-daygrid-week-number {
- position: absolute;
- z-index: 5;
- top: 0;
- padding: 2px;
- min-width: 1.5em;
- text-align: center;
- background-color: rgba(208, 208, 208, 0.3);
- background-color: var(--fc-neutral-bg-color, rgba(208, 208, 208, 0.3));
- color: #808080;
- color: var(--fc-neutral-text-color, #808080);
- }
-.fc {
-
- /* popover */
-
-}
-.fc .fc-more-popover {
- z-index: 8;
- }
-.fc .fc-more-popover .fc-popover-body {
- min-width: 220px;
- padding: 10px;
- }
-.fc-direction-ltr .fc-daygrid-event.fc-event-start,
-.fc-direction-rtl .fc-daygrid-event.fc-event-end {
- margin-left: 2px;
-}
-.fc-direction-ltr .fc-daygrid-event.fc-event-end,
-.fc-direction-rtl .fc-daygrid-event.fc-event-start {
- margin-right: 2px;
-}
-.fc-direction-ltr .fc-daygrid-week-number {
- left: 0;
- border-radius: 0 0 3px 0;
- }
-.fc-direction-rtl .fc-daygrid-week-number {
- right: 0;
- border-radius: 0 0 0 3px;
- }
-.fc-liquid-hack .fc-daygrid-day-frame {
- position: static; /* will cause inner absolute stuff to expand to | */
- }
-.fc-daygrid-event { /* make root-level, because will be dragged-and-dropped outside of a component root */
- position: relative; /* for z-indexes assigned later */
- white-space: nowrap;
- border-radius: 3px; /* dot event needs this to when selected */
- font-size: .85em;
- font-size: var(--fc-small-font-size, .85em);
-}
-/* --- the rectangle ("block") style of event --- */
-.fc-daygrid-block-event .fc-event-time {
- font-weight: bold;
- }
-.fc-daygrid-block-event .fc-event-time,
- .fc-daygrid-block-event .fc-event-title {
- padding: 1px;
- }
-/* --- the dot style of event --- */
-.fc-daygrid-dot-event {
- display: flex;
- align-items: center;
- padding: 2px 0
-
-}
-.fc-daygrid-dot-event .fc-event-title {
- flex-grow: 1;
- flex-shrink: 1;
- min-width: 0; /* important for allowing to shrink all the way */
- overflow: hidden;
- font-weight: bold;
- }
-.fc-daygrid-dot-event:hover,
- .fc-daygrid-dot-event.fc-event-mirror {
- background: rgba(0, 0, 0, 0.1);
- }
-.fc-daygrid-dot-event.fc-event-selected:before {
- /* expand hit area */
- top: -10px;
- bottom: -10px;
- }
-.fc-daygrid-event-dot { /* the actual dot */
- margin: 0 4px;
- box-sizing: content-box;
- width: 0;
- height: 0;
- border: 4px solid #3788d8;
- border: calc(var(--fc-daygrid-event-dot-width, 8px) / 2) solid var(--fc-event-border-color, #3788d8);
- border-radius: 4px;
- border-radius: calc(var(--fc-daygrid-event-dot-width, 8px) / 2);
-}
-/* --- spacing between time and title --- */
-.fc-direction-ltr .fc-daygrid-event .fc-event-time {
- margin-right: 3px;
- }
-.fc-direction-rtl .fc-daygrid-event .fc-event-time {
- margin-left: 3px;
- }
-
-
-/*
-A VERTICAL event
-*/
-
-.fc-v-event { /* allowed to be top-level */
- display: block;
- border: 1px solid #3788d8;
- border: 1px solid var(--fc-event-border-color, #3788d8);
- background-color: #3788d8;
- background-color: var(--fc-event-bg-color, #3788d8)
-
-}
-
-.fc-v-event .fc-event-main {
- color: #fff;
- color: var(--fc-event-text-color, #fff);
- height: 100%;
- }
-
-.fc-v-event .fc-event-main-frame {
- height: 100%;
- display: flex;
- flex-direction: column;
- }
-
-.fc-v-event .fc-event-time {
- flex-grow: 0;
- flex-shrink: 0;
- max-height: 100%;
- overflow: hidden;
- }
-
-.fc-v-event .fc-event-title-container { /* a container for the sticky cushion */
- flex-grow: 1;
- flex-shrink: 1;
- min-height: 0; /* important for allowing to shrink all the way */
- }
-
-.fc-v-event .fc-event-title { /* will have fc-sticky on it */
- top: 0;
- bottom: 0;
- max-height: 100%; /* clip overflow */
- overflow: hidden;
- }
-
-.fc-v-event:not(.fc-event-start) {
- border-top-width: 0;
- border-top-left-radius: 0;
- border-top-right-radius: 0;
- }
-
-.fc-v-event:not(.fc-event-end) {
- border-bottom-width: 0;
- border-bottom-left-radius: 0;
- border-bottom-right-radius: 0;
- }
-
-.fc-v-event.fc-event-selected:before {
- /* expand hit area */
- left: -10px;
- right: -10px;
- }
-
-.fc-v-event {
-
- /* resizer (mouse AND touch) */
-
-}
-
-.fc-v-event .fc-event-resizer-start {
- cursor: n-resize;
- }
-
-.fc-v-event .fc-event-resizer-end {
- cursor: s-resize;
- }
-
-.fc-v-event {
-
- /* resizer for MOUSE */
-
-}
-
-.fc-v-event:not(.fc-event-selected) .fc-event-resizer {
- height: 8px;
- height: var(--fc-event-resizer-thickness, 8px);
- left: 0;
- right: 0;
- }
-
-.fc-v-event:not(.fc-event-selected) .fc-event-resizer-start {
- top: -4px;
- top: calc(var(--fc-event-resizer-thickness, 8px) / -2);
- }
-
-.fc-v-event:not(.fc-event-selected) .fc-event-resizer-end {
- bottom: -4px;
- bottom: calc(var(--fc-event-resizer-thickness, 8px) / -2);
- }
-
-.fc-v-event {
-
- /* resizer for TOUCH (when event is "selected") */
-
-}
-
-.fc-v-event.fc-event-selected .fc-event-resizer {
- left: 50%;
- margin-left: -4px;
- margin-left: calc(var(--fc-event-resizer-dot-total-width, 8px) / -2);
- }
-
-.fc-v-event.fc-event-selected .fc-event-resizer-start {
- top: -4px;
- top: calc(var(--fc-event-resizer-dot-total-width, 8px) / -2);
- }
-
-.fc-v-event.fc-event-selected .fc-event-resizer-end {
- bottom: -4px;
- bottom: calc(var(--fc-event-resizer-dot-total-width, 8px) / -2);
- }
-.fc .fc-timegrid .fc-daygrid-body { /* the all-day daygrid within the timegrid view */
- z-index: 2; /* put above the timegrid-body so that more-popover is above everything. TODO: better solution */
- }
-.fc .fc-timegrid-divider {
- padding: 0 0 2px; /* browsers get confused when you set height. use padding instead */
- }
-.fc .fc-timegrid-body {
- position: relative;
- z-index: 1; /* scope the z-indexes of slots and cols */
- min-height: 100%; /* fill height always, even when slat table doesn't grow */
- }
-.fc .fc-timegrid-axis-chunk { /* for advanced ScrollGrid */
- position: relative /* offset parent for now-indicator-container */
-
- }
-.fc .fc-timegrid-axis-chunk > table {
- position: relative;
- z-index: 1; /* above the now-indicator-container */
- }
-.fc .fc-timegrid-slots {
- position: relative;
- z-index: 1;
- }
-.fc .fc-timegrid-slot { /* a | */
- height: 1.5em;
- border-bottom: 0 /* each cell owns its top border */
- }
-.fc .fc-timegrid-slot:empty:before {
- content: '\00a0'; /* make sure there's at least an empty space to create height for height syncing */
- }
-.fc .fc-timegrid-slot-minor {
- border-top-style: dotted;
- }
-.fc .fc-timegrid-slot-label-cushion {
- display: inline-block;
- white-space: nowrap;
- }
-.fc .fc-timegrid-slot-label {
- vertical-align: middle; /* vertical align the slots */
- }
-.fc {
-
-
- /* slots AND axis cells (top-left corner of view including the "all-day" text) */
-
-}
-.fc .fc-timegrid-axis-cushion,
- .fc .fc-timegrid-slot-label-cushion {
- padding: 0 4px;
- }
-.fc {
-
-
- /* axis cells (top-left corner of view including the "all-day" text) */
- /* vertical align is more complicated, uses flexbox */
-
-}
-.fc .fc-timegrid-axis-frame-liquid {
- height: 100%; /* will need liquid-hack in FF */
- }
-.fc .fc-timegrid-axis-frame {
- overflow: hidden;
- display: flex;
- align-items: center; /* vertical align */
- justify-content: flex-end; /* horizontal align. matches text-align below */
- }
-.fc .fc-timegrid-axis-cushion {
- max-width: 60px; /* limits the width of the "all-day" text */
- flex-shrink: 0; /* allows text to expand how it normally would, regardless of constrained width */
- }
-.fc-direction-ltr .fc-timegrid-slot-label-frame {
- text-align: right;
- }
-.fc-direction-rtl .fc-timegrid-slot-label-frame {
- text-align: left;
- }
-.fc-liquid-hack .fc-timegrid-axis-frame-liquid {
- height: auto;
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- }
-.fc .fc-timegrid-col.fc-day-today {
- background-color: rgba(255, 220, 40, 0.15);
- background-color: var(--fc-today-bg-color, rgba(255, 220, 40, 0.15));
- }
-.fc .fc-timegrid-col-frame {
- min-height: 100%; /* liquid-hack is below */
- position: relative;
- }
-.fc-liquid-hack .fc-timegrid-col-frame {
- height: auto;
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- }
-.fc-media-screen .fc-timegrid-cols {
- position: absolute; /* no z-index. children will decide and go above slots */
- top: 0;
- left: 0;
- right: 0;
- bottom: 0
- }
-.fc-media-screen .fc-timegrid-cols > table {
- height: 100%;
- }
-.fc-media-screen .fc-timegrid-col-bg,
- .fc-media-screen .fc-timegrid-col-events,
- .fc-media-screen .fc-timegrid-now-indicator-container {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- }
-.fc-media-screen .fc-timegrid-event-harness {
- position: absolute; /* top/left/right/bottom will all be set by JS */
- }
-.fc {
-
- /* bg */
-
-}
-.fc .fc-timegrid-col-bg {
- z-index: 2; /* TODO: kill */
- }
-.fc .fc-timegrid-col-bg .fc-non-business { z-index: 1 }
-.fc .fc-timegrid-col-bg .fc-bg-event { z-index: 2 }
-.fc .fc-timegrid-col-bg .fc-highlight { z-index: 3 }
-.fc .fc-timegrid-bg-harness {
- position: absolute; /* top/bottom will be set by JS */
- left: 0;
- right: 0;
- }
-.fc {
-
- /* fg events */
- /* (the mirror segs are put into a separate container with same classname, */
- /* and they must be after the normal seg container to appear at a higher z-index) */
-
-}
-.fc .fc-timegrid-col-events {
- z-index: 3;
- /* child event segs have z-indexes that are scoped within this div */
- }
-.fc {
-
- /* now indicator */
-
-}
-.fc .fc-timegrid-now-indicator-container {
- bottom: 0;
- overflow: hidden; /* don't let overflow of lines/arrows cause unnecessary scrolling */
- /* z-index is set on the individual elements */
- }
-.fc-direction-ltr .fc-timegrid-col-events {
- margin: 0 2.5% 0 2px;
- }
-.fc-direction-rtl .fc-timegrid-col-events {
- margin: 0 2px 0 2.5%;
- }
-.fc-timegrid-event-harness-inset .fc-timegrid-event,
-.fc-timegrid-event.fc-event-mirror {
- box-shadow: 0px 0px 0px 1px #fff;
- box-shadow: 0px 0px 0px 1px var(--fc-page-bg-color, #fff);
-}
-.fc-timegrid-event { /* events need to be root */
-
- font-size: .85em;
-
- font-size: var(--fc-small-font-size, .85em);
- border-radius: 3px
-
-}
-.fc-timegrid-event .fc-event-main {
- padding: 1px 1px 0;
- }
-.fc-timegrid-event .fc-event-time {
- white-space: nowrap;
- font-size: .85em;
- font-size: var(--fc-small-font-size, .85em);
- margin-bottom: 1px;
- }
-.fc-timegrid-event-condensed .fc-event-main-frame {
- flex-direction: row;
- overflow: hidden;
- }
-.fc-timegrid-event-condensed .fc-event-time:after {
- content: '\00a0-\00a0'; /* dash surrounded by non-breaking spaces */
- }
-.fc-timegrid-event-condensed .fc-event-title {
- font-size: .85em;
- font-size: var(--fc-small-font-size, .85em)
- }
-.fc-media-screen .fc-timegrid-event {
- position: absolute; /* absolute WITHIN the harness */
- top: 0;
- bottom: 1px; /* stay away from bottom slot line */
- left: 0;
- right: 0;
- }
-.fc {
-
- /* line */
-
-}
-.fc .fc-timegrid-now-indicator-line {
- position: absolute;
- z-index: 4;
- left: 0;
- right: 0;
- border-style: solid;
- border-color: red;
- border-color: var(--fc-now-indicator-color, red);
- border-width: 1px 0 0;
- }
-.fc {
-
- /* arrow */
-
-}
-.fc .fc-timegrid-now-indicator-arrow {
- position: absolute;
- z-index: 4;
- margin-top: -5px; /* vertically center on top coordinate */
- border-style: solid;
- border-color: red;
- border-color: var(--fc-now-indicator-color, red);
- }
-.fc-direction-ltr .fc-timegrid-now-indicator-arrow {
- left: 0;
-
- /* triangle pointing right. TODO: mixin */
- border-width: 5px 0 5px 6px;
- border-top-color: transparent;
- border-bottom-color: transparent;
- }
-.fc-direction-rtl .fc-timegrid-now-indicator-arrow {
- right: 0;
-
- /* triangle pointing left. TODO: mixin */
- border-width: 5px 6px 5px 0;
- border-top-color: transparent;
- border-bottom-color: transparent;
- }
-
-
-:root {
- --fc-list-event-dot-width: 10px;
- --fc-list-event-hover-bg-color: #f5f5f5;
-}
-.fc-theme-standard .fc-list {
- border: 1px solid #ddd;
- border: 1px solid var(--fc-border-color, #ddd);
- }
-.fc {
-
- /* message when no events */
-
-}
-.fc .fc-list-empty {
- background-color: rgba(208, 208, 208, 0.3);
- background-color: var(--fc-neutral-bg-color, rgba(208, 208, 208, 0.3));
- height: 100%;
- display: flex;
- justify-content: center;
- align-items: center; /* vertically aligns fc-list-empty-inner */
- }
-.fc .fc-list-empty-cushion {
- margin: 5em 0;
- }
-.fc {
-
- /* table within the scroller */
- /* ---------------------------------------------------------------------------------------------------- */
-
-}
-.fc .fc-list-table {
- width: 100%;
- border-style: hidden; /* kill outer border on theme */
- }
-.fc .fc-list-table tr > * {
- border-left: 0;
- border-right: 0;
- }
-.fc .fc-list-sticky .fc-list-day > * { /* the cells */
- position: -webkit-sticky;
- position: sticky;
- top: 0;
- background: #fff;
- background: var(--fc-page-bg-color, #fff); /* for when headers are styled to be transparent and sticky */
- }
-.fc .fc-list-table th {
- padding: 0; /* uses an inner-wrapper instead... */
- }
-.fc .fc-list-table td,
- .fc .fc-list-day-cushion {
- padding: 8px 14px;
- }
-.fc {
-
-
- /* date heading rows */
- /* ---------------------------------------------------------------------------------------------------- */
-
-}
-.fc .fc-list-day-cushion:after {
- content: "";
- clear: both;
- display: table; /* clear floating */
- }
-.fc-theme-standard .fc-list-day-cushion {
- background-color: rgba(208, 208, 208, 0.3);
- background-color: var(--fc-neutral-bg-color, rgba(208, 208, 208, 0.3));
- }
-.fc-direction-ltr .fc-list-day-text,
-.fc-direction-rtl .fc-list-day-side-text {
- float: left;
-}
-.fc-direction-ltr .fc-list-day-side-text,
-.fc-direction-rtl .fc-list-day-text {
- float: right;
-}
-/* make the dot closer to the event title */
-.fc-direction-ltr .fc-list-table .fc-list-event-graphic { padding-right: 0 }
-.fc-direction-rtl .fc-list-table .fc-list-event-graphic { padding-left: 0 }
-.fc .fc-list-event.fc-event-forced-url {
- cursor: pointer; /* whole row will seem clickable */
- }
-.fc .fc-list-event:hover td {
- background-color: #f5f5f5;
- background-color: var(--fc-list-event-hover-bg-color, #f5f5f5);
- }
-.fc {
-
- /* shrink certain cols */
-
-}
-.fc .fc-list-event-graphic,
- .fc .fc-list-event-time {
- white-space: nowrap;
- width: 1px;
- }
-.fc .fc-list-event-dot {
- display: inline-block;
- box-sizing: content-box;
- width: 0;
- height: 0;
- border: 5px solid #3788d8;
- border: calc(var(--fc-list-event-dot-width, 10px) / 2) solid var(--fc-event-border-color, #3788d8);
- border-radius: 5px;
- border-radius: calc(var(--fc-list-event-dot-width, 10px) / 2);
- }
-.fc {
-
- /* reset styling */
-
-}
-.fc .fc-list-event-title a {
- color: inherit;
- text-decoration: none;
- }
-.fc {
-
- /* underline link when hovering over any part of row */
-
-}
-.fc .fc-list-event.fc-event-forced-url:hover a {
- text-decoration: underline;
- }
-
-
-
- .fc-theme-bootstrap a:not([href]) {
- color: inherit; /* natural color for navlinks */
- }
diff --git a/src/assets/scss/soft-design-system-pro/plugins/pro/plugins-extend.scss b/src/assets/scss/soft-design-system-pro/plugins/pro/plugins-extend.scss
index 0e1b485a..00354f90 100755
--- a/src/assets/scss/soft-design-system-pro/plugins/pro/plugins-extend.scss
+++ b/src/assets/scss/soft-design-system-pro/plugins/pro/plugins-extend.scss
@@ -3,8 +3,6 @@
@import "choices";
@import "glidejs";
@import "green-audio-player";
-@import "fullcalendar";
-@import "fullcalendar-extend";
@import "datatable";
@import "datatable-extend";
@import "dragula";
diff --git a/src/bytedesk-integration/components/BytedeskWidget.jsx b/src/bytedesk-integration/components/BytedeskWidget.jsx
index af2eddcf..9fac11d3 100644
--- a/src/bytedesk-integration/components/BytedeskWidget.jsx
+++ b/src/bytedesk-integration/components/BytedeskWidget.jsx
@@ -19,6 +19,10 @@ import PropTypes from 'prop-types';
let widgetInitialized = false;
let idleCallbackId = null;
+// ⚡ STOMP WebSocket 重连限制(最多重试 3 次)
+let stompRetryCount = 0;
+const MAX_STOMP_RETRIES = 3;
+
const BytedeskWidget = ({
config,
autoLoad = true,
@@ -118,18 +122,79 @@ const BytedeskWidget = ({
// 5 秒后停止监听(避免性能问题)
setTimeout(() => observer.disconnect(), 5000);
- // ⚡ 屏蔽 STOMP WebSocket 错误日志(不影响功能)
+ // ⚡ 屏蔽 STOMP WebSocket 相关日志(控制台降噪)
+ // STOMP 连接失败是因为后端服务配置问题,不影响客服功能(使用 HTTP 轮询降级)
+ const isStompLog = (args) => {
+ const msg = args.map(a => String(a)).join(' ');
+ // 只屏蔽包含 /stomp 路径的日志,避免误屏蔽其他 WebSocket 日志
+ return msg.includes('/stomp') || msg.includes('stomp onWebSocketError');
+ };
+
+ // 屏蔽 console.error
const originalConsoleError = console.error;
console.error = function(...args) {
- const errorMsg = args.join(' ');
- if (errorMsg.includes('/stomp') ||
- errorMsg.includes('stomp onWebSocketError') ||
- (errorMsg.includes('WebSocket connection to') && errorMsg.includes('/stomp'))) {
- return;
- }
+ if (isStompLog(args)) return;
originalConsoleError.apply(console, args);
};
+ // 屏蔽 console.warn
+ const originalConsoleWarn = console.warn;
+ console.warn = function(...args) {
+ if (isStompLog(args)) return;
+ originalConsoleWarn.apply(console, args);
+ };
+
+ // 屏蔽 console.log(仅屏蔽 STOMP 相关)
+ const originalConsoleLog = console.log;
+ console.log = function(...args) {
+ if (isStompLog(args)) return;
+ originalConsoleLog.apply(console, args);
+ };
+
+ // ⚡ 限制 STOMP WebSocket 重连次数(最多 3 次)
+ // 通过代理 WebSocket 构造函数实现
+ const OriginalWebSocket = window.WebSocket;
+ const WebSocketProxy = function(url, protocols) {
+ // 检查是否是 STOMP 连接
+ if (url && url.includes('/stomp')) {
+ stompRetryCount++;
+
+ // 超过最大重试次数,阻止连接
+ if (stompRetryCount > MAX_STOMP_RETRIES) {
+ // 返回一个假的 WebSocket 对象,不实际连接
+ const fakeWs = Object.create(OriginalWebSocket.prototype);
+ fakeWs.url = url;
+ fakeWs.readyState = 3; // CLOSED
+ fakeWs.send = () => {};
+ fakeWs.close = () => {};
+ fakeWs.addEventListener = () => {};
+ fakeWs.removeEventListener = () => {};
+ fakeWs.onopen = null;
+ fakeWs.onclose = null;
+ fakeWs.onerror = null;
+ fakeWs.onmessage = null;
+ // 异步触发 onerror 和 onclose
+ setTimeout(() => {
+ if (fakeWs.onerror) fakeWs.onerror(new Event('error'));
+ if (fakeWs.onclose) fakeWs.onclose(new CloseEvent('close', { code: 1006 }));
+ }, 0);
+ return fakeWs;
+ }
+ }
+
+ // 正常创建 WebSocket
+ return protocols
+ ? new OriginalWebSocket(url, protocols)
+ : new OriginalWebSocket(url);
+ };
+ // 保留原始 WebSocket 的静态属性和原型链
+ WebSocketProxy.prototype = OriginalWebSocket.prototype;
+ WebSocketProxy.CONNECTING = OriginalWebSocket.CONNECTING;
+ WebSocketProxy.OPEN = OriginalWebSocket.OPEN;
+ WebSocketProxy.CLOSING = OriginalWebSocket.CLOSING;
+ WebSocketProxy.CLOSED = OriginalWebSocket.CLOSED;
+ window.WebSocket = WebSocketProxy;
+
if (onLoad) {
onLoad(bytedesk);
}
diff --git a/src/components/EventDetailPanel/DynamicNewsDetailPanel.js b/src/components/EventDetailPanel/DynamicNewsDetailPanel.js
index 6d92d16b..56f34308 100644
--- a/src/components/EventDetailPanel/DynamicNewsDetailPanel.js
+++ b/src/components/EventDetailPanel/DynamicNewsDetailPanel.js
@@ -18,6 +18,7 @@ import { getImportanceConfig } from '@constants/importanceLevels';
import { eventService } from '@services/eventService';
import { useEventStocks } from '@components/Charts/Stock';
import { toggleEventFollow, selectEventFollowStatus } from '@store/slices/communityDataSlice';
+import { selectSubscriptionInfo } from '@store/slices/subscriptionSlice';
import { useAuth } from '@contexts/AuthContext';
import EventHeaderInfo from './EventHeaderInfo';
import CompactMetaBar from './CompactMetaBar';
@@ -102,6 +103,10 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => {
// 获取用户会员等级(修复:字段名从 subscription_tier 改为 subscription_type)
const userTier = user?.subscription_type || 'free';
+ // 获取订阅信息,用于判断会员是否过期
+ const subscriptionInfo = useSelector(selectSubscriptionInfo);
+ const isSubscriptionExpired = subscriptionInfo.type !== 'free' && !subscriptionInfo.is_active;
+
// 从 Redux 读取关注状态
const eventFollowStatus = useSelector(selectEventFollowStatus);
const isFollowing = event?.id ? (eventFollowStatus[event.id]?.isFollowing || false) : false;
@@ -111,12 +116,16 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => {
const [fullEventDetail, setFullEventDetail] = useState(null);
const [loadingDetail, setLoadingDetail] = useState(false);
- // 权限判断函数
+ // 权限判断函数 - 会员过期时视为无权限
const hasAccess = useCallback((requiredTier) => {
+ // 会员已过期,视为无权限
+ if (isSubscriptionExpired) {
+ return false;
+ }
const tierLevel = { free: 0, pro: 1, max: 2 };
const result = tierLevel[userTier] >= tierLevel[requiredTier];
return result;
- }, [userTier]);
+ }, [userTier, isSubscriptionExpired]);
// 升级弹窗状态
const [upgradeModal, setUpgradeModal] = useState({
@@ -169,14 +178,14 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => {
// 子区块折叠状态管理 - 使用 useReducer 整合
const [sectionState, dispatchSection] = useReducer(sectionReducer, initialSectionState);
- // 锁定点击处理 - 弹出升级弹窗
+ // 锁定点击处理 - 弹出升级弹窗(会员过期时显示续费提示)
const handleLockedClick = useCallback((featureName, requiredLevel) => {
setUpgradeModal({
isOpen: true,
- requiredLevel,
- featureName
+ requiredLevel: isSubscriptionExpired ? subscriptionInfo.type : requiredLevel,
+ featureName: isSubscriptionExpired ? `${featureName}(会员已过期,请续费)` : featureName
});
- }, []);
+ }, [isSubscriptionExpired, subscriptionInfo.type]);
// 关闭升级弹窗
const handleCloseUpgradeModal = useCallback(() => {
diff --git a/src/components/EventDetailPanel/RelatedConceptsSection/index.js b/src/components/EventDetailPanel/RelatedConceptsSection/index.js
index 2cc2f335..d93a483b 100644
--- a/src/components/EventDetailPanel/RelatedConceptsSection/index.js
+++ b/src/components/EventDetailPanel/RelatedConceptsSection/index.js
@@ -2,6 +2,7 @@
// 相关概念区组件 - 便当盒网格布局
import React, { useState, useEffect } from 'react';
+import { useSelector } from 'react-redux';
import {
Box,
Flex,
@@ -18,6 +19,7 @@ import {
import { useNavigate } from 'react-router-dom';
import { logger } from '@utils/logger';
import { getApiBase } from '@utils/apiConfig';
+import { selectSubscriptionInfo } from '@store/slices/subscriptionSlice';
/**
* 单个概念卡片组件(便当盒样式)
@@ -100,6 +102,10 @@ const RelatedConceptsSection = ({
const [error, setError] = useState(null);
const navigate = useNavigate();
+ // 获取订阅信息,用于判断会员是否过期
+ const subscriptionInfo = useSelector(selectSubscriptionInfo);
+ const isSubscriptionExpired = subscriptionInfo.type !== 'free' && !subscriptionInfo.is_active;
+
// 颜色配置 - 使用深色主题固定颜色
const sectionBg = 'transparent';
const headingColor = '#e2e8f0';
@@ -107,7 +113,7 @@ const RelatedConceptsSection = ({
const countBadgeBg = '#3182ce';
const countBadgeColor = '#ffffff';
- // 获取相关概念
+ // 获取相关概念 - 如果被锁定或会员过期则跳过 API 请求
useEffect(() => {
const fetchConcepts = async () => {
if (!eventId) {
@@ -115,6 +121,13 @@ const RelatedConceptsSection = ({
return;
}
+ // 如果被锁定或会员已过期,不发起 API 请求
+ if (isLocked || isSubscriptionExpired) {
+ setLoading(false);
+ setConcepts([]);
+ return;
+ }
+
try {
setLoading(true);
setError(null);
@@ -152,7 +165,7 @@ const RelatedConceptsSection = ({
};
fetchConcepts();
- }, [eventId]);
+ }, [eventId, isLocked, isSubscriptionExpired]);
// 跳转到概念中心
const handleNavigate = (concept) => {
diff --git a/src/components/Navbars/HomeNavbar.js b/src/components/Navbars/HomeNavbar.js
index e864a34c..33d7dd90 100644
--- a/src/components/Navbars/HomeNavbar.js
+++ b/src/components/Navbars/HomeNavbar.js
@@ -152,7 +152,7 @@ export default function HomeNavbar() {
borderColor={navbarBorder}
py={{ base: 2, md: 3 }}
>
-
+
{/* Logo - 价小前投研 */}
@@ -177,7 +177,7 @@ export default function HomeNavbar() {
followingEvents={followingEvents}
/>
-
+
{/* 移动端抽屉菜单 (Phase 5 优化) */}
void;
- /** 股票信息 */
- stock: StockInfo;
- /** 事件时间(ISO 字符串,可选) */
- eventTime?: string;
- /** 事件标题(用于标记标签,可选) */
- eventTitle?: string;
-}
-
-// ==================== 主组件 ====================
-
-const StockChartKLineModal: React.FC = ({
- visible,
- onClose,
- stock,
- eventTime,
- eventTitle,
-}) => {
- // ==================== 状态管理 ====================
-
- /** 图表类型(分时图/日K线) */
- const [chartType, setChartType] = useState('daily');
-
- /** 选中的副图指标 */
- const [selectedIndicators, setSelectedIndicators] = useState(
- DEFAULT_SUB_INDICATORS
- );
-
- // ==================== 自定义 Hooks ====================
-
- /** 图表实例管理 */
- const { chart, chartRef, isInitialized, error: chartError } = useKLineChart({
- containerId: `kline-chart-${stock.stock_code}`,
- height: CHART_HEIGHTS.main,
- autoResize: true,
- });
-
- /** 数据加载管理 */
- const {
- data,
- loading: dataLoading,
- error: dataError,
- loadData,
- } = useKLineData({
- chart,
- stockCode: stock.stock_code,
- chartType,
- eventTime,
- autoLoad: visible, // 模态框打开时自动加载
- });
-
- /** 事件标记管理 */
- const { marker } = useEventMarker({
- chart,
- data,
- eventTime,
- eventTitle,
- autoCreate: true,
- });
-
- // ==================== 事件处理 ====================
-
- /**
- * 切换图表类型(分时图 ↔ 日K线)
- */
- const handleChartTypeChange = useCallback((e: RadioChangeEvent) => {
- const newType = e.target.value as ChartType;
- setChartType(newType);
- }, []);
-
- /**
- * 切换副图指标
- */
- const handleIndicatorChange = useCallback(
- (values: string[]) => {
- setSelectedIndicators(values);
-
- if (!chart) {
- return;
- }
-
- // 先移除所有副图指标(KLineChart 会自动移除)
- // 然后创建新的指标
- createSubIndicators(chart, values);
- },
- [chart]
- );
-
- /**
- * 刷新数据
- */
- const handleRefresh = useCallback(() => {
- loadData();
- }, [loadData]);
-
- // ==================== 计算属性 ====================
-
- /** 是否有错误 */
- const hasError = useMemo(() => {
- return !!chartError || !!dataError;
- }, [chartError, dataError]);
-
- /** 错误消息 */
- const errorMessage = useMemo(() => {
- if (chartError) {
- return `图表初始化失败: ${chartError.message}`;
- }
- if (dataError) {
- return `数据加载失败: ${dataError.message}`;
- }
- return null;
- }, [chartError, dataError]);
-
- /** 模态框标题 */
- const modalTitle = useMemo(() => {
- return `${stock.stock_name}(${stock.stock_code}) - ${CHART_TYPE_CONFIG[chartType].label}`;
- }, [stock, chartType]);
-
- /** 是否显示加载状态 */
- const showLoading = useMemo(() => {
- return dataLoading || !isInitialized;
- }, [dataLoading, isInitialized]);
-
- // ==================== 副作用 ====================
-
- // 无副作用,都在 Hooks 中管理
-
- // ==================== 渲染 ====================
-
- return (
-
- {/* 工具栏 */}
-
-
- {/* 图表类型切换 */}
-
-
- 分时图
-
-
- 日K线
-
-
-
- {/* 副图指标选择 */}
-
-
- {/* 刷新按钮 */}
- }
- onClick={handleRefresh}
- loading={dataLoading}
- >
- 刷新
-
-
-
-
- {/* 错误提示 */}
- {hasError && (
-
- )}
-
- {/* 图表容器 */}
-
- {/* 加载遮罩 */}
- {showLoading && (
-
-
-
- )}
-
- {/* KLineChart 容器 */}
-
-
-
- {/* 数据信息(调试用,生产环境可移除) */}
- {process.env.NODE_ENV === 'development' && (
-
-
- 数据点数: {data.length}
- 事件标记: {marker ? marker.label : '无'}
- 图表ID: {chart?.id || '未初始化'}
-
-
- )}
-
- );
-};
-
-export default StockChartKLineModal;
diff --git a/src/components/StockChart/StockChartModal.js.backup b/src/components/StockChart/StockChartModal.js.backup
deleted file mode 100644
index f4b0ca80..00000000
--- a/src/components/StockChart/StockChartModal.js.backup
+++ /dev/null
@@ -1,558 +0,0 @@
-// src/components/StockChart/StockChartModal.js - 统一的股票图表组件
-import React, { useState, useEffect, useRef, useMemo } from 'react';
-import { Modal, ModalOverlay, ModalContent, ModalHeader, ModalCloseButton, ModalBody, Button, ButtonGroup, VStack, HStack, Text, Badge, Box, Flex, CircularProgress } from '@chakra-ui/react';
-import ReactECharts from 'echarts-for-react';
-import * as 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';
-
-const StockChartModal = ({
- isOpen,
- onClose,
- stock,
- eventTime,
- isChakraUI = true, // 是否使用Chakra UI,默认true;如果false则使用Antd
- size = "6xl",
- initialChartType = 'timeline' // 初始图表类型(timeline/daily)
-}) => {
- 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({});
-
- // 预加载数据
- const preloadData = async (type) => {
- 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.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) => {
- 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.message
- });
- }
- }
-
- response = await stockService.getKlineData(stock.stock_code, type, adjustedEventTime);
- }
-
- setChartData(response);
-
- // 初始化图表
- if (chartRef.current && !chartInstanceRef.current) {
- const chart = echarts.init(chartRef.current);
- 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, type, originalEventTime, adjustedEventTime) => {
- 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);
- const avgPrices = stockData.map(item => item.avg_price);
- 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';
-
- // 计算事件标记线位置
- let eventMarkLineData = [];
- if (originalEventTime && times.length > 0) {
- const eventMoment = dayjs(originalEventTime);
- const eventDate = eventMoment.format('YYYY-MM-DD');
- const eventTime = eventMoment.format('HH:mm');
-
- 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 = [{
- 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) {
- 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) {
- return (value >= 0 ? '+' : '') + value.toFixed(2) + '%';
- }
- },
- splitLine: {
- show: true,
- lineStyle: {
- color: '#f0f0f0'
- }
- }
- },
- {
- type: 'value',
- gridIndex: 0,
- scale: false,
- position: 'right',
- axisLabel: {
- formatter: function(value) {
- return (value >= 0 ? '+' : '') + value.toFixed(2) + '%';
- }
- }
- },
- { type: 'value', gridIndex: 1, scale: true, axisLabel: { formatter: v => 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);
-
- // 计算事件标记线位置(重要修复)
- let eventMarkLineData = [];
- 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 = '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 = [{
- 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) {
- 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, scale: true, boundaryGap: true, gridIndex: 0 },
- { type: 'category', data: dates, gridIndex: 1, axisLabel: { show: false } }
- ],
- yAxis: [
- { scale: true, splitArea: { show: true }, gridIndex: 0 },
- { scale: true, gridIndex: 1, axisLabel: { formatter: (value) => 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;
\ No newline at end of file
diff --git a/src/components/VisxPieChart/VisxPieChart.js b/src/components/VisxPieChart/VisxPieChart.js
deleted file mode 100755
index f658ba6c..00000000
--- a/src/components/VisxPieChart/VisxPieChart.js
+++ /dev/null
@@ -1,55 +0,0 @@
-import { useState } from "react";
-import { Pie } from "@visx/shape";
-import { Group } from "@visx/group";
-import { Text } from "@visx/text";
-import { useColorModeValue } from "@chakra-ui/react";
-
-const VisxPieChart = ({ data, title, width }) => {
- const [active, setActive] = useState(null);
- const half = width / 2;
-
- const textColor = useColorModeValue("gray.700", "white");
-
- return (
-
- );
-};
-
-export default VisxPieChart;
diff --git a/src/layouts/AppFooter.js b/src/layouts/AppFooter.js
index 33499fa7..65432374 100644
--- a/src/layouts/AppFooter.js
+++ b/src/layouts/AppFooter.js
@@ -1,5 +1,5 @@
import React from 'react';
-import { Box, Container, VStack, HStack, Text, Link, useColorModeValue } from '@chakra-ui/react';
+import { Box, VStack, HStack, Text, Link, useColorModeValue } from '@chakra-ui/react';
import RiskDisclaimer from '../components/RiskDisclaimer';
/**
@@ -8,8 +8,7 @@ import RiskDisclaimer from '../components/RiskDisclaimer';
*/
const AppFooter = () => {
return (
-
-
+
@@ -32,7 +31,6 @@ const AppFooter = () => {
-
);
};
diff --git a/src/layouts/MainLayout.js b/src/layouts/MainLayout.js
index c61bea72..4e673f5e 100644
--- a/src/layouts/MainLayout.js
+++ b/src/layouts/MainLayout.js
@@ -9,7 +9,7 @@ import BackToTopButton from "./components/BackToTopButton";
import ErrorBoundary from "../components/ErrorBoundary";
import PageLoader from "../components/Loading/PageLoader";
import GlobalSidebar from "../components/GlobalSidebar";
-import { BACK_TO_TOP_CONFIG, LAYOUT_SIZE } from "./config/layoutConfig";
+import { BACK_TO_TOP_CONFIG, LAYOUT_SIZE, LAYOUT_PADDING } from "./config/layoutConfig";
// ✅ P0 性能优化:缓存静态组件,避免路由切换时不必要的重新渲染
// HomeNavbar (1623行) 和 AppFooter 不依赖路由参数,使用 memo 可大幅减少渲染次数
@@ -40,7 +40,7 @@ export default function MainLayout() {
{/* 页面内容区域 - 全宽度,与导航栏对齐 */}
-
+
}>
diff --git a/src/layouts/config/layoutConfig.js b/src/layouts/config/layoutConfig.js
index 68c514e9..51eee300 100644
--- a/src/layouts/config/layoutConfig.js
+++ b/src/layouts/config/layoutConfig.js
@@ -170,6 +170,15 @@ export const LAYOUT_SIZE = {
contentMinHeight: 'calc(100vh - 60px)', // 100vh - navbar高度
};
+/**
+ * 布局内边距配置
+ * 统一控制页面内容的水平内边距
+ * 右侧预留空间给 GlobalSidebar(收起宽度 72px)
+ */
+export const LAYOUT_PADDING = {
+ x: { base: 4, md: 6, lg: '80px' }, // 移动端 16px,中屏 24px,大屏 80px(容纳工具栏)
+};
+
/**
* 响应式断点
* 与 Chakra UI 断点保持一致
@@ -189,5 +198,6 @@ export default {
BACK_TO_TOP_CONFIG,
PAGE_LOADER_CONFIG,
LAYOUT_SIZE,
+ LAYOUT_PADDING,
BREAKPOINTS
};
diff --git a/src/mocks/handlers/limitAnalyse.js b/src/mocks/handlers/limitAnalyse.js
index 91956d03..2921c387 100644
--- a/src/mocks/handlers/limitAnalyse.js
+++ b/src/mocks/handlers/limitAnalyse.js
@@ -216,24 +216,39 @@ const generateDailyAnalysis = (date) => {
// 统计数据
const morningCount = Math.floor(totalStocks * 0.35); // 早盘涨停
+ const middayCount = Math.floor(totalStocks * 0.25); // 午盘涨停
+ const afternoonCount = totalStocks - morningCount - middayCount; // 尾盘涨停
const announcementCount = sectorData['公告']?.count || 0;
const topSector = sectorNames.filter(s => s !== '公告' && s !== '其他')
.reduce((max, name) =>
(sectorData[name]?.count || 0) > (sectorData[max]?.count || 0) ? name : max
, '人工智能');
+ // 生成 chart_data(板块分布饼图需要)
+ const sortedSectors = Object.entries(sectorData)
+ .filter(([name]) => name !== '其他' && name !== '公告')
+ .sort((a, b) => b[1].count - a[1].count)
+ .slice(0, 10);
+
+ const chartData = {
+ labels: sortedSectors.map(([name]) => name),
+ counts: sortedSectors.map(([, info]) => info.count)
+ };
+
return {
date: date,
total_stocks: totalStocks,
total_sectors: Object.keys(sectorData).length,
sector_data: sectorData, // 👈 SectorDetails 组件需要的数据
+ chart_data: chartData, // 👈 板块分布饼图需要的数据
summary: {
top_sector: topSector,
top_sector_count: sectorData[topSector]?.count || 0,
announcement_stocks: announcementCount,
zt_time_distribution: {
morning: morningCount,
- afternoon: totalStocks - morningCount,
+ midday: middayCount,
+ afternoon: afternoonCount,
}
}
};
@@ -382,6 +397,22 @@ const generateDailyJson = (date) => {
{ name: '区块链', value: Math.floor(Math.random() * 5) + 2 },
];
+ // 生成 chart_data(板块分布饼图需要)
+ const sortedSectors = Object.entries(sectorData)
+ .filter(([name]) => name !== '其他' && name !== '公告')
+ .sort((a, b) => b[1].count - a[1].count)
+ .slice(0, 10);
+
+ const chartData = {
+ labels: sortedSectors.map(([name]) => name),
+ counts: sortedSectors.map(([, info]) => info.count)
+ };
+
+ // 时间分布(早盘、午盘、尾盘)
+ const morningCount = Math.floor(stocks.length * 0.35);
+ const middayCount = Math.floor(stocks.length * 0.25);
+ const afternoonCount = stocks.length - morningCount - middayCount;
+
return {
date: date,
total_stocks: stocks.length,
@@ -389,13 +420,15 @@ const generateDailyJson = (date) => {
stocks: stocks,
sector_data: sectorData,
word_freq_data: wordFreqData,
+ chart_data: chartData, // 👈 板块分布饼图需要的数据
summary: {
top_sector: '人工智能',
top_sector_count: sectorData['人工智能']?.count || 0,
announcement_stocks: sectorData['公告']?.count || 0,
zt_time_distribution: {
- morning: Math.floor(stocks.length * 0.4),
- afternoon: Math.floor(stocks.length * 0.6),
+ morning: morningCount, // 早盘 9:30-11:30
+ midday: middayCount, // 午盘 11:30-13:00
+ afternoon: afternoonCount, // 尾盘 13:00-15:00
}
}
};
diff --git a/src/store/slices/stockSlice.js b/src/store/slices/stockSlice.js
index 0aba0392..b49bd9d4 100644
--- a/src/store/slices/stockSlice.js
+++ b/src/store/slices/stockSlice.js
@@ -58,11 +58,25 @@ const saveWatchlistToCache = (data) => {
/**
* 获取事件相关股票(Redux 缓存)
+ * @param {Object} params
+ * @param {string} params.eventId - 事件ID
+ * @param {boolean} params.forceRefresh - 是否强制刷新
+ * @param {boolean} params.skipIfNoAccess - 如果无权限则跳过请求(会员过期场景)
*/
export const fetchEventStocks = createAsyncThunk(
'stock/fetchEventStocks',
- async ({ eventId, forceRefresh = false }, { getState }) => {
- logger.debug('stockSlice', 'fetchEventStocks', { eventId, forceRefresh });
+ async ({ eventId, forceRefresh = false, skipIfNoAccess = false }, { getState }) => {
+ logger.debug('stockSlice', 'fetchEventStocks', { eventId, forceRefresh, skipIfNoAccess });
+
+ // 检查订阅状态,如果会员过期则跳过请求
+ if (skipIfNoAccess) {
+ const subscriptionInfo = getState().subscription?.info;
+ const isExpired = subscriptionInfo?.type !== 'free' && !subscriptionInfo?.is_active;
+ if (isExpired) {
+ logger.debug('stockSlice', '会员已过期,跳过 fetchEventStocks 请求', { eventId });
+ return { eventId, stocks: [], skipped: true };
+ }
+ }
// Redux 状态缓存
if (!forceRefresh) {
@@ -132,11 +146,25 @@ export const fetchEventDetail = createAsyncThunk(
/**
* 获取历史事件对比(Redux 缓存)
+ * @param {Object} params
+ * @param {string} params.eventId - 事件ID
+ * @param {boolean} params.forceRefresh - 是否强制刷新
+ * @param {boolean} params.skipIfNoAccess - 如果无权限则跳过请求(会员过期场景)
*/
export const fetchHistoricalEvents = createAsyncThunk(
'stock/fetchHistoricalEvents',
- async ({ eventId, forceRefresh = false }, { getState }) => {
- logger.debug('stockSlice', 'fetchHistoricalEvents', { eventId });
+ async ({ eventId, forceRefresh = false, skipIfNoAccess = false }, { getState }) => {
+ logger.debug('stockSlice', 'fetchHistoricalEvents', { eventId, skipIfNoAccess });
+
+ // 检查订阅状态,如果会员过期则跳过请求
+ if (skipIfNoAccess) {
+ const subscriptionInfo = getState().subscription?.info;
+ const isExpired = subscriptionInfo?.type !== 'free' && !subscriptionInfo?.is_active;
+ if (isExpired) {
+ logger.debug('stockSlice', '会员已过期,跳过 fetchHistoricalEvents 请求', { eventId });
+ return { eventId, events: [], skipped: true };
+ }
+ }
// Redux 缓存
if (!forceRefresh) {
@@ -158,11 +186,25 @@ export const fetchHistoricalEvents = createAsyncThunk(
/**
* 获取传导链分析(Redux 缓存)
+ * @param {Object} params
+ * @param {string} params.eventId - 事件ID
+ * @param {boolean} params.forceRefresh - 是否强制刷新
+ * @param {boolean} params.skipIfNoAccess - 如果无权限则跳过请求(会员过期场景)
*/
export const fetchChainAnalysis = createAsyncThunk(
'stock/fetchChainAnalysis',
- async ({ eventId, forceRefresh = false }, { getState }) => {
- logger.debug('stockSlice', 'fetchChainAnalysis', { eventId });
+ async ({ eventId, forceRefresh = false, skipIfNoAccess = false }, { getState }) => {
+ logger.debug('stockSlice', 'fetchChainAnalysis', { eventId, skipIfNoAccess });
+
+ // 检查订阅状态,如果会员过期则跳过请求
+ if (skipIfNoAccess) {
+ const subscriptionInfo = getState().subscription?.info;
+ const isExpired = subscriptionInfo?.type !== 'free' && !subscriptionInfo?.is_active;
+ if (isExpired) {
+ logger.debug('stockSlice', '会员已过期,跳过 fetchChainAnalysis 请求', { eventId });
+ return { eventId, analysis: null, skipped: true };
+ }
+ }
// Redux 缓存
if (!forceRefresh) {
diff --git a/src/views/Center/index.tsx b/src/views/Center/index.tsx
index 459b4756..e660d252 100644
--- a/src/views/Center/index.tsx
+++ b/src/views/Center/index.tsx
@@ -23,7 +23,8 @@ const Center: React.FC = () => {
return (
-
+ {/* padding 由 MainLayout 统一设置 */}
+
{/* 市场概览仪表盘 */}
diff --git a/src/views/Community/index.js b/src/views/Community/index.js
index 5245a209..3b397414 100644
--- a/src/views/Community/index.js
+++ b/src/views/Community/index.js
@@ -8,8 +8,6 @@ import {
} from '@/store/slices/communityDataSlice';
import {
Box,
- Container,
- useBreakpointValue,
Skeleton,
} from '@chakra-ui/react';
@@ -45,23 +43,6 @@ const Community = () => {
// Ref:用于首次滚动到内容区域
const containerRef = useRef(null);
- // 响应式容器宽度
- const containerMaxW = useBreakpointValue({
- base: '100%', // 移动端:全宽
- sm: '100%', // 小屏:全宽
- md: '100%', // 中屏:全宽
- lg: '1200px', // 大屏:1200px
- xl: '1400px', // 超大屏:1400px
- });
-
- // 响应式内边距
- const containerPx = useBreakpointValue({
- base: 2, // 移动端:最小内边距
- sm: 3,
- md: 4,
- lg: 6,
- });
-
// ⚡ 通知权限引导
const { browserPermission, requestBrowserPermission, registerEventUpdateCallback } = useNotification();
@@ -165,8 +146,8 @@ const Community = () => {
return (
- {/* 主内容区域 */}
-
+ {/* 主内容区域 - padding 由 MainLayout 统一设置 */}
+
{/* ⚡ 顶部说明面板(懒加载):产品介绍 + 沪深指数 + 热门概念词云 */}
@@ -200,7 +181,7 @@ const Community = () => {
events={hotEvents}
onEventClick={communityEvents.trackNewsArticleClicked}
/>
-
+
);
};
diff --git a/src/views/Company/components/CompanyHeader/index.tsx b/src/views/Company/components/CompanyHeader/index.tsx
index 494efcbe..7150788d 100644
--- a/src/views/Company/components/CompanyHeader/index.tsx
+++ b/src/views/Company/components/CompanyHeader/index.tsx
@@ -7,6 +7,7 @@ import { Box, Flex, HStack, VStack, Text } from '@chakra-ui/react';
import { AutoComplete, Input, Spin } from 'antd';
import { SearchOutlined } from '@ant-design/icons';
import { useStockSearch } from '@hooks/useStockSearch';
+import { LAYOUT_PADDING } from '@/layouts/config/layoutConfig';
import { THEME } from '../../config';
import { FUI_COLORS, FUI_GLOW } from '../../theme/fui';
import type { CompanyHeaderProps, StockSearchResult } from '../../types';
@@ -115,17 +116,17 @@ SearchBox.displayName = 'SearchBox';
const CompanyHeader: React.FC = memo(({ onStockChange }) => (
diff --git a/src/views/Company/index.tsx b/src/views/Company/index.tsx
index 431915bb..6704c643 100644
--- a/src/views/Company/index.tsx
+++ b/src/views/Company/index.tsx
@@ -332,7 +332,8 @@ const CompanyIndex: React.FC = () => {
position="relative"
bg={THEME.bg}
minH="calc(100vh - 60px)"
- overflow="hidden"
+ overflowX="visible"
+ overflowY="hidden"
>
{/* ========================================
全局环境光效果
@@ -371,12 +372,10 @@ const CompanyIndex: React.FC = () => {
{/*
内容容器
- - maxW="container.xl": 最大宽度限制,保持内容可读性
- - mx="auto": 水平居中
- - px={4}: 左右内边距 16px
- py={6}: 上下内边距 24px
+ - 水平 padding 由 MainLayout 统一设置
*/}
-
+
{/* ========================================
股票行情卡片
========================================
diff --git a/src/views/Concept/index.js b/src/views/Concept/index.js
index 9ce1b7b5..47a9bcd1 100644
--- a/src/views/Concept/index.js
+++ b/src/views/Concept/index.js
@@ -4,7 +4,6 @@ import { logger } from '../../utils/logger';
import defaultEventImage from '../../assets/img/default-event.jpg';
import {
Box,
- Container,
Heading,
Text,
Input,
@@ -1538,12 +1537,14 @@ const ConceptCenter = () => {
{/* 导航栏已由 MainLayout 提供 */}
{/* Hero Section - 精简版 */}
+ {/* Hero Section - 使用负 margin 抵消 Layout 的 padding 实现全宽背景 */}
{/* 科幻网格背景 */}
{
filter="blur(50px)"
/>
-
+
{/* 标题区域 */}
@@ -1743,11 +1744,11 @@ const ConceptCenter = () => {
-
+
- {/* 主内容区域 - 深色主题 */}
-
+ {/* 主内容区域 - padding 由 MainLayout 统一设置 */}
+
@@ -2102,7 +2103,7 @@ const ConceptCenter = () => {
-
+
{/* 股票详情Modal - 复用通用组件 */}
{
pointerEvents="none"
/>
-
+ {/* padding 由 MainLayout 统一设置 */}
+
{/* 标题区域 */}
{
-
+
{/* 指标数据详情模态框 */}
{selectedMetric && (
diff --git a/src/views/EventDetail/components/TransmissionChainAnalysis.js b/src/views/EventDetail/components/TransmissionChainAnalysis.js
index 412fdf43..cf10f159 100644
--- a/src/views/EventDetail/components/TransmissionChainAnalysis.js
+++ b/src/views/EventDetail/components/TransmissionChainAnalysis.js
@@ -1,4 +1,5 @@
import React, { useEffect, useState, useRef } from 'react';
+import { useSelector } from 'react-redux';
import {
Box,
Button,
@@ -39,6 +40,7 @@ import CitedContent from '../../../components/Citation/CitedContent';
import { logger } from '../../../utils/logger';
import { getApiBase } from '../../../utils/apiConfig';
import { PROFESSIONAL_COLORS } from '../../../constants/professionalTheme';
+import { selectSubscriptionInfo } from '../../../store/slices/subscriptionSlice';
// 节点样式配置 - 完全复刻Flask版本
const NODE_STYLES = {
@@ -460,6 +462,10 @@ function getSankeyOption(data) {
}
const TransmissionChainAnalysis = ({ eventId }) => {
+ // 获取订阅信息,用于判断会员是否过期
+ const subscriptionInfo = useSelector(selectSubscriptionInfo);
+ const isSubscriptionExpired = subscriptionInfo.type !== 'free' && !subscriptionInfo.is_active;
+
// 状态管理
const [graphData, setGraphData] = useState(null);
const [sankeyData, setSankeyData] = useState(null);
@@ -474,7 +480,7 @@ const TransmissionChainAnalysis = ({ eventId }) => {
const [stats, setStats] = useState({
totalNodes: 0,
involvedIndustries: 0,
- relatedCompanies: 0,
+ relatedCompanies: 0,
positiveImpact: 0,
negativeImpact: 0,
circularEffect: 0
@@ -514,9 +520,18 @@ const TransmissionChainAnalysis = ({ eventId }) => {
}
}, [graphData]);
- // 加载数据
+ // 加载数据 - 如果会员过期则跳过 API 请求
useEffect(() => {
async function fetchData() {
+ // 会员已过期,不发起 API 请求
+ if (isSubscriptionExpired) {
+ logger.debug('TransmissionChain', '会员已过期,跳过传导链数据加载', { eventId });
+ setLoading(false);
+ setGraphData(null);
+ setSankeyData(null);
+ return;
+ }
+
setLoading(true);
setError(null);
try {
@@ -563,7 +578,7 @@ const TransmissionChainAnalysis = ({ eventId }) => {
if (eventId) {
fetchData();
}
- }, [eventId]);
+ }, [eventId, isSubscriptionExpired]);
// BFS路径查找 - 完全复刻Flask版本
function findPath(nodes, edges, fromId, toId) {
diff --git a/src/views/LimitAnalyse/components/DataVisualizationComponents.js b/src/views/LimitAnalyse/components/DataVisualizationComponents.js
index 362395a9..b8261a8b 100644
--- a/src/views/LimitAnalyse/components/DataVisualizationComponents.js
+++ b/src/views/LimitAnalyse/components/DataVisualizationComponents.js
@@ -1,4 +1,4 @@
-import React, { useMemo, useState, useEffect, useRef } from 'react';
+import React, { useMemo } from 'react';
import {
Box,
Card,
@@ -39,21 +39,6 @@ import {
import { getFormattedTextProps } from '../../../utils/textUtils';
import { ExternalLinkIcon } from '@chakra-ui/icons';
import RiskDisclaimer from '../../../components/RiskDisclaimer';
-import {
- BarChart, Bar,
- PieChart, Pie, Cell,
- XAxis, YAxis,
- CartesianGrid,
- Tooltip as RechartsTooltip,
- Legend,
- ResponsiveContainer,
- Treemap,
- Area, AreaChart,
-} from 'recharts';
-// 词云库 - 支持两种实现
-import { Wordcloud } from '@visx/wordcloud';
-import { scaleLog } from '@visx/scale';
-import { Text as VisxText } from '@visx/text';
import ReactECharts from 'echarts-for-react';
import 'echarts-wordcloud';
// 颜色配置
@@ -65,97 +50,7 @@ const CHART_COLORS = [
// 词云颜色常量
const WORDCLOUD_COLORS = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEEAD'];
-// ==================== 词云组件实现 1: @visx/wordcloud ====================
-// 使用 SVG 渲染,React 18 原生支持,配置灵活
-const VisxWordCloud = ({ data }) => {
- const [dimensions, setDimensions] = useState({ width: 0, height: 400 });
- const containerRef = useRef(null);
-
- // 监听容器尺寸变化
- useEffect(() => {
- if (!containerRef.current) return;
-
- const updateDimensions = () => {
- if (containerRef.current) {
- setDimensions({
- width: containerRef.current.offsetWidth,
- height: 400
- });
- }
- };
-
- updateDimensions();
- const resizeObserver = new ResizeObserver(updateDimensions);
- resizeObserver.observe(containerRef.current);
-
- return () => resizeObserver.disconnect();
- }, []);
-
- if (!data || data.length === 0) {
- return (
-
-
- 暂无词云数据
-
-
- );
- }
-
- const words = data.slice(0, 100).map(item => ({
- name: item.name || item.text,
- value: item.value || item.count || 1
- }));
-
- // 计算字体大小比例
- const fontScale = scaleLog({
- domain: [
- Math.min(...words.map(w => w.value)),
- Math.max(...words.map(w => w.value))
- ],
- range: [16, 80],
- });
-
- const fontSizeSetter = (datum) => fontScale(datum.value);
-
- return (
-
- {dimensions.width > 0 && (
-
- )}
-
- );
-};
-
-// ==================== 词云组件实现 2: ECharts Wordcloud ====================
-// 使用 Canvas 渲染,内置交互效果(tooltip、emphasis),配置简单
+// 词云组件 - 使用 ECharts 实现
const EChartsWordCloud = ({ data }) => {
if (!data || data.length === 0) {
return (
@@ -216,9 +111,8 @@ const EChartsWordCloud = ({ data }) => {
);
};
-// ==================== 词云组件包装器 ====================
-// 统一接口,支持切换两种实现方式
-const WordCloud = ({ data, engine = 'echarts' }) => {
+// 词云组件
+const WordCloud = ({ data }) => {
if (!data || data.length === 0) {
return (
@@ -229,8 +123,150 @@ const WordCloud = ({ data, engine = 'echarts' }) => {
);
}
- // 根据 engine 参数选择实现方式
- return engine === 'visx' ? : ;
+ return ;
+};
+
+// 板块分布饼图 - ECharts 实现
+const SectorPieChart = ({ data }) => {
+ if (!data || data.length === 0) {
+ return (
+
+ 暂无数据
+
+ );
+ }
+
+ const option = {
+ tooltip: {
+ trigger: 'item',
+ formatter: '{b}: {c} ({d}%)'
+ },
+ legend: {
+ orient: 'vertical',
+ right: 10,
+ top: 'center',
+ textStyle: { fontSize: 12 }
+ },
+ series: [{
+ type: 'pie',
+ radius: ['0%', '65%'],
+ center: ['40%', '50%'],
+ avoidLabelOverlap: true,
+ itemStyle: {
+ borderRadius: 4,
+ borderColor: '#fff',
+ borderWidth: 2
+ },
+ label: {
+ show: true,
+ formatter: '{b} {d}%',
+ fontSize: 12
+ },
+ labelLine: {
+ show: true,
+ length: 10,
+ length2: 15
+ },
+ emphasis: {
+ label: {
+ show: true,
+ fontSize: 14,
+ fontWeight: 'bold'
+ },
+ itemStyle: {
+ shadowBlur: 10,
+ shadowOffsetX: 0,
+ shadowColor: 'rgba(0, 0, 0, 0.5)'
+ }
+ },
+ data: data.map((item, index) => ({
+ name: item.name,
+ value: item.value,
+ itemStyle: { color: CHART_COLORS[index % CHART_COLORS.length] }
+ })),
+ animationDuration: 800
+ }]
+ };
+
+ return (
+
+ );
+};
+
+// 时间分布环形图 - ECharts 实现
+const TimeDistributionChart = ({ data }) => {
+ if (!data || data.length === 0) {
+ return (
+
+ 暂无数据
+
+ );
+ }
+
+ const option = {
+ tooltip: {
+ trigger: 'item',
+ formatter: '{b}: {c} ({d}%)'
+ },
+ legend: {
+ orient: 'horizontal',
+ bottom: 10,
+ textStyle: { fontSize: 12 }
+ },
+ series: [{
+ type: 'pie',
+ radius: ['45%', '70%'],
+ center: ['50%', '45%'],
+ avoidLabelOverlap: true,
+ padAngle: 3,
+ itemStyle: {
+ borderRadius: 6,
+ borderColor: '#fff',
+ borderWidth: 2
+ },
+ label: {
+ show: true,
+ position: 'outside',
+ formatter: '{b}\n{c}只',
+ fontSize: 11
+ },
+ labelLine: {
+ show: true,
+ length: 8,
+ length2: 12
+ },
+ emphasis: {
+ label: {
+ show: true,
+ fontSize: 13,
+ fontWeight: 'bold'
+ },
+ itemStyle: {
+ shadowBlur: 10,
+ shadowOffsetX: 0,
+ shadowColor: 'rgba(0, 0, 0, 0.5)'
+ }
+ },
+ data: data.map(item => ({
+ name: item.name,
+ value: item.value,
+ itemStyle: { color: item.color }
+ })),
+ animationDuration: 800
+ }]
+ };
+
+ return (
+
+ );
};
// 板块热力图组件
@@ -592,27 +628,7 @@ export const DataAnalysis = ({ dailyData, wordCloudData, totalStocks, dateStr })
-
-
- `${name} ${(percent * 100).toFixed(0)}%`}
- outerRadius={120}
- fill="#8884d8"
- dataKey="value"
- animationBegin={0}
- animationDuration={800}
- >
- {pieData.map((entry, index) => (
- |
- ))}
-
-
-
-
+
@@ -641,25 +657,7 @@ export const DataAnalysis = ({ dailyData, wordCloudData, totalStocks, dateStr })
-
-
-
- {timeDistributionData.map((entry, index) => (
- |
- ))}
-
-
-
-
+
{timeDistributionData.map((item, index) => (
diff --git a/src/views/LimitAnalyse/index.js b/src/views/LimitAnalyse/index.js
index ac0a1e7b..72e44dec 100755
--- a/src/views/LimitAnalyse/index.js
+++ b/src/views/LimitAnalyse/index.js
@@ -1,7 +1,6 @@
import React, { useState, useEffect } from 'react';
import {
Box,
- Container,
VStack,
HStack,
Heading,
@@ -263,8 +262,7 @@ export default function LimitAnalyse() {
{/* 导航栏已由 MainLayout 提供 */}
{/* 顶部Header */}
-
-
+
{/* 左侧:标题置顶,注释与图例贴底 */}
@@ -366,11 +364,10 @@ export default function LimitAnalyse() {
-
- {/* 主内容区 */}
-
+ {/* 主内容区 - padding 由 MainLayout 统一设置 */}
+
{/* 搜索框 */}
@@ -402,7 +399,7 @@ export default function LimitAnalyse() {
{/* 高位股统计 */}
-
+
{/* 弹窗 */}
+
{/* 页面标题 */}
@@ -627,6 +627,6 @@ export default function ProfilePage() {
-
+
);
}
\ No newline at end of file
diff --git a/src/views/Profile/index.js b/src/views/Profile/index.js
index 336fcbf3..375c06cb 100644
--- a/src/views/Profile/index.js
+++ b/src/views/Profile/index.js
@@ -135,7 +135,6 @@ const ProfilePage = () => {
return (
-
{/* 用户信息头部 */}
{
-
);
};
diff --git a/src/views/Settings/SettingsPage.js b/src/views/Settings/SettingsPage.js
index 4837f2c5..41693b4e 100644
--- a/src/views/Settings/SettingsPage.js
+++ b/src/views/Settings/SettingsPage.js
@@ -2,7 +2,6 @@
import React, { useState } from 'react';
import {
Box,
- Container,
VStack,
HStack,
Text,
@@ -219,7 +218,7 @@ export default function SettingsPage() {
};
return (
-
+
{/* 页面标题 */}
账户设置
@@ -543,6 +542,6 @@ export default function SettingsPage() {
-
+
);
}
\ No newline at end of file
diff --git a/src/views/StockOverview/index.js b/src/views/StockOverview/index.js
index ad113709..c2d32061 100644
--- a/src/views/StockOverview/index.js
+++ b/src/views/StockOverview/index.js
@@ -3,7 +3,6 @@ import { useNavigate } from 'react-router-dom';
import { getApiBase } from '@utils/apiConfig';
import {
Box,
- Container,
Heading,
Text,
Input,
@@ -633,6 +632,7 @@ const StockOverview = () => {
pt={{ base: 20, md: 24 }}
pb={{ base: 16, md: 20 }}
borderBottom={`1px solid rgba(139, 92, 246, 0.3)`}
+ borderRadius="xl"
zIndex={1}
>
{/* 背景装饰 */}
@@ -648,7 +648,7 @@ const StockOverview = () => {
filter="blur(60px)"
/>
-
+
@@ -855,11 +855,11 @@ const StockOverview = () => {
-
+
{/* 主内容区 */}
-
+
{/* 日期选择器 */}
@@ -1195,7 +1195,7 @@ const StockOverview = () => {
)}
-
+
{/* 个股列表弹窗 */}
{
return (
-
{/* 返回按钮 */}
}
@@ -374,7 +372,6 @@ const PostDetail = () => {
-
{/* 图片预览弹窗 */}
{
return (
-
{/* 头部:返回按钮 */}
-
{/* 交易模态框 */}
{
pt="80px"
pb="20"
>
-
+ {/* padding 由 MainLayout 统一设置 */}
+
{/* 顶部横幅 */}
{
-
+
{/* 发帖模态框 */}
|