feat:页面调整2

dev_na
puz 2026-07-01 22:00:19 +08:00
parent 4bc3f65a10
commit 0a33ff8e56
31 changed files with 1128 additions and 220 deletions

View File

@ -2,17 +2,18 @@
margin-top: auto;
flex-shrink: 0;
width: 100%;
height: 50px;
padding-top: 8px;
min-height: 52px;
padding: 8px 0;
background: #fff;
box-sizing: border-box;
min-width: 0;
overflow: hidden;
overflow: visible;
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
gap: 8px 16px;
flex-wrap: nowrap;
}
.app-pagination-total {
@ -22,15 +23,128 @@
white-space: nowrap;
}
.app-pagination-container .ant-pagination {
.app-pagination-controls {
flex: 1 1 auto;
width: auto;
min-width: 0;
display: flex;
align-items: center;
justify-content: flex-end;
flex-wrap: nowrap;
gap: 8px;
overflow: visible;
}
.app-pagination-container .ant-pagination {
flex: 0 1 auto;
min-width: 0;
min-height: 32px;
display: flex;
align-items: center;
justify-content: flex-end;
margin: 0 !important;
overflow: visible;
flex-wrap: nowrap;
row-gap: 8px;
}
.app-pagination-container .app-global-pagination.ant-pagination {
width: auto;
padding: 0;
background: transparent;
border-top: 0;
border-radius: 0;
}
.app-pagination-container .ant-pagination .ant-pagination-options {
height: 32px;
display: inline-flex;
flex: 0 0 auto;
align-items: center;
margin-inline-start: 8px !important;
row-gap: 8px;
}
.app-pagination-container .ant-pagination .ant-pagination-options-quick-jumper {
height: 32px;
display: inline-flex;
align-items: center;
white-space: nowrap;
}
.app-pagination-container .ant-select {
height: 32px;
display: inline-flex;
align-items: center;
}
.app-pagination-container .ant-select-selector {
height: 32px !important;
display: inline-flex !important;
align-items: center !important;
}
.app-pagination-container .ant-select-selection-item {
display: inline-flex !important;
align-items: center;
line-height: 30px !important;
}
.app-pagination-quick-jumper {
height: 32px;
display: inline-flex;
flex: 0 0 auto;
align-items: stretch;
align-self: center;
gap: 6px;
color: #333;
font-size: 14px;
line-height: 32px;
white-space: nowrap;
}
.app-pagination-quick-jumper__label {
box-sizing: border-box;
height: 32px;
display: inline-flex;
align-items: center;
line-height: 32px;
transform: translateY(-1px);
}
.app-pagination-quick-jumper input {
box-sizing: border-box;
display: block;
flex: 0 0 48px;
width: 48px;
height: 32px;
padding: 0 8px;
color: #333;
font-size: 14px;
line-height: 30px;
text-align: center;
background-color: #fff;
border: 1px solid #d9d9d9;
border-radius: 4px;
outline: none;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.app-pagination-quick-jumper input:hover {
border-color: #1677ff;
}
.app-pagination-quick-jumper input:focus {
border-color: #1677ff;
box-shadow: 0 0 0 2px rgba(5, 145, 255, 0.1);
}
.app-pagination-quick-jumper input:disabled {
color: rgba(0, 0, 0, 0.25);
cursor: not-allowed;
background-color: rgba(0, 0, 0, 0.04);
border-color: #d9d9d9;
box-shadow: none;
}
.app-pagination-container .ant-pagination-total-text {
@ -42,6 +156,68 @@
cursor: pointer;
}
.app-pagination-container--compact {
align-items: flex-start;
}
.app-pagination-container--compact .app-pagination-total {
padding-top: 5px;
}
.app-pagination-container--compact .app-pagination-controls {
flex-wrap: wrap;
}
.app-pagination-container--compact .ant-pagination {
flex-wrap: wrap;
}
.app-pagination-container--narrow {
height: auto;
min-height: 50px;
align-items: flex-start;
flex-direction: column;
gap: 8px;
}
.app-pagination-container--narrow .app-pagination-controls {
width: 100%;
justify-content: flex-start;
flex-wrap: wrap;
row-gap: 8px;
}
.app-pagination-container--narrow .ant-pagination {
justify-content: flex-start;
flex-wrap: wrap;
}
.app-pagination-container--simple {
min-height: 40px;
align-items: center;
flex-direction: row;
gap: 8px;
flex-wrap: nowrap;
}
.app-pagination-container--simple .app-pagination-total {
flex: 1 1 auto;
padding-top: 0;
}
.app-pagination-container--simple .app-pagination-controls {
flex: 0 0 auto;
width: auto;
min-width: max-content;
justify-content: flex-end;
flex-wrap: nowrap;
}
.app-pagination-container--simple .ant-pagination {
justify-content: flex-end;
flex-wrap: nowrap;
}
@media (max-width: 768px) {
.app-pagination-container {
height: auto;
@ -53,9 +229,30 @@
}
.app-pagination-container .ant-pagination {
width: auto;
justify-content: flex-start;
}
.app-pagination-controls {
width: 100%;
justify-content: flex-start;
flex-wrap: wrap;
row-gap: 8px;
}
.app-pagination-container--simple {
flex-direction: row;
align-items: center;
}
.app-pagination-container--simple .app-pagination-controls {
width: auto;
justify-content: flex-end;
flex-wrap: nowrap;
}
.app-pagination-container--simple .ant-pagination {
justify-content: flex-end;
flex-wrap: nowrap;
}
}

View File

@ -1,7 +1,7 @@
import React from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Pagination, PaginationProps } from 'antd';
import { useTranslation } from 'react-i18next';
import { getDefaultPageSize, getPageSizeOptions, type PaginationVariant } from '@/utils/pagination';
import { getMinPageSize, getPageSizeOptions, type PaginationVariant } from '@/utils/pagination';
import './index.css';
export interface AppPaginationProps extends PaginationProps {
@ -11,33 +11,130 @@ export interface AppPaginationProps extends PaginationProps {
export default function AppPagination(props: AppPaginationProps) {
const { t } = useTranslation();
const { className, showSizeChanger, showTotal, total, variant = 'table', pageSizeOptions, ...restProps } = props;
const {
className,
showQuickJumper,
showSizeChanger,
showTotal,
simple,
total,
variant = 'table',
pageSizeOptions,
onChange,
...restProps
} = props;
const [jumperValue, setJumperValue] = useState('');
const [layoutMode, setLayoutMode] = useState<'regular' | 'compact' | 'narrow'>('regular');
const containerRef = useRef<HTMLDivElement | null>(null);
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 defaultPageSize = getMinPageSize(mergedPageSizeOptions);
const current = Number(restProps.current ?? restProps.defaultCurrent ?? 1);
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 });
const pageCount = useMemo(() => Math.max(1, Math.ceil(total / pageSize)), [pageSize, total]);
const shouldShowQuickJumper = showQuickJumper !== false && !simple;
const isDisabled = Boolean(restProps.disabled);
const containerClassName = [
'app-pagination-container',
`app-pagination-container--${layoutMode}`,
simple && 'app-pagination-container--simple',
shouldShowQuickJumper && 'app-pagination-container--with-jumper',
].filter(Boolean).join(' ');
useEffect(() => {
setJumperValue('');
}, [current, pageSize, total]);
useEffect(() => {
if (simple) {
setLayoutMode('regular');
return;
}
const node = containerRef.current;
if (!node || typeof ResizeObserver === 'undefined') {
return;
}
const updateLayoutMode = (width: number) => {
if (width < 520) {
setLayoutMode('narrow');
} else if (width < 760) {
setLayoutMode('compact');
} else {
setLayoutMode('regular');
}
};
updateLayoutMode(node.getBoundingClientRect().width);
const observer = new ResizeObserver(([entry]) => {
updateLayoutMode(entry.contentRect.width);
});
observer.observe(node);
return () => observer.disconnect();
}, [simple]);
const handleJump = () => {
if (isDisabled) {
return;
}
if (!jumperValue.trim()) {
return;
}
const targetPage = Number(jumperValue);
if (!Number.isFinite(targetPage)) {
setJumperValue('');
return;
}
const nextPage = Math.min(Math.max(Math.trunc(targetPage), 1), pageCount);
if (nextPage !== current) {
onChange?.(nextPage, pageSize);
}
setJumperValue('');
};
return (
<div className="app-pagination-container">
<div ref={containerRef} className={containerClassName}>
<div className="app-pagination-total">{totalContent}</div>
<Pagination
className={mergedClassName}
showSizeChanger={mergedShowSizeChanger}
showQuickJumper
defaultPageSize={defaultPageSize}
pageSizeOptions={mergedPageSizeOptions}
size="default"
total={total}
{...restProps}
/>
<div className="app-pagination-controls">
<Pagination
className={mergedClassName}
showSizeChanger={mergedShowSizeChanger}
showQuickJumper={false}
simple={simple}
defaultPageSize={defaultPageSize}
pageSizeOptions={mergedPageSizeOptions}
size="default"
total={total}
onChange={onChange}
{...restProps}
/>
{shouldShowQuickJumper ? (
<span className="app-pagination-quick-jumper">
<span className="app-pagination-quick-jumper__label"></span>
<input
aria-label="跳转页码"
disabled={isDisabled}
value={jumperValue}
onChange={(event) => setJumperValue(event.target.value.replace(/[^\d]/g, ''))}
onBlur={handleJump}
onKeyDown={(event) => {
if (event.key === 'Enter') {
handleJump();
}
}}
/>
<span className="app-pagination-quick-jumper__label"></span>
</span>
) : null}
</div>
</div>
);
}

View File

@ -7,7 +7,7 @@
flex-direction: column;
box-sizing: border-box;
overflow: hidden;
padding: 8px;
padding: 12px 16px 10px;
border-radius: 4px;
background-color: #fff;
}
@ -221,6 +221,10 @@
}
@media (max-width: 768px) {
.data-list-panel {
padding: 12px;
}
.data-list-panel__toolbar {
align-items: stretch;
flex-direction: column;

View File

@ -1,5 +1,6 @@
/* 列表表格容器 */
.list-table-container {
position: relative;
width: 100%;
height: 100%;
display: flex;
@ -113,6 +114,53 @@
overflow-y: auto !important;
}
.list-table-container .list-table-table--y-scroll.ant-table-wrapper .ant-table-sticky-scroll {
display: none !important;
height: 0 !important;
overflow: hidden !important;
}
.list-table-container .list-table-table--empty.list-table-table--y-scroll.ant-table-wrapper .ant-table-body {
position: relative;
}
.list-table-container .list-table-table--empty.ant-table-wrapper .ant-table-placeholder {
display: none !important;
border: 0 !important;
box-shadow: none !important;
}
.list-table-container .list-table-table--empty.ant-table-wrapper .ant-table-placeholder > td {
padding: 0 !important;
border: 0 !important;
box-shadow: none !important;
position: static !important;
}
.list-table-container .list-table-table--empty.ant-table-wrapper .ant-table-placeholder .ant-table-cell {
border: 0 !important;
box-shadow: none !important;
}
.list-table-empty-overlay {
position: absolute;
top: 47px;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
min-height: 0;
background: #fff;
pointer-events: none;
}
.list-table-empty-overlay .ant-empty {
margin: 0;
}
.list-table-container .ant-table-wrapper,
.list-table-container .ant-spin-nested-loading,
.list-table-container .ant-spin-container,

View File

@ -1,5 +1,5 @@
import React from "react";
import { Table } from "antd";
import { Empty, Table } from "antd";
import type { TablePaginationConfig, TableProps } from "antd";
import "./ListTable.css";
import i18n from "../../../i18n";
@ -20,6 +20,7 @@ export type ListTableProps<T extends Record<string, any>> = {
selectedRow?: T | null;
loading?: boolean;
className?: string;
locale?: TableProps<T>["locale"];
onChange?: TableProps<T>["onChange"];
};
@ -43,6 +44,7 @@ function ListTable<T extends Record<string, any>>({
selectedRow,
loading = false,
className = "",
locale,
onChange,
}: ListTableProps<T>) {
const mergedScroll = React.useMemo(() => {
@ -53,6 +55,13 @@ function ListTable<T extends Record<string, any>>({
}, [scroll]);
const hasVerticalScroll = mergedScroll.y !== undefined;
const isEmptyTable = !loading && dataSource.length === 0;
const tableClassName = [
hasVerticalScroll ? "list-table-table--y-scroll" : undefined,
isEmptyTable ? "list-table-table--empty" : undefined,
]
.filter(Boolean)
.join(" ");
const rowSelection: TableProps<T>["rowSelection"] = onSelectionChange
? {
selectedRowKeys,
@ -122,11 +131,18 @@ function ListTable<T extends Record<string, any>>({
["--list-table-scroll-y" as string]: typeof mergedScroll.y === "number" ? `${mergedScroll.y}px` : mergedScroll.y,
} as React.CSSProperties)
: undefined;
const emptyText = locale?.emptyText ?? (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无数据" />
);
const tableLocale = {
...locale,
emptyText: null,
};
return (
<div className={`list-table-container ${className}`} style={wrapperStyle}>
<Table
className={hasVerticalScroll ? "list-table-table--y-scroll" : undefined}
className={tableClassName || undefined}
size="middle"
rowSelection={rowSelection}
columns={columns}
@ -135,12 +151,14 @@ function ListTable<T extends Record<string, any>>({
pagination={mergedPagination}
scroll={mergedScroll}
loading={loading}
locale={tableLocale}
onChange={onChange}
onRow={(record) => ({
onClick: () => onRowClick?.(record),
className: selectedRow?.[rowKey] === record[rowKey] ? "row-selected" : "",
})}
/>
{isEmptyTable ? <div className="list-table-empty-overlay">{emptyText}</div> : null}
</div>
);
}

View File

@ -8,6 +8,10 @@
background: #fafafa;
}
.page-container.page-container {
padding: 16px;
}
.page-container__header {
position: relative;
display: flex;
@ -90,3 +94,9 @@
border-radius: 0 0 4px 4px;
background: #fff;
}
@media (max-width: 768px) {
.page-container.page-container {
padding: 12px;
}
}

View File

@ -909,44 +909,55 @@ body::after {
/* Global Pagination Style */
.ant-table-wrapper {
.app-page__table-wrap .ant-table-wrapper,
.orgs-table-shell .ant-table-wrapper {
height: 100%;
}
.ant-table-wrapper .ant-spin-nested-loading {
.app-page__table-wrap .ant-table-wrapper .ant-spin-nested-loading,
.orgs-table-shell .ant-table-wrapper .ant-spin-nested-loading {
height: 100%;
}
.ant-table-wrapper .ant-spin-container {
.app-page__table-wrap .ant-table-wrapper .ant-spin-container,
.orgs-table-shell .ant-table-wrapper .ant-spin-container {
height: 100%;
display: flex;
flex-direction: column;
min-height: 0;
}
.ant-table-wrapper .ant-table {
.app-page__table-wrap .ant-table-wrapper .ant-table,
.orgs-table-shell .ant-table-wrapper .ant-table {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.ant-table-wrapper .ant-table-container {
.app-page__table-wrap .ant-table-wrapper .ant-table-container,
.orgs-table-shell .ant-table-wrapper .ant-table-container {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.ant-table-wrapper .ant-table-content,
.ant-table-wrapper .ant-table-body {
.app-page__table-wrap .ant-table-wrapper .ant-table-content,
.app-page__table-wrap .ant-table-wrapper .ant-table-body,
.orgs-table-shell .ant-table-wrapper .ant-table-content,
.orgs-table-shell .ant-table-wrapper .ant-table-body {
flex: 1;
min-height: 0;
}
.ant-table-wrapper .ant-table-header {
.app-page__table-wrap .ant-table-wrapper .ant-table-header,
.orgs-table-shell .ant-table-wrapper .ant-table-header {
flex-shrink: 0;
}
.ant-table-wrapper .ant-table-body {
.app-page__table-wrap .ant-table-wrapper .ant-table-body,
.orgs-table-shell .ant-table-wrapper .ant-table-body {
overflow-y: auto !important;
}
@ -982,11 +993,44 @@ body::after {
.app-global-pagination.ant-pagination .ant-pagination-options,
.ant-table-wrapper .ant-table-pagination.ant-pagination.app-global-pagination .ant-pagination-options {
margin-inline-start: 12px;
height: 32px;
display: inline-flex;
align-items: center;
}
.app-global-pagination.ant-pagination .ant-pagination-item-active,
.app-global-pagination.ant-pagination .ant-pagination-item-1.ant-pagination-item-disabled,
.ant-table-wrapper .ant-table-pagination.ant-pagination.app-global-pagination .ant-pagination-item-active,
.ant-table-wrapper .ant-table-pagination.ant-pagination.app-global-pagination .ant-pagination-item-1.ant-pagination-item-disabled {
color: #1677ff !important;
background: #fff !important;
border: 1px solid #1677ff !important;
}
.app-global-pagination.ant-pagination .ant-pagination-item-active a,
.app-global-pagination.ant-pagination .ant-pagination-item-1.ant-pagination-item-disabled a,
.ant-table-wrapper .ant-table-pagination.ant-pagination.app-global-pagination .ant-pagination-item-active a,
.ant-table-wrapper .ant-table-pagination.ant-pagination.app-global-pagination .ant-pagination-item-1.ant-pagination-item-disabled a {
color: #1677ff !important;
background: transparent !important;
border: 0 !important;
}
.app-global-pagination.ant-pagination .ant-pagination-options-quick-jumper,
.ant-table-wrapper .ant-table-pagination.ant-pagination.app-global-pagination .ant-pagination-options-quick-jumper {
margin-inline-start: 12px;
height: 32px;
display: inline-flex;
align-items: center;
line-height: 32px;
white-space: nowrap;
}
.app-global-pagination.ant-pagination .ant-pagination-options-quick-jumper input,
.ant-table-wrapper .ant-table-pagination.ant-pagination.app-global-pagination .ant-pagination-options-quick-jumper input {
height: 32px;
line-height: 30px;
vertical-align: top;
}
.app-global-pagination.ant-pagination .ant-select-selection-search-input,
@ -1299,6 +1343,18 @@ body::after,
backdrop-filter: none;
}
.route-page-fallback {
width: 100%;
height: 100%;
min-height: 320px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: 0;
box-shadow: none;
}
.ant-menu-light {
background: transparent !important;
color: #333 !important;

View File

@ -76,6 +76,63 @@
color: #8c8c8c;
}
.user-affiliation-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
max-width: 100%;
min-width: 0;
}
.user-affiliation-tag {
display: inline-flex;
align-items: center;
gap: 4px;
max-width: 100%;
height: 24px;
padding: 0 8px;
border: 1px solid #d6e4ff;
border-radius: 4px;
background: #f5f9ff;
color: #315efb;
font-size: 12px;
line-height: 22px;
vertical-align: top;
}
.user-affiliation-tag--tenant {
border-color: #b7ebd5;
background: #f1fffa;
color: #08936a;
}
.user-affiliation-tag--org {
border-color: #d6e4ff;
background: #f5f9ff;
color: #315efb;
}
.user-affiliation-tag .anticon {
flex: 0 0 auto;
font-size: 12px;
}
.user-affiliation-tag--tenant .anticon {
color: #13a878;
}
.user-affiliation-tag--org .anticon {
color: #6b8cff;
}
.user-affiliation-tag__text {
min-width: 0;
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-phone {
color: #8c8c8c;
font-size: 13px;
@ -84,6 +141,7 @@
.user-drawer-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 600;
}
@ -99,6 +157,24 @@
margin-bottom: 20px;
}
.user-membership-section {
margin-bottom: 20px;
}
.user-membership-title {
margin-top: 8px !important;
margin-bottom: 16px !important;
}
.user-membership-title--required::before {
content: "*";
display: inline-block;
margin-inline-end: 4px;
color: #ff4d4f;
font-family: SimSun, sans-serif;
line-height: 1;
}
/* Custom alignment for Row/Col in Form */
.user-form .ant-row {
margin-left: -8px !important;

View File

@ -66,9 +66,13 @@ const { Title, Text } = Typography;
type OrgTreeNode = { value: number; title: string; children: OrgTreeNode[] };
type Membership = { tenantId?: number; orgId?: number; orgName?: string };
type Membership = { tenantId?: number; orgId?: number; orgName?: string; orgPath?: string; orgFullName?: string };
type ResetPasswordValues = { newPassword: string; confirmPassword: string };
const getMembershipOrgName = (membership: Membership) => {
return membership.orgFullName || membership.orgPath || membership.orgName || "";
};
function buildOrgTree(list: SysOrg[]): OrgTreeNode[] {
const map = new Map<number, OrgTreeNode>();
const roots: OrgTreeNode[] = [];
@ -400,16 +404,26 @@ export default function Users() {
? [{
title: t("users.tenant"),
key: "tenant",
width: 260,
render: (_: any, record: SysUser) => {
if (record.memberships && record.memberships.length > 0) {
const tenantNames = Array.from(
new Set(
record.memberships
.map((membership: Membership) => tenantMap[membership.tenantId || 0] || (membership.tenantId ? `Tenant ${membership.tenantId}` : ""))
.filter(Boolean)
)
);
return (
<div className="flex flex-col gap-1">
{record.memberships.slice(0, 2).map((membership: any) => (
<Tag key={membership.tenantId} color="blue" style={{ margin: 0, padding: "0 4px", fontSize: 11 }}>
{tenantMap[membership.tenantId] || `Tenant ${membership.tenantId}`}
</Tag>
<div className="user-affiliation-list">
{tenantNames.map((name, index) => (
<Tooltip key={`${name}-${index}`} title={name}>
<span className="user-affiliation-tag user-affiliation-tag--tenant">
<ShopOutlined aria-hidden="true" />
<span className="user-affiliation-tag__text">{name}</span>
</span>
</Tooltip>
))}
{record.memberships.length > 2 && <Text type="secondary" style={{ fontSize: 11 }}>+{record.memberships.length - 2} more</Text>}
</div>
);
}
@ -420,17 +434,22 @@ export default function Users() {
{
title: t("users.orgNode"),
key: "org",
width: 340,
render: (_: any, record: SysUser) => {
if (record.memberships && record.memberships.length > 0) {
const orgNames = record.memberships.map((membership: any) => membership.orgName).filter(Boolean);
const orgNames = Array.from(
new Set(record.memberships.map((membership: Membership) => getMembershipOrgName(membership).trim()).filter(Boolean))
);
if (orgNames.length > 0) {
return (
<div className="flex flex-col gap-1">
<div className="user-affiliation-list">
{orgNames.map((name: string, index: number) => (
<Space key={index} size={4} style={{ fontSize: 13, color: "#555" }}>
<ApartmentOutlined />
<span>{name}</span>
</Space>
<Tooltip key={`${name}-${index}`} title={name}>
<span className="user-affiliation-tag user-affiliation-tag--org">
<ApartmentOutlined aria-hidden="true" />
<span className="user-affiliation-tag__text">{name}</span>
</span>
</Tooltip>
))}
</div>
);
@ -531,8 +550,7 @@ export default function Users() {
</DataListPanel>
</SectionCard>
<Drawer title={<div className="user-drawer-title"><UserOutlined className="mr-2"
aria-hidden="true"/>{editing ? t("users.drawerTitleEdit") : t("users.drawerTitleCreate")}
<Drawer title={<div className="user-drawer-title"><UserOutlined aria-hidden="true"/><span>{editing ? t("users.drawerTitleEdit") : t("users.drawerTitleCreate")}</span>
</div>} open={drawerOpen} onClose={() => setDrawerOpen(false)} width={520} destroyOnClose
footer={<div className="app-page__drawer-footer"><Button
onClick={() => setDrawerOpen(false)}>{t("common.cancel")}</Button><Button type="primary"
@ -571,6 +589,63 @@ export default function Users() {
<Button icon={<UploadOutlined/>} loading={avatarUploading}
style={{marginBottom: 16}}>{t("profile.uploadAvatar")}</Button>
</Upload>
{isPlatformMode && (
<div className="user-membership-section">
<Title level={5} className="user-membership-title user-membership-title--required">{t("usersExt.membershipsTitle")}</Title>
<Form.List
name="memberships"
rules={[
{
validator: async (_, value?: Membership[]) => {
if (value?.some((membership) => membership?.tenantId)) {
return;
}
throw new Error(t("usersExt.membershipRequired"));
}
}
]}
>
{(fields, { add, remove }, { errors }) => (
<>
{fields.map(({ key, name, ...restField }) => (
<Card key={key} size="small" className="mb-3" styles={{ body: { padding: "12px" } }} title={t("usersExt.membershipTitle", { index: name + 1 })} extra={fields.length > 1 && <Button type="text" danger icon={<MinusCircleOutlined />} onClick={() => remove(name)} />}>
<Row gutter={12}>
<Col span={12}>
<Form.Item {...restField} label={t("users.tenant")} name={[name, "tenantId"]} rules={[{ required: true, message: t("usersExt.membershipRequired") }]}>
<Select options={tenants.map((tenant) => ({ label: tenant.tenantName, value: tenant.id }))} placeholder={t("usersExt.selectTenant")} />
</Form.Item>
</Col>
<Col span={12}>
<MembershipOrgSelect fieldProps={{ ...restField }} name={name} tenantId={form.getFieldValue(["memberships", name, "tenantId"])} />
</Col>
</Row>
</Card>
))}
<Form.ErrorList errors={errors} />
<Button type="dashed" onClick={() => add()} block
icon={<PlusOutlined/>}>{t("usersExt.addMembership")}</Button>
</>
)}
</Form.List>
</div>
)}
{!isPlatformMode && (
<Form.Item
label={t("users.orgNode")}
name="orgId"
rules={[{ required: true, message: t("usersExt.selectOrgPlaceholder") }]}
>
<TreeSelect placeholder={t("usersExt.selectOrgPlaceholder")} allowClear treeData={orgTreeData}/>
</Form.Item>
)}
<Form.Item label={t("users.roles")} name="roleIds"
rules={[{required: true, message: t("users.roles")}]}><Select mode="multiple"
placeholder={t("users.roles")}
options={roleOptions}
open={roleSelectOpen}
onDropdownVisibleChange={setRoleSelectOpen}
onChange={() => setRoleSelectOpen(false)}
optionFilterProp={isPlatformMode ? "searchText" : "label"}/></Form.Item>
{!editing ? (
<Form.Item
label={t("users.password")}
@ -607,49 +682,12 @@ export default function Users() {
<Input.Password placeholder={t("usersExt.confirmPasswordPlaceholder")} autoComplete="new-password" />
</Form.Item>
) : null}
<Form.Item label={t("users.roles")} name="roleIds"
rules={[{required: true, message: t("users.roles")}]}><Select mode="multiple"
placeholder={t("users.roles")}
options={roleOptions}
open={roleSelectOpen}
onDropdownVisibleChange={setRoleSelectOpen}
onChange={() => setRoleSelectOpen(false)}
optionFilterProp={isPlatformMode ? "searchText" : "label"}/></Form.Item>
{!isPlatformMode && <Form.Item label={t("users.orgNode")} name="orgId"><TreeSelect
placeholder={t("usersExt.selectOrgPlaceholder")} allowClear treeData={orgTreeData}/></Form.Item>}
<Row gutter={16}>
<Col span={12}><Form.Item label={t("common.status")} name="status" initialValue={1}><Select
options={statusDict.map((item) => ({label: item.itemLabel, value: Number(item.itemValue)}))}/></Form.Item></Col>
{isPlatformMode &&
<Col span={12}><Form.Item label={t("users.platformAdmin")} name="isPlatformAdmin" valuePropName="checked"><Switch/></Form.Item></Col>}
</Row>
{isPlatformMode && (
<>
<Title level={5} style={{ marginTop: 24, marginBottom: 16 }}>{t("usersExt.membershipsTitle")}</Title>
<Form.List name="memberships">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }) => (
<Card key={key} size="small" className="mb-3" styles={{ body: { padding: "12px" } }} title={t("usersExt.membershipTitle", { index: name + 1 })} extra={fields.length > 1 && <Button type="text" danger icon={<MinusCircleOutlined />} onClick={() => remove(name)} />}>
<Row gutter={12}>
<Col span={12}>
<Form.Item {...restField} label={t("users.tenant")} name={[name, "tenantId"]} rules={[{ required: true, message: t("usersExt.membershipRequired") }]}>
<Select options={tenants.map((tenant) => ({ label: tenant.tenantName, value: tenant.id }))} placeholder={t("usersExt.selectTenant")} />
</Form.Item>
</Col>
<Col span={12}>
<MembershipOrgSelect fieldProps={{ ...restField }} name={name} tenantId={form.getFieldValue(["memberships", name, "tenantId"])} />
</Col>
</Row>
</Card>
))}
<Button type="dashed" onClick={() => add()} block
icon={<PlusOutlined/>}>{t("usersExt.addMembership")}</Button>
</>
)}
</Form.List>
</>
)}
</Form>
</Drawer>

View File

@ -731,12 +731,12 @@ const AiModels: React.FC = () => {
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="temperature" label="Temperature">
<Form.Item name="temperature" label="温度">
<InputNumber min={0} max={2} step={0.1} style={{ width: "100%" }} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="topP" label="Top P">
<Form.Item name="topP" label="Top P(核采样)">
<InputNumber min={0} max={1} step={0.1} style={{ width: "100%" }} />
</Form.Item>
</Col>

View File

@ -45,9 +45,101 @@
font-weight: 600;
}
.external-app-icon-form-item {
margin-bottom: 20px;
}
.external-app-icon-form-item .ant-upload {
display: inline-block;
}
.external-app-icon-picker {
width: 128px;
min-height: 128px;
padding: 12px 10px;
display: inline-flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
color: #667085;
text-align: center;
cursor: pointer;
background: #f8fafc;
border: 1px dashed #cbd5e1;
border-radius: 6px;
transition: border-color 0.2s ease, background 0.2s ease, color 0.2s ease, box-shadow 0.2s ease;
}
.external-app-icon-picker:hover,
.external-app-icon-picker:focus-visible {
color: #1677ff;
background: #f0f7ff;
border-color: #1677ff;
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.08);
outline: none;
}
.external-app-icon-picker:disabled {
cursor: not-allowed;
opacity: 0.72;
}
.external-app-icon-picker__preview {
width: 72px;
height: 72px;
display: inline-flex;
align-items: center;
justify-content: center;
overflow: hidden;
color: #94a3b8;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 8px;
}
.external-app-icon-picker__preview > .anticon {
font-size: 28px;
}
.external-app-icon-picker__preview .ant-avatar {
background: #fff;
border-radius: 8px;
}
.external-app-icon-picker__text {
color: #1f2937;
font-size: 13px;
font-weight: 600;
line-height: 18px;
}
.external-app-icon-picker__hint {
max-width: 100%;
color: #98a2b3;
font-size: 12px;
line-height: 16px;
}
.external-app-icon-picker.has-image {
background: #fff;
border-style: solid;
border-color: #d9e4f5;
}
.external-app-icon-picker.has-image .external-app-icon-picker__preview {
border-color: transparent;
box-shadow: 0 0 0 1px rgba(15, 23, 42, 0.06);
}
@media (max-width: 768px) {
.external-app-summary-chips,
.external-app-summary-chip {
width: 100%;
}
.external-app-icon-picker {
width: 100%;
min-height: 116px;
}
}

View File

@ -55,6 +55,7 @@ function compactObject<T extends Record<string, unknown>>(value: T): T {
export default function ExternalAppManagement() {
const { message } = App.useApp();
const [form] = Form.useForm<ExternalAppFormValues>();
const watchedIconUrl = Form.useWatch("iconUrl", form);
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [drawerOpen, setDrawerOpen] = useState(false);
@ -353,14 +354,36 @@ export default function ExternalAppManagement() {
<Col span={8}><Form.Item name="appType" label="应用类型" rules={[{ required: true, message: "请选择应用类型" }]}><Select options={[{ label: "原生应用", value: "native" }, { label: "Web 应用", value: "web" }]} /></Form.Item></Col>
</Row>
<Space align="start" className="mb-4">
<Upload showUploadList={false} beforeUpload={(file) => { void handleUploadIcon(file as File); return Upload.LIST_IGNORE; }}>
<Avatar shape="square" size={72} src={form.getFieldValue("iconUrl")} icon={<PictureOutlined />} style={{ cursor: "pointer", border: "1px dashed var(--app-border-color)", background: "var(--app-bg-card)" }} />
<Form.Item name="iconUrl" hidden>
<Input />
</Form.Item>
<Form.Item label="应用图标" className="external-app-icon-form-item">
<Upload
accept="image/png,image/jpeg,image/webp,image/svg+xml"
showUploadList={false}
beforeUpload={(file) => { void handleUploadIcon(file as File); return Upload.LIST_IGNORE; }}
>
<button
type="button"
className={watchedIconUrl ? "external-app-icon-picker has-image" : "external-app-icon-picker"}
disabled={uploadingIcon}
aria-label="上传应用图标"
>
<span className="external-app-icon-picker__preview">
{watchedIconUrl ? (
<Avatar shape="square" size={72} src={watchedIconUrl} icon={<PictureOutlined />} />
) : (
<PictureOutlined />
)}
</span>
<span className="external-app-icon-picker__text">
{uploadingIcon ? "上传中..." : watchedIconUrl ? "更换图标" : "点击上传图标"}
</span>
<span className="external-app-icon-picker__hint">PNG / JPG / WebP / SVG</span>
</button>
</Upload>
<div style={{ flex: 1 }}>
<Form.Item name="iconUrl" label="图标地址"><Input placeholder="上传图标后自动回填,或手动填写" /></Form.Item>
</div>
</Space>
</Form.Item>
<Form.Item noStyle shouldUpdate={(prev, curr) => prev.appType !== curr.appType}>
{({ getFieldValue }) => getFieldValue("appType") === "native" ? (

View File

@ -178,7 +178,7 @@
.hotwords-group-panel .app-pagination-container {
flex-shrink: 0;
height: auto;
min-height: 54px;
min-height: 48px;
padding: 8px 12px;
gap: 8px;
overflow: hidden;
@ -193,6 +193,13 @@
font-size: 12px;
}
.hotwords-group-panel .app-pagination-controls {
flex: 0 0 auto;
width: auto;
min-width: max-content;
justify-content: flex-end;
}
.hotwords-group-panel .app-pagination-container .ant-pagination {
flex: 0 0 auto;
width: auto;
@ -243,7 +250,11 @@
}
.hotwords-pinyin-tag.ant-tag {
max-width: 132px;
overflow: hidden;
border-radius: 4px;
text-overflow: ellipsis;
white-space: nowrap;
}
.hotwords-search__word {
@ -258,6 +269,27 @@
margin-top: 16px;
}
.hotwords-drawer-form.hotwords-modal-form {
margin-top: 0;
}
.hotwords-drawer-form .ant-form-item-label > label {
color: #333;
font-weight: 500;
}
.hotwords-drawer-form .ant-input,
.hotwords-drawer-form .ant-input-number,
.hotwords-drawer-form .ant-select-selector,
.hotwords-drawer-form .ant-input-affix-wrapper,
.hotwords-drawer-form textarea.ant-input {
border-radius: 4px;
}
.hotwords-drawer-form textarea.ant-input {
resize: none;
}
.hotwords-weight-input {
width: 100%;
}
@ -279,6 +311,14 @@
overflow-y: auto !important;
}
.hotwords-table .ant-table-thead > tr > th {
white-space: nowrap;
}
.hotwords-table .ant-table-tbody > tr > td {
height: 65px;
}
@media (max-width: 992px) {
.hotwords-layout {
flex-direction: column;

View File

@ -20,12 +20,14 @@ import {
} from "antd";
import PageContainer from "@/components/shared/PageContainer";
import DataListPanel from "@/components/shared/DataListPanel";
import FormDrawer from "@/components/shared/FormDrawer";
import SectionCard from "@/components/shared/SectionCard";
import {
DeleteOutlined,
EditOutlined,
PlusOutlined,
ReloadOutlined,
SaveOutlined,
SearchOutlined,
} from "@ant-design/icons";
import { useTranslation } from "react-i18next";
@ -205,9 +207,9 @@ const HotWords: React.FC = () => {
await loadGroupPage();
};
const handleSubmit = async () => {
const handleSubmit = async (formValues?: HotWordFormValues) => {
try {
const values = await form.validateFields();
const values = formValues ?? await form.validateFields();
setSubmitLoading(true);
const payload = {
...values,
@ -308,6 +310,7 @@ const HotWords: React.FC = () => {
title: "热词原文",
dataIndex: "word",
key: "word",
width: 180,
ellipsis: true,
render: (text: string) => <Text strong ellipsis={{ tooltip: text }}>{text}</Text>,
},
@ -315,18 +318,21 @@ const HotWords: React.FC = () => {
title: "拼音",
dataIndex: "pinyinList",
key: "pinyinList",
width: 160,
render: (list: string[]) => list?.[0] ? <Tag className="hotwords-pinyin-tag">{list[0]}</Tag> : <Text type="secondary">-</Text>,
},
{
title: "分类",
dataIndex: "category",
key: "category",
width: 140,
render: (value: string) => categories.find((item) => item.itemValue === value)?.itemLabel || value || "-",
},
{
title: "热词组",
dataIndex: "hotWordGroupId",
key: "hotWordGroupId",
width: 160,
render: (value?: number, record?: HotWordVO) => {
const name = record?.hotWordGroupName || (value ? groupNameMap[value] : undefined);
return name ? <Tag color="blue">{name}</Tag> : <Text type="secondary"></Text>;
@ -336,17 +342,21 @@ const HotWords: React.FC = () => {
title: "权重",
dataIndex: "weight",
key: "weight",
width: 90,
render: (value: number) => <Tag color="orange">{value}</Tag>,
},
{
title: "状态",
dataIndex: "status",
key: "status",
width: 100,
render: (value: number) => value === 1 ? <Badge status="success" text="启用" /> : <Badge status="default" text="禁用" />,
},
{
title: "操作",
key: "action",
width: 140,
fixed: "right" as const,
render: (_: unknown, record: HotWordVO) => (
<Space size="middle">
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => handleOpenModal(record)}>
@ -556,18 +566,27 @@ const HotWords: React.FC = () => {
</div>
</SectionCard>
<Modal
<FormDrawer
title={editingId ? "编辑热词" : "新增热词"}
subtitle={editingId ? "调整热词分类、分组、权重和启用状态" : "补充识别热词,提升会议转写命中率"}
open={modalVisible}
onOk={() => void handleSubmit()}
onCancel={() => setModalVisible(false)}
confirmLoading={submitLoading}
width={560}
destroyOnHidden
onClose={() => setModalVisible(false)}
size="md"
bodyDensity="compact"
okText="确定"
okIcon={<SaveOutlined />}
okLoading={submitLoading}
okButtonProps={{ htmlType: "submit", form: "hotword-form" }}
>
<Form form={form} layout="vertical" className="hotwords-modal-form">
<Form
id="hotword-form"
form={form}
layout="vertical"
className="hotwords-modal-form hotwords-drawer-form"
onFinish={(values) => void handleSubmit(values)}
>
<Form.Item name="word" label="热词原文" rules={[{ required: true, message: "请输入热词原文" }]}>
<Input placeholder="输入识别关键词" onBlur={handleWordBlur} />
<Input placeholder="输入识别关键词" maxLength={80} onBlur={handleWordBlur} />
</Form.Item>
<Form.Item name="pinyin" hidden>
@ -575,7 +594,7 @@ const HotWords: React.FC = () => {
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Col xs={24} sm={12}>
<Form.Item name="category" label="热词分类">
<Select placeholder="请选择分类" allowClear>
{categories.map((item) => (
@ -586,7 +605,7 @@ const HotWords: React.FC = () => {
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Col xs={24} sm={12}>
<Form.Item name="hotWordGroupId" label="所属热词组">
<Select placeholder="请选择热词组" allowClear options={groupOptions.map((item) => ({ label: `${item.groupName} (${item.hotWordCount}/200)`, value: item.id }))} />
</Form.Item>
@ -594,7 +613,7 @@ const HotWords: React.FC = () => {
</Row>
<Row gutter={16}>
<Col span={12}>
<Col xs={24} sm={12}>
<Form.Item
name="weight"
label="识别权重 (1-5)"
@ -603,7 +622,7 @@ const HotWords: React.FC = () => {
<InputNumber min={1} max={5} precision={1} step={0.1} className="hotwords-weight-input" />
</Form.Item>
</Col>
<Col span={12}>
<Col xs={24} sm={12}>
<Form.Item name="status" label="使用状态">
<Select>
<Option value={1}></Option>
@ -614,10 +633,10 @@ const HotWords: React.FC = () => {
</Row>
<Form.Item name="remark" label="备注">
<Input.TextArea rows={2} placeholder="记录热词来源或适用场景" />
<Input.TextArea autoSize={{ minRows: 3, maxRows: 6 }} maxLength={255} showCount placeholder="记录热词来源或适用场景" />
</Form.Item>
</Form>
</Modal>
</FormDrawer>
<Modal
title={editingGroupId ? "编辑热词组" : "新增热词组"}

View File

@ -2752,7 +2752,7 @@ const MeetingDetail: React.FC = () => {
)}
<div ref={transcriptSectionRef} className="transcript-player-anchor">
<Card className="left-flow-card" variant="borderless" title={<span><AudioOutlined /> </span>}>
<Card className="left-flow-card" variant="borderless" title={<Space size={8}><AudioOutlined /></Space>}>
{playbackAudioUrl && (
<audio ref={audioRef} style={{ display: 'none' }} preload="auto">
<source src={playbackAudioUrl} type={resolveAudioMimeType(playbackAudioUrl)} />

View File

@ -62,9 +62,20 @@
.meetings-filter-actions {
min-width: 0;
flex: 1;
display: inline-flex;
align-items: center;
justify-content: flex-end;
}
.meetings-filter-actions.ant-space {
align-items: center;
}
.meetings-filter-actions .ant-space-item {
display: inline-flex;
align-items: center;
}
.meetings-scope-row {
min-width: 0;
display: flex;
@ -90,6 +101,7 @@
.meetings-card-scroll .ant-list .ant-row {
align-items: stretch;
row-gap: 10px !important;
}
.meetings-card-scroll .ant-list .ant-col {
@ -104,23 +116,41 @@
.meeting-card-v2.ant-card {
width: 100%;
height: 164px;
min-height: 164px;
max-height: 164px;
height: 156px;
min-height: 156px;
max-height: 156px;
display: flex;
flex-direction: column;
overflow: hidden;
border: 1px solid #e6e6e6 !important;
border-radius: 4px !important;
border-radius: 16px !important;
background: #fff !important;
box-shadow: none !important;
transition: border-color 0.2s ease, background 0.2s ease;
box-shadow: 0 2px 8px rgba(24, 39, 75, 0.04) !important;
cursor: pointer;
transform: translateY(0);
will-change: transform, box-shadow;
transition:
border-color 0.18s ease,
background 0.18s ease,
box-shadow 0.18s ease,
transform 0.18s ease;
}
.meeting-card-v2.ant-card:hover {
border-color: #b7cdfd !important;
border-color: #8fb0fb !important;
background: #f9fafe !important;
box-shadow: none !important;
box-shadow: 0 8px 20px rgba(60, 112, 245, 0.14) !important;
transform: translateY(-3px);
}
.meeting-card-v2.ant-card:active {
box-shadow: 0 4px 12px rgba(60, 112, 245, 0.12) !important;
transform: translateY(-1px);
}
.meeting-card-v2.ant-card:focus-visible {
outline: 2px solid rgba(60, 112, 245, 0.36);
outline-offset: 2px;
}
.meeting-card-v2 .ant-card-body {
@ -128,7 +158,7 @@
min-height: 0;
display: flex;
flex-direction: column;
padding: 12px 14px !important;
padding: 11px 14px !important;
}
.meeting-status-tag {
@ -141,7 +171,7 @@
overflow: hidden;
padding: 0 8px;
border: 1px solid var(--meeting-status-border);
border-radius: 4px;
border-radius: 10px;
background: var(--meeting-status-bg);
color: var(--meeting-status-color);
font-size: 12px;
@ -197,7 +227,7 @@
.meeting-card-v2__top {
justify-content: space-between;
gap: 8px;
margin-bottom: 10px;
margin-bottom: 8px;
}
.meeting-card-v2__top-actions {
@ -213,7 +243,7 @@
gap: 5px;
padding: 1px 7px;
border: 1px solid #e6e6e6;
border-radius: 4px;
border-radius: 10px;
color: var(--meeting-source-color);
background: #fff;
font-size: 11px;
@ -235,7 +265,7 @@
.meeting-card-v2__title-row {
min-width: 0;
display: block;
margin-bottom: 10px;
margin-bottom: 8px;
}
.meeting-card-v2__title.ant-typography {
@ -255,7 +285,7 @@
.meeting-card-v2__actions .ant-btn {
width: 24px;
height: 24px;
border-radius: 4px;
border-radius: 10px;
border: 1px solid #f0f0f0;
background: #fff;
}
@ -269,25 +299,30 @@
}
.meeting-card-v2__status-note {
padding: 7px 9px;
border: 1px solid #e6e6e6;
border-radius: 4px;
background: #f9fafe;
width: fit-content;
max-width: 100%;
align-self: flex-start;
padding: 0;
border: none;
border-radius: 0;
background: transparent;
font-size: 12px;
}
.meeting-card-v2__status-inner {
justify-content: space-between;
gap: 10px;
justify-content: flex-start;
gap: 8px;
}
.meeting-card-v2__status-text {
min-width: 0;
max-width: 240px;
display: flex;
align-items: center;
gap: 6px;
color: var(--meeting-progress-color);
font-weight: 500;
line-height: 20px;
}
.meeting-card-v2__status-text span {
@ -299,6 +334,7 @@
.meeting-card-v2__retry.ant-btn {
height: 20px;
flex-shrink: 0;
margin-left: 2px;
padding-inline: 0;
font-size: 12px;
font-weight: 500;
@ -325,7 +361,7 @@
justify-content: space-between;
gap: 10px;
margin-top: auto;
padding-top: 10px;
padding-top: 8px;
border-top: 1px solid #f0f0f0;
}
@ -341,7 +377,7 @@
overflow: hidden;
padding: 1px 7px;
border: 1px solid #e6e6e6;
border-radius: 4px;
border-radius: 999px;
background: #f9fafe;
color: #596275;
font-size: 11px;
@ -365,7 +401,7 @@
overflow: hidden;
padding: 0 8px;
border: 1px solid #e6e6e6;
border-radius: 4px;
border-radius: 999px;
color: #596275;
background: #fff;
font-size: 11px;
@ -384,17 +420,15 @@
text-overflow: ellipsis;
}
.meetings-filter-count.ant-tag {
margin: 0;
padding-inline: 8px;
border-radius: 4px;
line-height: 24px;
}
.meetings-status-filter {
width: 170px;
}
.meetings-status-filter .ant-select-selector {
display: flex;
align-items: center;
}
.meetings-status-filter__suffix {
color: #8c8c8c;
}
@ -403,6 +437,17 @@
width: 240px;
}
.meetings-search-input .ant-input-group,
.meetings-search-input .ant-input-affix-wrapper,
.meetings-search-input .ant-input-search-button {
height: 32px;
}
.meetings-search-input .ant-input-affix-wrapper {
display: inline-flex;
align-items: center;
}
.meetings-status-filter-dot {
width: 8px;
height: 8px;

View File

@ -75,6 +75,8 @@ const PAUSED_DISPLAY_STATUS = 5;
const REALTIME_ACTIVE_DISPLAY_STATUS = 6;
const REALTIME_IDLE_DISPLAY_STATUS = 7;
const ALL_STATUS_FILTER = "all";
const CARD_PAGE_SIZE = 12;
const TABLE_PAGE_SIZE = 10;
const QUEUED_RETRY_THRESHOLD_MS = 2 * 60 * 1000;
const MEETING_STATUS_FILTER_OPTIONS = [
{ label: "全部状态", value: ALL_STATUS_FILTER, color: "#8c8c8c", bgColor: "#f5f5f5" },
@ -305,6 +307,17 @@ const MeetingCardItem: React.FC<{
<Card
hoverable
onClick={() => onOpenMeeting(item)}
onKeyDown={(event) => {
if (event.target !== event.currentTarget) {
return;
}
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
onOpenMeeting(item);
}
}}
role="button"
tabIndex={0}
className="meeting-card-v2"
>
<div className="meeting-card-v2__top">
@ -419,7 +432,7 @@ const Meetings: React.FC = () => {
const [total, setTotal] = useState(0);
const [current, setCurrent] = useState(1);
const [displayMode, setDisplayMode] = useState<"card" | "list">("card");
const [size, setSize] = useState(8);
const [size, setSize] = useState(CARD_PAGE_SIZE);
const [searchTitle, setSearchTitle] = useState("");
const [searchKeyword, setSearchKeyword] = useState("");
const [viewType, setViewType] = useState<"all" | "created" | "involved">("all");
@ -445,7 +458,7 @@ const Meetings: React.FC = () => {
const handleDisplayModeChange = (mode: "card" | "list") => {
setDisplayMode(mode);
setSize(mode === "card" ? 8 : 10);
setSize(mode === "card" ? CARD_PAGE_SIZE : TABLE_PAGE_SIZE);
setCurrent(1);
};
@ -795,17 +808,21 @@ const Meetings: React.FC = () => {
toolbarClassName="meetings-list-toolbar"
leftActions={
<div className="meetings-toolbar-stack">
<div className="meetings-scope-row">
<Radio.Group
value={viewType}
onChange={(e) => { setViewType(e.target.value); setCurrent(1); }}
buttonStyle="solid"
className="meetings-scope-switch"
>
<Radio.Button value="all"></Radio.Button>
<Radio.Button value="created"></Radio.Button>
<Radio.Button value="involved"></Radio.Button>
</Radio.Group>
</div>
<div className="meetings-toolbar-primary">
<Space wrap size={10} align="center" className="meetings-primary-actions">
<Radio.Group
value={displayMode}
onChange={(e) => handleDisplayModeChange(e.target.value)}
buttonStyle="solid"
className="meetings-display-switch"
>
<Radio.Button value="card"><AppstoreOutlined /></Radio.Button>
<Radio.Button value="list"><UnorderedListOutlined /></Radio.Button>
</Radio.Group>
{configLoaded && (
<Button
type="primary"
@ -824,15 +841,15 @@ const Meetings: React.FC = () => {
</Space>
<Space wrap size={10} align="center" className="meetings-filter-actions">
<Tag
color={activeFilterCount > 0 ? "processing" : "default"}
className="meetings-filter-count"
<Radio.Group
value={displayMode}
onChange={(e) => handleDisplayModeChange(e.target.value)}
buttonStyle="solid"
className="meetings-display-switch"
>
<Space size={6}>
<FilterOutlined />
<span>{activeFilterCount > 0 ? `已筛选 ${activeFilterCount}` : "未筛选"}</span>
</Space>
</Tag>
<Radio.Button value="card"><AppstoreOutlined /></Radio.Button>
<Radio.Button value="list"><UnorderedListOutlined /></Radio.Button>
</Radio.Group>
<Select
value={statusFilter}
onChange={(value) => { setStatusFilter(value); setCurrent(1); }}
@ -877,19 +894,6 @@ const Meetings: React.FC = () => {
)}
</Space>
</div>
<div className="meetings-scope-row">
<Radio.Group
value={viewType}
onChange={(e) => { setViewType(e.target.value); setCurrent(1); }}
buttonStyle="solid"
className="meetings-scope-switch"
>
<Radio.Button value="all"></Radio.Button>
<Radio.Button value="created"></Radio.Button>
<Radio.Button value="involved"></Radio.Button>
</Radio.Group>
</div>
</div>
}
footer={
@ -897,6 +901,7 @@ const Meetings: React.FC = () => {
variant={displayMode === "card" ? "card" : "table"}
current={current}
pageSize={size}
pageSizeOptions={displayMode === "card" ? ["12", "24", "36", "48"] : undefined}
total={total}
onChange={handlePaginationChange}
/>

View File

@ -107,6 +107,10 @@
margin: 0 !important;
}
.prompt-template-form-drawer .form-drawer__body {
min-width: 0;
}
.prompt-template-editor {
height: calc(100vh - 400px);
}
@ -124,6 +128,10 @@
font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
}
.prompt-template-editor__preview-col {
height: 100%;
}
.prompt-template-editor__preview {
height: 100%;
overflow-y: auto;
@ -137,6 +145,17 @@
margin-bottom: 12px;
}
@media (max-width: 1200px) {
.prompt-template-editor {
height: auto;
}
.prompt-template-editor__col,
.prompt-template-editor__preview-col {
height: 320px;
}
}
@media (max-width: 768px) {
.prompt-templates-search,
.prompt-templates-search .ant-form-item,
@ -150,7 +169,7 @@
}
.prompt-template-editor__col,
.prompt-template-editor__preview {
.prompt-template-editor__preview-col {
height: 320px;
}
}

View File

@ -467,6 +467,8 @@ const PromptTemplates: React.FC = () => {
<FormDrawer
title={editingId ? '编辑模板' : '创建新模板'}
size="md"
width="min(1536px, 80vw)"
className="prompt-template-form-drawer"
bodyDensity="compact"
onClose={() => setDrawerVisible(false)}
open={drawerVisible}
@ -489,14 +491,14 @@ const PromptTemplates: React.FC = () => {
}
}}
>
<Row gutter={16}>
<Col span={24}>
<Row gutter={24}>
<Col xs={24} md={12} xl={6}>
<Form.Item name="templateName" label="模板名称" rules={[{ required: true }]}>
<Input />
</Form.Item>
</Col>
{(isPlatformAdmin || isTenantAdmin) && (
<Col span={24}>
<Col xs={24} md={12} xl={6}>
<Form.Item name="isSystem" label="模板属性" rules={[{ required: true }]}>
<Select placeholder="选择属性">
{promptLevels.length > 0 ? (
@ -511,14 +513,14 @@ const PromptTemplates: React.FC = () => {
</Form.Item>
</Col>
)}
<Col span={24}>
<Col xs={24} md={12} xl={6}>
<Form.Item name="category" label="分类" rules={[{ required: true }]}>
<Select loading={dictLoading}>
{categories.map((i) => <Option key={i.itemValue} value={i.itemValue}>{i.itemLabel}</Option>)}
</Select>
</Form.Item>
</Col>
<Col span={24}>
<Col xs={24} md={12} xl={6}>
<Form.Item name="status" label="状态">
<Select>
<Option value={1}></Option>
@ -537,15 +539,15 @@ const PromptTemplates: React.FC = () => {
/>
</Form.Item>
<Row gutter={16}>
<Col span={24}>
<Row gutter={24}>
<Col xs={24} xl={12}>
<Form.Item name="tags" label="业务标签" tooltip="可从现有标签中选择,也可输入新内容按回车保存">
<Select mode="tags" placeholder="选择或输入新标签" allowClear tokenSeparators={[',', ' ', ';']}>
{dictTags.map((item) => <Option key={item.itemValue} value={item.itemValue}>{item.itemLabel}</Option>)}
</Select>
</Form.Item>
</Col>
<Col span={24}>
<Col xs={24} xl={12}>
<Form.Item
name="hotWordGroupId"
label="绑定热词组"
@ -561,8 +563,8 @@ const PromptTemplates: React.FC = () => {
</Row>
<Divider orientation="left"> (Markdown )</Divider>
<Row gutter={[0, 16]} className="prompt-template-editor">
<Col span={24} className="prompt-template-editor__col">
<Row gutter={[12, 16]} className="prompt-template-editor">
<Col xs={24} xl={12} className="prompt-template-editor__col">
<Form.Item name="promptContent" noStyle rules={[{ required: true }]}>
<Input.TextArea
onChange={(e) => setPreviewContent(e.target.value)}
@ -571,7 +573,7 @@ const PromptTemplates: React.FC = () => {
/>
</Form.Item>
</Col>
<Col span={24} className="prompt-template-editor__preview">
<Col xs={24} xl={12} className="prompt-template-editor__preview-col">
<div className="prompt-template-editor__preview-meta">
{selectedHotWordGroupId ? (
<Tag color="blue">
@ -582,7 +584,7 @@ const PromptTemplates: React.FC = () => {
<Tag></Tag>
)}
</div>
<div className="markdown-preview"><ReactMarkdown>{previewContent}</ReactMarkdown></div>
<div className="prompt-template-editor__preview markdown-preview"><ReactMarkdown>{previewContent}</ReactMarkdown></div>
</Col>
</Row>
</Form>

View File

@ -67,6 +67,9 @@
.screen-saver-page .screen-saver-table-thumb {
width: 120px;
aspect-ratio: 8 / 5;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
border-radius: 4px;
border: 1px solid #e6e6e6;
@ -74,11 +77,21 @@
}
.screen-saver-page .screen-saver-table-thumb img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.screen-saver-page .screen-saver-table-thumb__empty {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: #f5f6fa;
}
.screen-saver-drawer .screen-saver-drawer__footer {
display: flex;
justify-content: flex-end;
@ -121,11 +134,19 @@
}
.screen-saver-drawer .screen-saver-preview-stage img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.screen-saver-drawer .screen-saver-preview-stage__empty {
height: 100%;
display: grid;
place-items: center;
color: #8c9bb3;
}
.screen-saver-crop-modal .ant-modal-content {
overflow: hidden;
border-radius: 4px;

View File

@ -614,13 +614,13 @@ export default function ScreenSaverManagement() {
{
title: "预览",
key: "thumb",
width: 90,
width: 140,
render: (_, record) => (
<div className="screen-saver-table-thumb">
{record.imageUrl ? (
<img src={record.imageUrl} alt={record.name} style={{ maxWidth: 70, maxHeight: 52, objectFit: 'cover', borderRadius: 4 }} />
<img src={record.imageUrl} alt={record.name} />
) : (
<div style={{ width: 70, height: 52, background: '#f5f5f5', borderRadius: 4, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div className="screen-saver-table-thumb__empty">
<PictureOutlined style={{ color: '#ccc', fontSize: 20 }} />
</div>
)}
@ -884,9 +884,9 @@ export default function ScreenSaverManagement() {
</Space>
<div className="screen-saver-preview-stage">
{currentImageUrl ? (
<img src={currentImageUrl} alt="屏保预览" style={{ maxWidth: '100%', maxHeight: 300, objectFit: 'contain', borderRadius: 8 }} />
<img src={currentImageUrl} alt="屏保预览" />
) : (
<div style={{ height: "100%", display: "grid", placeItems: "center", color: "rgba(235,244,255,.72)" }}>
<div className="screen-saver-preview-stage__empty">
<Space direction="vertical" align="center" size={10}>
<ScissorOutlined style={{ fontSize: 26 }} />
<span></span>

View File

@ -14,6 +14,7 @@
.dashboard-monitor-page__content {
gap: 12px;
padding: 8px 8px 0;
}
.dashboard-monitor-page__stats {
@ -29,6 +30,10 @@
box-shadow: none;
}
.dashboard-monitor-page__stat-card .ant-card-body {
padding: 18px 24px;
}
.dashboard-monitor-page__stat-label {
font-size: 13px;
}
@ -36,6 +41,7 @@
.dashboard-monitor-page__task-panel {
flex: 1;
min-height: 0;
padding: 12px 16px 10px;
}
.dashboard-monitor-page__task-panel .data-list-panel__toolbar {
@ -55,12 +61,12 @@
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding-right: 4px;
padding: 0 6px 0 0;
overscroll-behavior: contain;
}
.dashboard-monitor-page__task-item {
padding: 20px 0;
padding: 18px 0;
border-bottom: 1px solid #f0f2f5;
}

View File

@ -60,29 +60,40 @@
.device-cell {
width: 100%;
min-width: 0;
display: flex;
align-items: center;
gap: 10px;
}
.device-cell__content {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.device-name {
display: block;
font-weight: 600;
color: #262626;
max-width: 100%;
}
.device-code {
display: block;
font-size: 12px;
color: #8c8c8c;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.device-drawer-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 600;
}

View File

@ -1,4 +1,4 @@
import { Button, Drawer, Form, Input, Popconfirm, Select, Space, Tag, Typography, message } from "antd";
import { Button, Drawer, Form, Input, Popconfirm, Select, Space, Tag, Tooltip, Typography, message } from "antd";
import { CheckCircleOutlined, DeleteOutlined, DesktopOutlined, DisconnectOutlined, EditOutlined, ReloadOutlined, SearchOutlined, ThunderboltOutlined, UndoOutlined, UserOutlined } from "@ant-design/icons";
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
@ -10,10 +10,12 @@ import AppPagination from "@/components/shared/AppPagination";
import SectionCard from "@/components/shared/SectionCard";
import { useDict } from "@/hooks/useDict";
import { usePermission } from "@/hooks/usePermission";
import { getDefaultPageSize } from "@/utils/pagination";
import type { DeviceInfo } from "@/types";
import "./index.less";
const { Text } = Typography;
const DEFAULT_TABLE_PAGE_SIZE = getDefaultPageSize("table");
type DeviceFormValues = {
deviceName?: string;
@ -21,6 +23,16 @@ type DeviceFormValues = {
weatherCityName?: string;
};
const formatDeviceCode = (code?: string) => {
if (!code) {
return "-";
}
if (code.length <= 16) {
return code;
}
return `${code.slice(0, 8)}...${code.slice(-6)}`;
};
export default function Devices() {
const { t } = useTranslation();
const { can } = usePermission();
@ -32,7 +44,7 @@ export default function Devices() {
const [open, setOpen] = useState(false);
const [editing, setEditing] = useState<DeviceInfo | null>(null);
const [form] = Form.useForm<DeviceFormValues>();
const [pagination, setPagination] = useState({ current: 1, pageSize: 20 });
const [pagination, setPagination] = useState({ current: 1, pageSize: DEFAULT_TABLE_PAGE_SIZE });
const loadData = async () => {
setLoading(true);
@ -141,10 +153,9 @@ export default function Devices() {
{
title: t("devicesExt.device"),
key: "device",
width: 280,
ellipsis: {showTitle: true},
width: 320,
render: (_value: unknown, record: DeviceInfo) => (
<Space className="device-cell">
<div className="device-cell">
<div className="device-icon-placeholder">
<DesktopOutlined aria-hidden="true" />
</div>
@ -152,11 +163,11 @@ export default function Devices() {
<Text className="device-name" ellipsis={{tooltip: record.deviceName || t("devicesExt.unnamedDevice")}}>
{record.deviceName || t("devicesExt.unnamedDevice")}
</Text>
<Text className="device-code tabular-nums" ellipsis={{tooltip: record.deviceCode}}>
{record.deviceCode}
</Text>
<Tooltip title={record.deviceCode}>
<span className="device-code tabular-nums">{formatDeviceCode(record.deviceCode)}</span>
</Tooltip>
</div>
</Space>
</div>
)
},
{
@ -351,8 +362,8 @@ export default function Devices() {
<Drawer
title={
<div className="device-drawer-title">
<DesktopOutlined className="mr-2" aria-hidden="true" />
{t("devices.drawerTitleEdit")}
<DesktopOutlined aria-hidden="true" />
<span>{t("devices.drawerTitleEdit")}</span>
</div>
}
open={open}

View File

@ -17,8 +17,18 @@
background: transparent;
}
.home-shell-card {
padding: 0;
}
.home-shell-card .section-card__content {
padding: 8px;
padding: 0;
background: transparent;
}
.home-data-panel {
padding: 0;
background: transparent;
}
.home-data-panel .data-list-panel__table-area,
@ -27,6 +37,12 @@
min-height: 0;
}
.home-data-panel .data-list-panel__table-container,
.home-data-panel .data-list-panel__table-area,
.home-data-panel .app-page__table-wrap {
overflow: hidden;
}
.home-container {
position: relative;
flex: 1;

View File

@ -54,19 +54,46 @@
display: flex;
flex-direction: column;
border: 1px solid #e6e6e6 !important;
border-radius: 4px !important;
box-shadow: none !important;
border-radius: 16px !important;
background: #fff !important;
box-shadow: 0 2px 8px rgba(24, 39, 75, 0.04) !important;
overflow: hidden;
transform: translateY(0);
will-change: transform, box-shadow;
transition:
border-color 0.18s ease,
background 0.18s ease,
box-shadow 0.18s ease,
transform 0.18s ease;
}
.tenant-card:hover {
border-color: #8fb0fb !important;
background: #f9fafe !important;
box-shadow: 0 8px 20px rgba(60, 112, 245, 0.14) !important;
transform: translateY(-3px);
}
.tenant-card:active {
box-shadow: 0 4px 12px rgba(60, 112, 245, 0.12) !important;
transform: translateY(-1px);
}
.tenant-card .ant-card-body {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
padding: 24px !important;
}
.tenant-card .ant-card-actions {
border-top-color: #f0f0f0;
background: rgba(255, 255, 255, 0.82);
}
.tenant-card:hover .ant-card-actions {
background: #f9fafe;
}
.tenant-card__header {
@ -77,7 +104,7 @@
.tenant-card__avatar {
margin-right: 12px;
border-radius: 8px;
border-radius: 10px;
}
.tenant-card__avatar--enabled {
@ -112,7 +139,7 @@
.tenant-card__status {
flex: 0 0 auto;
margin: 0;
border-radius: 4px;
border-radius: 10px;
}
.tenant-card__code {
@ -161,6 +188,22 @@
color: #ff4d4f;
}
.tenant-card__action {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 10px;
transition:
color 0.18s ease,
background 0.18s ease;
}
.tenant-card__action:hover {
background: rgba(60, 112, 245, 0.08);
}
.tenants-search-form {
justify-content: flex-end;
gap: 8px 0;

View File

@ -234,7 +234,7 @@ export default function Dictionaries() {
</Col>
<Col span={16} className="h-full flex flex-col overflow-hidden">
<Card title={<Space><ProfileOutlined aria-hidden="true" /><span>{t("dicts.dictItem")}{selectedType ? ` - ${selectedType.typeName}` : ""}</span></Space>} className="app-page__panel-card flex-1 flex flex-col overflow-hidden" styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }} extra={can("sys_dict:item:create") && <Button type="primary" size="small" icon={<PlusOutlined aria-hidden="true" />} onClick={handleAddItem} disabled={!selectedType}>{t("common.create")}</Button>}>
<Card title={<Space><ProfileOutlined aria-hidden="true" /><span>{t("dicts.dictItem")}{selectedType ? ` - ${selectedType.typeName}` : ""}</span></Space>} className="app-page__panel-card flex-1 flex flex-col overflow-hidden" styles={{ body: { padding: 12, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }} extra={can("sys_dict:item:create") && <Button type="primary" size="small" icon={<PlusOutlined aria-hidden="true" />} onClick={handleAddItem} disabled={!selectedType}>{t("common.create")}</Button>}>
{selectedType ? (
<div className="dict-item-table-wrap">
<Table

View File

@ -9,11 +9,13 @@ import DataListPanel from "@/components/shared/DataListPanel";
import ListTable from "@/components/shared/ListTable/ListTable";
import AppPagination from "@/components/shared/AppPagination";
import SectionCard from "@/components/shared/SectionCard";
import { getDefaultPageSize } from "@/utils/pagination";
import type { SysLog, SysTenant, UserProfile } from "@/types";
import "./index.less";
const { RangePicker } = DatePicker;
const { Text } = Typography;
const DEFAULT_TABLE_PAGE_SIZE = getDefaultPageSize("table");
export default function Logs() {
const { t } = useTranslation();
@ -29,7 +31,7 @@ export default function Logs() {
const requestSeqRef = useRef(0);
const [params, setParams] = useState({
current: 1,
size: 20,
size: DEFAULT_TABLE_PAGE_SIZE,
username: "",
moduleName: "",
status: undefined as number | undefined,
@ -154,7 +156,7 @@ export default function Logs() {
const handleReset = () => {
const resetParams = {
current: 1,
size: 20,
size: DEFAULT_TABLE_PAGE_SIZE,
username: "",
moduleName: "",
status: undefined,

View File

@ -1,4 +1,4 @@
import { Button, Card, Col, Form, Input, Row, Upload, message } from "antd";
import { Button, Card, Col, Form, Input, Row, Space, Upload, message } from "antd";
import { FileTextOutlined, GlobalOutlined, PictureOutlined, SaveOutlined, UploadOutlined } from "@ant-design/icons";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
@ -91,7 +91,7 @@ export default function PlatformSettings() {
<Form form={form} layout="vertical" onFinish={onFinish} initialValues={{ projectName: "UnisBase" }} className="platform-settings-form">
<Row gutter={16}>
<Col span={24}>
<Card title={<><GlobalOutlined className="mr-2" />{t("platformSettings.basicInfo")}</>} className="mb-6" loading={loading}>
<Card title={<Space size={8}><GlobalOutlined />{t("platformSettings.basicInfo")}</Space>} className="mb-6" loading={loading}>
<Form.Item label={t("platformSettings.projectName")} name="projectName" rules={[
{ required: true, message: t("platformSettings.projectNameRequired") },
{
@ -108,7 +108,7 @@ export default function PlatformSettings() {
</Col>
<Col span={24}>
<Card title={<><PictureOutlined className="mr-2" />{t("platformSettings.brandAssets")}</>} className="app-page__content-card mb-6" loading={loading}>
<Card title={<Space size={8}><PictureOutlined />{t("platformSettings.brandAssets")}</Space>} className="app-page__content-card mb-6" loading={loading}>
<Row gutter={24}>
<Col xs={24} md={8}>
<Form.Item label={t("platformSettings.logoUrl")} name="logoUrl">
@ -142,7 +142,7 @@ export default function PlatformSettings() {
</Col>
<Col span={24}>
<Card title={<><FileTextOutlined className="mr-2" />{t("platformSettings.complianceFooter")}</>} className="app-page__content-card" loading={loading}>
<Card title={<Space size={8}><FileTextOutlined />{t("platformSettings.complianceFooter")}</Space>} className="app-page__content-card" loading={loading}>
<Row gutter={16}>
<Col xs={24} md={12}>
<Form.Item label={t("platformSettings.icp")} name="icpInfo">

View File

@ -37,7 +37,7 @@ const MeetingDetail = lazy(() => import("../pages/business/MeetingDetail"));
function RouteFallback() {
return (
<div className="app-page__empty-state" style={{ minHeight: 320 }}>
<div className="route-page-fallback">
<Spin />
</div>
);

View File

@ -6,11 +6,19 @@ 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 const getMinPageSize = (pageSizeOptions: Array<string | number>) => {
const sizes = pageSizeOptions
.map((item) => Number(item))
.filter((item) => Number.isFinite(item) && item > 0);
return sizes.length > 0 ? Math.min(...sizes) : 10;
};
export const getDefaultPageSize = (variant: PaginationVariant = 'table') =>
getMinPageSize(getPageSizeOptions(variant));
export interface StandardPaginationConfig extends TablePaginationConfig {
variant?: PaginationVariant;
}
@ -26,6 +34,7 @@ export const getStandardPagination = (
overrides: StandardPaginationConfig = {}
): TablePaginationConfig => {
const { variant = 'table', pageSizeOptions, className, showSizeChanger, ...restOverrides } = overrides;
const mergedPageSizeOptions = pageSizeOptions ?? getPageSizeOptions(variant);
const mergedClassName = ['app-global-pagination', className].filter(Boolean).join(' ');
const mergedShowSizeChanger =
showSizeChanger === undefined || showSizeChanger === true
@ -39,7 +48,7 @@ export const getStandardPagination = (
onChange,
showQuickJumper: true,
showTotal: (totalCount) => i18n.t('common.total', { total: totalCount }),
pageSizeOptions: pageSizeOptions ?? getPageSizeOptions(variant),
pageSizeOptions: mergedPageSizeOptions,
size: 'default',
position: ['bottomRight'],
...restOverrides,