diff --git a/.gitignore b/.gitignore index e6a8075..744fc80 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,5 @@ backend/src/main/resources/application-local.yml /backend/m2repo_local/ /backend/src/test/ /backend/target/ +/.claude +/web-fe/ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e45e729..ecc5be5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -48,12 +48,24 @@ export default function App() { algorithm: themeMode === 'tech' ? theme.darkAlgorithm : theme.defaultAlgorithm, token: { colorPrimary: colorPrimary, - borderRadius: 10, - colorBgLayout: 'transparent', + borderRadius: 4, + colorBgLayout: '#f5f6fa', }, components: { Card: { - borderRadiusLG: 16 + borderRadiusLG: 4 + }, + Button: { + borderRadius: 4 + }, + Input: { + borderRadius: 4 + }, + Select: { + borderRadius: 4 + }, + Modal: { + borderRadiusLG: 4 } } }} diff --git a/frontend/src/assets/home/mask.png b/frontend/src/assets/home/mask.png new file mode 100644 index 0000000..f98ec47 Binary files /dev/null and b/frontend/src/assets/home/mask.png differ diff --git a/frontend/src/components/ThemeSelector/ThemeSelector.tsx b/frontend/src/components/ThemeSelector/ThemeSelector.tsx index da7634b..3ce4228 100644 --- a/frontend/src/components/ThemeSelector/ThemeSelector.tsx +++ b/frontend/src/components/ThemeSelector/ThemeSelector.tsx @@ -1,15 +1,15 @@ -import { ColorPicker, Space, Drawer, Segmented, Typography, Button } from 'antd'; -import { FormatPainterOutlined, LayoutOutlined } from '@ant-design/icons'; +import { ColorPicker, Space, Drawer, Segmented, Typography } from 'antd'; +import { FormatPainterOutlined } from '@ant-design/icons'; import { useTranslation } from 'react-i18next'; import { useState } from 'react'; -import { useThemeStore, type ThemeMode, type LayoutMode } from '@/store/themeStore'; +import { useThemeStore, type ThemeMode } from '@/store/themeStore'; const { Text } = Typography; export default function ThemeSelector() { const { t } = useTranslation(); const [open, setOpen] = useState(false); - const { colorPrimary, themeMode, layoutMode, setColorPrimary, setThemeMode, setLayoutMode } = useThemeStore(); + const { colorPrimary, themeMode, setColorPrimary, setThemeMode } = useThemeStore(); const themeOptions = [ { label: t('theme.default', 'Default'), value: 'default' }, @@ -17,11 +17,6 @@ export default function ThemeSelector() { { label: t('theme.tech', 'Tech'), value: 'tech' }, ]; - const layoutOptions = [ - { label: t('theme.layoutSide', 'Side Menu'), value: 'side' }, - { label: t('theme.layoutTop', 'Top Menu'), value: 'top' }, - ]; - return (
setOpen(true)}> @@ -60,19 +55,6 @@ export default function ThemeSelector() { onChange={(value) => setThemeMode(value as ThemeMode)} />
- -
-
- - {t('theme.layout', 'Navigation Mode')} -
- setLayoutMode(value as LayoutMode)} - /> -
diff --git a/frontend/src/components/shared/AppPagination/index.css b/frontend/src/components/shared/AppPagination/index.css index fa9710d..6808be1 100644 --- a/frontend/src/components/shared/AppPagination/index.css +++ b/frontend/src/components/shared/AppPagination/index.css @@ -2,21 +2,60 @@ margin-top: auto; flex-shrink: 0; width: 100%; + height: 50px; + padding-top: 8px; + background: #fff; + box-sizing: border-box; + min-width: 0; + overflow: hidden; + position: relative; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.app-pagination-total { + flex: 0 0 auto; + min-width: 0; + color: #333; + white-space: nowrap; } .app-pagination-container .ant-pagination { - flex: 1 1 100%; - width: 100%; + flex: 1 1 auto; + width: auto; + min-width: 0; display: flex; align-items: center; + justify-content: flex-end; margin: 0 !important; + overflow: visible; } .app-pagination-container .ant-pagination-total-text { - margin-right: auto; + display: none; } .app-pagination-container .ant-select-selection-search-input { caret-color: transparent; cursor: pointer; } + +@media (max-width: 768px) { + .app-pagination-container { + height: auto; + min-height: 50px; + align-items: flex-start; + flex-direction: column; + gap: 8px; + overflow: visible; + } + + .app-pagination-container .ant-pagination { + width: 100%; + justify-content: flex-start; + flex-wrap: wrap; + row-gap: 8px; + } +} diff --git a/frontend/src/components/shared/AppPagination/index.tsx b/frontend/src/components/shared/AppPagination/index.tsx index 1551321..e8a4687 100644 --- a/frontend/src/components/shared/AppPagination/index.tsx +++ b/frontend/src/components/shared/AppPagination/index.tsx @@ -9,22 +9,28 @@ export interface AppPaginationProps extends PaginationProps { export default function AppPagination(props: AppPaginationProps) { const { t } = useTranslation(); - const { className, showSizeChanger, ...restProps } = props; + const { className, showSizeChanger, showTotal, total, ...restProps } = props; const mergedClassName = ['app-global-pagination', className].filter(Boolean).join(' '); const mergedShowSizeChanger = showSizeChanger === undefined || showSizeChanger === true ? { showSearch: false } : showSizeChanger; + const current = Number(restProps.current ?? restProps.defaultCurrent ?? 1); + const pageSize = Number(restProps.pageSize ?? restProps.defaultPageSize ?? 10); + const rangeStart = total > 0 ? (current - 1) * pageSize + 1 : 0; + const rangeEnd = total > 0 ? Math.min(current * pageSize, total) : 0; + const totalContent = showTotal ? showTotal(total, [rangeStart, rangeEnd]) : t('common.total', { total }); return (
+
{totalContent}
t('common.total', { total })} pageSizeOptions={['8','10', '20', '50', '100']} size="default" + total={total} {...restProps} />
diff --git a/frontend/src/components/shared/ListTable/ListTable.css b/frontend/src/components/shared/ListTable/ListTable.css index 02e27ff..1a57d25 100644 --- a/frontend/src/components/shared/ListTable/ListTable.css +++ b/frontend/src/components/shared/ListTable/ListTable.css @@ -6,11 +6,13 @@ flex-direction: column; flex: 1; min-height: 0; + min-width: 0; + background: #fff; } /* 行选中样式 */ -.list-table-container .row-selected { - background-color: var(--item-hover-bg); +.list-table-container .row-selected > td { + background-color: var(--item-hover-bg) !important; } .list-table-container .row-selected:hover > td { @@ -25,7 +27,7 @@ } .selection-count { - color: var(--text-color-secondary); + color: #9095a1; font-size: 14px; } @@ -48,6 +50,25 @@ opacity: 0.8; } +.list-table-container .ant-table { + color: #333; +} + +.list-table-container .ant-table-thead > tr > th { + background: #fafafa !important; + color: #333; + font-weight: 600; + border-bottom: 1px solid #f0f0f0; +} + +.list-table-container .ant-table-tbody > tr > td { + border-bottom: 1px solid #f0f0f0; +} + +.list-table-container .ant-table-tbody > tr:hover > td { + background: #f5f9ff !important; +} + .list-table-container .list-table-table--y-scroll.ant-table-wrapper, .list-table-container .list-table-table--y-scroll.ant-table-wrapper .ant-spin-nested-loading, .list-table-container .list-table-table--y-scroll.ant-table-wrapper .ant-spin-container, @@ -79,3 +100,17 @@ max-height: var(--list-table-scroll-y) !important; overflow-y: auto !important; } + +.list-table-container .ant-table-wrapper, +.list-table-container .ant-spin-nested-loading, +.list-table-container .ant-spin-container, +.list-table-container .ant-table, +.list-table-container .ant-table-container { + min-width: 0; +} + +.list-table-container .ant-table-wrapper { + flex: 1; + min-height: 0; + overflow: hidden; +} diff --git a/frontend/src/components/shared/PageContainer/PageContainer.css b/frontend/src/components/shared/PageContainer/PageContainer.css new file mode 100644 index 0000000..14de5f9 --- /dev/null +++ b/frontend/src/components/shared/PageContainer/PageContainer.css @@ -0,0 +1,92 @@ +.page-container { + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; + padding: 16px; + gap: 0; + background: #fafafa; +} + +.page-container__header { + position: relative; + display: flex; + justify-content: space-between; + align-items: flex-start; + flex-wrap: wrap; + gap: 16px; + padding: 16px 16px 0; + border: 1px solid #e6e6e6; + border-bottom: none; + border-radius: 4px 4px 0 0; + background: #fff; +} + +.page-container__header--actions-only { + justify-content: flex-end; +} + +.page-container__title-wrap { + flex: 1; + min-width: 220px; + padding-left: 8px; +} + +.page-container__title.ant-typography { + margin: 0 !important; + font-weight: 600; + font-size: 18px; + line-height: 28px; + color: #333333; + position: relative; +} + +.page-container__title.ant-typography::before { + content: ""; + position: absolute; + left: -8px; + top: 6px; + width: 4px; + height: 16px; + background: #3c70f5; +} + +.page-container__subtitle.ant-typography { + display: block; + margin-top: 8px; + padding-bottom: 16px; + font-size: 14px; + line-height: 24px; + color: #9095a1; +} + +.page-container__header-extra { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.page-container__toolbar { + display: flex; + width: 100%; + min-width: 0; + justify-content: flex-end; + align-items: center; + padding: 0 16px 8px; + border-left: 1px solid #e6e6e6; + border-right: 1px solid #e6e6e6; + background: #fff; +} + +.page-container__body { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + padding: 0 16px 16px; + border: 1px solid #e6e6e6; + border-top: none; + border-radius: 0 0 4px 4px; + background: #fff; +} diff --git a/frontend/src/components/shared/PageContainer/index.tsx b/frontend/src/components/shared/PageContainer/index.tsx index f8fd532..8274e89 100644 --- a/frontend/src/components/shared/PageContainer/index.tsx +++ b/frontend/src/components/shared/PageContainer/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import { Space, Typography } from 'antd'; +import { Typography } from 'antd'; import type { ReactNode } from 'react'; +import './PageContainer.css'; const { Title, Text } = Typography; @@ -23,76 +24,37 @@ const PageContainer: React.FC = ({ className = '', style }) => { + const hasTitle = title !== null && title !== undefined && title !== false; + const hasSubtitle = subtitle !== null && subtitle !== undefined && subtitle !== false; + const hasHeader = hasTitle || hasSubtitle || Boolean(headerExtra); + return ( -
- {(title || headerExtra) && ( -
-
- - {title} - - {subtitle && ( - +
+ {hasHeader && ( +
+ {(hasTitle || hasSubtitle) && ( +
+ {hasTitle && ( + + {title} + + )} + {hasSubtitle && ( + {subtitle} - )} -
- {headerExtra && ( -
- {headerExtra} + )}
)} + {headerExtra &&
{headerExtra}
}
)} - {toolbar && ( -
- {toolbar} -
- )} + {toolbar &&
{toolbar}
} -
- {children} -
+
{children}
); }; -export default PageContainer; \ No newline at end of file +export default PageContainer; diff --git a/frontend/src/index.css b/frontend/src/index.css index 0cf96a7..fc78a3b 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -20,6 +20,9 @@ --app-bg-surface-soft: rgba(255, 255, 255, 0.56); --app-bg-surface-strong: rgba(255, 255, 255, 0.82); --app-text-muted: #66758f; + --item-hover-bg: rgba(22, 119, 255, 0.08); + --text-color-secondary: #66758f; + --link-color: #1677ff; } :root[data-theme="minimal"] { @@ -39,6 +42,9 @@ --app-bg-surface-soft: rgba(255, 255, 255, 0.62); --app-bg-surface-strong: rgba(255, 255, 255, 0.86); --app-text-muted: #5b6474; + --item-hover-bg: rgba(22, 119, 255, 0.08); + --text-color-secondary: #5b6474; + --link-color: #1677ff; } :root[data-theme="tech"] { @@ -59,6 +65,9 @@ --app-bg-surface-soft: rgba(10, 21, 37, 0.72); --app-bg-surface-strong: rgba(8, 17, 31, 0.88); --app-text-muted: rgba(190, 206, 229, 0.74); + --item-hover-bg: rgba(88, 151, 255, 0.18); + --text-color-secondary: rgba(190, 206, 229, 0.74); + --link-color: #60a5fa; } html { @@ -1172,3 +1181,98 @@ body::after { 0% { opacity: 0.4; } 100% { opacity: 1; text-shadow: 0 0 8px rgba(22, 119, 255, 0.4); } } + +/* web-fe baseline overrides */ +:root, +:root[data-theme="minimal"], +:root[data-theme="tech"] { + --app-bg-main: #f5f6fa; + --app-bg-overlay: none; + --app-bg-card: #fff; + --app-text-main: #333333; + --app-text-secondary: #9095a1; + --app-border-color: #e6e6e6; + --app-shadow: none; + --app-bg-page: #f5f6fa; + --app-bg-surface: #fff; + --app-bg-surface-soft: #fafafa; + --app-bg-surface-strong: #fff; + --app-text-muted: #9095a1; + --text-color-secondary: #9095a1; + --link-color: #1677ff; +} + +html, +body { + background: #f5f6fa; +} + +body { + font-family: "Microsoft YaHei UI", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; +} + +body::before, +body::after, +#root::before, +#root::after { + display: none !important; +} + +.ant-layout-sider { + background: #fff !important; + border-right: 1px solid #f5f6fa; + backdrop-filter: none; +} + +.ant-menu-light { + background: transparent !important; + color: #333 !important; +} + +.ant-menu-light .ant-menu-item a { + color: #333 !important; +} + +.app-page__filter-card, +.app-page__content-card, +.app-page__panel-card { + border: none; + border-radius: 4px !important; + box-shadow: none; + background: #fff; + backdrop-filter: none; +} + +.app-page__filter-card { + margin-bottom: 0; +} + +.app-page__toolbar .ant-input, +.app-page__toolbar .ant-input-affix-wrapper, +.app-page__toolbar .ant-select-selector, +.app-page__toolbar .ant-picker, +.app-page__toolbar .ant-input-number, +.app-page__toolbar .ant-btn, +.app-page__page-actions .ant-btn, +.ant-btn, +.ant-input, +.ant-input-affix-wrapper, +.ant-select-selector, +.ant-picker { + border-radius: 4px !important; +} + +.app-page__empty-state { + background: #fff; + border-radius: 4px; + border: 1px dashed #d9d9d9; + backdrop-filter: none; +} + +.ant-table-wrapper .ant-table-pagination.ant-pagination.app-global-pagination, +.app-global-pagination.ant-pagination { + padding: 8px 0 0; + background: #fff; + border-top: none; + border-radius: 0; +} diff --git a/frontend/src/layouts/AppLayout.css b/frontend/src/layouts/AppLayout.css new file mode 100644 index 0000000..6444236 --- /dev/null +++ b/frontend/src/layouts/AppLayout.css @@ -0,0 +1,271 @@ +.main-layout { + min-height: 100vh; + height: 100vh; + overflow: hidden; + background: #f5f6fa; +} + +.main-shell { + min-height: 0; + flex: 1; + background: #f5f6fa; +} + +.main-header { + height: 64px; + padding: 0 24px; + background: #fff; + display: flex; + align-items: center; + justify-content: space-between; + flex-shrink: 0; + z-index: 20; +} + +.app-header-logo { + display: flex; + align-items: center; + gap: 12px; + min-width: 0; +} + +.app-header-logo img { + height: 36px; + width: auto; + max-width: 160px; + object-fit: contain; + flex-shrink: 0; +} + +.app-header-logo-title { + color: #1f2a37; + font-size: 18px; + font-weight: 600; + line-height: 1; + max-width: 220px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.header-right { + display: flex; + align-items: center; + gap: 16px; +} + +.main-sider { + position: relative; + height: calc(100vh - 64px); + overflow: visible; + border-right: 1px solid #f5f6fa; + border-top: 2px solid #f5f6fa; + background: #fff !important; + transition: + flex-basis 0.24s ease, + max-width 0.24s ease, + min-width 0.24s ease, + width 0.24s ease; + will-change: width; +} + +.main-sider .ant-layout-sider-children { + height: calc(100vh - 64px); + overflow: visible; +} + +.main-sider .ant-layout-sider-children::-webkit-scrollbar, +.main-menu::-webkit-scrollbar { + width: 6px; +} + +.main-sider .ant-layout-sider-children::-webkit-scrollbar-track, +.main-menu::-webkit-scrollbar-track { + background: #f8f8f8 !important; +} + +.main-sider .ant-layout-sider-children::-webkit-scrollbar-thumb, +.main-menu::-webkit-scrollbar-thumb { + background: #f8f8f8 !important; + border-radius: 3px !important; +} + +.main-sider .ant-layout-sider-children::-webkit-scrollbar-thumb:hover, +.main-menu::-webkit-scrollbar-thumb:hover { + background: #e0e0e0 !important; +} + +.main-sider .ant-layout-sider-trigger { + display: none; +} + +.main-sider .ant-layout-sider-zero-width-trigger { + display: none; +} + +.main-sider .ant-menu { + border-inline-end: none !important; + transition: + width 0.24s ease, + min-width 0.24s ease, + max-width 0.24s ease; +} + +.main-sider .ant-menu-title-content, +.main-sider .ant-menu-submenu-arrow { + transition: + opacity 0.18s ease, + transform 0.18s ease, + color 0.18s ease; +} + +.main-sider--collapsed .ant-menu-title-content, +.main-sider--collapsed .ant-menu-submenu-arrow { + opacity: 0; + transform: translateX(-4px); +} + +.main-sider .ant-menu-item, +.main-sider .ant-menu-submenu-title { + width: calc(100% - 8px); + height: 44px; + line-height: 44px; + margin-inline: 4px; + margin-block: 4px; + border-radius: 8px; + transition: + background-color 0.18s ease, + color 0.18s ease, + padding 0.24s ease, + margin 0.24s ease; +} + +.main-sider--collapsed .ant-menu-item, +.main-sider--collapsed .ant-menu-submenu-title { + width: 56px; + margin-inline: 12px; + box-sizing: border-box; +} + +.main-sider--collapsed .ant-menu-item .ant-menu-item-icon, +.main-sider--collapsed .ant-menu-submenu-title .ant-menu-item-icon { + margin-inline-end: 0; +} + +.main-sider--collapsed .ant-menu-inline-collapsed > .ant-menu-item, +.main-sider--collapsed .ant-menu-inline-collapsed > .ant-menu-submenu > .ant-menu-submenu-title { + padding-inline: 20px !important; +} + +.main-sider--collapsed .ant-menu-inline-collapsed > .ant-menu-item .ant-menu-item-icon, +.main-sider--collapsed .ant-menu-inline-collapsed > .ant-menu-submenu > .ant-menu-submenu-title .ant-menu-item-icon { + line-height: 44px; +} + +.main-sider--collapsed .ant-menu-inline-collapsed { + width: 80px; +} + +.main-sider--collapsed .main-menu { + overflow-x: hidden; +} + +.main-sider .collapsedCenter { + position: absolute; + right: -17px; + top: 18px; + cursor: pointer; + z-index: 100; + width: 35px; + height: 35px; + border-radius: 50%; + text-align: center; + line-height: 35px; + border: 1px solid #f0f0f0; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05); + background: #fff; + transition: + border-color 0.18s ease, + box-shadow 0.18s ease, + transform 0.24s ease; +} + +.main-sider .collapsedCenter:hover { + border-color: rgba(24, 144, 255, 0.4); + box-shadow: 0 4px 8px rgba(24, 144, 255, 0.4); +} + +.main-sider--collapsed .collapsedCenter { + transform: translateX(1px); +} + +.main-sider .collapsedCenter .trigger { + width: 100%; + height: 100%; + min-width: 0; + padding: 0; + background: #fff; + color: #666; + border: none; + box-shadow: none; + display: flex; + align-items: center; + justify-content: center; + line-height: 1; + transition: + color 0.18s ease, + background-color 0.18s ease, + transform 0.24s ease; +} + +.main-sider .collapsedCenter .trigger.ant-btn { + border-radius: 50% !important; +} + +.main-sider .collapsedCenter .trigger .anticon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + line-height: 1; + vertical-align: middle; +} + +.main-sider .collapsedCenter .trigger .anticon svg { + display: block; + width: 16px; + height: 16px; +} + +.main-sider--collapsed .collapsedCenter .trigger .anticon svg { + transform: translateX(1px); +} + +.main-sider .collapsedCenter .trigger:hover { + color: #1890ff; + background: #fff; +} + +.main-menu { + height: calc(100% - 10px); + overflow-y: auto; + overflow-x: hidden; + border-inline-end: none !important; + padding-top: 8px; + background: #fff; +} + +.main-content-layout { + min-width: 0; + min-height: 0; + background: #f5f6fa; +} + +.main-content { + position: relative; + height: calc(100vh - 64px); + min-height: 0; + overflow: hidden; + background: #f5f6fa; +} diff --git a/frontend/src/layouts/AppLayout.tsx b/frontend/src/layouts/AppLayout.tsx index 0af531c..0b70611 100644 --- a/frontend/src/layouts/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout.tsx @@ -6,7 +6,6 @@ import { DashboardOutlined, DesktopOutlined, GlobalOutlined, - LayoutOutlined, LogoutOutlined, MenuFoldOutlined, MenuUnfoldOutlined, @@ -27,9 +26,9 @@ import { useAuth } from "@/hooks/useAuth"; import { usePermission } from "@/hooks/usePermission"; import type { PlatformRuntime, SysPermission, SysPlatformConfig } from "@/types"; import ThemeSelector from "@/components/ThemeSelector/ThemeSelector"; -import { useThemeStore } from "@/store/themeStore"; +import "./AppLayout.css"; -const { Header, Sider, Content, Footer } = Layout; +const { Header, Sider, Content } = Layout; const iconMap: Record = { dashboard: , @@ -75,7 +74,7 @@ function findActiveMenu(nodes: PermissionMenuNode[], path: string, parentKeys: s for (const node of nodes) { const key = getMenuKey(node); - if (node.path === path) { + if (node.path === path || (node.path && node.path !== "/" && path.startsWith(`${node.path}/`))) { return { key, parentKeys }; } @@ -129,7 +128,6 @@ export default function AppLayout() { const navigate = useNavigate(); const { logout } = useAuth(); const { load: loadPermissions } = usePermission(); - const { layoutMode } = useThemeStore(); const fetchInitialData = useCallback(async () => { try { @@ -380,155 +378,56 @@ export default function AppLayout() { ); - const renderLogo = (isTop: boolean = false) => ( -
- logo - {(!collapsed || isTop) && ( - - {platformConfig?.projectName || "UnisBase"} - - )} + const renderLogo = () => ( +
+ {platformConfig?.projectName + {platformConfig?.projectName || "智听云"}
); return ( - - {layoutMode === 'side' && ( + +
+ {renderLogo()} +
{headerRightTools}
+
+ + - {renderLogo(false)} -
- -
-
- )} - - -
- {layoutMode === 'side' ? ( +
- - - - - -
-
- {platformConfig?.icpInfo ? {platformConfig.icpInfo} : null} - {platformConfig?.icpInfo && platformConfig?.copyrightInfo ? : null} - {platformConfig?.copyrightInfo ? {platformConfig.copyrightInfo} : null}
-
+ + + + + + + + + ); diff --git a/frontend/src/pages/business/MeetingPointsManagement.css b/frontend/src/pages/business/MeetingPointsManagement.css new file mode 100644 index 0000000..ad60bb9 --- /dev/null +++ b/frontend/src/pages/business/MeetingPointsManagement.css @@ -0,0 +1,330 @@ +/* 外层容器:完全对齐 web-fe CardWrapper */ +.meeting-points-page { + padding: 8px; + background: #f5f6fa; + min-width: 0; +} + +.meeting-points-page > .page-container__body { + padding: 0; + border: none; + border-radius: 0; + background: transparent; + overflow: hidden; +} + +.meeting-points-page__card-wrapper { + position: relative; + flex: 1; + min-height: 0; + min-width: 0; + padding: 16px; + border: 1px solid #e6e6e6; + border-radius: 4px; + background-color: #fff; + background-image: url("../../assets/home/mask.png"); + background-position: right top; + background-size: contain; + background-repeat: no-repeat; + display: flex; + flex-direction: column; + box-sizing: border-box; + overflow: hidden; +} + +.meeting-points-page__card-title { + z-index: 1; + position: relative; + padding-bottom: 8px; + color: #333; + font-family: "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; + font-size: 18px; + font-weight: 600; + line-height: 28px; + letter-spacing: 0; +} + +.meeting-points-page__card-description { + z-index: 1; + padding-bottom: 16px; + color: #9095a1; + font-family: "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; + font-size: 14px; + font-weight: 400; + line-height: 24px; + letter-spacing: 0; +} + +.meeting-points-page__tabs { + z-index: 1; + flex-shrink: 0; + margin-bottom: 0; + background: transparent; +} + +.meeting-points-page__tabs > .ant-tabs-nav { + margin: 0 !important; + border-bottom: none !important; +} + +.meeting-points-page__tabs .ant-tabs-nav-list { + transition: none !important; +} + +.meeting-points-page__tabs .ant-tabs-content-holder { + display: none; +} + +.meeting-points-page__tabs > .ant-tabs-nav::before { + border-bottom: none !important; +} + +.meeting-points-page__tabs .ant-tabs-ink-bar { + display: none !important; +} + +.meeting-points-page__tabs .ant-tabs-tab.ant-tabs-tab-active { + background-color: #f9fafe !important; + border: none !important; + border-radius: 0 !important; +} + +.meeting-points-page__tabs .ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn { + color: #1677ff !important; + font-weight: 600; +} + +.meeting-points-page__tabs .ant-tabs-tab { + margin-left: 0 !important; + border: 0 solid transparent !important; + border-radius: 0 !important; + background-color: rgba(249, 250, 254, 0) !important; + padding: 10px 16px !important; + transition: none !important; +} + +.meeting-points-page__tabs .ant-tabs-tab-btn { + color: #333; + font-family: "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; + font-size: 14px; + line-height: 22px; + letter-spacing: 0; + transition: none !important; +} + +.meeting-points-page__content-wrap { + z-index: 1; + flex: 1; + min-height: 0; + min-width: 0; + width: 100%; + padding: 8px; + background-color: #f9fafe; + border-radius: 4px; + display: flex; + flex-direction: column; + box-sizing: border-box; + overflow: hidden; +} + +.meeting-points-page__inner-list { + width: 100%; + height: 100%; + min-height: 0; + min-width: 0; + padding: 8px 12px; + background-color: #fff; + border-radius: 4px; + display: flex; + flex-direction: column; + box-sizing: border-box; + overflow: hidden; +} + +.meeting-points-page__inner-list, +.meeting-points-page__inner-list .ant-btn, +.meeting-points-page__inner-list .ant-input, +.meeting-points-page__inner-list .ant-input-affix-wrapper, +.meeting-points-page__inner-list .ant-select, +.meeting-points-page__inner-list .ant-select-selector, +.meeting-points-page__inner-list .ant-table { + font-family: "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; + font-size: 14px; + letter-spacing: 0; +} + +.meeting-points-page__inner-list .ant-btn { + height: 32px; + border-radius: 4px !important; + box-shadow: none; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.meeting-points-page__inner-list .ant-btn .ant-btn-icon { + display: inline-flex; + align-items: center; + line-height: 1; +} + +.meeting-points-page__inner-list .ant-input, +.meeting-points-page__inner-list .ant-input-affix-wrapper, +.meeting-points-page__inner-list .ant-select-selector { + height: 32px !important; + border-radius: 4px !important; +} + +.meeting-points-page__inner-list .ant-input-affix-wrapper { + display: inline-flex; + align-items: center; + padding-top: 0; + padding-bottom: 0; +} + +.meeting-points-page__inner-list .ant-input-affix-wrapper .ant-input { + height: 30px !important; + line-height: 30px; +} + +.meeting-points-page__inner-list .ant-input-prefix { + display: inline-flex; + align-items: center; + height: 100%; + margin-inline-end: 6px; +} + +.meeting-points-page__inner-list .ant-input-prefix .anticon { + display: inline-flex; + align-items: center; + line-height: 1; +} + +.meeting-points-page__inner-list .ant-select-selector { + align-items: center; +} + +.meeting-points-page__search-box { + flex-shrink: 0; + min-width: 0; + margin-bottom: 16px; + min-height: 34px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.meeting-points-page__left-actions { + min-width: 0; + min-height: 32px; + display: flex; + align-items: center; +} + +.meeting-points-page__search-input { + min-width: 0; + display: flex; + justify-content: flex-end; +} + +.meeting-points-page__search-input .ant-space { + min-width: 0; + min-height: 32px; + align-items: center; +} + +.meeting-points-page__table-container { + flex: 1; + min-height: 0; + min-width: 0; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.meeting-points-page__table-area { + flex: 1; + min-height: 0; + min-width: 0; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.meeting-points-page__table-area .app-page__table-wrap, +.meeting-points-page__table-area .list-table-container { + height: 100%; +} + +.meeting-points-page__table-area .ant-table-thead > tr > th { + height: 45px; + color: #000; + font-size: 14px; + font-weight: 600; + background: #fafafa !important; +} + +.meeting-points-page__table-area .ant-table-tbody > tr > td { + height: 47px; + color: #000; + font-size: 14px; + font-weight: 400; +} + +.meeting-points-page__table-area .ant-table-cell { + line-height: 22px; +} + +.meeting-points-page__table-area .ant-table-content, +.meeting-points-page__table-area .ant-table-body { + overflow-x: hidden !important; +} + +.meeting-points-page__table-area .ant-tag { + margin-inline-end: 0; + border-radius: 4px; + font-family: "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; +} + +.meeting-points-page .app-pagination-container { + background: #fff; + border-radius: 0; + box-sizing: border-box; + min-width: 0; + overflow: visible; +} + +.meeting-points-page .app-pagination-container .ant-pagination { + min-width: 0; + overflow: visible; +} + +.meeting-points-page .app-pagination-container .ant-pagination-options { + margin-inline-start: 8px; +} + +.meeting-points-page .app-pagination-container, +.meeting-points-page .app-pagination-container .ant-pagination, +.meeting-points-page .app-pagination-total { + color: #333; + font-family: "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; + font-size: 14px; +} + +@media (max-width: 768px) { + .meeting-points-page__search-box { + align-items: stretch; + flex-direction: column; + } + + .meeting-points-page__search-input, + .meeting-points-page__search-input .ant-space { + width: 100%; + } + + .meeting-points-page__search-input .ant-input-affix-wrapper, + .meeting-points-page__search-input .ant-select { + width: 100% !important; + } +} diff --git a/frontend/src/pages/business/MeetingPointsManagement.tsx b/frontend/src/pages/business/MeetingPointsManagement.tsx index 256d396..39ed167 100644 --- a/frontend/src/pages/business/MeetingPointsManagement.tsx +++ b/frontend/src/pages/business/MeetingPointsManagement.tsx @@ -2,24 +2,21 @@ import { PlusOutlined, ReloadOutlined, SearchOutlined } from "@ant-design/icons" import { listUsers } from "@/api"; import { Button, - Card, - Col, Form, Input, InputNumber, message, Modal, - Row, Select, Space, - Statistic, Tag, + Tabs, Typography, } from "antd"; import { useEffect, useMemo, useState } from "react"; import PageContainer from "@/components/shared/PageContainer"; -import ListTable from "@/components/shared/ListTable/ListTable"; import AppPagination from "@/components/shared/AppPagination"; +import ListTable from "@/components/shared/ListTable/ListTable"; import { getMeetingPointsLedgerPage, getMeetingPointsOverview, @@ -29,6 +26,7 @@ import { type MeetingPointsPersonalAccountVO, } from "@/api/business/meetingPoints"; import type { SysUser } from "@/types"; +import "./MeetingPointsManagement.css"; const { Text } = Typography; @@ -83,74 +81,12 @@ function formatDateTime(value?: string) { return value ? value.replace("T", " ").substring(0, 19) : "-"; } -function buildSummaryCards(overview: MeetingPointsOverviewVO | null) { - if (!overview) { - return []; - } - - const isAdmin = Boolean(overview.admin); - const isPublicOnly = overview.accountMode === ACCOUNT_MODE_PUBLIC; - const isPersonalOnly = overview.accountMode === ACCOUNT_MODE_PERSONAL; - const showPublicSummary = !isPersonalOnly || isAdmin; - const showPersonalSummary = !isPublicOnly; - const isUnlimitedBalanceMode = overview.balanceCheckEnabled === false; - - const cards: Array<{ - key: string; - title: string; - value: number | string; - note: string; - }> = [ - // { - // key: "available-balance", - // title: "当前可用额度", - // value: isUnlimitedBalanceMode ? "无限" : (overview.totalAvailableBalance ?? 0), - // note: isUnlimitedBalanceMode ? "关闭余额校验后只记录消耗和流水" : "按当前账户模式计算的可用额度", - // }, - { - key: "charge-count", - title: "累计消耗次数", - value: overview.totalChargeCount ?? 0, - note: "已发生扣费的总结记录数", - }, - ]; - - if (showPublicSummary) { - cards.unshift( - { - key: "public-balance", - title: "公共账户余额", - value: overview.publicBalance ?? 0, - note: "当前账面公共余额", - }, - { - key: "public-used", - title: "公共账户累计消耗", - value: overview.publicTotalPointsUsed ?? 0, - note: "公共账户历史累计消耗", - }, - ); - } - - if (showPersonalSummary) { - cards.push( - { - key: "personal-balance", - title: isAdmin ? "个人账户余额汇总" : "个人账户余额", - value: overview.personalBalance ?? 0, - note: isAdmin ? "管理员视角下的个人账户余额汇总" : "当前账号账面余额", - }, - { - key: "personal-used", - title: isAdmin ? "个人账户累计消耗汇总" : "个人账户累计消耗", - value: overview.personalTotalPointsUsed ?? 0, - note: isAdmin ? "管理员视角下的个人账户累计消耗" : "当前账号历史累计消耗", - }, - ); - } - - return cards; -} +type OverviewRow = { + id: string; + metric: string; + value: string | number; + note: string; +}; export default function MeetingPointsManagement() { const [overview, setOverview] = useState(null); @@ -161,14 +97,14 @@ export default function MeetingPointsManagement() { const [total, setTotal] = useState(0); const [transferOpen, setTransferOpen] = useState(false); const [users, setUsers] = useState([]); - const [contentTab, setContentTab] = useState("ledger"); + const [activeTabKey, setActiveTabKey] = useState("ledger"); const [personalAccountPagination, setPersonalAccountPagination] = useState({ current: 1, - pageSize: 10, + pageSize: 8, }); const [params, setParams] = useState({ current: 1, - size: 20, + size: 8, username: "", pointsType: "", }); @@ -176,21 +112,28 @@ export default function MeetingPointsManagement() { const isAdmin = Boolean(overview?.admin); const isPublicOnly = overview?.accountMode === ACCOUNT_MODE_PUBLIC; - const isPersonalOnly = overview?.accountMode === ACCOUNT_MODE_PERSONAL; const isUnlimitedBalanceMode = overview?.balanceCheckEnabled === false; const showTransferButton = isAdmin && !isPublicOnly && !isUnlimitedBalanceMode; const showPersonalAccountSection = Boolean(overview) && !isPublicOnly; - const summaryCards = useMemo(() => buildSummaryCards(overview), [overview]); + + const sectionTabs = useMemo( + () => [ + { key: "ledger", label: "积分流水" }, + { key: "overview", label: "账户概览" }, + ...(showPersonalAccountSection ? [{ key: "personal", label: "个人账户" }] : []), + ], + [showPersonalAccountSection], + ); + + useEffect(() => { + if (!sectionTabs.some((tab) => tab.key === activeTabKey)) { + setActiveTabKey("ledger"); + } + }, [activeTabKey, sectionTabs]); const personalAccountRows = useMemo(() => { - if (!overview || isPublicOnly) { - return []; - } - - if (isAdmin) { - return overview.personalAccounts || []; - } - + if (!overview || isPublicOnly) return []; + if (isAdmin) return overview.personalAccounts || []; return [ { userId: -1, @@ -214,12 +157,6 @@ export default function MeetingPointsManagement() { }); }, [personalAccountRows.length]); - useEffect(() => { - if (!isPersonalOnly && contentTab !== "ledger") { - setContentTab("ledger"); - } - }, [contentTab, isPersonalOnly]); - const loadOverview = async () => { const data = await getMeetingPointsOverview(); setOverview(data); @@ -259,7 +196,7 @@ export default function MeetingPointsManagement() { const handleReset = () => { const nextParams = { current: 1, - size: 20, + size: 8, username: "", pointsType: "", }; @@ -386,182 +323,176 @@ export default function MeetingPointsManagement() { }, ]; - const ledgerTableContent = ( -
-
- - rowKey="id" - columns={ledgerColumns} - dataSource={records} - loading={loading} - totalCount={total} - scroll={{x: 1100, y: 280}} - pagination={false} - /> -
-
- { - const nextParams = { ...params, current: page, size: pageSize }; - setParams(nextParams); - void loadPage(nextParams); - }} - /> -
-
- ); + const overviewRows = useMemo(() => { + if (!overview) return []; + return [ + { id: "accountMode", metric: "账户模式", value: getAccountModeLabel(overview.accountMode), note: "当前租户积分账户组合方式" }, + { id: "chargePriority", metric: "扣费优先级", value: getChargePriorityLabel(overview.chargePriority), note: "公共账户与个人账户的扣费顺序" }, + { id: "balanceCheckEnabled", metric: "余额校验状态", value: overview.balanceCheckEnabled ? "校验余额模式" : "无限余额模式", note: "控制会议提交时是否执行余额拦截" }, + { id: "publicBalance", metric: "公共账户余额", value: overview.publicBalance ?? 0, note: "公共账户当前可用积分" }, + { id: "publicTotalPointsUsed", metric: "公共账户累计消耗", value: overview.publicTotalPointsUsed ?? 0, note: "公共账户已消耗积分总量" }, + { id: "personalBalance", metric: "个人账户余额", value: overview.personalBalance ?? 0, note: "个人账户当前可用积分" }, + { id: "personalTotalPointsUsed", metric: "个人账户累计消耗", value: overview.personalTotalPointsUsed ?? 0, note: "个人账户已消耗积分总量" }, + { id: "totalAvailableBalance", metric: "总可用余额", value: overview.totalAvailableBalance ?? 0, note: "当前账户体系可直接使用的积分余额" }, + { id: "totalChargeCount", metric: "累计扣费次数", value: overview.totalChargeCount ?? 0, note: "已产生的积分扣费记录数" }, + ]; + }, [overview]); - const personalAccountTableContent = ( -
-
- - rowKey="userId" - columns={personalAccountColumns} - dataSource={pagedPersonalAccounts} - totalCount={personalAccountRows.length} - scroll={{x: 900, y: 280}} - pagination={false} - /> -
-
- { - setPersonalAccountPagination({ current: page, pageSize }); - }} - /> -
-
- ); + const overviewColumns = [ + { + title: "指标", + dataIndex: "metric", + key: "metric", + width: 180, + render: (value: string) => {value || "-"}, + }, + { + title: "数值", + dataIndex: "value", + key: "value", + width: 180, + render: (value: string | number, record: OverviewRow) => + record.id === "balanceCheckEnabled" ? ( + {value} + ) : ( + {value ?? "-"} + ), + }, + { + title: "说明", + dataIndex: "note", + key: "note", + ellipsis: true, + render: (value: string) => {value || "-"}, + }, + ]; return ( - - {showTransferButton ? ( - - ) : null} - - - } - toolbar={ - - setParams((prev) => ({ ...prev, username: event.target.value }))} - style={{ width: 220 }} - prefix={} - allowClear - /> - setParams((prev) => ({ ...prev, username: event.target.value }))} + style={{ width: 220 }} + prefix={} + allowClear + /> +