feat: 积分、算力、模型页面重构

dev_na
puz 2026-06-29 10:17:30 +08:00
parent 08d9024401
commit 6c970536b2
19 changed files with 1132 additions and 714 deletions

View File

@ -1,22 +1,26 @@
import React from 'react'; import React from 'react';
import { Pagination, PaginationProps } from 'antd'; import { Pagination, PaginationProps } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { getDefaultPageSize, getPageSizeOptions, type PaginationVariant } from '@/utils/pagination';
import './index.css'; import './index.css';
export interface AppPaginationProps extends PaginationProps { export interface AppPaginationProps extends PaginationProps {
total: number; total: number;
variant?: PaginationVariant;
} }
export default function AppPagination(props: AppPaginationProps) { export default function AppPagination(props: AppPaginationProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { className, showSizeChanger, showTotal, total, ...restProps } = props; const { className, showSizeChanger, showTotal, total, variant = 'table', pageSizeOptions, ...restProps } = props;
const mergedClassName = ['app-global-pagination', className].filter(Boolean).join(' '); const mergedClassName = ['app-global-pagination', className].filter(Boolean).join(' ');
const mergedShowSizeChanger = const mergedShowSizeChanger =
showSizeChanger === undefined || showSizeChanger === true showSizeChanger === undefined || showSizeChanger === true
? { showSearch: false } ? { showSearch: false }
: showSizeChanger; : showSizeChanger;
const defaultPageSize = getDefaultPageSize(variant);
const mergedPageSizeOptions = pageSizeOptions ?? getPageSizeOptions(variant);
const current = Number(restProps.current ?? restProps.defaultCurrent ?? 1); const current = Number(restProps.current ?? restProps.defaultCurrent ?? 1);
const pageSize = Number(restProps.pageSize ?? restProps.defaultPageSize ?? 10); const pageSize = Number(restProps.pageSize ?? restProps.defaultPageSize ?? defaultPageSize);
const rangeStart = total > 0 ? (current - 1) * pageSize + 1 : 0; const rangeStart = total > 0 ? (current - 1) * pageSize + 1 : 0;
const rangeEnd = total > 0 ? Math.min(current * pageSize, total) : 0; const rangeEnd = total > 0 ? Math.min(current * pageSize, total) : 0;
const totalContent = showTotal ? showTotal(total, [rangeStart, rangeEnd]) : t('common.total', { total }); const totalContent = showTotal ? showTotal(total, [rangeStart, rangeEnd]) : t('common.total', { total });
@ -28,7 +32,8 @@ export default function AppPagination(props: AppPaginationProps) {
className={mergedClassName} className={mergedClassName}
showSizeChanger={mergedShowSizeChanger} showSizeChanger={mergedShowSizeChanger}
showQuickJumper showQuickJumper
pageSizeOptions={['8','10', '20', '50', '100']} defaultPageSize={defaultPageSize}
pageSizeOptions={mergedPageSizeOptions}
size="default" size="default"
total={total} total={total}
{...restProps} {...restProps}

View File

@ -0,0 +1,218 @@
.data-list-panel {
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
box-sizing: border-box;
overflow: hidden;
padding: 8px 12px;
border-radius: 4px;
background-color: #fff;
}
.data-list-panel--auto {
height: auto;
overflow: visible;
}
.data-list-panel,
.data-list-panel .ant-btn,
.data-list-panel .ant-input,
.data-list-panel .ant-input-affix-wrapper,
.data-list-panel .ant-select,
.data-list-panel .ant-select-selector,
.data-list-panel .ant-table {
font-family: "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;
font-size: 14px;
letter-spacing: 0;
}
.data-list-panel .ant-btn {
height: 32px;
border-radius: 4px !important;
box-shadow: none;
display: inline-flex;
align-items: center;
justify-content: center;
}
.data-list-panel .ant-btn .ant-btn-icon,
.data-list-panel .ant-input-prefix,
.data-list-panel .ant-input-prefix .anticon {
display: inline-flex;
align-items: center;
line-height: 1;
}
.data-list-panel .ant-input,
.data-list-panel .ant-input-affix-wrapper,
.data-list-panel .ant-select-selector {
height: 32px !important;
border-radius: 4px !important;
}
.data-list-panel .ant-input-affix-wrapper {
display: inline-flex;
align-items: center;
padding-top: 0;
padding-bottom: 0;
}
.data-list-panel .ant-input-affix-wrapper .ant-input {
height: 30px !important;
line-height: 30px;
}
.data-list-panel .ant-input-prefix {
height: 100%;
margin-inline-end: 6px;
}
.data-list-panel .ant-select-selector {
align-items: center;
}
.data-list-panel__toolbar {
flex-shrink: 0;
flex-wrap: nowrap;
min-width: 0;
min-height: 34px;
margin-bottom: 16px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.data-list-panel__left-actions {
flex: 0 0 auto;
min-width: 0;
min-height: 32px;
display: flex;
align-items: center;
}
.data-list-panel__right-actions {
flex: 1;
min-width: 0;
display: flex;
justify-content: flex-end;
}
.data-list-panel__right-actions .ant-space {
min-width: 0;
min-height: 32px;
align-items: center;
}
.data-list-panel__table-container {
flex: 1;
height: 100%;
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.data-list-panel--auto .data-list-panel__table-container,
.data-list-panel--auto .data-list-panel__table-area {
height: auto;
overflow: visible;
}
.data-list-panel__table-area {
flex: 1;
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.data-list-panel__table-area .app-page__table-wrap,
.data-list-panel__table-area .list-table-container {
height: 100%;
}
.data-list-panel__table-area .ant-table-thead > tr > th {
height: 45px;
color: #000;
font-size: 14px;
font-weight: 600;
background: #fafafa !important;
}
.data-list-panel__table-area .ant-table-tbody > tr > td {
height: 47px;
color: #000;
font-size: 14px;
font-weight: 400;
}
.data-list-panel__table-area .ant-table-cell {
line-height: 22px;
}
.data-list-panel__table-area .ant-table-content,
.data-list-panel__table-area .ant-table-body {
overflow-x: auto !important;
}
.data-list-panel__table-area .ant-tag {
margin-inline-end: 0;
border-radius: 4px;
font-family: "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;
}
.data-list-panel__footer {
flex-shrink: 0;
min-width: 0;
min-height: 56px;
}
.data-list-panel__footer .app-pagination-container {
min-width: 0;
overflow: visible;
border-radius: 0;
background: #fff;
box-sizing: border-box;
}
.data-list-panel__footer .app-pagination-container .ant-pagination {
min-width: 0;
overflow: visible;
}
.data-list-panel__footer .app-pagination-container .ant-pagination-options {
margin-inline-start: 8px;
}
.data-list-panel__footer .app-pagination-container,
.data-list-panel__footer .app-pagination-container .ant-pagination,
.data-list-panel__footer .app-pagination-total {
color: #333;
font-family: "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;
font-size: 14px;
}
@media (max-width: 768px) {
.data-list-panel__toolbar {
align-items: stretch;
flex-direction: column;
}
.data-list-panel__left-actions,
.data-list-panel__right-actions {
flex: none;
}
.data-list-panel__right-actions,
.data-list-panel__right-actions .ant-space,
.data-list-panel__right-actions .ant-input-affix-wrapper,
.data-list-panel__right-actions .ant-select {
width: 100% !important;
}
}

View File

@ -0,0 +1,42 @@
import type { ReactNode } from "react";
import "./DataListPanel.css";
interface DataListPanelProps {
leftActions?: ReactNode;
rightActions?: ReactNode;
children: ReactNode;
footer?: ReactNode;
layout?: "fixed" | "auto";
className?: string;
toolbarClassName?: string;
}
export default function DataListPanel({
leftActions,
rightActions,
children,
footer,
layout = "fixed",
className = "",
toolbarClassName = "",
}: DataListPanelProps) {
const classes = ["data-list-panel", `data-list-panel--${layout}`, className].filter(Boolean).join(" ");
const toolbarClasses = ["data-list-panel__toolbar", toolbarClassName].filter(Boolean).join(" ");
return (
<div className={classes}>
{(leftActions || rightActions) ? (
<div className={toolbarClasses}>
<div className="data-list-panel__left-actions">{leftActions}</div>
<div className="data-list-panel__right-actions">{rightActions}</div>
</div>
) : null}
<div className="data-list-panel__table-container">
<div className="data-list-panel__table-area">
<div className="app-page__table-wrap">{children}</div>
</div>
{footer ? <div className="data-list-panel__footer">{footer}</div> : null}
</div>
</div>
);
}

View File

@ -0,0 +1,180 @@
.section-card {
position: relative;
flex: 1;
height: 100%;
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
box-sizing: border-box;
overflow: hidden;
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;
}
.section-card--auto {
height: auto;
min-height: 0;
overflow: visible;
}
.section-card__header {
z-index: 1;
position: relative;
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
min-width: 0;
flex-shrink: 0;
}
.section-card__title-wrap {
flex: 1;
min-width: 0;
}
.section-card__title {
margin: 0;
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;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: flex;
align-items: center;
gap: 8px;
}
.section-card__title::before {
content: "";
flex: 0 0 auto;
width: 4px;
height: 16px;
border-radius: 1px;
background: #3c70f5;
}
.section-card__description {
padding: 0 0 16px 12px;
color: #9095a1;
font-family: "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;
font-size: 14px;
font-weight: 400;
line-height: 24px;
letter-spacing: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.section-card__extra {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
flex-shrink: 0;
}
.section-card__tabs {
z-index: 1;
flex-shrink: 0;
min-width: 0;
}
.section-card__tabs > .ant-tabs {
width: 100%;
}
.section-card__tabs > .ant-tabs > .ant-tabs-nav {
margin: 0 !important;
border-bottom: none !important;
}
.section-card__tabs > .ant-tabs > .ant-tabs-nav::before {
border-bottom: none !important;
}
.section-card__tabs .ant-tabs-nav-list {
transition: none !important;
}
.section-card__tabs .ant-tabs-content-holder,
.section-card__tabs .ant-tabs-ink-bar {
display: none !important;
}
.section-card__tabs .ant-tabs-tab {
margin-left: 0 !important;
padding: 10px 16px !important;
border: 0 solid transparent !important;
border-radius: 0 !important;
background-color: rgba(249, 250, 254, 0) !important;
transition: none !important;
}
.section-card__tabs .ant-tabs-tab.ant-tabs-tab-active {
border: none !important;
border-radius: 0 !important;
background-color: #f9fafe !important;
}
.section-card__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;
}
.section-card__tabs .ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn {
color: #1677ff !important;
font-weight: 600;
}
.section-card__content {
z-index: 1;
flex: 1;
height: 100%;
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
box-sizing: border-box;
overflow: hidden;
padding: 8px;
border-radius: 4px;
background-color: #f9fafe;
}
.section-card--auto .section-card__content {
height: auto;
overflow: visible;
}
@media (max-width: 768px) {
.section-card {
padding: 12px;
}
.section-card__header {
flex-direction: column;
}
.section-card__extra {
width: 100%;
justify-content: flex-start;
}
}

View File

@ -0,0 +1,46 @@
import type { CSSProperties, ReactNode } from "react";
import "./SectionCard.css";
interface SectionCardProps {
title?: ReactNode;
description?: ReactNode;
extra?: ReactNode;
tabs?: ReactNode;
children: ReactNode;
layout?: "fixed" | "auto";
className?: string;
contentClassName?: string;
style?: CSSProperties;
}
export default function SectionCard({
title,
description,
extra,
tabs,
children,
layout = "fixed",
className = "",
contentClassName = "",
style,
}: SectionCardProps) {
const hasHeader = Boolean(title) || Boolean(description) || Boolean(extra);
const classes = ["section-card", `section-card--${layout}`, className].filter(Boolean).join(" ");
const contentClasses = ["section-card__content", contentClassName].filter(Boolean).join(" ");
return (
<section className={classes} style={style}>
{hasHeader ? (
<div className="section-card__header">
<div className="section-card__title-wrap">
{title ? <h2 className="section-card__title">{title}</h2> : null}
{description ? <div className="section-card__description">{description}</div> : null}
</div>
{extra ? <div className="section-card__extra">{extra}</div> : null}
</div>
) : null}
{tabs ? <div className="section-card__tabs">{tabs}</div> : null}
<div className={contentClasses}>{children}</div>
</section>
);
}

View File

@ -0,0 +1,13 @@
.ai-models-page {
padding: 8px;
min-width: 0;
background: #f5f6fa;
}
.ai-models-page > .page-container__body {
padding: 0;
overflow: hidden;
border: none;
border-radius: 0;
background: transparent;
}

View File

@ -1,6 +1,8 @@
import React, { useEffect, useMemo, useRef, useState } from "react"; import React, { useEffect, useMemo, useRef, useState } from "react";
import { AutoComplete, Button, Card, Col, Divider, Drawer, Form, Input, InputNumber, Popconfirm, Row, Select, Space, Switch, Table, Tabs, Tag, Tooltip, Typography, App } from 'antd'; import { AutoComplete, Button, Col, Divider, Drawer, Form, Input, InputNumber, Popconfirm, Row, Select, Space, Switch, Table, Tabs, Tag, Tooltip, Typography, App } from 'antd';
import PageContainer from "@/components/shared/PageContainer"; import PageContainer from "@/components/shared/PageContainer";
import DataListPanel from "@/components/shared/DataListPanel";
import SectionCard from "@/components/shared/SectionCard";
import { import {
DeleteOutlined, DeleteOutlined,
EditOutlined, EditOutlined,
@ -26,6 +28,7 @@ import {
} from "../../api/business/aimodel"; } from "../../api/business/aimodel";
import {getMeetingCreateConfig, type MeetingCreateConfig} from "../../api/business/meeting"; import {getMeetingCreateConfig, type MeetingCreateConfig} from "../../api/business/meeting";
import AppPagination from "../../components/shared/AppPagination"; import AppPagination from "../../components/shared/AppPagination";
import "./AiModels.css";
const { Option } = Select; const { Option } = Select;
const { Title } = Typography; const { Title } = Typography;
@ -449,23 +452,13 @@ const AiModels: React.FC = () => {
return ( return (
<PageContainer <PageContainer
title="AI 模型配置" title={null}
subtitle="管理ASR语音识别和LLM大语言模型" className="ai-models-page"
headerExtra={
<Button type="primary" icon={<PlusOutlined />} onClick={() => openDrawer()}>
</Button>
}
toolbar={
<Input
placeholder="搜索模型名称"
prefix={<SearchOutlined />}
allowClear
onPressEnter={(event) => setSearchName((event.target as HTMLInputElement).value)}
style={{ width: 220 }}
/>
}
> >
<SectionCard
title="AI 模型配置"
description="管理 ASR 语音识别和 LLM 大语言模型。"
tabs={
<Tabs <Tabs
activeKey={activeType} activeKey={activeType}
onChange={(key) => { onChange={(key) => {
@ -476,19 +469,27 @@ const AiModels: React.FC = () => {
{ key: "ASR", label: "ASR 模型" }, { key: "ASR", label: "ASR 模型" },
{ key: "LLM", label: "LLM 模型" }, { key: "LLM", label: "LLM 模型" },
]} ]}
style={{ marginBottom: 16 }} size="middle"
type="card"
/> />
<Card className="app-page__content-card" style={{ flex: 1, minHeight: 0 }} styles={{ body: { padding: 0, flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' } }}> }
<div className="app-page__table-wrap" style={{ flex: 1, minHeight: 0, overflow: "auto" }}> >
<Table <DataListPanel
rowKey="id" leftActions={
columns={columns} <Button type="primary" icon={<PlusOutlined />} onClick={() => openDrawer()}>
dataSource={data}
loading={loading} </Button>
scroll={{ x: "max-content", y: "100%" }} }
pagination={false} rightActions={
<Input
placeholder="搜索模型名称"
prefix={<SearchOutlined />}
allowClear
onPressEnter={(event) => setSearchName((event.target as HTMLInputElement).value)}
style={{ width: 220 }}
/> />
</div> }
footer={
<AppPagination <AppPagination
current={current} current={current}
pageSize={size} pageSize={size}
@ -498,7 +499,18 @@ const AiModels: React.FC = () => {
setSize(pageSize); setSize(pageSize);
}} }}
/> />
</Card> }
>
<Table
rowKey="id"
columns={columns}
dataSource={data}
loading={loading}
scroll={{ x: "max-content", y: "100%" }}
pagination={false}
/>
</DataListPanel>
</SectionCard>
<Drawer <Drawer
width={600} width={600}

View File

@ -1,330 +1,19 @@
/* 外层容器:完全对齐 web-fe CardWrapper */
.meeting-points-page { .meeting-points-page {
padding: 8px; padding: 8px;
background: #f5f6fa;
min-width: 0; min-width: 0;
background: #f5f6fa;
} }
.meeting-points-page > .page-container__body { .meeting-points-page > .page-container__body {
padding: 0; padding: 0;
overflow: hidden;
border: none; border: none;
border-radius: 0; border-radius: 0;
background: transparent; background: transparent;
overflow: hidden;
} }
.meeting-points-page__card-wrapper { .meeting-points-page__static-pagination {
position: relative; min-height: 56px;
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; display: flex;
align-items: center; 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

@ -16,7 +16,9 @@ import {
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import PageContainer from "@/components/shared/PageContainer"; import PageContainer from "@/components/shared/PageContainer";
import AppPagination from "@/components/shared/AppPagination"; import AppPagination from "@/components/shared/AppPagination";
import DataListPanel from "@/components/shared/DataListPanel";
import ListTable from "@/components/shared/ListTable/ListTable"; import ListTable from "@/components/shared/ListTable/ListTable";
import SectionCard from "@/components/shared/SectionCard";
import { import {
getMeetingPointsLedgerPage, getMeetingPointsLedgerPage,
getMeetingPointsOverview, getMeetingPointsOverview,
@ -100,11 +102,11 @@ export default function MeetingPointsManagement() {
const [activeTabKey, setActiveTabKey] = useState("ledger"); const [activeTabKey, setActiveTabKey] = useState("ledger");
const [personalAccountPagination, setPersonalAccountPagination] = useState({ const [personalAccountPagination, setPersonalAccountPagination] = useState({
current: 1, current: 1,
pageSize: 8, pageSize: 10,
}); });
const [params, setParams] = useState({ const [params, setParams] = useState({
current: 1, current: 1,
size: 8, size: 10,
username: "", username: "",
pointsType: "", pointsType: "",
}); });
@ -196,7 +198,7 @@ export default function MeetingPointsManagement() {
const handleReset = () => { const handleReset = () => {
const nextParams = { const nextParams = {
current: 1, current: 1,
size: 8, size: 10,
username: "", username: "",
pointsType: "", pointsType: "",
}; };
@ -369,33 +371,28 @@ export default function MeetingPointsManagement() {
return ( return (
<PageContainer title={null} className="meeting-points-page"> <PageContainer title={null} className="meeting-points-page">
<div className="meeting-points-page__card-wrapper"> <SectionCard
<div className="meeting-points-page__card-title"></div> title="积分管理"
<div className="meeting-points-page__card-description"> description="查看当前租户下的积分账面余额、累计消耗和会议消耗记录。"
tabs={
</div>
<Tabs <Tabs
className="meeting-points-page__tabs"
activeKey={activeTabKey} activeKey={activeTabKey}
onChange={setActiveTabKey} onChange={setActiveTabKey}
items={sectionTabs} items={sectionTabs}
size="middle" size="middle"
type="card" type="card"
/> />
}
<div className="meeting-points-page__content-wrap"> >
<div className="meeting-points-page__inner-list"> <DataListPanel
<div className="meeting-points-page__search-box"> leftActions={
<div className="meeting-points-page__left-actions"> isLookupTab(activeTabKey) && showTransferButton ? (
{isLookupTab(activeTabKey) && showTransferButton ? (
<Button icon={<PlusOutlined />} onClick={() => void handleOpenTransfer()}> <Button icon={<PlusOutlined />} onClick={() => void handleOpenTransfer()}>
</Button> </Button>
) : null} ) : null
</div> }
rightActions={
<div className="meeting-points-page__search-input">
<Space wrap> <Space wrap>
{activeTabKey === "ledger" ? ( {activeTabKey === "ledger" ? (
<> <>
@ -426,12 +423,33 @@ export default function MeetingPointsManagement() {
aria-label="刷新" aria-label="刷新"
/> />
</Space> </Space>
}
footer={
activeTabKey === "overview" ? (
<div className="app-pagination-container meeting-points-page__static-pagination">
<div className="app-pagination-total"> {overviewRows.length} </div>
</div> </div>
</div> ) : activeTabKey === "personal" ? (
<AppPagination
<div className="meeting-points-page__table-container"> current={personalAccountPagination.current}
<div className="meeting-points-page__table-area"> pageSize={personalAccountPagination.pageSize}
<div className="app-page__table-wrap"> 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);
}}
/>
)
}
>
{activeTabKey === "overview" ? ( {activeTabKey === "overview" ? (
<ListTable<any> <ListTable<any>
key="overview" key="overview"
@ -463,36 +481,8 @@ export default function MeetingPointsManagement() {
pagination={false} pagination={false}
/> />
)} )}
</div> </DataListPanel>
</div> </SectionCard>
{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 <Modal
title="从公共账户分配积分" title="从公共账户分配积分"

View File

@ -986,7 +986,13 @@ const Meetings: React.FC = () => {
</div> </div>
<div style={{ padding: "16px 4px 0", flexShrink: 0, display: 'flex', justifyContent: 'center' }}> <div style={{ padding: "16px 4px 0", flexShrink: 0, display: 'flex', justifyContent: 'center' }}>
<AppPagination current={current} pageSize={size} total={total} onChange={handlePaginationChange} /> <AppPagination
variant={displayMode === "card" ? "card" : "table"}
current={current}
pageSize={size}
total={total}
onChange={handlePaginationChange}
/>
</div> </div>
</Card> </Card>

View File

@ -54,7 +54,7 @@ const PromptTemplates: React.FC = () => {
const [data, setData] = useState<PromptTemplateVO[]>([]); const [data, setData] = useState<PromptTemplateVO[]>([]);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [current, setCurrent] = useState(1); const [current, setCurrent] = useState(1);
const [pageSize, setPageSize] = useState(12); const [pageSize, setPageSize] = useState(8);
const [drawerVisible, setDrawerVisible] = useState(false); const [drawerVisible, setDrawerVisible] = useState(false);
const [editingId, setEditingId] = useState<number | null>(null); const [editingId, setEditingId] = useState<number | null>(null);
@ -379,6 +379,7 @@ const PromptTemplates: React.FC = () => {
})} })}
</div> </div>
<AppPagination <AppPagination
variant="card"
current={current} current={current}
pageSize={pageSize} pageSize={pageSize}
total={total} total={total}

View File

@ -785,10 +785,10 @@ const SpeakerReg: React.FC = () => {
</div> </div>
<div style={{ flexShrink: 0 }}> <div style={{ flexShrink: 0 }}>
<AppPagination <AppPagination
variant="card"
current={current} current={current}
pageSize={pageSize} pageSize={pageSize}
total={total} total={total}
pageSizeOptions={['8', '12', '20', '50']}
onChange={(page, size) => { onChange={(page, size) => {
setCurrent(page); setCurrent(page);
setPageSize(size); setPageSize(size);

View File

@ -0,0 +1,44 @@
.tenant-meeting-points {
padding: 8px;
min-width: 0;
background: #f5f6fa;
}
.tenant-meeting-points > .page-container__body {
padding: 0;
overflow: hidden;
border: none;
border-radius: 0;
background: transparent;
}
.tenant-meeting-points__tenant-card {
width: 100%;
padding: 8px 12px;
border-radius: 4px;
background: #fff;
}
.tenant-meeting-points__tenant-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
}
.tenant-meeting-points__mode-card {
border-radius: 4px;
}
.tenant-meeting-points__mode-card--enabled {
background: #f6ffed;
}
.tenant-meeting-points__mode-card--unlimited {
background: #fff7e6;
}
.tenant-meeting-points__stats {
width: 100%;
}

View File

@ -1,8 +1,10 @@
import { ReloadOutlined, SearchOutlined } from "@ant-design/icons"; import { ReloadOutlined, SearchOutlined } from "@ant-design/icons";
import { getCurrentUser } from "@/api"; import { getCurrentUser } from "@/api";
import AppPagination from "@/components/shared/AppPagination"; import AppPagination from "@/components/shared/AppPagination";
import DataListPanel from "@/components/shared/DataListPanel";
import ListTable from "@/components/shared/ListTable/ListTable"; import ListTable from "@/components/shared/ListTable/ListTable";
import PageContainer from "@/components/shared/PageContainer"; import PageContainer from "@/components/shared/PageContainer";
import SectionCard from "@/components/shared/SectionCard";
import {usePermission} from "@/hooks/usePermission"; import {usePermission} from "@/hooks/usePermission";
import { import {
getCurrentTenantMeetingPointsSetting, getCurrentTenantMeetingPointsSetting,
@ -13,6 +15,7 @@ import {
import type { UserProfile } from "@/types"; import type { UserProfile } from "@/types";
import { Button, Card, Input, message, Modal, Select, Space, Statistic, Tag, Typography } from "antd"; import { Button, Card, Input, message, Modal, Select, Space, Statistic, Tag, Typography } from "antd";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import "./TenantMeetingPointsSettings.css";
const { Text } = Typography; const { Text } = Typography;
const BALANCE_CHECK_UPDATE_PERMISSION = "biz:tenant-meeting-points:balance-check:update"; const BALANCE_CHECK_UPDATE_PERMISSION = "biz:tenant-meeting-points:balance-check:update";
@ -37,7 +40,7 @@ export default function TenantMeetingPointsSettings() {
const [currentTenantSetting, setCurrentTenantSetting] = useState<TenantMeetingPointsSettingVO | null>(null); const [currentTenantSetting, setCurrentTenantSetting] = useState<TenantMeetingPointsSettingVO | null>(null);
const [params, setParams] = useState({ const [params, setParams] = useState({
current: 1, current: 1,
size: 20, size: 10,
tenantName: "", tenantName: "",
tenantCode: "", tenantCode: "",
balanceCheckEnabled: "", balanceCheckEnabled: "",
@ -205,9 +208,13 @@ export default function TenantMeetingPointsSettings() {
return null; return null;
} }
return ( return (
<Card> <SectionCard
<Space direction="vertical" size="large" style={{ width: "100%" }}> title="当前租户"
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 16, flexWrap: "wrap" }}> description="租户管理员可查看并切换当前租户的积分余额校验模式。"
layout="auto"
>
<Space direction="vertical" size="large" className="tenant-meeting-points__tenant-card">
<div className="tenant-meeting-points__tenant-heading">
<Space direction="vertical" size={4}> <Space direction="vertical" size={4}>
<Text strong style={{ fontSize: 18 }}>{currentTenantSetting.tenantName || "当前租户"}</Text> <Text strong style={{ fontSize: 18 }}>{currentTenantSetting.tenantName || "当前租户"}</Text>
<Text type="secondary">{currentTenantSetting.tenantCode || "-"}</Text> <Text type="secondary">{currentTenantSetting.tenantCode || "-"}</Text>
@ -215,7 +222,14 @@ export default function TenantMeetingPointsSettings() {
{renderStatusTag(currentTenantSetting.balanceCheckEnabled)} {renderStatusTag(currentTenantSetting.balanceCheckEnabled)}
</div> </div>
<Card size="small" style={{ background: currentTenantSetting.balanceCheckEnabled ? "#f6ffed" : "#fff7e6" }}> <Card
size="small"
className={
currentTenantSetting.balanceCheckEnabled
? "tenant-meeting-points__mode-card tenant-meeting-points__mode-card--enabled"
: "tenant-meeting-points__mode-card tenant-meeting-points__mode-card--unlimited"
}
>
<Space direction="vertical" size={4}> <Space direction="vertical" size={4}>
<Text strong> <Text strong>
{currentTenantSetting.balanceCheckEnabled ? "当前为校验余额模式" : "当前为无限余额模式"} {currentTenantSetting.balanceCheckEnabled ? "当前为校验余额模式" : "当前为无限余额模式"}
@ -228,7 +242,7 @@ export default function TenantMeetingPointsSettings() {
</Space> </Space>
</Card> </Card>
<Space size={40} wrap> <Space size={40} wrap className="tenant-meeting-points__stats">
<Statistic title="当前可用额度" value={currentTenantSetting.balanceCheckEnabled ? currentTenantSetting.publicBalance ?? 0 : "无限"} /> <Statistic title="当前可用额度" value={currentTenantSetting.balanceCheckEnabled ? currentTenantSetting.publicBalance ?? 0 : "无限"} />
<Statistic title="公共账户余额" value={currentTenantSetting.publicBalance ?? 0} /> <Statistic title="公共账户余额" value={currentTenantSetting.publicBalance ?? 0} />
<Statistic title="公共账户累计消耗" value={currentTenantSetting.publicTotalPointsUsed ?? 0} /> <Statistic title="公共账户累计消耗" value={currentTenantSetting.publicTotalPointsUsed ?? 0} />
@ -254,21 +268,28 @@ export default function TenantMeetingPointsSettings() {
</Button> </Button>
</Space> </Space>
</Space> </Space>
</Card> </SectionCard>
); );
}; };
return ( return (
<PageContainer <PageContainer
title={null}
className="tenant-meeting-points"
>
{isPlatformAdmin ? (
<SectionCard
title="租户积分校验" title="租户积分校验"
subtitle="按租户控制会议积分是否校验余额,关闭后按无限余额模式记录消耗" description="按租户控制会议积分是否校验余额,关闭后按无限余额模式记录消耗。"
headerExtra={ >
<DataListPanel
leftActions={
<Button icon={<ReloadOutlined />} onClick={() => void handleRefresh()}> <Button icon={<ReloadOutlined />} onClick={() => void handleRefresh()}>
</Button> </Button>
} }
toolbar={isPlatformAdmin ? ( rightActions={
<Space wrap size="middle"> <Space wrap>
<Input <Input
placeholder="按租户名称搜索" placeholder="按租户名称搜索"
value={params.tenantName} value={params.tenantName}
@ -307,7 +328,7 @@ export default function TenantMeetingPointsSettings() {
</Button> </Button>
<Button <Button
onClick={() => { onClick={() => {
const nextParams = { current: 1, size: 20, tenantName: "", tenantCode: "", balanceCheckEnabled: "" }; const nextParams = { current: 1, size: 10, tenantName: "", tenantCode: "", balanceCheckEnabled: "" };
setParams(nextParams); setParams(nextParams);
void loadPlatformPage(nextParams); void loadPlatformPage(nextParams);
}} }}
@ -315,28 +336,8 @@ export default function TenantMeetingPointsSettings() {
</Button> </Button>
</Space> </Space>
) : undefined} }
> footer={
{isPlatformAdmin ? (
<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"></Text>
</div>
</div>
<div className="app-page__table-wrap" style={{overflow: "hidden", padding: "0 24px"}}>
<ListTable<TenantMeetingPointsSettingVO>
rowKey="tenantId"
columns={columns}
dataSource={records}
loading={loading}
totalCount={total}
scroll={{x: 1200, y: "calc(100vh - 380px)"}}
pagination={false}
/>
</div>
<div style={{ padding: "16px 24px" }}>
<AppPagination <AppPagination
current={params.current} current={params.current}
pageSize={params.size} pageSize={params.size}
@ -347,8 +348,19 @@ export default function TenantMeetingPointsSettings() {
void loadPlatformPage(nextParams); void loadPlatformPage(nextParams);
}} }}
/> />
</div> }
</Card> >
<ListTable<TenantMeetingPointsSettingVO>
rowKey="tenantId"
columns={columns}
dataSource={records}
loading={loading}
totalCount={total}
scroll={{x: 1200, y: "100%"}}
pagination={false}
/>
</DataListPanel>
</SectionCard>
) : renderTenantAdminCard()} ) : renderTenantAdminCard()}
</PageContainer> </PageContainer>
); );

View File

@ -0,0 +1,162 @@
.dashboard-monitor-page {
padding: 8px;
min-width: 0;
background: #f5f6fa;
}
.dashboard-monitor-page > .page-container__body {
padding: 0;
overflow: hidden;
border: none;
border-radius: 0;
background: transparent;
}
.dashboard-monitor-page__content {
gap: 12px;
}
.dashboard-monitor-page__stats {
flex-shrink: 0;
min-width: 0;
}
.dashboard-monitor-page__stat-card {
height: 100%;
border: 1px solid #e6e6e6;
border-radius: 4px;
background: #fff;
box-shadow: none;
}
.dashboard-monitor-page__stat-label {
font-size: 13px;
}
.dashboard-monitor-page__task-panel {
flex: 1;
min-height: 0;
}
.dashboard-monitor-page__task-panel .data-list-panel__toolbar {
margin-bottom: 8px;
}
.dashboard-monitor-page__task-list,
.dashboard-monitor-page__task-list .ant-list,
.dashboard-monitor-page__task-list .ant-list-items,
.dashboard-monitor-page__task-item {
min-width: 0;
}
.dashboard-monitor-page__task-list {
flex: 1;
height: 100%;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding-right: 4px;
overscroll-behavior: contain;
}
.dashboard-monitor-page__task-item {
padding: 20px 0;
border-bottom: 1px solid #f0f2f5;
}
.dashboard-monitor-page__task-item-inner {
width: 100%;
min-width: 0;
}
.dashboard-monitor-page__task-meta {
width: 100%;
}
.dashboard-monitor-page__task-title.ant-typography {
margin: 0;
cursor: pointer;
word-break: break-word;
}
.dashboard-monitor-page__task-divider {
margin: 0;
}
.dashboard-monitor-page__task-tags {
margin-top: 8px;
display: flex;
flex-wrap: wrap;
gap: 8px;
min-height: 22px;
}
.dashboard-monitor-page__task-tag {
margin-inline-end: 0;
border: 1px solid var(--app-border-color);
border-radius: 4px;
background: color-mix(in srgb, var(--app-primary-color) 12%, var(--app-bg-surface-strong));
color: var(--app-text-main);
font-size: 11px;
}
.dashboard-monitor-page__task-action {
width: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
}
.dashboard-monitor-page__steps {
width: 100%;
max-width: 100%;
}
.dashboard-monitor-page__success-icon {
color: #52c41a;
}
.dashboard-monitor-page__progress {
margin-top: 12px;
padding: 12px 16px;
border: 1px solid var(--app-border-color);
border-radius: 4px;
background: var(--app-bg-surface-soft);
}
.dashboard-monitor-page__progress-header {
margin-bottom: 6px;
display: flex;
justify-content: space-between;
gap: 12px;
}
.dashboard-monitor-page__progress-message {
min-width: 0;
font-size: 12px;
}
.dashboard-monitor-page__progress-icon {
margin-right: 6px;
color: #1890ff;
}
.dashboard-monitor-page__progress-percent {
flex-shrink: 0;
color: #1890ff;
}
.dashboard-monitor-page .ant-steps-item-title {
font-size: 13px !important;
font-weight: 600 !important;
}
.dashboard-monitor-page .ant-steps-item-description {
font-size: 11px !important;
}
@media (max-width: 1199px) {
.dashboard-monitor-page__task-action {
justify-content: flex-start;
}
}

View File

@ -1,6 +1,8 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import PageContainer from "@/components/shared/PageContainer"; import PageContainer from "@/components/shared/PageContainer";
import AppPagination from '@/components/shared/AppPagination'; import AppPagination from '@/components/shared/AppPagination';
import DataListPanel from "@/components/shared/DataListPanel";
import SectionCard from "@/components/shared/SectionCard";
import { Row, Col, Card, Statistic, List, Tag, Typography, Button, Space, Empty, Steps, Progress, Divider } from 'antd'; import { Row, Col, Card, Statistic, List, Tag, Typography, Button, Space, Empty, Steps, Progress, Divider } from 'antd';
import { import {
HistoryOutlined, HistoryOutlined,
@ -19,6 +21,7 @@ import { useNavigate } from 'react-router-dom';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { getDashboardStats, DashboardStats } from '@/api/business/dashboard'; import { getDashboardStats, DashboardStats } from '@/api/business/dashboard';
import { MeetingVO, getMeetingPage, getMeetingProgress, MeetingProgress } from '@/api/business/meeting'; import { MeetingVO, getMeetingPage, getMeetingProgress, MeetingProgress } from '@/api/business/meeting';
import './index.css';
const { Title, Text } = Typography; const { Title, Text } = Typography;
@ -50,13 +53,13 @@ const MeetingProgressDisplay: React.FC<{ meeting: MeetingVO }> = ({ meeting }) =
const isError = percent < 0; const isError = percent < 0;
return ( return (
<div style={{ marginTop: 12, padding: '12px 16px', background: 'var(--app-bg-surface-soft)', borderRadius: 8, border: '1px solid var(--app-border-color)' }}> <div className="dashboard-monitor-page__progress">
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 12, marginBottom: 6 }}> <div className="dashboard-monitor-page__progress-header">
<Text type="secondary" style={{ fontSize: 12 }}> <Text type="secondary" className="dashboard-monitor-page__progress-message">
<LoadingOutlined style={{ marginRight: 6, color: '#1890ff' }} spin={!isError} /> <LoadingOutlined className="dashboard-monitor-page__progress-icon" spin={!isError} />
{progress?.message || '准备分析中...'} {progress?.message || '准备分析中...'}
</Text> </Text>
{!isError && <Text strong style={{ color: '#1890ff' }}>{percent}%</Text>} {!isError && <Text strong className="dashboard-monitor-page__progress-percent">{percent}%</Text>}
</div> </div>
<Progress <Progress
percent={isError ? 100 : percent} percent={isError ? 100 : percent}
@ -129,7 +132,7 @@ export const Dashboard: React.FC = () => {
const currentStep = item.status === 4 ? 0 : (item.status === 3 ? 2 : item.status); const currentStep = item.status === 4 ? 0 : (item.status === 3 ? 2 : item.status);
return ( return (
<div style={{ width: '100%', maxWidth: '100%' }}> <div className="dashboard-monitor-page__steps">
<Steps <Steps
size="small" size="small"
current={currentStep} current={currentStep}
@ -147,7 +150,7 @@ export const Dashboard: React.FC = () => {
}, },
{ {
title: '分析完成', title: '分析完成',
icon: item.status === 3 ? <CheckCircleOutlined style={{ color: '#52c41a' }} /> : <FileTextOutlined />, icon: item.status === 3 ? <CheckCircleOutlined className="dashboard-monitor-page__success-icon" /> : <FileTextOutlined />,
} }
]} ]}
/> />
@ -169,16 +172,21 @@ export const Dashboard: React.FC = () => {
return ( return (
<PageContainer <PageContainer
title="仪表盘" title={null}
subtitle="系统运行概览与最近任务动态" className="dashboard-monitor-page"
style={{ overflow: 'hidden' }}
> >
<Row gutter={[16, 16]} style={{ marginBottom: 16, flexShrink: 0 }}> <SectionCard
title="任务监控"
description="系统运行概览与最近任务动态。"
contentClassName="dashboard-monitor-page__content"
>
<div className="dashboard-monitor-page__stats">
<Row gutter={[16, 16]}>
{statCards.map((s, idx) => ( {statCards.map((s, idx) => (
<Col xs={24} sm={12} xl={6} key={idx}> <Col xs={24} sm={12} xl={6} key={idx}>
<Card variant="borderless" style={{ borderRadius: 16, boxShadow: 'var(--app-shadow)', background: 'var(--app-bg-card)', border: '1px solid var(--app-border-color)', backdropFilter: 'blur(16px)' }}> <Card className="dashboard-monitor-page__stat-card" variant="borderless">
<Statistic <Statistic
title={<Text type="secondary" style={{ fontSize: 13 }}>{s.label}</Text>} title={<Text type="secondary" className="dashboard-monitor-page__stat-label">{s.label}</Text>}
value={s.value || 0} value={s.value || 0}
valueStyle={{ color: s.color, fontWeight: 700 }} valueStyle={{ color: s.color, fontWeight: 700 }}
prefix={React.cloneElement(s.icon as React.ReactElement, { style: { marginRight: 8 } })} prefix={React.cloneElement(s.icon as React.ReactElement, { style: { marginRight: 8 } })}
@ -187,39 +195,50 @@ export const Dashboard: React.FC = () => {
</Col> </Col>
))} ))}
</Row> </Row>
<Card
className="dashboard-task-card"
title={
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 12 }}>
<Space><ClockCircleOutlined /> </Space>
<Button type="link" onClick={() => navigate('/meetings')}></Button>
</div> </div>
<DataListPanel
className="dashboard-monitor-page__task-panel"
leftActions={
<Space>
<ClockCircleOutlined />
<Text strong></Text>
</Space>
}
rightActions={
<Button type="link" onClick={() => navigate('/meetings')}>
</Button>
}
footer={
<AppPagination
current={pagination.current}
pageSize={pagination.pageSize}
total={pagination.total}
onChange={handlePageChange}
/>
} }
variant="borderless"
style={{ flex: 1, minHeight: 0, borderRadius: 16, boxShadow: 'var(--app-shadow)', background: 'var(--app-bg-card)', border: '1px solid var(--app-border-color)', backdropFilter: 'blur(16px)', overflow: 'hidden' }}
styles={{ body: { display: 'flex', flexDirection: 'column', gap: 16, flex: 1, minHeight: 0, overflow: 'hidden',height:'90%' } }}
> >
<div className="dashboard-task-list"> <div className="dashboard-monitor-page__task-list">
<List <List
loading={dashboardLoading} loading={dashboardLoading}
dataSource={recentTasks} dataSource={recentTasks}
renderItem={(item) => ( renderItem={(item) => (
<List.Item style={{ padding: '24px 0', borderBottom: '1px solid #f0f2f5' }}> <List.Item className="dashboard-monitor-page__task-item">
<div style={{ width: '100%' }}> <div className="dashboard-monitor-page__task-item-inner">
<Row gutter={[24, 16]} align="middle"> <Row gutter={[24, 16]} align="middle">
<Col xs={24} xl={8}> <Col xs={24} xl={8}>
<Space direction="vertical" size={4} style={{ width: '100%' }}> <Space direction="vertical" size={4} className="dashboard-monitor-page__task-meta">
<Title level={5} style={{ margin: 0, cursor: 'pointer', wordBreak: 'break-word' }} onClick={() => navigate(`/meetings/${item.id}`)}> <Title level={5} className="dashboard-monitor-page__task-title" onClick={() => navigate(`/meetings/${item.id}`)}>
{item.title} {item.title}
</Title> </Title>
<Space size={12} wrap split={<Divider type="vertical" style={{ margin: 0 }} />}> <Space size={12} wrap split={<Divider type="vertical" className="dashboard-monitor-page__task-divider" />}>
<Text type="secondary"><CalendarOutlined /> {dayjs(item.meetingTime).format('MM-DD HH:mm')}</Text> <Text type="secondary"><CalendarOutlined /> {dayjs(item.meetingTime).format('MM-DD HH:mm')}</Text>
<Text type="secondary"><TeamOutlined /> {item.participants || item.creatorName || '未指定'}</Text> <Text type="secondary"><TeamOutlined /> {item.participants || item.creatorName || '未指定'}</Text>
</Space> </Space>
<div style={{ marginTop: 8, display: 'flex', flexWrap: 'wrap', gap: 8 }}> <div className="dashboard-monitor-page__task-tags">
{item.tags?.split(',').filter(Boolean).map((t) => ( {item.tags?.split(',').filter(Boolean).map((t) => (
<Tag key={t} style={{ marginInlineEnd: 0, border: '1px solid var(--app-border-color)', background: 'color-mix(in srgb, var(--app-primary-color) 12%, var(--app-bg-surface-strong))', color: 'var(--app-text-main)', borderRadius: 4, fontSize: 11 }}>{t}</Tag> <Tag key={t} className="dashboard-monitor-page__task-tag">{t}</Tag>
))} ))}
</div> </div>
</Space> </Space>
@ -230,7 +249,7 @@ export const Dashboard: React.FC = () => {
</Col> </Col>
<Col xs={24} xl={4}> <Col xs={24} xl={4}>
<div className="dashboard-task-action"> <div className="dashboard-monitor-page__task-action">
<Button <Button
type={item.status === 3 ? 'primary' : 'default'} type={item.status === 3 ? 'primary' : 'default'}
ghost={item.status === 3} ghost={item.status === 3}
@ -250,44 +269,8 @@ export const Dashboard: React.FC = () => {
locale={{ emptyText: <Empty description="暂无近期分析任务" /> }} locale={{ emptyText: <Empty description="暂无近期分析任务" /> }}
/> />
</div> </div>
<AppPagination </DataListPanel>
current={pagination.current} </SectionCard>
pageSize={pagination.pageSize}
total={pagination.total}
onChange={handlePageChange}
/>
</Card>
<style>{`
.dashboard-task-card .ant-card-head,
.dashboard-task-card .ant-card-body,
.dashboard-task-card .ant-list,
.dashboard-task-card .ant-list-items,
.dashboard-task-card .ant-list-item {
min-width: 0;
}
.dashboard-task-list {
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding-right: 4px;
overscroll-behavior: contain;
}
.dashboard-task-action {
display: flex;
justify-content: flex-end;
align-items: center;
width: 100%;
}
.ant-steps-item-title { font-size: 13px !important; font-weight: 600 !important; }
.ant-steps-item-description { font-size: 11px !important; }
@media (max-width: 1199px) {
.dashboard-task-action {
justify-content: flex-start;
}
}
`}</style>
</PageContainer> </PageContainer>
); );
}; };

View File

@ -22,7 +22,7 @@ export default function Tenants() {
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [data, setData] = useState<SysTenant[]>([]); const [data, setData] = useState<SysTenant[]>([]);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [queryParams, setQueryParams] = useState({ current: 1, size: 12, name: "", code: "" }); const [queryParams, setQueryParams] = useState({ current: 1, size: 8, name: "", code: "" });
const [drawerOpen, setDrawerOpen] = useState(false); const [drawerOpen, setDrawerOpen] = useState(false);
const [editing, setEditing] = useState<SysTenant | null>(null); const [editing, setEditing] = useState<SysTenant | null>(null);
const [runtime, setRuntime] = useState<PlatformRuntime | null>(null); const [runtime, setRuntime] = useState<PlatformRuntime | null>(null);
@ -56,7 +56,7 @@ export default function Tenants() {
const handleReset = () => { const handleReset = () => {
searchForm.resetFields(); searchForm.resetFields();
setQueryParams({ current: 1, size: 12, name: "", code: "" }); setQueryParams({ current: 1, size: 8, name: "", code: "" });
}; };
const handlePageChange = (page: number, size: number) => { const handlePageChange = (page: number, size: number) => {
@ -221,7 +221,7 @@ export default function Tenants() {
/> />
<Pagination <Pagination
{...getStandardPagination(total, queryParams.current, queryParams.size, handlePageChange)} {...getStandardPagination(total, queryParams.current, queryParams.size, handlePageChange, { variant: "card" })}
className="app-global-pagination" className="app-global-pagination"
style={{ style={{
marginTop: 24, marginTop: 24,

View File

@ -1,6 +1,20 @@
import { TablePaginationConfig } from 'antd'; import { TablePaginationConfig } from 'antd';
import i18n from '../i18n'; import i18n from '../i18n';
export type PaginationVariant = 'table' | 'card';
export const TABLE_PAGE_SIZE_OPTIONS = ['10', '20', '50', '100'];
export const CARD_PAGE_SIZE_OPTIONS = ['8', '16', '24', '40'];
export const getDefaultPageSize = (variant: PaginationVariant = 'table') => (variant === 'card' ? 8 : 10);
export const getPageSizeOptions = (variant: PaginationVariant = 'table') =>
variant === 'card' ? CARD_PAGE_SIZE_OPTIONS : TABLE_PAGE_SIZE_OPTIONS;
export interface StandardPaginationConfig extends TablePaginationConfig {
variant?: PaginationVariant;
}
/** /**
* Returns a standardized Ant Design pagination configuration. * Returns a standardized Ant Design pagination configuration.
*/ */
@ -9,13 +23,14 @@ export const getStandardPagination = (
current: number, current: number,
pageSize: number, pageSize: number,
onChange?: (page: number, size: number) => void, onChange?: (page: number, size: number) => void,
overrides: TablePaginationConfig = {} overrides: StandardPaginationConfig = {}
): TablePaginationConfig => { ): TablePaginationConfig => {
const mergedClassName = ['app-global-pagination', overrides.className].filter(Boolean).join(' '); const { variant = 'table', pageSizeOptions, className, showSizeChanger, ...restOverrides } = overrides;
const mergedClassName = ['app-global-pagination', className].filter(Boolean).join(' ');
const mergedShowSizeChanger = const mergedShowSizeChanger =
overrides.showSizeChanger === undefined || overrides.showSizeChanger === true showSizeChanger === undefined || showSizeChanger === true
? { showSearch: false } ? { showSearch: false }
: overrides.showSizeChanger; : showSizeChanger;
return { return {
total, total,
@ -24,10 +39,10 @@ export const getStandardPagination = (
onChange, onChange,
showQuickJumper: true, showQuickJumper: true,
showTotal: (totalCount) => i18n.t('common.total', { total: totalCount }), showTotal: (totalCount) => i18n.t('common.total', { total: totalCount }),
pageSizeOptions: ['10', '20', '50', '100'], pageSizeOptions: pageSizeOptions ?? getPageSizeOptions(variant),
size: 'default', size: 'default',
position: ['bottomRight'], position: ['bottomRight'],
...overrides, ...restOverrides,
showSizeChanger: mergedShowSizeChanger, showSizeChanger: mergedShowSizeChanger,
className: mergedClassName className: mergedClassName
}; };

View File

@ -14,11 +14,11 @@ export default defineConfig({
server: { server: {
port: 5174, port: 5174,
proxy: { proxy: {
"/auth": "http://localhost:8080", "/auth": "http://10.100.53.199:8080",
"/sys": "http://localhost:8080", "/sys": "http://10.100.53.199:8080",
"/api": "http://localhost:8080", "/api": "http://10.100.53.199:8080",
"/ws": { "/ws": {
target: "ws://localhost:8080", target: "ws://10.100.53.199:8080",
ws: true ws: true
} }
} }