feat:页面调整2
parent
4bc3f65a10
commit
0a33ff8e56
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" ? (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 ? "编辑热词组" : "新增热词组"}
|
||||
|
|
|
|||
|
|
@ -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)} />
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue