📱 feat(ui): Enhance mobile log table UX & fix StrictMode warning
Summary 1. CardTable • Added collapsible “Details / Collapse” section on mobile cards using Semi-UI Button + Collapsible with chevron icons. • Integrated i18n (`useTranslation`) for the toggle labels. • Restored original variable-width skeleton placeholders (50 % / 60 % / 70 % …) for more natural loading states. 2. UsageLogsColumnDefs • Wrapped each `Tag` inside a native `<span>` when used as Tooltip trigger, removing `findDOMNode` deprecation warnings in React StrictMode. Impact • Cleaner, shorter rows on small screens with optional expansion. • Fully translated UI controls. • No more console noise in development & CI caused by StrictMode warnings.
This commit is contained in:
@@ -18,7 +18,9 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { Table, Card, Skeleton, Pagination, Empty } from '@douyinfe/semi-ui';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Table, Card, Skeleton, Pagination, Empty, Button, Collapsible } from '@douyinfe/semi-ui';
|
||||||
|
import { IconChevronDown, IconChevronUp } from '@douyinfe/semi-icons';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
||||||
|
|
||||||
@@ -30,6 +32,7 @@ import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
|||||||
*/
|
*/
|
||||||
const CardTable = ({ columns = [], dataSource = [], loading = false, rowKey = 'key', ...tableProps }) => {
|
const CardTable = ({ columns = [], dataSource = [], loading = false, rowKey = 'key', ...tableProps }) => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
// Skeleton 显示控制,确保至少展示 500ms 动效
|
// Skeleton 显示控制,确保至少展示 500ms 动效
|
||||||
const [showSkeleton, setShowSkeleton] = useState(loading);
|
const [showSkeleton, setShowSkeleton] = useState(loading);
|
||||||
@@ -94,7 +97,14 @@ const CardTable = ({ columns = [], dataSource = [], loading = false, rowKey = 'k
|
|||||||
return (
|
return (
|
||||||
<div key={idx} className="flex justify-between items-center py-1 border-b last:border-b-0 border-dashed" style={{ borderColor: 'var(--semi-color-border)' }}>
|
<div key={idx} className="flex justify-between items-center py-1 border-b last:border-b-0 border-dashed" style={{ borderColor: 'var(--semi-color-border)' }}>
|
||||||
<Skeleton.Title active style={{ width: 80, height: 14 }} />
|
<Skeleton.Title active style={{ width: 80, height: 14 }} />
|
||||||
<Skeleton.Title active style={{ width: `${50 + (idx % 3) * 10}%`, maxWidth: 180, height: 14 }} />
|
<Skeleton.Title
|
||||||
|
active
|
||||||
|
style={{
|
||||||
|
width: `${50 + (idx % 3) * 10}%`,
|
||||||
|
maxWidth: 180,
|
||||||
|
height: 14,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -118,6 +128,78 @@ const CardTable = ({ columns = [], dataSource = [], loading = false, rowKey = 'k
|
|||||||
// 渲染移动端卡片
|
// 渲染移动端卡片
|
||||||
const isEmpty = !showSkeleton && (!dataSource || dataSource.length === 0);
|
const isEmpty = !showSkeleton && (!dataSource || dataSource.length === 0);
|
||||||
|
|
||||||
|
// 移动端行卡片组件(含可折叠详情)
|
||||||
|
const MobileRowCard = ({ record, index }) => {
|
||||||
|
const [showDetails, setShowDetails] = useState(false);
|
||||||
|
const rowKeyVal = getRowKey(record, index);
|
||||||
|
|
||||||
|
const hasDetails =
|
||||||
|
tableProps.expandedRowRender &&
|
||||||
|
(!tableProps.rowExpandable || tableProps.rowExpandable(record));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card key={rowKeyVal} className="!rounded-2xl shadow-sm">
|
||||||
|
{columns.map((col, colIdx) => {
|
||||||
|
// 忽略隐藏列
|
||||||
|
if (tableProps?.visibleColumns && !tableProps.visibleColumns[col.key]) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = col.title;
|
||||||
|
const cellContent = col.render
|
||||||
|
? col.render(record[col.dataIndex], record, index)
|
||||||
|
: record[col.dataIndex];
|
||||||
|
|
||||||
|
// 空标题列(通常为操作按钮)单独渲染
|
||||||
|
if (!title) {
|
||||||
|
return (
|
||||||
|
<div key={col.key || colIdx} className="mt-2 flex justify-end">
|
||||||
|
{cellContent}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={col.key || colIdx}
|
||||||
|
className="flex justify-between items-start py-1 border-b last:border-b-0 border-dashed"
|
||||||
|
style={{ borderColor: 'var(--semi-color-border)' }}
|
||||||
|
>
|
||||||
|
<span className="font-medium text-gray-600 mr-2 whitespace-nowrap select-none">
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
<div className="flex-1 break-all flex justify-end items-center gap-1">
|
||||||
|
{cellContent !== undefined && cellContent !== null ? cellContent : '-'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{hasDetails && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
theme='borderless'
|
||||||
|
size='small'
|
||||||
|
className='w-full flex justify-center mt-2'
|
||||||
|
icon={showDetails ? <IconChevronUp /> : <IconChevronDown />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setShowDetails(!showDetails);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{showDetails ? t('收起') : t('详情')}
|
||||||
|
</Button>
|
||||||
|
<Collapsible isOpen={showDetails} keepDOM>
|
||||||
|
<div className="pt-2">
|
||||||
|
{tableProps.expandedRowRender(record, index)}
|
||||||
|
</div>
|
||||||
|
</Collapsible>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
if (isEmpty) {
|
if (isEmpty) {
|
||||||
// 若传入 empty 属性则使用之,否则使用默认 Empty
|
// 若传入 empty 属性则使用之,否则使用默认 Empty
|
||||||
if (tableProps.empty) return tableProps.empty;
|
if (tableProps.empty) return tableProps.empty;
|
||||||
@@ -130,52 +212,9 @@ const CardTable = ({ columns = [], dataSource = [], loading = false, rowKey = 'k
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{dataSource.map((record, index) => {
|
{dataSource.map((record, index) => (
|
||||||
const rowKeyVal = getRowKey(record, index);
|
<MobileRowCard key={getRowKey(record, index)} record={record} index={index} />
|
||||||
return (
|
))}
|
||||||
<Card key={rowKeyVal} className="!rounded-2xl shadow-sm">
|
|
||||||
{columns.map((col, colIdx) => {
|
|
||||||
// 忽略隐藏列
|
|
||||||
if (tableProps?.visibleColumns && !tableProps.visibleColumns[col.key]) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const title = col.title;
|
|
||||||
// 计算单元格内容
|
|
||||||
const cellContent = col.render
|
|
||||||
? col.render(record[col.dataIndex], record, index)
|
|
||||||
: record[col.dataIndex];
|
|
||||||
|
|
||||||
// 空标题列(通常为操作按钮)单独渲染
|
|
||||||
if (!title) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={col.key || colIdx}
|
|
||||||
className="mt-2 flex justify-end"
|
|
||||||
>
|
|
||||||
{cellContent}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={col.key || colIdx}
|
|
||||||
className="flex justify-between items-start py-1 border-b last:border-b-0 border-dashed"
|
|
||||||
style={{ borderColor: 'var(--semi-color-border)' }}
|
|
||||||
>
|
|
||||||
<span className="font-medium text-gray-600 mr-2 whitespace-nowrap select-none">
|
|
||||||
{title}
|
|
||||||
</span>
|
|
||||||
<div className="flex-1 break-all flex justify-end items-center gap-1">
|
|
||||||
{cellContent !== undefined && cellContent !== null ? cellContent : '-'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{/* 分页组件 */}
|
{/* 分页组件 */}
|
||||||
{tableProps.pagination && dataSource.length > 0 && (
|
{tableProps.pagination && dataSource.length > 0 && (
|
||||||
<div className="mt-2 flex justify-center">
|
<div className="mt-2 flex justify-center">
|
||||||
|
|||||||
@@ -268,12 +268,14 @@ export const getLogsColumns = ({
|
|||||||
return isAdminUser && (record.type === 0 || record.type === 2 || record.type === 5) ? (
|
return isAdminUser && (record.type === 0 || record.type === 2 || record.type === 5) ? (
|
||||||
<Space>
|
<Space>
|
||||||
<Tooltip content={record.channel_name || t('未知渠道')}>
|
<Tooltip content={record.channel_name || t('未知渠道')}>
|
||||||
<Tag
|
<span>
|
||||||
color={colors[parseInt(text) % colors.length]}
|
<Tag
|
||||||
shape='circle'
|
color={colors[parseInt(text) % colors.length]}
|
||||||
>
|
shape='circle'
|
||||||
{text}
|
>
|
||||||
</Tag>
|
{text}
|
||||||
|
</Tag>
|
||||||
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{isMultiKey && (
|
{isMultiKey && (
|
||||||
<Tag color='white' shape='circle'>
|
<Tag color='white' shape='circle'>
|
||||||
@@ -466,15 +468,17 @@ export const getLogsColumns = ({
|
|||||||
render: (text, record, index) => {
|
render: (text, record, index) => {
|
||||||
return (record.type === 2 || record.type === 5) && text ? (
|
return (record.type === 2 || record.type === 5) && text ? (
|
||||||
<Tooltip content={text}>
|
<Tooltip content={text}>
|
||||||
<Tag
|
<span>
|
||||||
color='orange'
|
<Tag
|
||||||
shape='circle'
|
color='orange'
|
||||||
onClick={(event) => {
|
shape='circle'
|
||||||
copyText(event, text);
|
onClick={(event) => {
|
||||||
}}
|
copyText(event, text);
|
||||||
>
|
}}
|
||||||
{text}
|
>
|
||||||
</Tag>
|
{text}
|
||||||
|
</Tag>
|
||||||
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
|
|||||||
Reference in New Issue
Block a user