diff --git a/craco.config.js b/craco.config.js index e5edcef6..64a5afb8 100644 --- a/craco.config.js +++ b/craco.config.js @@ -74,9 +74,9 @@ module.exports = { priority: 20, reuseExistingChunk: true, }, - // 日期/日历库 + // 日期库 calendar: { - test: /[\\/]node_modules[\\/](dayjs|date-fns|@fullcalendar)[\\/]/, + test: /[\\/]node_modules[\\/](dayjs|date-fns)[\\/]/, name: 'calendar-lib', priority: 18, reuseExistingChunk: true, diff --git a/package.json b/package.json index 315f05be..91627049 100755 --- a/package.json +++ b/package.json @@ -15,14 +15,6 @@ "@fontsource/raleway": "^4.5.0", "@fontsource/roboto": "^4.5.0", "@reduxjs/toolkit": "^2.9.2", - "@splidejs/react-splide": "^0.7.12", - "@tanstack/react-virtual": "^3.13.12", - "@tippyjs/react": "^4.2.6", - "@visx/responsive": "^3.12.0", - "@visx/scale": "^3.12.0", - "@visx/text": "^3.12.0", - "@visx/visx": "^3.12.0", - "@visx/wordcloud": "^3.12.0", "antd": "^5.27.4", "axios": "^1.10.0", "classnames": "^2.5.1", @@ -32,34 +24,26 @@ "echarts-for-react": "^3.0.2", "echarts-wordcloud": "^2.1.0", "framer-motion": "^12.23.24", - "fullcalendar": "^5.9.0", "history": "^5.3.0", "klinecharts": "^10.0.0-beta1", "lucide-react": "^0.540.0", "posthog-js": "^1.295.0", "react": "^19.0.0", - "react-custom-scrollbars-2": "^4.4.0", "react-dom": "^19.0.0", - "react-force-graph-3d": "^1.29.0", "react-github-btn": "^1.2.1", "react-icons": "^4.12.0", "react-input-pin-code": "^1.1.5", "react-is": "^19.0.0", - "react-just-parallax": "^3.1.16", "react-markdown": "^10.1.0", "react-redux": "^9.2.0", - "react-responsive": "^10.0.1", "react-router-dom": "^6.30.1", "react-scripts": "^5.0.1", "react-scroll": "^1.8.4", - "react-scroll-into-view": "^2.1.3", "react-table": "^7.7.0", "react-to-print": "^3.0.3", - "recharts": "^3.1.2", "remark-gfm": "^4.0.1", "sass": "^1.49.9", "socket.io-client": "^4.7.4", - "styled-components": "^5.3.11", "stylis": "^4.0.10", "stylis-plugin-rtl": "^2.1.1", "typescript": "^5.9.3" diff --git a/src/assets/scss/soft-design-system-pro/plugins/pro/_fullcalendar-extend.scss b/src/assets/scss/soft-design-system-pro/plugins/pro/_fullcalendar-extend.scss deleted file mode 100755 index 89014b74..00000000 --- a/src/assets/scss/soft-design-system-pro/plugins/pro/_fullcalendar-extend.scss +++ /dev/null @@ -1,94 +0,0 @@ -/*! -FullCalendar v5.6.0 -Docs & License: https://fullcalendar.io/ -(c) 2020 Adam Shaw -*/ - -// Creative Tim Changes - -.fc-theme-standard .fc-scrollgrid, -.fc-scrollgrid { - border: none; -} - -.fc-theme-standard td, -.fc-theme-standard th { - border-color: $gray-200; -} - -.fc-theme-standard thead tr th{ - border-right: none; - border-left: none; -} - -.fc-theme-standard td{ - &:last-child{ - border-right: none; - } -} - -.fc-theme-standard tr{ - &:last-child td{ - border-bottom: none; - } -} - -.fc-h-event{ - border: none; -} - -.fc-daygrid-block-event .fc-event-time, -.fc-daygrid-block-event .fc-event-title{ - font-weight: $font-weight-bold; - padding: $fc-event-title-padding-y $fc-event-title-padding-x; -} - -.fc .fc-daygrid-body-natural .fc-daygrid-day-events{ - margin-bottom: 0; -} - -.fc .fc-col-header-cell-cushion{ - font-size: $font-size-sm; - font-weight: $font-weight-bold; - color: $gray-500; -} - -.fc .fc-daygrid-day-number{ - color: $gray-700; - font-size: $font-size-sm; - font-weight: $font-weight-bold; - width: 100%; - text-align: center; -} - -.fc .fc-toolbar-title{ - font-size: $font-size-lg; -} - -.fc .fc-button{ - font-size: $font-size-sm; - padding-top: $spacer*0.25; - box-shadow: $btn-box-shadow; - transition: $btn-transition; - .fc-icon{ - font-size: $font-size-sm; - } -} - -.fc .fc-button-primary{ - background-color: $secondary; - border-color: $secondary; - &:hover, - &:focus, - &:not(:disabled):active, - &:not(:disabled):active:focus{ - transform: $btn-hover-transform; - box-shadow: $btn-hover-box-shadow; - background-color: $secondary; - border-color: $secondary; - } -} - -.fc-event.fc-daygrid-event{ - border-radius: $fc-daygrid-event-border-radius; -} diff --git a/src/assets/scss/soft-design-system-pro/plugins/pro/_fullcalendar.scss b/src/assets/scss/soft-design-system-pro/plugins/pro/_fullcalendar.scss deleted file mode 100755 index 2976902f..00000000 --- a/src/assets/scss/soft-design-system-pro/plugins/pro/_fullcalendar.scss +++ /dev/null @@ -1,1436 +0,0 @@ -/*! -FullCalendar v5.6.0 -Docs & License: https://fullcalendar.io/ -(c) 2020 Adam Shaw -*/ -/* classes attached to */ - -/* classes attached to */ - -.fc-not-allowed, -.fc-not-allowed .fc-event { /* override events' custom cursors */ - cursor: not-allowed; -} - -.fc-unselectable { - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - -webkit-touch-callout: none; - -webkit-tap-highlight-color: rgba(0, 0, 0, 0); -} -.fc { - /* layout of immediate children */ - display: flex; - flex-direction: column; - - font-size: 1em -} -.fc, - .fc *, - .fc *:before, - .fc *:after { - box-sizing: border-box; - } -.fc table { - border-collapse: collapse; - border-spacing: 0; - font-size: 1em; /* normalize cross-browser */ - } -.fc th { - text-align: center; - } -.fc th, - .fc td { - vertical-align: top; - padding: 0; - } -.fc a[data-navlink] { - cursor: pointer; - } -.fc a[data-navlink]:hover { - text-decoration: underline; - } -.fc-direction-ltr { - direction: ltr; - text-align: left; -} -.fc-direction-rtl { - direction: rtl; - text-align: right; -} -.fc-theme-standard td, - .fc-theme-standard th { - border: 1px solid #ddd; - border: 1px solid var(--fc-border-color, #ddd); - } -/* for FF, which doesn't expand a 100% div within a table cell. use absolute positioning */ -/* inner-wrappers are responsible for being absolute */ -/* TODO: best place for this? */ -.fc-liquid-hack td, - .fc-liquid-hack th { - position: relative; - } - -@font-face { - font-family: 'fcicons'; - src: url("data:application/x-font-ttf;charset=utf-8;base64,AAEAAAALAIAAAwAwT1MvMg8SBfAAAAC8AAAAYGNtYXAXVtKNAAABHAAAAFRnYXNwAAAAEAAAAXAAAAAIZ2x5ZgYydxIAAAF4AAAFNGhlYWQUJ7cIAAAGrAAAADZoaGVhB20DzAAABuQAAAAkaG10eCIABhQAAAcIAAAALGxvY2ED4AU6AAAHNAAAABhtYXhwAA8AjAAAB0wAAAAgbmFtZXsr690AAAdsAAABhnBvc3QAAwAAAAAI9AAAACAAAwPAAZAABQAAApkCzAAAAI8CmQLMAAAB6wAzAQkAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADpBgPA/8AAQAPAAEAAAAABAAAAAAAAAAAAAAAgAAAAAAADAAAAAwAAABwAAQADAAAAHAADAAEAAAAcAAQAOAAAAAoACAACAAIAAQAg6Qb//f//AAAAAAAg6QD//f//AAH/4xcEAAMAAQAAAAAAAAAAAAAAAQAB//8ADwABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAWIAjQKeAskAEwAAJSc3NjQnJiIHAQYUFwEWMjc2NCcCnuLiDQ0MJAz/AA0NAQAMJAwNDcni4gwjDQwM/wANIwz/AA0NDCMNAAAAAQFiAI0CngLJABMAACUBNjQnASYiBwYUHwEHBhQXFjI3AZ4BAA0N/wAMJAwNDeLiDQ0MJAyNAQAMIw0BAAwMDSMM4uINIwwNDQAAAAIA4gC3Ax4CngATACcAACUnNzY0JyYiDwEGFB8BFjI3NjQnISc3NjQnJiIPAQYUHwEWMjc2NCcB87e3DQ0MIw3VDQ3VDSMMDQ0BK7e3DQ0MJAzVDQ3VDCQMDQ3zuLcMJAwNDdUNIwzWDAwNIwy4twwkDA0N1Q0jDNYMDA0jDAAAAgDiALcDHgKeABMAJwAAJTc2NC8BJiIHBhQfAQcGFBcWMjchNzY0LwEmIgcGFB8BBwYUFxYyNwJJ1Q0N1Q0jDA0Nt7cNDQwjDf7V1Q0N1QwkDA0Nt7cNDQwkDLfWDCMN1Q0NDCQMt7gMIw0MDNYMIw3VDQ0MJAy3uAwjDQwMAAADAFUAAAOrA1UAMwBoAHcAABMiBgcOAQcOAQcOARURFBYXHgEXHgEXHgEzITI2Nz4BNz4BNz4BNRE0JicuAScuAScuASMFITIWFx4BFx4BFx4BFREUBgcOAQcOAQcOASMhIiYnLgEnLgEnLgE1ETQ2Nz4BNz4BNz4BMxMhMjY1NCYjISIGFRQWM9UNGAwLFQkJDgUFBQUFBQ4JCRULDBgNAlYNGAwLFQkJDgUFBQUFBQ4JCRULDBgN/aoCVgQIBAQHAwMFAQIBAQIBBQMDBwQECAT9qgQIBAQHAwMFAQIBAQIBBQMDBwQECASAAVYRGRkR/qoRGRkRA1UFBAUOCQkVDAsZDf2rDRkLDBUJCA4FBQUFBQUOCQgVDAsZDQJVDRkLDBUJCQ4FBAVVAgECBQMCBwQECAX9qwQJAwQHAwMFAQICAgIBBQMDBwQDCQQCVQUIBAQHAgMFAgEC/oAZEhEZGRESGQAAAAADAFUAAAOrA1UAMwBoAIkAABMiBgcOAQcOAQcOARURFBYXHgEXHgEXHgEzITI2Nz4BNz4BNz4BNRE0JicuAScuAScuASMFITIWFx4BFx4BFx4BFREUBgcOAQcOAQcOASMhIiYnLgEnLgEnLgE1ETQ2Nz4BNz4BNz4BMxMzFRQWMzI2PQEzMjY1NCYrATU0JiMiBh0BIyIGFRQWM9UNGAwLFQkJDgUFBQUFBQ4JCRULDBgNAlYNGAwLFQkJDgUFBQUFBQ4JCRULDBgN/aoCVgQIBAQHAwMFAQIBAQIBBQMDBwQECAT9qgQIBAQHAwMFAQIBAQIBBQMDBwQECASAgBkSEhmAERkZEYAZEhIZgBEZGREDVQUEBQ4JCRUMCxkN/asNGQsMFQkIDgUFBQUFBQ4JCBUMCxkNAlUNGQsMFQkJDgUEBVUCAQIFAwIHBAQIBf2rBAkDBAcDAwUBAgICAgEFAwMHBAMJBAJVBQgEBAcCAwUCAQL+gIASGRkSgBkSERmAEhkZEoAZERIZAAABAOIAjQMeAskAIAAAExcHBhQXFjI/ARcWMjc2NC8BNzY0JyYiDwEnJiIHBhQX4uLiDQ0MJAzi4gwkDA0N4uINDQwkDOLiDCQMDQ0CjeLiDSMMDQ3h4Q0NDCMN4uIMIw0MDOLiDAwNIwwAAAABAAAAAQAAa5n0y18PPPUACwQAAAAAANivOVsAAAAA2K85WwAAAAADqwNVAAAACAACAAAAAAAAAAEAAAPA/8AAAAQAAAAAAAOrAAEAAAAAAAAAAAAAAAAAAAALBAAAAAAAAAAAAAAAAgAAAAQAAWIEAAFiBAAA4gQAAOIEAABVBAAAVQQAAOIAAAAAAAoAFAAeAEQAagCqAOoBngJkApoAAQAAAAsAigADAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAA4ArgABAAAAAAABAAcAAAABAAAAAAACAAcAYAABAAAAAAADAAcANgABAAAAAAAEAAcAdQABAAAAAAAFAAsAFQABAAAAAAAGAAcASwABAAAAAAAKABoAigADAAEECQABAA4ABwADAAEECQACAA4AZwADAAEECQADAA4APQADAAEECQAEAA4AfAADAAEECQAFABYAIAADAAEECQAGAA4AUgADAAEECQAKADQApGZjaWNvbnMAZgBjAGkAYwBvAG4Ac1ZlcnNpb24gMS4wAFYAZQByAHMAaQBvAG4AIAAxAC4AMGZjaWNvbnMAZgBjAGkAYwBvAG4Ac2ZjaWNvbnMAZgBjAGkAYwBvAG4Ac1JlZ3VsYXIAUgBlAGcAdQBsAGEAcmZjaWNvbnMAZgBjAGkAYwBvAG4Ac0ZvbnQgZ2VuZXJhdGVkIGJ5IEljb01vb24uAEYAbwBuAHQAIABnAGUAbgBlAHIAYQB0AGUAZAAgAGIAeQAgAEkAYwBvAE0AbwBvAG4ALgAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") format('truetype'); - font-weight: normal; - font-style: normal; -} - -.fc-icon { - /* added for fc */ - display: inline-block; - width: 1em; - height: 1em; - text-align: center; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - - /* use !important to prevent issues with browser extensions that change fonts */ - font-family: 'fcicons' !important; - speak: none; - font-style: normal; - font-weight: normal; - font-variant: normal; - text-transform: none; - line-height: 1; - - /* Better Font Rendering =========== */ - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -.fc-icon-chevron-left:before { - content: "\e900"; -} - -.fc-icon-chevron-right:before { - content: "\e901"; -} - -.fc-icon-chevrons-left:before { - content: "\e902"; -} - -.fc-icon-chevrons-right:before { - content: "\e903"; -} - -.fc-icon-minus-square:before { - content: "\e904"; -} - -.fc-icon-plus-square:before { - content: "\e905"; -} - -.fc-icon-x:before { - content: "\e906"; -} -/* -Lots taken from Flatly (MIT): https://bootswatch.com/4/flatly/bootstrap.css - -These styles only apply when the standard-theme is activated. -When it's NOT activated, the fc-button classes won't even be in the DOM. -*/ -.fc { - - /* reset */ - -} -.fc .fc-button { - border-radius: 0; - overflow: visible; - text-transform: none; - margin: 0; - font-family: inherit; - font-size: inherit; - line-height: inherit; - } -.fc .fc-button:focus { - outline: 1px dotted; - outline: 5px auto -webkit-focus-ring-color; - } -.fc .fc-button { - -webkit-appearance: button; - } -.fc .fc-button:not(:disabled) { - cursor: pointer; - } -.fc .fc-button::-moz-focus-inner { - padding: 0; - border-style: none; - } -.fc { - - /* theme */ - -} -.fc .fc-button { - display: inline-block; - font-weight: 400; - text-align: center; - vertical-align: middle; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - background-color: transparent; - border: 1px solid transparent; - padding: 0.4em 0.65em; - font-size: 1em; - line-height: 1.5; - border-radius: 0.25em; - } -.fc .fc-button:hover { - text-decoration: none; - } -.fc .fc-button:focus { - outline: 0; - box-shadow: 0 0 0 0.2rem rgba(44, 62, 80, 0.25); - } -.fc .fc-button:disabled { - opacity: 0.65; - } -.fc { - - /* "primary" coloring */ - -} -.fc .fc-button-primary { - color: #0A0A14; - color: var(--fc-button-text-color, #0A0A14); - background-color: #D4AF37; - background-color: var(--fc-button-bg-color, #D4AF37); - border-color: #D4AF37; - border-color: var(--fc-button-border-color, #D4AF37); - font-weight: 600; - } -.fc .fc-button-primary:hover { - color: #0A0A14; - color: var(--fc-button-text-color, #0A0A14); - background-color: #B8960C; - background-color: var(--fc-button-hover-bg-color, #B8960C); - border-color: #B8960C; - border-color: var(--fc-button-hover-border-color, #B8960C); - } -.fc .fc-button-primary:disabled { /* not DRY */ - color: #0A0A14; - color: var(--fc-button-text-color, #0A0A14); - background-color: #B8960C; - background-color: var(--fc-button-bg-color, #B8960C); - border-color: #B8960C; - border-color: var(--fc-button-border-color, #B8960C); /* overrides :hover */ - opacity: 1; - } -.fc .fc-button-primary:focus { - box-shadow: 0 0 0 0.2rem rgba(212, 175, 55, 0.5); - } -.fc .fc-button-primary:not(:disabled):active, - .fc .fc-button-primary:not(:disabled).fc-button-active { - color: #0A0A14; - color: var(--fc-button-text-color, #0A0A14); - background-color: #B8960C; - background-color: var(--fc-button-active-bg-color, #B8960C); - border-color: #B8960C; - border-color: var(--fc-button-active-border-color, #B8960C); - } -.fc .fc-button-primary:not(:disabled):active:focus, - .fc .fc-button-primary:not(:disabled).fc-button-active:focus { - box-shadow: 0 0 0 0.2rem rgba(212, 175, 55, 0.5); - } -.fc { - - /* icons within buttons */ - -} -.fc .fc-button .fc-icon { - vertical-align: middle; - font-size: 1.5em; /* bump up the size (but don't make it bigger than line-height of button, which is 1.5em also) */ - } -.fc .fc-button-group { - position: relative; - display: inline-flex; - vertical-align: middle; - } -.fc .fc-button-group > .fc-button { - position: relative; - flex: 1 1 auto; - } -.fc .fc-button-group > .fc-button:hover { - z-index: 1; - } -.fc .fc-button-group > .fc-button:focus, - .fc .fc-button-group > .fc-button:active, - .fc .fc-button-group > .fc-button.fc-button-active { - z-index: 1; - } -.fc-direction-ltr .fc-button-group > .fc-button:not(:first-child) { - margin-left: -1px; - border-top-left-radius: 0; - border-bottom-left-radius: 0; - } -.fc-direction-ltr .fc-button-group > .fc-button:not(:last-child) { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } -.fc-direction-rtl .fc-button-group > .fc-button:not(:first-child) { - margin-right: -1px; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } -.fc-direction-rtl .fc-button-group > .fc-button:not(:last-child) { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - } -.fc .fc-toolbar { - display: flex; - justify-content: space-between; - align-items: center; - } -.fc .fc-toolbar.fc-header-toolbar { - margin-bottom: 1.5em; - } -.fc .fc-toolbar.fc-footer-toolbar { - margin-top: 1.5em; - } -.fc .fc-toolbar-title { - font-size: 1.75em; - margin: 0; - } -.fc-direction-ltr .fc-toolbar > * > :not(:first-child) { - margin-left: .75em; /* space between */ - } -.fc-direction-rtl .fc-toolbar > * > :not(:first-child) { - margin-right: .75em; /* space between */ - } -.fc-direction-rtl .fc-toolbar-ltr { /* when the toolbar-chunk positioning system is explicitly left-to-right */ - flex-direction: row-reverse; - } -.fc .fc-scroller { - -webkit-overflow-scrolling: touch; - position: relative; /* for abs-positioned elements within */ - } -.fc .fc-scroller-liquid { - height: 100%; - } -.fc .fc-scroller-liquid-absolute { - position: absolute; - top: 0; - right: 0; - left: 0; - bottom: 0; - } -.fc .fc-scroller-harness { - position: relative; - overflow: hidden; - direction: ltr; - /* hack for chrome computing the scroller's right/left wrong for rtl. undone below... */ - /* TODO: demonstrate in codepen */ - } -.fc .fc-scroller-harness-liquid { - height: 100%; - } -.fc-direction-rtl .fc-scroller-harness > .fc-scroller { /* undo above hack */ - direction: rtl; - } -.fc-theme-standard .fc-scrollgrid { - border: 1px solid #ddd; - border: 1px solid var(--fc-border-color, #ddd); /* bootstrap does this. match */ - } -.fc .fc-scrollgrid, - .fc .fc-scrollgrid table { /* all tables (self included) */ - width: 100%; /* because tables don't normally do this */ - table-layout: fixed; - } -.fc .fc-scrollgrid table { /* inner tables */ - border-top-style: hidden; - border-left-style: hidden; - border-right-style: hidden; - } -.fc .fc-scrollgrid { - - border-collapse: separate; - border-right-width: 0; - border-bottom-width: 0; - - } -.fc .fc-scrollgrid-liquid { - height: 100%; - } -.fc .fc-scrollgrid-section { /* a */ - height: 1px /* better than 0, for firefox */ - - } -.fc .fc-scrollgrid-section > td { - height: 1px; /* needs a height so inner div within grow. better than 0, for firefox */ - } -.fc .fc-scrollgrid-section table { - height: 1px; - /* for most browsers, if a height isn't set on the table, can't do liquid-height within cells */ - /* serves as a min-height. harmless */ - } -.fc .fc-scrollgrid-section-liquid { - height: auto - - } -.fc .fc-scrollgrid-section-liquid > td { - height: 100%; /* better than `auto`, for firefox */ - } -.fc .fc-scrollgrid-section > * { - border-top-width: 0; - border-left-width: 0; - } -.fc .fc-scrollgrid-section-header > *, - .fc .fc-scrollgrid-section-footer > * { - border-bottom-width: 0; - } -.fc .fc-scrollgrid-section-body table, - .fc .fc-scrollgrid-section-footer table { - border-bottom-style: hidden; /* head keeps its bottom border tho */ - } -.fc { - - /* stickiness */ - -} -.fc .fc-scrollgrid-section-sticky > * { - background: #fff; - background: var(--fc-page-bg-color, #fff); - position: -webkit-sticky; - position: sticky; - z-index: 2; /* TODO: var */ - /* TODO: box-shadow when sticking */ - } -.fc .fc-scrollgrid-section-header.fc-scrollgrid-section-sticky > * { - top: 0; /* because border-sharing causes a gap at the top */ - /* TODO: give safari -1. has bug */ - } -.fc .fc-scrollgrid-section-footer.fc-scrollgrid-section-sticky > * { - bottom: 0; /* known bug: bottom-stickiness doesn't work in safari */ - } -.fc .fc-scrollgrid-sticky-shim { /* for horizontal scrollbar */ - height: 1px; /* needs height to create scrollbars */ - margin-bottom: -1px; - } -.fc-sticky { /* no .fc wrap because used as child of body */ - position: -webkit-sticky; - position: sticky; -} -.fc .fc-view-harness { - flex-grow: 1; /* because this harness is WITHIN the .fc's flexbox */ - position: relative; - } -.fc { - - /* when the harness controls the height, make the view liquid */ - -} -.fc .fc-view-harness-active > .fc-view { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - } -.fc .fc-col-header-cell-cushion { - display: inline-block; /* x-browser for when sticky (when multi-tier header) */ - padding: 2px 4px; - } -.fc .fc-bg-event, - .fc .fc-non-business, - .fc .fc-highlight { - /* will always have a harness with position:relative/absolute, so absolutely expand */ - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - } -.fc .fc-non-business { - background: rgba(215, 215, 215, 0.3); - background: var(--fc-non-business-color, rgba(215, 215, 215, 0.3)); - } -.fc .fc-bg-event { - background: rgb(143, 223, 130); - background: var(--fc-bg-event-color, rgb(143, 223, 130)); - opacity: 0.3; - opacity: var(--fc-bg-event-opacity, 0.3) - } -.fc .fc-bg-event .fc-event-title { - margin: .5em; - font-size: .85em; - font-size: var(--fc-small-font-size, .85em); - font-style: italic; - } -.fc .fc-highlight { - background: rgba(188, 232, 241, 0.3); - background: var(--fc-highlight-color, rgba(188, 232, 241, 0.3)); - } -.fc .fc-cell-shaded, - .fc .fc-day-disabled { - background: rgba(208, 208, 208, 0.3); - background: var(--fc-neutral-bg-color, rgba(208, 208, 208, 0.3)); - } -/* link resets */ -/* ---------------------------------------------------------------------------------------------------- */ -a.fc-event, -a.fc-event:hover { - text-decoration: none; -} -/* cursor */ -.fc-event[href], -.fc-event.fc-event-draggable { - cursor: pointer; -} -/* event text content */ -/* ---------------------------------------------------------------------------------------------------- */ -.fc-event .fc-event-main { - position: relative; - z-index: 2; - } -/* dragging */ -/* ---------------------------------------------------------------------------------------------------- */ -.fc-event-dragging:not(.fc-event-selected) { /* MOUSE */ - opacity: 0.75; - } -.fc-event-dragging.fc-event-selected { /* TOUCH */ - box-shadow: 0 2px 7px rgba(0, 0, 0, 0.3); - } -/* resizing */ -/* ---------------------------------------------------------------------------------------------------- */ -/* (subclasses should hone positioning for touch and non-touch) */ -.fc-event .fc-event-resizer { - display: none; - position: absolute; - z-index: 4; - } -.fc-event:hover, /* MOUSE */ -.fc-event-selected { /* TOUCH */ - -} -.fc-event:hover .fc-event-resizer, .fc-event-selected .fc-event-resizer { - display: block; - } -.fc-event-selected .fc-event-resizer { - border-radius: 4px; - border-radius: calc(var(--fc-event-resizer-dot-total-width, 8px) / 2); - border-width: 1px; - border-width: var(--fc-event-resizer-dot-border-width, 1px); - width: 8px; - width: var(--fc-event-resizer-dot-total-width, 8px); - height: 8px; - height: var(--fc-event-resizer-dot-total-width, 8px); - border-style: solid; - border-color: inherit; - background: #fff; - background: var(--fc-page-bg-color, #fff) - - /* expand hit area */ - - } -.fc-event-selected .fc-event-resizer:before { - content: ''; - position: absolute; - top: -20px; - left: -20px; - right: -20px; - bottom: -20px; - } -/* selecting (always TOUCH) */ -/* ---------------------------------------------------------------------------------------------------- */ -.fc-event-selected { - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2) - - /* expand hit area (subclasses should expand) */ - -} -.fc-event-selected:before { - content: ""; - position: absolute; - z-index: 3; - top: 0; - left: 0; - right: 0; - bottom: 0; - } -.fc-event-selected { - - /* dimmer effect */ - -} -.fc-event-selected:after { - content: ""; - background: rgba(0, 0, 0, 0.25); - background: var(--fc-event-selected-overlay-color, rgba(0, 0, 0, 0.25)); - position: absolute; - z-index: 1; - - /* assume there's a border on all sides. overcome it. */ - /* sometimes there's NOT a border, in which case the dimmer will go over */ - /* an adjacent border, which looks fine. */ - top: -1px; - left: -1px; - right: -1px; - bottom: -1px; - } -/* -A HORIZONTAL event -*/ -.fc-h-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-h-event .fc-event-main { - color: #fff; - color: var(--fc-event-text-color, #fff); - } -.fc-h-event .fc-event-main-frame { - display: flex; /* for make fc-event-title-container expand */ - } -.fc-h-event .fc-event-time { - max-width: 100%; /* clip overflow on this element */ - overflow: hidden; - } -.fc-h-event .fc-event-title-container { /* serves as a container for the sticky cushion */ - flex-grow: 1; - flex-shrink: 1; - min-width: 0; /* important for allowing to shrink all the way */ - } -.fc-h-event .fc-event-title { - display: inline-block; /* need this to be sticky cross-browser */ - vertical-align: top; /* for not messing up line-height */ - left: 0; /* for sticky */ - right: 0; /* for sticky */ - max-width: 100%; /* clip overflow on this element */ - overflow: hidden; - } -.fc-h-event.fc-event-selected:before { - /* expand hit area */ - top: -10px; - bottom: -10px; - } -/* adjust border and border-radius (if there is any) for non-start/end */ -.fc-direction-ltr .fc-daygrid-block-event:not(.fc-event-start), -.fc-direction-rtl .fc-daygrid-block-event:not(.fc-event-end) { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - border-left-width: 0; -} -.fc-direction-ltr .fc-daygrid-block-event:not(.fc-event-end), -.fc-direction-rtl .fc-daygrid-block-event:not(.fc-event-start) { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - border-right-width: 0; -} -/* resizers */ -.fc-h-event:not(.fc-event-selected) .fc-event-resizer { - top: 0; - bottom: 0; - width: 8px; - width: var(--fc-event-resizer-thickness, 8px); -} -.fc-direction-ltr .fc-h-event:not(.fc-event-selected) .fc-event-resizer-start, -.fc-direction-rtl .fc-h-event:not(.fc-event-selected) .fc-event-resizer-end { - cursor: w-resize; - left: -4px; - left: calc(var(--fc-event-resizer-thickness, 8px) / -2); -} -.fc-direction-ltr .fc-h-event:not(.fc-event-selected) .fc-event-resizer-end, -.fc-direction-rtl .fc-h-event:not(.fc-event-selected) .fc-event-resizer-start { - cursor: e-resize; - right: -4px; - right: calc(var(--fc-event-resizer-thickness, 8px) / -2); -} -/* resizers for TOUCH */ -.fc-h-event.fc-event-selected .fc-event-resizer { - top: 50%; - margin-top: -4px; - margin-top: calc(var(--fc-event-resizer-dot-total-width, 8px) / -2); -} -.fc-direction-ltr .fc-h-event.fc-event-selected .fc-event-resizer-start, -.fc-direction-rtl .fc-h-event.fc-event-selected .fc-event-resizer-end { - left: -4px; - left: calc(var(--fc-event-resizer-dot-total-width, 8px) / -2); -} -.fc-direction-ltr .fc-h-event.fc-event-selected .fc-event-resizer-end, -.fc-direction-rtl .fc-h-event.fc-event-selected .fc-event-resizer-start { - right: -4px; - right: calc(var(--fc-event-resizer-dot-total-width, 8px) / -2); -} - - -:root { - --fc-daygrid-event-dot-width: 8px; -} -.fc .fc-popover { - position: fixed; - top: 0; /* for when not positioned yet */ - box-shadow: 0 2px 6px rgba(0,0,0,.15); - } -.fc .fc-popover-header { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - padding: 3px 4px; - } -.fc .fc-popover-title { - margin: 0 2px; - } -.fc .fc-popover-close { - cursor: pointer; - opacity: 0.65; - font-size: 1.1em; - } -.fc-theme-standard .fc-popover { - border: 1px solid #ddd; - border: 1px solid var(--fc-border-color, #ddd); - background: #fff; - background: var(--fc-page-bg-color, #fff); - } -.fc-theme-standard .fc-popover-header { - background: rgba(208, 208, 208, 0.3); - background: var(--fc-neutral-bg-color, rgba(208, 208, 208, 0.3)); - } -/* help things clear margins of inner content */ -.fc-daygrid-day-frame, -.fc-daygrid-day-events, -.fc-daygrid-event-harness { /* for event top/bottom margins */ -} -.fc-daygrid-day-frame:before, .fc-daygrid-day-events:before, .fc-daygrid-event-harness:before { - content: ""; - clear: both; - display: table; } -.fc-daygrid-day-frame:after, .fc-daygrid-day-events:after, .fc-daygrid-event-harness:after { - content: ""; - clear: both; - display: table; } -.fc .fc-daygrid-body { /* a
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线 - - - - {/* 副图指标选择 */} - - - {/* 刷新按钮 */} - - - - - {/* 错误提示 */} - {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 ( - - - data.percentage} - outerRadius={half} - innerRadius={({ data }) => { - const size = active && active.name == data.name ? 12 : 8; - return half - size; - }} - padAngle={0.01} - > - {(pie) => { - return pie.arcs.map((arc) => { - return ( - setActive(arc.data)} - onMouseLeave={() => setActive(null)} - > - - - ); - }); - }} - - - - <> - - - - {title} - - - - - - ); -}; - -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 && ( - - 0.5} - > - {(cloudWords) => - cloudWords.map((w, i) => ( - - {w.text} - - )) - } - - - )} - - ); -}; - -// ==================== 词云组件实现 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 ( - {/* 返回按钮 */}