feat(web): table ui

This commit is contained in:
zhaoying
2026-03-25 17:13:54 +08:00
parent 0380c13a3b
commit 56fd5680cf
12 changed files with 91 additions and 53 deletions

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-02 15:29:46 * @Date: 2026-02-02 15:29:46
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-24 16:12:56 * @Last Modified time: 2026-03-25 17:11:55
*/ */
/** /**
* RbTable Component * RbTable Component
@@ -30,21 +30,21 @@ import Empty from '@/components/Empty';
interface TablePaginationConfig { pagesize: number; page: number; } interface TablePaginationConfig { pagesize: number; page: number; }
/** Props interface for Table component */ /** Props interface for Table component */
interface TableComponentProps extends Omit<TableProps, 'pagination'> { interface TableComponentProps<T = Record<string, unknown>, Q = Record<string, unknown>> extends Omit<TableProps<T>, 'pagination'> {
/** Table column definitions */ /** Table column definitions */
columns: ColumnsType; columns: ColumnsType<T>;
/** API endpoint URL for data fetching */ /** API endpoint URL for data fetching */
apiUrl?: string; apiUrl?: string;
/** Query parameters for API request */ /** Query parameters for API request */
apiParams?: Record<string, unknown>; apiParams?: Q;
/** Pagination configuration or boolean to enable/disable */ /** Pagination configuration or boolean to enable/disable */
pagination?: boolean | TablePaginationConfig; pagination?: boolean | TablePaginationConfig;
/** Key to use for row identification */ /** Key to use for row identification */
rowKey: string; rowKey: string;
/** Row selection configuration */ /** Row selection configuration */
rowSelection?: TableProps['rowSelection']; rowSelection?: TableProps<T>['rowSelection'];
/** Initial data to display (used when no API) */ /** Initial data to display (used when no API) */
initialData?: Record<string, unknown>[]; initialData?: T[];
/** Size of empty state icon */ /** Size of empty state icon */
emptySize?: number; emptySize?: number;
/** Custom empty state text */ /** Custom empty state text */
@@ -81,7 +81,7 @@ const dealSo = (params: any) => {
} }
/** Table component with pagination and API integration */ /** Table component with pagination and API integration */
const RbTable = forwardRef<TableRef, TableComponentProps>(({ const RbTable = forwardRef(<T = Record<string, unknown>, Q = Record<string, unknown>>({
columns, columns,
apiUrl, apiUrl,
apiParams, apiParams,
@@ -96,9 +96,9 @@ const RbTable = forwardRef<TableRef, TableComponentProps>(({
scrollY, scrollY,
currentPageKey = 'page', currentPageKey = 'page',
...props ...props
}, ref) => { }: TableComponentProps<T, Q>, ref: React.Ref<TableRef>) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [data, setData] = useState<Record<string, unknown>[]>(initialData || []) const [data, setData] = useState<T[]>(initialData || [])
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [currentPagination, setCurrentPagination] = useState({ const [currentPagination, setCurrentPagination] = useState({
page: 1, page: 1,
@@ -122,7 +122,7 @@ const RbTable = forwardRef<TableRef, TableComponentProps>(({
}) })
} }
} }
/** Fetch data from API with pagination */ /** Fetch data from API with pagination */
const getList = (pageData: TablePaginationConfig) => { const getList = (pageData: TablePaginationConfig) => {
if (!apiUrl) { if (!apiUrl) {
@@ -134,7 +134,7 @@ const RbTable = forwardRef<TableRef, TableComponentProps>(({
...currentPagination, ...currentPagination,
...pageData, ...pageData,
}) })
params = { ...params, ...pageData, [currentPageKey]: pageData.page} params = { ...params, ...pageData, [currentPageKey]: pageData.page }
} }
setLoading(true) setLoading(true)
/** Build query parameters and call API */ /** Build query parameters and call API */
@@ -151,7 +151,7 @@ const RbTable = forwardRef<TableRef, TableComponentProps>(({
setLoading(false) setLoading(false)
}) })
} }
/** Reload data when initialized or apiParams changes */ /** Reload data when initialized or apiParams changes */
useEffect(() => { useEffect(() => {
loadData() loadData()
@@ -164,7 +164,7 @@ const RbTable = forwardRef<TableRef, TableComponentProps>(({
pagesize pagesize
}) })
} }
/** Pagination configuration with i18n support */ /** Pagination configuration with i18n support */
const paginationConfig = pagination ? ({ const paginationConfig = pagination ? ({
...(typeof pagination === 'object' ? pagination : {}), ...(typeof pagination === 'object' ? pagination : {}),
@@ -173,7 +173,7 @@ const RbTable = forwardRef<TableRef, TableComponentProps>(({
onChange: handlePageChange, onChange: handlePageChange,
showSizeChanger: true, showSizeChanger: true,
showQuickJumper: true, showQuickJumper: true,
showTotal: (totalCount: number) => t('table.totalRecords', {total: totalCount}) showTotal: (totalCount: number) => t('table.totalRecords', { total: totalCount })
}) : false; }) : false;
@@ -186,27 +186,27 @@ const RbTable = forwardRef<TableRef, TableComponentProps>(({
/** Calculate scroll configuration based on props */ /** Calculate scroll configuration based on props */
const getScrollConfig = () => { const getScrollConfig = () => {
if (!isScroll && !scrollX && !scrollY) return undefined; if (!isScroll && !scrollX && !scrollY) return undefined;
const config: { x?: number | string | true; y?: number | string } = {}; const config: { x?: number | string | true; y?: number | string } = {};
/** Only apply horizontal scroll when there is data */ /** Only apply horizontal scroll when there is data */
if (scrollX !== undefined && data.length > 0) { if (scrollX !== undefined && data.length > 0) {
config.x = scrollX; config.x = scrollX;
} else if (isScroll) { } else if (isScroll) {
config.x = 'max-content'; config.x = 'max-content';
} }
if (scrollY !== undefined) { if (scrollY !== undefined) {
config.y = scrollY; config.y = scrollY;
} else if (isScroll) { } else if (isScroll) {
config.y = 'calc(100vh - 256px)'; config.y = 'calc(100vh - 232px)';
} }
return Object.keys(config).length > 0 ? config : undefined; return Object.keys(config).length > 0 ? config : undefined;
}; };
return ( return (
<Table <Table<T>
{...props} {...props}
rowKey={rowKey} rowKey={rowKey}
loading={loading} loading={loading}
@@ -219,6 +219,6 @@ const RbTable = forwardRef<TableRef, TableComponentProps>(({
tableLayout="auto" tableLayout="auto"
/> />
); );
}); }) as <T = Record<string, unknown>, Q = Record<string, unknown>>(props: TableComponentProps<T, Q> & { ref?: React.Ref<TableRef> }) => React.ReactElement;
export default RbTable; export default RbTable;

View File

@@ -1444,6 +1444,7 @@ export const en = {
noShareAuth: 'No permission to share apps', noShareAuth: 'No permission to share apps',
appCount: '{{count}} apps shared to this space', appCount: '{{count}} apps shared to this space',
resetFeaturesTip: 'Please reconfigure the [Conversation Features - File Upload] settings', resetFeaturesTip: 'Please reconfigure the [Conversation Features - File Upload] settings',
logTitle: 'Description',
}, },
userMemory: { userMemory: {
userMemory: 'User Memory', userMemory: 'User Memory',

View File

@@ -819,6 +819,7 @@ export const zh = {
noShareAuth: '无共享应用的权限', noShareAuth: '无共享应用的权限',
appCount: '{{count}}个应用共享到此空间', appCount: '{{count}}个应用共享到此空间',
resetFeaturesTip: '请重新配置【对话功能-文件上传】功能', resetFeaturesTip: '请重新配置【对话功能-文件上传】功能',
logTitle: '描述',
}, },
table: { table: {
totalRecords: '共 {{total}} 条记录' totalRecords: '共 {{total}} 条记录'

View File

@@ -156,6 +156,19 @@ body {
border-radius: 12px; border-radius: 12px;
border: none; border: none;
} }
.ant-table-wrapper .ant-table.ant-table-bordered {
border-radius: 12px;
}
.ant-table-wrapper table.ant-table-bordered {
border-radius: 12px;
}
.ant-table-bordered .ant-table-row:last-child .ant-table-cell:first-child {
border-bottom-left-radius: 12px;
}
.ant-table-bordered .ant-table-row:last-child .ant-table-cell:last-child {
border-bottom-right-radius: 12px;
}
.ant-table-wrapper table { .ant-table-wrapper table {
border-radius: 12px; border-radius: 12px;
} }
@@ -200,6 +213,18 @@ body {
.table-header.ant-table-wrapper .ant-table-thead>tr>th, .table-header.ant-table-wrapper .ant-table-thead>tr>th,
.table-header.ant-table-wrapper .ant-table-thead>tr>td { .table-header.ant-table-wrapper .ant-table-thead>tr>td {
background-color: #F6F6F6; background-color: #F6F6F6;
border-bottom: 1px solid #EBEBEB;
}
.ant-table-wrapper .ant-table-bordered .ant-table-thead>tr>th,
.ant-table-wrapper .ant-table-bordered .ant-table-thead>tr>td,
.table-header.ant-table-wrapper .ant-table-bordered .ant-table-thead>tr>th,
.table-header.ant-table-wrapper .ant-table-bordered .ant-table-thead>tr>td {
background-color: #FFFFFF;
font-weight: 500;
}
.ant-table-wrapper .ant-table.ant-table-small.ant-table-bordered .ant-table-tbody>tr>th,
.ant-table-wrapper .ant-table.ant-table-small.ant-table-bordered .ant-table-tbody>tr>td {
color: #171719;
} }
.ant-table-wrapper .ant-btn { .ant-table-wrapper .ant-btn {
height: 24px; height: 24px;

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-03-24 15:41:20 * @Date: 2026-03-24 15:41:20
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-24 16:06:44 * @Last Modified time: 2026-03-25 16:20:32
*/ */
import { type FC, useRef } from 'react'; import { type FC, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -26,7 +26,7 @@ const Statistics: FC = () => {
} }
/** Table column configuration */ /** Table column configuration */
const columns: ColumnsType = [ const columns: ColumnsType<LogItem> = [
{ {
title: t('application.logTitle'), title: t('application.logTitle'),
dataIndex: 'title', dataIndex: 'title',
@@ -62,7 +62,7 @@ const Statistics: FC = () => {
]; ];
return ( return (
<div className="rb:bg-white rb:rounded-lg rb:pt-3 rb:px-3"> <div className="rb:bg-white rb:rounded-lg rb:pt-3 rb:px-3">
<Table <Table<LogItem>
apiUrl={getAppLogsUrl(id || '')} apiUrl={getAppLogsUrl(id || '')}
apiParams={{ apiParams={{
is_draft: false, is_draft: false,

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 16:26:32 * @Date: 2026-02-03 16:26:32
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-25 15:09:09 * @Last Modified time: 2026-03-25 17:10:30
*/ */
/** /**
* Variable List Component * Variable List Component
@@ -78,9 +78,10 @@ interface VariableListProps {
<> <>
{fields.length > 0 ? ( {fields.length > 0 ? (
<div className="rb:mt-3"> <div className="rb:mt-3">
<Table <Table<Variable>
size="small" size="small"
rowKey="index" rowKey="index"
bordered={true}
pagination={false} pagination={false}
columns={[ columns={[
{ {
@@ -123,7 +124,7 @@ interface VariableListProps {
), ),
}, },
]} ]}
initialData={value as unknown as Record<string, unknown>[]} initialData={value}
emptySize={88} emptySize={88}
/> />
</div> </div>

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 16:42:12 * @Date: 2026-02-03 16:42:12
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-24 16:49:45 * @Last Modified time: 2026-03-25 16:25:23
*/ */
/** /**
* Member Management Page * Member Management Page
@@ -13,7 +13,6 @@ import React, { useRef } from 'react';
import { App, Button, Space, Flex } from 'antd'; import { App, Button, Space, Flex } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { ColumnsType } from 'antd/es/table'; import type { ColumnsType } from 'antd/es/table';
import type { AnyObject } from 'antd/es/_util/type';
import { deleteMember, memberListUrl } from '@/api/member'; import { deleteMember, memberListUrl } from '@/api/member';
import MemberModal from './components/MemberModal'; import MemberModal from './components/MemberModal';
@@ -61,7 +60,7 @@ const MemberManagement: React.FC = () => {
}; };
/** Table column configuration */ /** Table column configuration */
const columns: ColumnsType = [ const columns: ColumnsType<Member> = [
{ {
title: t('member.username'), title: t('member.username'),
dataIndex: 'username', dataIndex: 'username',
@@ -89,15 +88,15 @@ const MemberManagement: React.FC = () => {
{ {
title: t('common.operation'), title: t('common.operation'),
key: 'action', key: 'action',
render: (_, record: AnyObject) => ( render: (_, record) => (
<Space size="large"> <Space size="large">
<Button <Button
type="link" type="link"
onClick={() => handleEdit(record as Member)} onClick={() => handleEdit(record)}
> >
{t('common.edit')} {t('common.edit')}
</Button> </Button>
<Button type="link" danger onClick={() => handleDelete(record as Member)}> <Button type="link" danger onClick={() => handleDelete(record)}>
{t('common.delete')} {t('common.delete')}
</Button> </Button>
</Space> </Space>
@@ -112,7 +111,7 @@ const MemberManagement: React.FC = () => {
{t('member.createMember')} {t('member.createMember')}
</Button> </Button>
</Flex> </Flex>
<Table <Table<Member>
ref={tableRef} ref={tableRef}
apiUrl={memberListUrl} apiUrl={memberListUrl}
columns={columns} columns={columns}

View File

@@ -12,21 +12,23 @@ const FormItem = Form.Item;
interface CustomToolModalProps { interface CustomToolModalProps {
refresh: () => void; refresh: () => void;
} }
interface OperationItem {
method: string;
path: string;
summary: string;
description: string;
parameters: Record<string, Record<string, string | null>>
request_body: null | string;
responses: Record<string, Record<string, string | null>>
tags: string[]
}
interface ParseSchemaData { interface ParseSchemaData {
title: string; title: string;
description: string; description: string;
version: string; version: string;
base_url: string; base_url: string;
operations: Array<{ operations: OperationItem[]
method: string;
path: string;
summary: string;
description: string;
parameters: Record<string, Record<string, string | null>>
request_body: null | string;
responses: Record<string, Record<string, string | null>>
tags: string[]
}>
} }
const authTypeList = ['none', 'api_key', 'basic_auth'] const authTypeList = ['none', 'api_key', 'basic_auth']
const CustomToolModal = forwardRef<CustomToolModalRef, CustomToolModalProps>(({ const CustomToolModal = forwardRef<CustomToolModalRef, CustomToolModalProps>(({
@@ -170,9 +172,10 @@ const CustomToolModal = forwardRef<CustomToolModalRef, CustomToolModalProps>(({
<Form.Item <Form.Item
label={t('tool.availableTools')} label={t('tool.availableTools')}
> >
<Table <Table<OperationItem>
rowKey="summary" rowKey="summary"
pagination={false} pagination={false}
bordered={true}
columns={[ columns={[
{ {
title: t('tool.name'), title: t('tool.name'),

View File

@@ -355,9 +355,10 @@ const McpServiceModal = forwardRef<McpServiceModalRef, McpServiceModalProps>(({
{requestHeaderList.length === 0 {requestHeaderList.length === 0
? <Empty size={88} /> ? <Empty size={88} />
: :
<Table <Table<RequestHeader>
rowKey="key" rowKey="key"
pagination={false} pagination={false}
bordered={true}
columns={[ columns={[
{ {
title: t('tool.requestHeaderName'), title: t('tool.requestHeaderName'),
@@ -381,7 +382,7 @@ const McpServiceModal = forwardRef<McpServiceModalRef, McpServiceModalProps>(({
<Space size="middle"> <Space size="middle">
<Button <Button
type="link" type="link"
onClick={() => handleEditRequestHeader(index, record as RequestHeader)} onClick={() => handleEditRequestHeader(index, record)}
> >
{t('common.edit')} {t('common.edit')}
</Button> </Button>

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 17:51:08 * @Date: 2026-02-03 17:51:08
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-25 10:41:25 * @Last Modified time: 2026-03-25 16:45:18
*/ */
/** /**
* User Management Page * User Management Page
@@ -63,7 +63,7 @@ const UserManagement: React.FC = () => {
}; };
/** Table column configuration */ /** Table column configuration */
const columns: ColumnsType = [ const columns: ColumnsType<User> = [
{ {
title: t('user.userId'), title: t('user.userId'),
dataIndex: 'id', dataIndex: 'id',
@@ -150,7 +150,7 @@ const UserManagement: React.FC = () => {
</Button> </Button>
</Flex> </Flex>
<Table <Table<User>
ref={tableRef} ref={tableRef}
apiUrl={getUserListUrl} apiUrl={getUserListUrl}
apiParams={{ apiParams={{
@@ -159,6 +159,7 @@ const UserManagement: React.FC = () => {
columns={columns} columns={columns}
rowKey="id" rowKey="id"
isScroll={true} isScroll={true}
scrollY="calc(100vh - 256px)"
/> />
<CreateModal <CreateModal

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 15:40:13 * @Date: 2026-02-03 15:40:13
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-25 16:03:59 * @Last Modified time: 2026-03-25 16:54:44
*/ */
import { type FC } from 'react' import { type FC } from 'react'
import clsx from 'clsx'; import clsx from 'clsx';
@@ -40,6 +40,7 @@ const VariableSelect: FC<VariableSelectProps> = ({
onChange, onChange,
size = 'middle', size = 'middle',
filterBooleanType = false, filterBooleanType = false,
mode,
...resetPorps ...resetPorps
}) => { }) => {
@@ -63,9 +64,10 @@ const VariableSelect: FC<VariableSelectProps> = ({
if (filterOption) { if (filterOption) {
return ( return (
<span <span
className={clsx("rb:max-w-full rb:wrap-break-word rb:line-clamp-1 rb-border rb:rounded-md rb:bg-white rb:text-[12px] rb:inline-flex rb:items-center rb:px-1.5 rb:cursor-pointer", { className={clsx("rb:max-w-full rb:wrap-break-word rb:line-clamp-1 rb:rounded-md rb:bg-white rb:text-[12px] rb:inline-flex rb:items-center rb:px-1.5 rb:cursor-pointer", {
'rb:leading-5.5!': size !== 'small', 'rb:leading-5.5!': size !== 'small',
'rb:leading-4! rb:text-[10px]!': size === 'small' 'rb:leading-4! rb:text-[10px]!': size === 'small',
'rb-border': mode !== "multiple"
})} })}
contentEditable={false} contentEditable={false}
> >
@@ -131,6 +133,7 @@ const VariableSelect: FC<VariableSelectProps> = ({
return ( return (
<Select <Select
{...resetPorps} {...resetPorps}
mode={mode}
size={size} size={size}
placeholder={placeholder} placeholder={placeholder}
value={value} value={value}

View File

@@ -15,6 +15,9 @@
.properties :global(.ant-select-multiple.ant-select-sm .ant-select-selector) { .properties :global(.ant-select-multiple.ant-select-sm .ant-select-selector) {
min-height: 28px; min-height: 28px;
} }
.properties :global(.ant-select-multiple.ant-select-sm .ant-select-selection-item) {
height: 18px;
}
.properties :global(:not(.select).ant-select-single.ant-select-sm.ant-select-borderless) { .properties :global(:not(.select).ant-select-single.ant-select-sm.ant-select-borderless) {
height: 22px; height: 22px;
} }