用 Tailwind CSS 重构所有共享组件

This commit is contained in:
雨霖铃
2026-05-22 00:31:52 +08:00
parent 37618eb71f
commit 427e668e7c
20 changed files with 561 additions and 1094 deletions
+71 -154
View File
@@ -30,19 +30,7 @@
*/
import { useRef, useState, useCallback, forwardRef, RefObject } from 'react';
import {
Box,
Button,
IconButton,
TextField,
Tooltip,
Typography,
alpha,
type SxProps,
} from '@mui/material';
import type { Theme } from '@mui/material/styles';
import CloseIcon from '@mui/icons-material/Close';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import { X, Copy } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import type { SnackbarOptions } from '@/components/GlobalSnackbar';
@@ -99,7 +87,7 @@ export interface TextInputAreaProps {
/** 外层容器样式 */
style?: React.CSSProperties;
/** 外层容器 sx */
sx?: SxProps<Theme>;
sx?: React.CSSProperties;
/** 是否显示字符计数 */
showCount?: boolean;
@@ -155,7 +143,7 @@ function ActionButton({
action,
value,
globalDisabled,
variant = 'text',
variant: _variant = 'text',
onAction,
size = 'small',
compact,
@@ -163,42 +151,31 @@ function ActionButton({
const isBtnDisabled =
typeof action.disabled === 'function' ? action.disabled(value) : action.disabled || !value;
const typeStyles: Record<string, unknown> = {};
const typeClasses: Record<string, string> = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
danger: 'text-red-600 hover:bg-red-50',
default: 'text-gray-500 hover:bg-gray-50',
};
if (action.type === 'primary') {
if (variant !== 'contained') {
typeStyles.bgcolor = 'primary.main';
typeStyles.color = 'primary.contrastText';
typeStyles['&:hover'] = { bgcolor: 'primary.dark' };
}
} else if (action.type === 'danger') {
typeStyles.color = 'error.main';
typeStyles['&:hover'] = {
bgcolor: (theme: Theme) => alpha(theme.palette.error.main, 0.08),
};
} else {
typeStyles.color = 'text.secondary';
typeStyles['&:hover'] = {
bgcolor: (theme: Theme) => alpha(theme.palette.grey[500], 0.1),
};
}
const sizeClasses = {
small: 'text-xs px-2 py-1',
medium: 'text-sm px-3 py-1.5',
};
return (
<Button
<button
type="button"
onClick={() => onAction(action)}
disabled={isBtnDisabled || globalDisabled}
size={size}
variant={variant}
startIcon={action.icon}
sx={{
fontWeight: 600,
borderRadius: 2,
...typeStyles,
...(compact ? { fontSize: '0.75rem', px: 1.5, minWidth: 0 } : { fontSize: '0.8rem' }),
}}
className={`rounded-md font-semibold transition-colors ${sizeClasses[size]} ${
typeClasses[action.type || 'default']
} ${compact ? 'text-xs px-2 min-w-0' : 'text-sm'} ${
isBtnDisabled || globalDisabled ? 'opacity-50 cursor-not-allowed' : ''
}`}
>
{action.icon && <span className="mr-1">{action.icon}</span>}
{action.label}
</Button>
</button>
);
}
@@ -352,27 +329,15 @@ const TextInputArea = forwardRef<HTMLTextAreaElement, TextInputAreaProps>((props
const hasTopBar = title || showCount || topActions.length > 0 || topExtra;
return (
<Box className={className} style={style} sx={containerSx}>
<div className={className} style={{ ...style, ...containerSx }}>
{hasTopBar && (
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
mb: 1,
px: 0.5,
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
{title && (
<Typography variant="body2" sx={{ fontWeight: 600, color: 'text.secondary' }}>
{title}
</Typography>
)}
<div className="flex items-center justify-between mb-2 px-1">
<div className="flex items-center gap-3">
{title && <span className="text-sm font-semibold text-gray-500">{title}</span>}
{topExtra}
</Box>
</div>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<div className="flex items-center gap-1">
{topActions.map((action) => (
<ActionButton
key={action.key}
@@ -384,80 +349,44 @@ const TextInputArea = forwardRef<HTMLTextAreaElement, TextInputAreaProps>((props
/>
))}
{showCount && (
<Typography
variant="caption"
sx={{ color: 'text.disabled', fontVariantNumeric: 'tabular-nums', ml: 0.5 }}
>
<span className="text-xs text-gray-400 tabular-nums ml-1">
{value.length}
{maxLength ? ` / ${maxLength}` : ''}
</Typography>
</span>
)}
</Box>
</Box>
</div>
</div>
)}
<Box sx={{ position: 'relative' }}>
<TextField
inputRef={handleInputRef}
multiline
fullWidth
minRows={autoResize ? minRows : undefined}
maxRows={autoResize ? maxRows : undefined}
rows={autoResize ? undefined : minRows}
<div className="relative">
<textarea
ref={handleInputRef}
placeholder={placeholder}
value={value}
onChange={handleChange}
onBlur={handleBlur}
disabled={disabled}
autoFocus={autoFocus}
error={Boolean(displayError)}
helperText={displayError || undefined}
slotProps={{
input: { readOnly },
formHelperText: {
sx: { mx: 1.5, fontWeight: 600, '&.Mui-error': { color: 'error.main' } },
},
}}
sx={{
'& .MuiOutlinedInput-root': {
bgcolor: 'background.paper',
borderRadius: 3,
fontSize: '0.875rem',
fontFamily: 'monospace',
lineHeight: 1.6,
transition: 'all 0.2s',
'&:hover': { bgcolor: 'action.hover' },
'&.Mui-focused': {
bgcolor: 'background.paper',
boxShadow: (theme) => `${alpha(theme.palette.primary.main, 0.08)} 0 0 0 3px`,
},
'&.Mui-error': {
boxShadow: (theme) => `${alpha(theme.palette.error.main, 0.08)} 0 0 0 3px`,
},
'& textarea': {
py: 1.5,
px: 1.5,
...(showClear || allowCopy || bottomActions.length > 0 ? { pb: 4 } : {}),
},
},
'& .MuiFormHelperText-root': {
mx: 0,
mt: 0.5,
},
readOnly={readOnly}
rows={autoResize ? undefined : minRows}
style={{
minHeight: autoResize ? `${minRows * 1.5}rem` : undefined,
maxHeight: autoResize ? `${maxRows * 1.5}rem` : undefined,
}}
className={`w-full rounded-lg border ${
displayError ? 'border-red-300' : 'border-gray-200'
} bg-white px-3 py-3 font-mono text-sm leading-relaxed transition-all resize-y ${
showClear || allowCopy || bottomActions.length > 0 ? 'pb-10' : ''
} focus:outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white hover:bg-gray-50 ${
displayError ? 'focus:ring-red-500' : ''
}`}
/>
{(showClear || allowCopy || bottomActions.length > 0) && (
<Box
sx={{
position: 'absolute',
bottom: displayError ? 32 : 8,
right: 12,
display: 'flex',
alignItems: 'center',
gap: 0.5,
zIndex: 1,
}}
<div
className={`absolute right-3 flex items-center gap-1 z-10 ${
displayError ? 'bottom-8' : 'bottom-2'
}`}
>
{bottomActions.map((action) => (
<ActionButton
@@ -470,43 +399,31 @@ const TextInputArea = forwardRef<HTMLTextAreaElement, TextInputAreaProps>((props
/>
))}
{allowCopy && value && (
<Tooltip title={t('textInputArea.copyContent')}>
<IconButton
onClick={handleCopy}
size="small"
sx={{
color: 'text.disabled',
'&:hover': {
color: 'primary.main',
bgcolor: (theme) => alpha(theme.palette.primary.main, 0.08),
},
}}
>
<ContentCopyIcon sx={{ fontSize: 16 }} />
</IconButton>
</Tooltip>
<button
type="button"
onClick={handleCopy}
title={t('textInputArea.copyContent')}
className="p-1 rounded-md text-gray-400 hover:text-blue-600 hover:bg-blue-50 transition-colors"
>
<Copy className="h-4 w-4" />
</button>
)}
{showClear && value && !disabled && !readOnly && (
<Tooltip title={t('textInputArea.clear')}>
<IconButton
onClick={handleClear}
size="small"
sx={{
color: 'text.disabled',
'&:hover': {
color: 'error.main',
bgcolor: (theme) => alpha(theme.palette.error.main, 0.08),
},
}}
>
<CloseIcon sx={{ fontSize: 16 }} />
</IconButton>
</Tooltip>
<button
type="button"
onClick={handleClear}
title={t('textInputArea.clear')}
className="p-1 rounded-md text-gray-400 hover:text-red-600 hover:bg-red-50 transition-colors"
>
<X className="h-4 w-4" />
</button>
)}
</Box>
</div>
)}
</Box>
</Box>
{displayError && <p className="mt-1 text-xs font-semibold text-red-500">{displayError}</p>}
</div>
</div>
);
});