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 { Pagination, PaginationProps } from 'antd';
import { useTranslation } from 'react-i18next';
import { getDefaultPageSize, getPageSizeOptions, type PaginationVariant } from '@/utils/pagination';
import './index.css';
export interface AppPaginationProps extends PaginationProps {
total: number;
variant?: PaginationVariant;
}
export default function AppPagination(props: AppPaginationProps) {
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 mergedShowSizeChanger =
showSizeChanger === undefined || showSizeChanger === true
? { showSearch: false }
: showSizeChanger;
const defaultPageSize = getDefaultPageSize(variant);
const mergedPageSizeOptions = pageSizeOptions ?? getPageSizeOptions(variant);
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 rangeEnd = total > 0 ? Math.min(current * pageSize, total) : 0;
const totalContent = showTotal ? showTotal(total, [rangeStart, rangeEnd]) : t('common.total', { total });
@ -28,7 +32,8 @@ export default function AppPagination(props: AppPaginationProps) {
className={mergedClassName}
showSizeChanger={mergedShowSizeChanger}
showQuickJumper
pageSizeOptions={['8','10', '20', '50', '100']}
defaultPageSize={defaultPageSize}
pageSizeOptions={mergedPageSizeOptions}
size="default"
total={total}
{...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 { 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 DataListPanel from "@/components/shared/DataListPanel";
import SectionCard from "@/components/shared/SectionCard";
import {
DeleteOutlined,
EditOutlined,
@ -26,6 +28,7 @@ import {
} from "../../api/business/aimodel";
import {getMeetingCreateConfig, type MeetingCreateConfig} from "../../api/business/meeting";
import AppPagination from "../../components/shared/AppPagination";
import "./AiModels.css";
const { Option } = Select;
const { Title } = Typography;
@ -449,56 +452,65 @@ const AiModels: React.FC = () => {
return (
<PageContainer
title="AI 模型配置"
subtitle="管理ASR语音识别和LLM大语言模型"
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 }}
/>
}
title={null}
className="ai-models-page"
>
<Tabs
activeKey={activeType}
onChange={(key) => {
setActiveType(key as ModelType);
setCurrent(1);
}}
items={[
{ key: "ASR", label: "ASR 模型" },
{ key: "LLM", label: "LLM 模型" },
]}
style={{ marginBottom: 16 }}
/>
<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
rowKey="id"
columns={columns}
dataSource={data}
loading={loading}
scroll={{ x: "max-content", y: "100%" }}
pagination={false}
/>
</div>
<AppPagination
current={current}
pageSize={size}
total={total}
onChange={(page, pageSize) => {
setCurrent(page);
setSize(pageSize);
}}
/>
</Card>
<SectionCard
title="AI 模型配置"
description="管理 ASR 语音识别和 LLM 大语言模型。"
tabs={
<Tabs
activeKey={activeType}
onChange={(key) => {
setActiveType(key as ModelType);
setCurrent(1);
}}
items={[
{ key: "ASR", label: "ASR 模型" },
{ key: "LLM", label: "LLM 模型" },
]}
size="middle"
type="card"
/>
}
>
<DataListPanel
leftActions={
<Button type="primary" icon={<PlusOutlined />} onClick={() => openDrawer()}>
</Button>
}
rightActions={
<Input
placeholder="搜索模型名称"
prefix={<SearchOutlined />}
allowClear
onPressEnter={(event) => setSearchName((event.target as HTMLInputElement).value)}
style={{ width: 220 }}
/>
}
footer={
<AppPagination
current={current}
pageSize={size}
total={total}
onChange={(page, pageSize) => {
setCurrent(page);
setSize(pageSize);
}}
/>
}
>
<Table
rowKey="id"
columns={columns}
dataSource={data}
loading={loading}
scroll={{ x: "max-content", y: "100%" }}
pagination={false}
/>
</DataListPanel>
</SectionCard>
<Drawer
width={600}

View File

@ -1,330 +1,19 @@
/* 外层容器:完全对齐 web-fe CardWrapper */
.meeting-points-page {
padding: 8px;
background: #f5f6fa;
min-width: 0;
background: #f5f6fa;
}
.meeting-points-page > .page-container__body {
padding: 0;
overflow: hidden;
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;
.meeting-points-page__static-pagination {
min-height: 56px;
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

@ -16,7 +16,9 @@ import {
import { useEffect, useMemo, useState } from "react";
import PageContainer from "@/components/shared/PageContainer";
import AppPagination from "@/components/shared/AppPagination";
import DataListPanel from "@/components/shared/DataListPanel";
import ListTable from "@/components/shared/ListTable/ListTable";
import SectionCard from "@/components/shared/SectionCard";
import {
getMeetingPointsLedgerPage,
getMeetingPointsOverview,
@ -100,11 +102,11 @@ export default function MeetingPointsManagement() {
const [activeTabKey, setActiveTabKey] = useState("ledger");
const [personalAccountPagination, setPersonalAccountPagination] = useState({
current: 1,
pageSize: 8,
pageSize: 10,
});
const [params, setParams] = useState({
current: 1,
size: 8,
size: 10,
username: "",
pointsType: "",
});
@ -196,7 +198,7 @@ export default function MeetingPointsManagement() {
const handleReset = () => {
const nextParams = {
current: 1,
size: 8,
size: 10,
username: "",
pointsType: "",
};
@ -369,130 +371,118 @@ export default function MeetingPointsManagement() {
return (
<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"
/>
<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="刷新"
<SectionCard
title="积分管理"
description="查看当前租户下的积分账面余额、累计消耗和会议消耗记录。"
tabs={
<Tabs
activeKey={activeTabKey}
onChange={setActiveTabKey}
items={sectionTabs}
size="middle"
type="card"
/>
}
>
<DataListPanel
leftActions={
isLookupTab(activeTabKey) && showTransferButton ? (
<Button icon={<PlusOutlined />} onClick={() => void handleOpenTransfer()}>
</Button>
) : null
}
rightActions={
<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
/>
</Space>
</div>
</div>
<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>
{activeTabKey === "overview" ? (
<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>
}
footer={
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>
) : 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);
}}
/>
)
}
>
{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}
/>
)}
</DataListPanel>
</SectionCard>
<Modal
title="从公共账户分配积分"

View File

@ -986,7 +986,13 @@ const Meetings: React.FC = () => {
</div>
<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>
</Card>

View File

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

View File

@ -785,10 +785,10 @@ const SpeakerReg: React.FC = () => {
</div>
<div style={{ flexShrink: 0 }}>
<AppPagination
variant="card"
current={current}
pageSize={pageSize}
total={total}
pageSizeOptions={['8', '12', '20', '50']}
onChange={(page, size) => {
setCurrent(page);
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 { getCurrentUser } from "@/api";
import AppPagination from "@/components/shared/AppPagination";
import DataListPanel from "@/components/shared/DataListPanel";
import ListTable from "@/components/shared/ListTable/ListTable";
import PageContainer from "@/components/shared/PageContainer";
import SectionCard from "@/components/shared/SectionCard";
import {usePermission} from "@/hooks/usePermission";
import {
getCurrentTenantMeetingPointsSetting,
@ -13,6 +15,7 @@ import {
import type { UserProfile } from "@/types";
import { Button, Card, Input, message, Modal, Select, Space, Statistic, Tag, Typography } from "antd";
import { useEffect, useState } from "react";
import "./TenantMeetingPointsSettings.css";
const { Text } = Typography;
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 [params, setParams] = useState({
current: 1,
size: 20,
size: 10,
tenantName: "",
tenantCode: "",
balanceCheckEnabled: "",
@ -205,9 +208,13 @@ export default function TenantMeetingPointsSettings() {
return null;
}
return (
<Card>
<Space direction="vertical" size="large" style={{ width: "100%" }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 16, flexWrap: "wrap" }}>
<SectionCard
title="当前租户"
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}>
<Text strong style={{ fontSize: 18 }}>{currentTenantSetting.tenantName || "当前租户"}</Text>
<Text type="secondary">{currentTenantSetting.tenantCode || "-"}</Text>
@ -215,7 +222,14 @@ export default function TenantMeetingPointsSettings() {
{renderStatusTag(currentTenantSetting.balanceCheckEnabled)}
</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}>
<Text strong>
{currentTenantSetting.balanceCheckEnabled ? "当前为校验余额模式" : "当前为无限余额模式"}
@ -228,7 +242,7 @@ export default function TenantMeetingPointsSettings() {
</Space>
</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.publicBalance ?? 0} />
<Statistic title="公共账户累计消耗" value={currentTenantSetting.publicTotalPointsUsed ?? 0} />
@ -254,101 +268,99 @@ export default function TenantMeetingPointsSettings() {
</Button>
</Space>
</Space>
</Card>
</SectionCard>
);
};
return (
<PageContainer
title="租户积分校验"
subtitle="按租户控制会议积分是否校验余额,关闭后按无限余额模式记录消耗"
headerExtra={
<Button icon={<ReloadOutlined />} onClick={() => void handleRefresh()}>
</Button>
}
toolbar={isPlatformAdmin ? (
<Space wrap size="middle">
<Input
placeholder="按租户名称搜索"
value={params.tenantName}
onChange={(event) => setParams((prev) => ({ ...prev, tenantName: event.target.value }))}
style={{ width: 220 }}
prefix={<SearchOutlined className="text-gray-400" />}
allowClear
/>
<Input
placeholder="按租户编码搜索"
value={params.tenantCode}
onChange={(event) => setParams((prev) => ({ ...prev, tenantCode: event.target.value }))}
style={{ width: 180 }}
allowClear
/>
<Select
style={{ width: 180 }}
value={params.balanceCheckEnabled}
onChange={(value) => setParams((prev) => ({ ...prev, balanceCheckEnabled: value }))}
options={[
{ label: "全部状态", value: "" },
{ label: "校验余额模式", value: "true" },
{ label: "无限余额模式", value: "false" },
]}
/>
<Button
type="primary"
icon={<SearchOutlined />}
onClick={() => {
const nextParams = { ...params, current: 1 };
setParams(nextParams);
void loadPlatformPage(nextParams);
}}
>
</Button>
<Button
onClick={() => {
const nextParams = { current: 1, size: 20, tenantName: "", tenantCode: "", balanceCheckEnabled: "" };
setParams(nextParams);
void loadPlatformPage(nextParams);
}}
>
</Button>
</Space>
) : undefined}
title={null}
className="tenant-meeting-points"
>
{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"}}>
<SectionCard
title="租户积分校验"
description="按租户控制会议积分是否校验余额,关闭后按无限余额模式记录消耗。"
>
<DataListPanel
leftActions={
<Button icon={<ReloadOutlined />} onClick={() => void handleRefresh()}>
</Button>
}
rightActions={
<Space wrap>
<Input
placeholder="按租户名称搜索"
value={params.tenantName}
onChange={(event) => setParams((prev) => ({ ...prev, tenantName: event.target.value }))}
style={{ width: 220 }}
prefix={<SearchOutlined className="text-gray-400" />}
allowClear
/>
<Input
placeholder="按租户编码搜索"
value={params.tenantCode}
onChange={(event) => setParams((prev) => ({ ...prev, tenantCode: event.target.value }))}
style={{ width: 180 }}
allowClear
/>
<Select
style={{ width: 180 }}
value={params.balanceCheckEnabled}
onChange={(value) => setParams((prev) => ({ ...prev, balanceCheckEnabled: value }))}
options={[
{ label: "全部状态", value: "" },
{ label: "校验余额模式", value: "true" },
{ label: "无限余额模式", value: "false" },
]}
/>
<Button
type="primary"
icon={<SearchOutlined />}
onClick={() => {
const nextParams = { ...params, current: 1 };
setParams(nextParams);
void loadPlatformPage(nextParams);
}}
>
</Button>
<Button
onClick={() => {
const nextParams = { current: 1, size: 10, tenantName: "", tenantCode: "", balanceCheckEnabled: "" };
setParams(nextParams);
void loadPlatformPage(nextParams);
}}
>
</Button>
</Space>
}
footer={
<AppPagination
current={params.current}
pageSize={params.size}
total={total}
onChange={(page, pageSize) => {
const nextParams = { ...params, current: page, size: pageSize };
setParams(nextParams);
void loadPlatformPage(nextParams);
}}
/>
}
>
<ListTable<TenantMeetingPointsSettingVO>
rowKey="tenantId"
columns={columns}
dataSource={records}
loading={loading}
totalCount={total}
scroll={{x: 1200, y: "calc(100vh - 380px)"}}
scroll={{x: 1200, y: "100%"}}
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 loadPlatformPage(nextParams);
}}
/>
</div>
</Card>
</DataListPanel>
</SectionCard>
) : renderTenantAdminCard()}
</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 PageContainer from "@/components/shared/PageContainer";
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 {
HistoryOutlined,
@ -19,6 +21,7 @@ import { useNavigate } from 'react-router-dom';
import dayjs from 'dayjs';
import { getDashboardStats, DashboardStats } from '@/api/business/dashboard';
import { MeetingVO, getMeetingPage, getMeetingProgress, MeetingProgress } from '@/api/business/meeting';
import './index.css';
const { Title, Text } = Typography;
@ -50,13 +53,13 @@ const MeetingProgressDisplay: React.FC<{ meeting: MeetingVO }> = ({ meeting }) =
const isError = percent < 0;
return (
<div style={{ marginTop: 12, padding: '12px 16px', background: 'var(--app-bg-surface-soft)', borderRadius: 8, border: '1px solid var(--app-border-color)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 12, marginBottom: 6 }}>
<Text type="secondary" style={{ fontSize: 12 }}>
<LoadingOutlined style={{ marginRight: 6, color: '#1890ff' }} spin={!isError} />
<div className="dashboard-monitor-page__progress">
<div className="dashboard-monitor-page__progress-header">
<Text type="secondary" className="dashboard-monitor-page__progress-message">
<LoadingOutlined className="dashboard-monitor-page__progress-icon" spin={!isError} />
{progress?.message || '准备分析中...'}
</Text>
{!isError && <Text strong style={{ color: '#1890ff' }}>{percent}%</Text>}
{!isError && <Text strong className="dashboard-monitor-page__progress-percent">{percent}%</Text>}
</div>
<Progress
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);
return (
<div style={{ width: '100%', maxWidth: '100%' }}>
<div className="dashboard-monitor-page__steps">
<Steps
size="small"
current={currentStep}
@ -147,7 +150,7 @@ export const Dashboard: React.FC = () => {
},
{
title: '分析完成',
icon: item.status === 3 ? <CheckCircleOutlined style={{ color: '#52c41a' }} /> : <FileTextOutlined />,
icon: item.status === 3 ? <CheckCircleOutlined className="dashboard-monitor-page__success-icon" /> : <FileTextOutlined />,
}
]}
/>
@ -169,125 +172,105 @@ export const Dashboard: React.FC = () => {
return (
<PageContainer
title="仪表盘"
subtitle="系统运行概览与最近任务动态"
style={{ overflow: 'hidden' }}
title={null}
className="dashboard-monitor-page"
>
<Row gutter={[16, 16]} style={{ marginBottom: 16, flexShrink: 0 }}>
{statCards.map((s, 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)' }}>
<Statistic
title={<Text type="secondary" style={{ fontSize: 13 }}>{s.label}</Text>}
value={s.value || 0}
valueStyle={{ color: s.color, fontWeight: 700 }}
prefix={React.cloneElement(s.icon as React.ReactElement, { style: { marginRight: 8 } })}
/>
</Card>
</Col>
))}
</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>
}
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%' } }}
<SectionCard
title="任务监控"
description="系统运行概览与最近任务动态。"
contentClassName="dashboard-monitor-page__content"
>
<div className="dashboard-task-list">
<List
loading={dashboardLoading}
dataSource={recentTasks}
renderItem={(item) => (
<List.Item style={{ padding: '24px 0', borderBottom: '1px solid #f0f2f5' }}>
<div style={{ width: '100%' }}>
<Row gutter={[24, 16]} align="middle">
<Col xs={24} xl={8}>
<Space direction="vertical" size={4} style={{ width: '100%' }}>
<Title level={5} style={{ margin: 0, cursor: 'pointer', wordBreak: 'break-word' }} onClick={() => navigate(`/meetings/${item.id}`)}>
{item.title}
</Title>
<Space size={12} wrap split={<Divider type="vertical" style={{ margin: 0 }} />}>
<Text type="secondary"><CalendarOutlined /> {dayjs(item.meetingTime).format('MM-DD HH:mm')}</Text>
<Text type="secondary"><TeamOutlined /> {item.participants || item.creatorName || '未指定'}</Text>
</Space>
<div style={{ marginTop: 8, display: 'flex', flexWrap: 'wrap', gap: 8 }}>
{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>
))}
</div>
</Space>
</Col>
<Col xs={24} xl={12}>
{renderTaskProgress(item)}
</Col>
<Col xs={24} xl={4}>
<div className="dashboard-task-action">
<Button
type={item.status === 3 ? 'primary' : 'default'}
ghost={item.status === 3}
icon={item.status === 3 ? <FileTextOutlined /> : <PlayCircleOutlined />}
onClick={() => navigate(`/meetings/${item.id}`)}
>
{item.status === 3 ? '查看纪要' : '监控详情'}
</Button>
</div>
</Col>
</Row>
<MeetingProgressDisplay meeting={item} />
</div>
</List.Item>
)}
locale={{ emptyText: <Empty description="暂无近期分析任务" /> }}
/>
<div className="dashboard-monitor-page__stats">
<Row gutter={[16, 16]}>
{statCards.map((s, idx) => (
<Col xs={24} sm={12} xl={6} key={idx}>
<Card className="dashboard-monitor-page__stat-card" variant="borderless">
<Statistic
title={<Text type="secondary" className="dashboard-monitor-page__stat-label">{s.label}</Text>}
value={s.value || 0}
valueStyle={{ color: s.color, fontWeight: 700 }}
prefix={React.cloneElement(s.icon as React.ReactElement, { style: { marginRight: 8 } })}
/>
</Card>
</Col>
))}
</Row>
</div>
<AppPagination
current={pagination.current}
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;
<DataListPanel
className="dashboard-monitor-page__task-panel"
leftActions={
<Space>
<ClockCircleOutlined />
<Text strong></Text>
</Space>
}
}
`}</style>
rightActions={
<Button type="link" onClick={() => navigate('/meetings')}>
</Button>
}
footer={
<AppPagination
current={pagination.current}
pageSize={pagination.pageSize}
total={pagination.total}
onChange={handlePageChange}
/>
}
>
<div className="dashboard-monitor-page__task-list">
<List
loading={dashboardLoading}
dataSource={recentTasks}
renderItem={(item) => (
<List.Item className="dashboard-monitor-page__task-item">
<div className="dashboard-monitor-page__task-item-inner">
<Row gutter={[24, 16]} align="middle">
<Col xs={24} xl={8}>
<Space direction="vertical" size={4} className="dashboard-monitor-page__task-meta">
<Title level={5} className="dashboard-monitor-page__task-title" onClick={() => navigate(`/meetings/${item.id}`)}>
{item.title}
</Title>
<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"><TeamOutlined /> {item.participants || item.creatorName || '未指定'}</Text>
</Space>
<div className="dashboard-monitor-page__task-tags">
{item.tags?.split(',').filter(Boolean).map((t) => (
<Tag key={t} className="dashboard-monitor-page__task-tag">{t}</Tag>
))}
</div>
</Space>
</Col>
<Col xs={24} xl={12}>
{renderTaskProgress(item)}
</Col>
<Col xs={24} xl={4}>
<div className="dashboard-monitor-page__task-action">
<Button
type={item.status === 3 ? 'primary' : 'default'}
ghost={item.status === 3}
icon={item.status === 3 ? <FileTextOutlined /> : <PlayCircleOutlined />}
onClick={() => navigate(`/meetings/${item.id}`)}
>
{item.status === 3 ? '查看纪要' : '监控详情'}
</Button>
</div>
</Col>
</Row>
<MeetingProgressDisplay meeting={item} />
</div>
</List.Item>
)}
locale={{ emptyText: <Empty description="暂无近期分析任务" /> }}
/>
</div>
</DataListPanel>
</SectionCard>
</PageContainer>
);
};

View File

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

View File

@ -1,6 +1,20 @@
import { TablePaginationConfig } from 'antd';
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.
*/
@ -9,13 +23,14 @@ export const getStandardPagination = (
current: number,
pageSize: number,
onChange?: (page: number, size: number) => void,
overrides: TablePaginationConfig = {}
overrides: StandardPaginationConfig = {}
): 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 =
overrides.showSizeChanger === undefined || overrides.showSizeChanger === true
showSizeChanger === undefined || showSizeChanger === true
? { showSearch: false }
: overrides.showSizeChanger;
: showSizeChanger;
return {
total,
@ -24,10 +39,10 @@ export const getStandardPagination = (
onChange,
showQuickJumper: true,
showTotal: (totalCount) => i18n.t('common.total', { total: totalCount }),
pageSizeOptions: ['10', '20', '50', '100'],
pageSizeOptions: pageSizeOptions ?? getPageSizeOptions(variant),
size: 'default',
position: ['bottomRight'],
...overrides,
...restOverrides,
showSizeChanger: mergedShowSizeChanger,
className: mergedClassName
};

View File

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