初次重构

dev_na
puz 2026-06-26 14:13:40 +08:00
parent 95f7f766c5
commit 21d356bb58
14 changed files with 1159 additions and 490 deletions

2
.gitignore vendored
View File

@ -39,3 +39,5 @@ backend/src/main/resources/application-local.yml
/backend/m2repo_local/
/backend/src/test/
/backend/target/
/.claude
/web-fe/

View File

@ -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
}
}
}}

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

View File

@ -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 (
<div className="theme-selector-container">
<Space align="center" style={{ cursor: 'pointer', padding: '0 8px' }} onClick={() => setOpen(true)}>
@ -60,19 +55,6 @@ export default function ThemeSelector() {
onChange={(value) => setThemeMode(value as ThemeMode)}
/>
</div>
<div>
<div style={{ marginBottom: 8, display: 'flex', alignItems: 'center', gap: 8 }}>
<LayoutOutlined style={{ color: 'var(--app-primary-color)' }} />
<Text strong>{t('theme.layout', 'Navigation Mode')}</Text>
</div>
<Segmented
block
options={layoutOptions}
value={layoutMode}
onChange={(value) => setLayoutMode(value as LayoutMode)}
/>
</div>
</Space>
</Drawer>
</div>

View File

@ -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;
}
}

View File

@ -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 (
<div className="app-pagination-container">
<div className="app-pagination-total">{totalContent}</div>
<Pagination
className={mergedClassName}
showSizeChanger={mergedShowSizeChanger}
showQuickJumper
showTotal={(total) => t('common.total', { total })}
pageSizeOptions={['8','10', '20', '50', '100']}
size="default"
total={total}
{...restProps}
/>
</div>

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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<PageContainerProps> = ({
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 (
<div
className={`page-container ${className}`}
style={{
display: 'flex',
flexDirection: 'column',
height: '100%',
gap: 16,
...style
}}
>
{(title || headerExtra) && (
<div
className="page-container__header"
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
flexWrap: 'wrap',
gap: 16,
paddingBottom: 12,
borderBottom: '1px solid var(--app-border-color)'
}}
>
<div style={{ flex: 1, minWidth: 200 }}>
<Title level={4} style={{ margin: 0, fontWeight: 600 }}>
{title}
</Title>
{subtitle && (
<Text type="secondary" style={{ display: 'block', fontSize: 14, marginTop: 4 }}>
<div className={`page-container ${className}`.trim()} style={style}>
{hasHeader && (
<div className={`page-container__header ${!hasTitle && !hasSubtitle ? 'page-container__header--actions-only' : ''}`.trim()}>
{(hasTitle || hasSubtitle) && (
<div className="page-container__title-wrap">
{hasTitle && (
<Title level={4} className="page-container__title">
{title}
</Title>
)}
{hasSubtitle && (
<Text type="secondary" className="page-container__subtitle">
{subtitle}
</Text>
)}
</div>
{headerExtra && (
<div className="page-container__header-extra" style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
{headerExtra}
)}
</div>
)}
{headerExtra && <div className="page-container__header-extra">{headerExtra}</div>}
</div>
)}
{toolbar && (
<div
className="page-container__toolbar"
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flexWrap: 'wrap',
gap: 12
}}
>
{toolbar}
</div>
)}
{toolbar && <div className="page-container__toolbar">{toolbar}</div>}
<div
className="page-container__body"
style={{
flex: 1,
minHeight: 0,
display: 'flex',
flexDirection: 'column'
}}
>
{children}
</div>
<div className="page-container__body">{children}</div>
</div>
);
};
export default PageContainer;
export default PageContainer;

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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<string, ReactNode> = {
dashboard: <DashboardOutlined />,
@ -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() {
</Space>
);
const renderLogo = (isTop: boolean = false) => (
<div
style={{
height: 64,
display: "flex",
alignItems: "center",
padding: isTop ? "0 24px 0 0" : "0 16px",
gap: "12px",
borderBottom: isTop ? "none" : "1px solid var(--app-border-color)",
flexShrink: 0
}}
>
<img src={platformConfig?.logoUrl || "/logo.svg"} alt="logo" style={{ width: 32, height: 32, objectFit: "contain" }} />
{(!collapsed || isTop) && (
<span
style={{
fontSize: "18px",
fontWeight: 700,
color: "var(--app-primary-color)",
letterSpacing: "0.5px",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap"
}}
>
{platformConfig?.projectName || "UnisBase"}
</span>
)}
const renderLogo = () => (
<div className="app-header-logo">
<img src={platformConfig?.logoUrl || "/logo.svg"} alt={platformConfig?.projectName || "logo"} />
<span className="app-header-logo-title">{platformConfig?.projectName || "智听云"}</span>
</div>
);
return (
<Layout style={{ height: "100vh", overflow: "hidden" }}>
{layoutMode === 'side' && (
<Layout className="main-layout">
<Header className="main-header">
{renderLogo()}
<div className="header-right">{headerRightTools}</div>
</Header>
<Layout className="main-shell">
<Sider
trigger={null}
collapsible
collapsed={collapsed}
className="app-sider"
style={{ boxShadow: "var(--app-shadow)", zIndex: 10 }}
className={`main-sider app-sider ${collapsed ? "main-sider--collapsed" : ""}`}
theme="light"
width={180}
>
{renderLogo(false)}
<div style={{ flex: 1, overflowY: "auto", overflowX: "hidden" }}>
<Menu
mode="inline"
selectedKeys={selectedMenuKeys}
openKeys={openKeys}
onOpenChange={setOpenKeys}
items={menuItems}
style={{ borderRight: 0, marginTop: 16 }}
/>
</div>
</Sider>
)}
<Layout style={{ flex: 1, minWidth: 0 }}>
<Header
style={{
background: "var(--app-bg-card)",
padding: "0 24px",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
boxShadow: "var(--app-shadow)",
zIndex: 9,
height: 64,
flexShrink: 0,
width: "100%"
}}
>
{layoutMode === 'side' ? (
<div className="collapsedCenter">
<Button
type="text"
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
shape="circle"
onClick={() => setCollapsed(!collapsed)}
style={{ fontSize: "16px", width: 64, height: 64, color: "var(--app-text-main)", marginLeft: -24 }}
className="trigger"
aria-label={collapsed ? "展开菜单" : "收起菜单"}
/>
) : (
renderLogo(true)
)}
{layoutMode === 'top' ? (
<div style={{ flex: 1, minWidth: 0, padding: "0 24px" }}>
<Menu
mode="horizontal"
selectedKeys={selectedMenuKeys}
items={menuItems}
style={{
borderBottom: 0,
lineHeight: '62px',
background: 'transparent',
color: 'var(--app-text-main)'
}}
theme={document.documentElement.getAttribute('data-theme') === 'tech' ? 'dark' : 'light'}
/>
</div>
) : (
<div style={{ flex: 1, display: "flex", alignItems: "center", paddingLeft: 12 }} />
)}
{headerRightTools}
</Header>
<Content
style={{
margin: "24px 24px 12px",
padding: 24,
background: "var(--app-bg-card)",
borderRadius: "8px",
boxShadow: "var(--app-shadow)",
overflow: "hidden",
display: "flex",
flexDirection: "column",
flex: 1,
minHeight: 0
}}
>
<Outlet />
</Content>
<Footer
style={{
flexShrink: 0,
padding: "0 24px 16px",
background: "transparent"
}}
>
<div
style={{
minHeight: 44,
borderRadius: 8,
border: "1px solid var(--app-border-color)",
background: "var(--app-bg-card)",
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 16,
padding: "10px 16px",
color: "var(--app-text-main)",
fontSize: 13,
textAlign: "center",
backdropFilter: "blur(10px)"
}}
>
{platformConfig?.icpInfo ? <span>{platformConfig.icpInfo}</span> : null}
{platformConfig?.icpInfo && platformConfig?.copyrightInfo ? <span aria-hidden="true" style={{ color: "#d0d5dd" }}>|</span> : null}
{platformConfig?.copyrightInfo ? <span>{platformConfig.copyrightInfo}</span> : null}
</div>
</Footer>
<Menu
mode="inline"
selectedKeys={selectedMenuKeys}
openKeys={collapsed ? [] : openKeys}
onOpenChange={setOpenKeys}
inlineCollapsed={collapsed}
items={menuItems}
className="main-menu"
/>
</Sider>
<Layout className="main-content-layout">
<Content className="main-content">
<Outlet />
</Content>
</Layout>
</Layout>
</Layout>
);

View File

@ -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;
}
}

View File

@ -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<MeetingPointsOverviewVO | null>(null);
@ -161,14 +97,14 @@ export default function MeetingPointsManagement() {
const [total, setTotal] = useState(0);
const [transferOpen, setTransferOpen] = useState(false);
const [users, setUsers] = useState<SysUser[]>([]);
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<MeetingPointsPersonalAccountVO[]>(() => {
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 = (
<div style={{ display: "flex", flexDirection: "column", height: "400px" }}>
<div className="app-page__table-wrap" style={{overflow: "hidden", padding: "0 24px"}}>
<ListTable<MeetingPointsLedgerListItemVO>
rowKey="id"
columns={ledgerColumns}
dataSource={records}
loading={loading}
totalCount={total}
scroll={{x: 1100, y: 280}}
pagination={false}
/>
</div>
<div style={{ padding: "16px 24px" }}>
<AppPagination
current={params.current}
pageSize={params.size}
total={total}
onChange={(page, pageSize) => {
const nextParams = { ...params, current: page, size: pageSize };
setParams(nextParams);
void loadPage(nextParams);
}}
/>
</div>
</div>
);
const overviewRows = useMemo<OverviewRow[]>(() => {
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 = (
<div style={{ display: "flex", flexDirection: "column", height: "400px" }}>
<div className="app-page__table-wrap" style={{overflow: "hidden", padding: "0 24px"}}>
<ListTable<MeetingPointsPersonalAccountVO>
rowKey="userId"
columns={personalAccountColumns}
dataSource={pagedPersonalAccounts}
totalCount={personalAccountRows.length}
scroll={{x: 900, y: 280}}
pagination={false}
/>
</div>
<div style={{ padding: "16px 24px" }}>
<AppPagination
current={personalAccountPagination.current}
pageSize={personalAccountPagination.pageSize}
total={personalAccountRows.length}
onChange={(page, pageSize) => {
setPersonalAccountPagination({ current: page, pageSize });
}}
/>
</div>
</div>
);
const overviewColumns = [
{
title: "指标",
dataIndex: "metric",
key: "metric",
width: 180,
render: (value: string) => <Text strong>{value || "-"}</Text>,
},
{
title: "数值",
dataIndex: "value",
key: "value",
width: 180,
render: (value: string | number, record: OverviewRow) =>
record.id === "balanceCheckEnabled" ? (
<Tag color={overview?.balanceCheckEnabled ? "green" : "volcano"}>{value}</Tag>
) : (
<Text strong>{value ?? "-"}</Text>
),
},
{
title: "说明",
dataIndex: "note",
key: "note",
ellipsis: true,
render: (value: string) => <Text type="secondary">{value || "-"}</Text>,
},
];
return (
<PageContainer
title="积分管理"
subtitle="查看当前租户下的积分账面余额、累计消耗和会议消耗记录"
style={{ height: "auto" }}
headerExtra={
<Space>
{showTransferButton ? (
<Button icon={<PlusOutlined />} onClick={() => void handleOpenTransfer()}>
</Button>
) : null}
<Button icon={<ReloadOutlined />} onClick={() => void handleRefresh()}>
</Button>
</Space>
}
toolbar={
<Space wrap size="middle">
<Input
placeholder="按用户名搜索"
value={params.username}
onChange={(event) => setParams((prev) => ({ ...prev, username: event.target.value }))}
style={{ width: 220 }}
prefix={<SearchOutlined className="text-gray-400" />}
allowClear
/>
<Select
style={{ width: 140 }}
value={params.pointsType}
onChange={(value) => setParams((prev) => ({ ...prev, pointsType: value }))}
options={POINTS_TYPE_OPTIONS}
/>
<Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}>
</Button>
<Button onClick={handleReset}></Button>
</Space>
}
>
<div style={{ marginBottom: 20, padding: "0 4px" }}>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: 16,
flexWrap: "wrap",
marginBottom: 16,
}}
>
<Text strong style={{ fontSize: 16 }}>
</Text>
{/*<Space wrap>*/}
{/* <Tag color="processing" bordered={false}>*/}
{/* 模式:{getAccountModeLabel(overview?.accountMode)}*/}
{/* </Tag>*/}
{/* <Tag color="blue" bordered={false}>*/}
{/* 优先级:{getChargePriorityLabel(overview?.chargePriority)}*/}
{/* </Tag>*/}
{/* <Tag color={isUnlimitedBalanceMode ? "volcano" : "green"} bordered={false}>*/}
{/* {isUnlimitedBalanceMode ? "无限余额模式" : "校验余额模式"}*/}
{/* </Tag>*/}
{/* <Tag color={isAdmin ? "gold" : "default"} bordered={false}>*/}
{/* {isAdmin ? "管理员视角" : "当前用户视角"}*/}
{/* </Tag>*/}
{/*</Space>*/}
<PageContainer title={null} className="meeting-points-page">
<div className="meeting-points-page__card-wrapper">
<div className="meeting-points-page__card-title"></div>
<div className="meeting-points-page__card-description">
</div>
<Tabs
className="meeting-points-page__tabs"
activeKey={activeTabKey}
onChange={setActiveTabKey}
items={sectionTabs}
size="middle"
type="card"
/>
<Row gutter={[40, 16]}>
{summaryCards.map((item) => (
<Col key={item.key}>
<Statistic
title={<span style={{ color: "rgba(0,0,0,0.45)", fontSize: 12 }}>{item.title}</span>}
value={item.value}
valueStyle={{ fontSize: 24, fontWeight: 700 }}
/>
<div style={{ fontSize: 11, color: "rgba(0,0,0,0.45)", marginTop: 2 }}>{item.note}</div>
</Col>
))}
</Row>
</div>
{isPersonalOnly ? (
<Card className="app-page__content-card" styles={{ body: { padding: 0 } }}>
<div style={{ padding: "20px 24px 8px" }}>
<Text strong style={{ fontSize: 16 }}>
</Text>
</div>
{ledgerTableContent}
</Card>
) : (
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
<Card className="app-page__content-card" styles={{ body: { padding: 0 } }}>
<div style={{ padding: "20px 24px 8px" }}>
<Text strong style={{ fontSize: 16 }}>
</Text>
<div className="meeting-points-page__content-wrap">
<div className="meeting-points-page__inner-list">
<div className="meeting-points-page__search-box">
<div className="meeting-points-page__left-actions">
{isLookupTab(activeTabKey) && showTransferButton ? (
<Button icon={<PlusOutlined />} onClick={() => void handleOpenTransfer()}>
</Button>
) : null}
</div>
<div className="meeting-points-page__search-input">
<Space wrap>
{activeTabKey === "ledger" ? (
<>
<Input
placeholder="按用户名搜索"
value={params.username}
onChange={(event) => setParams((prev) => ({ ...prev, username: event.target.value }))}
style={{ width: 220 }}
prefix={<SearchOutlined className="text-gray-400" />}
allowClear
/>
<Select
style={{ width: 140 }}
value={params.pointsType}
onChange={(value) => setParams((prev) => ({ ...prev, pointsType: value }))}
options={POINTS_TYPE_OPTIONS}
/>
<Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}>
</Button>
<Button onClick={handleReset}></Button>
</>
) : null}
<Button
icon={<ReloadOutlined />}
onClick={() => void handleRefresh()}
title="刷新"
aria-label="刷新"
/>
</Space>
</div>
</div>
{ledgerTableContent}
</Card>
{showPersonalAccountSection ? (
<Card className="app-page__content-card" styles={{ body: { padding: 0 } }}>
<div style={{ padding: "20px 24px 8px" }}>
<Text strong style={{ fontSize: 16 }}>
</Text>
<div style={{ marginTop: 4 }}>
<Text type="secondary">
{isAdmin ? "按当前余额展示全部个人账户。" : "展示当前账号的个人积分账户。"}
</Text>
<div className="meeting-points-page__table-container">
<div className="meeting-points-page__table-area">
<div className="app-page__table-wrap">
{activeTabKey === "overview" ? (
<ListTable<any>
key="overview"
rowKey="id"
columns={overviewColumns}
dataSource={overviewRows}
loading={false}
scroll={{ x: 900, y: "100%" }}
pagination={false}
/>
) : activeTabKey === "personal" ? (
<ListTable<any>
key="personal"
rowKey="userId"
columns={personalAccountColumns}
dataSource={pagedPersonalAccounts}
loading={false}
scroll={{ x: 900, y: "100%" }}
pagination={false}
/>
) : (
<ListTable<any>
key="ledger"
rowKey="id"
columns={ledgerColumns}
dataSource={records}
loading={loading}
scroll={{ x: 1100, y: "100%" }}
pagination={false}
/>
)}
</div>
</div>
{personalAccountTableContent}
</Card>
) : null}
{activeTabKey === "overview" ? (
<div className="app-pagination-container meeting-points-page__static-pagination">
<div className="app-pagination-total"> {overviewRows.length} </div>
</div>
) : activeTabKey === "personal" ? (
<AppPagination
current={personalAccountPagination.current}
pageSize={personalAccountPagination.pageSize}
total={personalAccountRows.length}
onChange={(page, size) => setPersonalAccountPagination({ current: page, pageSize: size })}
/>
) : (
<AppPagination
current={params.current}
pageSize={params.size}
total={total}
onChange={(page, size) => {
const nextParams = { ...params, current: page, size };
setParams(nextParams);
void loadPage(nextParams);
}}
/>
)}
</div>
</div>
</div>
)}
</div>
<Modal
title="从公共账户分配积分"
@ -599,3 +530,7 @@ export default function MeetingPointsManagement() {
</PageContainer>
);
}
function isLookupTab(tabKey: string) {
return tabKey === "ledger" || tabKey === "personal";
}