基于 Mui 封装常用 React 组件

Mui(MaterialUI)

Mui 是基于googleMateria设计风格开发的基于React框架的 UI 框架,之前的名称为MaterialUI

主题

Mui 提供了非常强大的主题解决方案,使用Mui提供的createTheme方法可以自定义配置生成主题:

import { createTheme } from "@mui/material";const theme = createTheme({palette: {mode: "light",primary: {main: primary,},success: {main: "#4CAF50",},error: {main: "#F44336",},background: {// paper: "#f1f1f1",},},shape: { borderRadius: 4 },custom: {contentPadding: 16,commonBg: "#fff",background: "#F5F9FF",headerAvatarColor: "red",navItemHoverBg: `linear-gradient(89deg, #ffffff61, ${primary});opacity: 1`,tableBgColor: "red",templateConfigCardPadding: "12px 32px",borderColor: "rgba(0, 0, 0, 0.12)",},typography: {fontSize: (baseFontSize / 16) * 14,fontFamily: "HarmonyOS_Sans_SC_Regular",h4: {fontSize: 16,color: "rgba(0, 0, 0, 0.6)",},subtitle1: {color: "rgba(0, 0, 0, 0.6)",},subtitle2: {color: "rgba(0, 0, 0, 0.6)",},},
});

生成的主题数据只需要传给Mui暴露的组件ThemeProvider即可,之后,ThemeProvider组建的所有后代的 Mui 组件都会使用到该主题:

<ThemeProvider theme={theme}><Routes />
</ThemeProvider>

动态切换主题

动态切换主题只需要生成主题数据传递给ThemeProvider组件即可。

Mui 样式处理 Api

Mui 提供了一些自定义样式的 Api,下面记录一些常用的

styled

styled是 Mui 暴露的一个 api,我们可以使用该 api 对 Mui 甚至 React 其他组件进行样式改造,比如对 Mui 的 TableRow 组件进行自定义样式:

import { lighten, styled, TableRow } from "@mui/material";const StyledRow = styled(TableRow)(({ theme }) => {return {backgroundColor: "#000","& .sticky-cell": {position: "sticky",backgroundColor: "#fff",},"&.MuiTableRow-root:hover": {backgroundColor: "#fbfbfb","& .sticky-cell": {backgroundColor: "#fbfbfb",},},"&.Mui-selected": {backgroundColor: lighten(theme.palette.primary.main, 0.95),"& .sticky-cell": {backgroundColor: lighten(theme.palette.primary.main, 0.95),},},"&.Mui-selected:hover": {backgroundColor: lighten(theme.palette.primary.main, 0.9),"& .sticky-cell": {backgroundColor: lighten(theme.palette.primary.main, 0.9),},},};
});export default StyledRow;

这段代码中有几种写法:

  • 直接写 css 样式:这种写法写的样式会直接加载到 TableRow 组件的根元素上。
  • “&selector”:使用&符号加 css 选择器,但是两者之间不带空格,这种写法是根元素如果能匹配这个选择器,那么这个选择器对应的样式会生效。
  • “& selector”:使用&符号加 css 选择器,并且两者之间用空格分割,代表 TableRow 根元素的后代元素如果能匹配这个选择器,那么这个选择器对应的样式会生效。
  • “&:hover” | “&selector:hover” | “& selector:hover”:分别代表 TableRow 的根元素的 hover 伪类、根元素如果匹配 selector 的 hover 伪类、根元素的后代元素如果匹配 selector 的 hover 伪类,其他伪类(active 等等)同理。

makeStyles

Mui 暴露了 makeStyles 和 useStyle 这两个方法,这两个方法配合使用可以生成样式以及对应的类名,我们生成后直接使用类名,就可以绑定对应的样式。这样写比起单独写样式文件的好处是代码可维护性更强,并且可以更加方便使用全局配置的主题:比如

import { Avatar, Box, Grid, Theme, Typography } from "@mui/material";
import { makeStyles } from "@mui/styles";const useStyle = makeStyles((theme: Theme) => {return {avatarContainer: {width: 96,height: 96,cursor: "pointer",position: "relative",},avatar: { width: 96, height: 96 },avatarBorder: {height: "100%",width: "100%",position: "absolute",borderRadius: "50%",top: 0,left: 0,border: "3px solid " + theme.palette.success.main,boxSizing: "border-box",},};
});export default function AvatarSelectDialog() {const classes = useStyle();return (<MatDialog><Box className="flex-box"><Box className={classes.avatarContainer}><Avatar className={classes.avatar}></Avatar><p className={classes.avatarBorder}></p></Box></Box></MatDialog>);
}

这里,Box 组件使用了 classes.avatarContainer 类名,就会应用 avatarContainer 对应的样式(实际上 Mui 编译的时候会生成一个随机类名分给 Box 组件的 class 属性,并且会生成对应的 css 样式然后会在加载该组件的时候加载该样式,因此该组件运行的时候就会使用该样式),p 组件使用了 avatarBorder 类名,该类名对应的样式里面使用了主题配置theme.palette.success.main,这就是 makeStyles 使用全局主题属性值的例子。

sx 属性

个人认为 sx 属性是 Mui 非常优秀的设计,所有的 Mui 组件均可以使用 sx 属性,该属性用来定义 Mui 组件的样式,并且该属性的值中的 css 属性有很多种简写形式,非常好用, 比如:

// padding、margin等值都和主题中的间距(spacing)有关,比如{p: 2}代表padding的值为2*theme.spacing
// 下面以spacing默认为4处理{*/ padding: 1 * 4 = 4px /*}
<Box sx={{p: 1}}></Box>
{*/ padding-left: 1 * 4 = 4px, padding-right: 1 * 4 = 4px /*}
<Box sx={{px: 1}}></Box>
{*/ padding-left: 1 * 4 = 4px /*}
<Box sx={{pl: 1}}></Box>
{*/ padding-top: 1 * 4 = 4px, padding-bottom: 1 * 4 = 4px /*}
<Box sx={{py: 1}}></Box>
{*/ padding-right: 1 * 4 = 4px, background-color: 主题色 /*}
<Box sx={{pr: 1, bgcolor: theme => theme.palette.primary.main}}></Box>

封装组件

上面大概介绍了 Mui 组件样式设置的方法,下面是一些常用的组件的封装

输入框(MatInput)
import { FormControl, InputBaseProps, SxProps, TextField } from "@mui/material";
import { memo } from "react";
import { useTranslation } from "react-i18next";
import { MatFormItemProps } from "../../../models/base.model";
import { isNull } from "../../../utils";interface MatInputProps extends MatFormItemProps {multiline?: boolean;maxRows?: number;inputProps?: InputBaseProps["inputProps"];sx?: SxProps;fullWidth?: boolean;
}export default memo(function MatInput(props: MatInputProps) {const { width, size = "small", fullWidth = true } = props;const { t } = useTranslation();const value = isNull(props.value) ? "" : props.value;return (<FormControlerror={props.error}sx={{ maxWidth: width || 1 / 1 }}fullWidth={fullWidth}><TextField{...props}size={size}label={props.label && t(props.label)}value={value}></TextField></FormControl>);
});
多选框(checkbox)
import { FormControlLabel, FormControl } from "@mui/material";
import Checkbox from "@mui/material/Checkbox";
import { MatFormItemProps } from "../../../models/base.model";
import { t } from "i18next";export interface MatCheckboxProps extends MatFormItemProps<boolean> {}export default function MatCheckbox(props: MatCheckboxProps) {return (<FormControlsx={{height: 1 / 1,display: "flex",justifyContent: "center",...(props.sx || {}),}}><FormControlLabelcontrol={<Checkboxdisabled={props.disabled}onBlur={props.onBlur}name={props.name}onChange={props.onChange}checked={props.value || false}/>}label={t(props.label) as string}/></FormControl>);
}
纸片(chip)
import { alpha, Chip, styled } from "@mui/material";const MatChip = styled(Chip)(({ theme }) => {return {backgroundColor: alpha(theme.palette.primary.main, 0.08),color: theme.palette.primary.main,fontSize: 14,paddingRight: 6,};
});export default MatChip;
确认框(confirm)
import * as React from "react";
import Button from "@mui/material/Button";
import Dialog from "@mui/material/Dialog";
import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent";
import DialogContentText from "@mui/material/DialogContentText";
import DialogTitle from "@mui/material/DialogTitle";
import { useDispatch, useSelector } from "react-redux";
import { selectConfirmConfig } from "../../../store/selectors";
import { closeConfirmAction } from "../../../store/actions/tools.action";
import { useTranslation } from "react-i18next";
import { LoadingButton } from "@mui/lab";
import { Box } from "@mui/material";
import Iconfont from "../tools/Iconfont";
import { makeStyles } from "@mui/styles";const useStyle = makeStyles(() => ({warningIcon: {paddingTop: "42px","& i": {height: "24px",lineHeight: "1",color: "orange",fontSize: "32px !important",marginLeft: "24px",},},
}));export default function MatConfirm() {const config = useSelector(selectConfirmConfig);const dispatch = useDispatch();const [loading, setLoading] = React.useState(false);const { t } = useTranslation();const classes = useStyle();const handleClose = () => {if (config.onCancel) {config.onCancel();}setLoading(false);dispatch(closeConfirmAction());};const handleOk = () => {if (config.onOk) {const re = config.onOk();if (re instanceof Promise) {setLoading(true);re.then(() => {handleClose();setLoading(false);}).catch(() => {setLoading(false);});} else {// setLoading(false)handleClose();}} else {handleClose();}};const okBtnVariant = config.okBtnColor === "error" ? "text" : "contained";return (<Box><Dialogopen={config.open || false}onClose={handleClose}aria-labelledby="alert-dialog-title"aria-describedby="alert-dialog-description"><Box sx={{ display: "flex" }}>{config.showWarningIcon && (<Box className={classes.warningIcon}><Iconfont mr={0} icon="ic_alert"></Iconfont></Box>)}<Box><DialogTitle id="alert-dialog-title">{t(config.title) as string}</DialogTitle><DialogContent sx={{ minWidth: 380 }}>{config.customContent ? (config.content) : (<DialogContentText id="alert-dialog-description">{t(config.content as string)}</DialogContentText>)}</DialogContent><DialogActions>{config.showCancelButton && (<Button onClick={handleClose}>{t(config.cancelText || "common.cancel")}</Button>)}<LoadingButtondisabled={config.okBtnDisabled}variant={okBtnVariant}loading={loading}onClick={handleOk}autoFocuscolor={config.okBtnColor || "primary"}>{t(config.okText || "common.confirm")}</LoadingButton></DialogActions></Box></Box></Dialog></Box>);
}// 使用 挂载在App组件 然后通过redux控制内容等等
/*** @param {ConfirmConfigData} config* @description 对话确认框 用showWarningIcon来控制是否展示警告图表*/
export function $confirm(config: ConfirmConfigData) {// 给定默认值const {showWarningIcon = true,showCancelButton = true,okText = "common.confirm",okBtnColor = "primary",customContent = false,okBtnDisabled = false,} = config;store.dispatch(openConfirmAction({...config,open: true,showWarningIcon,showCancelButton,okText,okBtnColor,customContent,okBtnDisabled,}));
}
/*** @param {ConfirmConfigData} config* @description 信息提示确认框*/
export function $info(config: ConfirmConfigData) {store.dispatch(openConfirmAction({...config,open: true,showCancelButton: false,okText: "common.confirm",}));
}
下拉菜单(dropdown)
import * as React, { ReactNode } from "react";
import MenuItem from "@mui/material/MenuItem";
import { Box, Divider, SxProps, Typography } from "@mui/material";
import MoreVertIcon from "@mui/icons-material/MoreVert";
// import { OperationMenu } from "../../../models/base.model";
import Iconfont from "../tools/Iconfont";
import { t } from "i18next";
import { StyledMenu } from "../styled/StyledMenu";
import TextButton from "./TextButton";// 操作按钮
export class OperationMenu<T = any> {public customStartComponent?: ReactNode;constructor(public action: T,public title: string,public icon?: any,public showDivider?: boolean,public disabled?: boolean) {}
}
/**** @interface MatDropdownProps* @template T menu的Action的类型*/
export interface MatDropdownProps<T> {menus: OperationMenu<T>[];selected?: T;onMenuClick?: (action: T) => void;// dropDown组件的触发组件  可以自定义 如果不自定义就是默认的TextButton(在tsx里面可以看到)trigger?: React.ReactNode;dividerKeys?: T[];sx?: SxProps;disabled?: boolean;
}/**** @export* @template T menu的Action的类型* @param {MatDropdownProps<T>} props* @returns*/
export default function MatDropdown<T>(props: React.PropsWithChildren<MatDropdownProps<T>>
) {const { menus } = props;const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);const open = Boolean(anchorEl);const handleClick = (event: React.MouseEvent<HTMLElement>) => {if (!props.disabled) {setAnchorEl(event.currentTarget);}};const handleClose = () => {setAnchorEl(null);};const onMenuClick = (e) => {if (props.onMenuClick) {props.onMenuClick(e);}handleClose();};return (<Box sx={{ pl: 2, ...props.sx }}>{props.trigger ? (<Box onClick={handleClick}>{props.trigger}</Box>) : (// 如果没有trigger这个props就用默认的TextButton<TextButtonclassName={props.disabled ? "btn-disabled" : ""}onClick={handleClick}sx={{ minWidth: "auto", borderRadius: 2 }}><MoreVertIconsx={{transform: "rotate(90deg)",color: (theme) => theme.palette.action.active,}}/></TextButton>)}{!props.disabled && (<StyledMenusx={{ maxHeight: 450 }}MenuListProps={{"aria-labelledby": "demo-customized-button",}}anchorEl={anchorEl}open={open}onClose={handleClose}>{props.children}{menus?.map((menu, index) => (<Box key={index}><MenuItemdisabled={menu.disabled}sx={{ p: 1 }}onClick={() => onMenuClick(menu.action)}selected={props.selected === menu.action}disableRipple>{menu.customStartComponent}{menu.icon && (<Iconfont fontSize={20} icon={menu.icon} mr={1}></Iconfont>)}<Typography sx={{ ml: 1 }}>{t(menu.title)}</Typography></MenuItem>{menu.showDivider && (<Divider sx={{ m: "0 !important" }}></Divider>)}</Box>))}</StyledMenu>)}</Box>);
}
密码框(password)
import { Visibility, VisibilityOff } from "@mui/icons-material";
import {FormHelperText,IconButton,InputAdornment,InputLabel,OutlinedInput,FormControl,
} from "@mui/material";
import { memo, PropsWithChildren, useState } from "react";
import { useTranslation } from "react-i18next";export default memo(function MatPassword(props: PropsWithChildren<any> = { onChange() {}, onBlur() {} }
) {const [showPassword, setShowPwd] = useState<boolean>(false);const { t } = useTranslation();const handleClickShowPassword = () => {setShowPwd(!showPassword);};const handleMouseDownPassword = (event: React.MouseEvent<HTMLButtonElement>) => {event.preventDefault();};return (<FormControlsize={props.size || "small"}sx={{ maxWidth: props.width || 1 / 1 }}fullWidth><p>{props.error}</p><InputLabel error={props.error}>{t(props.label)}</InputLabel><OutlinedInputdisabled={props.disabled}value={props.value}error={props.error}name={props.name}onChange={props.onChange}onBlur={props.onBlur}type={showPassword ? "text" : "password"}endAdornment={<InputAdornment position="end"><IconButtonsize="small"aria-label="toggle password visibility"onClick={handleClickShowPassword}onMouseDown={handleMouseDownPassword}edge="end">{showPassword ? <VisibilityOff /> : <Visibility />}</IconButton></InputAdornment>}label={props.label && t(props.label)}/><FormHelperText error={props.error}><span>{props.helperText}</span></FormHelperText></FormControl>);
});
进度条(progress)
import { Box, LinearProgress, Typography } from "@mui/material";
import { selectGreyColor } from "../../../utils/selectors";export interface MatProgressProps {progress: number;color?: "primary" | "success" | "warning";height?: number;label?: string;
}export default function MatProgress(props: MatProgressProps) {const value = (props.progress || 0).toFixed(2);const { height = 8, color = "primary", label } = props;const defaultLabel = value + "%";return (<Box className="flex-box-start"><Box sx={{ flex: 1 }}><LinearProgresscolor={color}sx={{ height, borderRadius: "5px", mr: 1 }}variant="determinate"value={props.progress}/></Box><Typography color={selectGreyColor}>{label || defaultLabel}</Typography></Box>);
}
表格组件(table)
import Box from "@mui/material/Box";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import Paper from "@mui/material/Paper";
import Checkbox from "@mui/material/Checkbox";
import { Pagination, Typography } from "@mui/material";
import { EnhancedTableHead, Order } from "./EnhancedTableHead";
import { BaseData, Id } from "../../../models/base.model";
import { TableCellProps } from "@mui/material";
import EmptyData from "./EmptyData";
import { ReactNode, useEffect, useMemo, useState } from "react";
import { commonBoxShadow, isNull, parseIdObject, timeFormat } from "../../../utils";
import TableLoading from "./TableLoading";
import { useCallback } from "react";
import AddDescription from "../tools/AddDescription";
import StyledRow from "../styled/StyledRow";
import { useObSelector } from "../../../hooks/useAuth";
import { SortOrder } from "../../../models/request.model";export interface HeadCell {id: string;label: string;
}export enum TableCellPadding {NORMAL,NONE,CHECKBOX,
}type StickyType = "left" | "right";
// 普通的tableColumns
export class TableColumns<T extends BaseData<string | Id> = BaseData> {public sortKey: string;constructor(public key: string,public title: string,public width?: number,public customCell?: (data: T, index?: number) => React.ReactNode | string,public keepWidth?: boolean,public sortAble?: boolean | string) {if (typeof sortAble === "string") {this.sortKey = sortAble;} else {this.sortKey = this.key;}}public padding = "normal";
}export class TableSortColumns<T extends BaseData<string | Id> = BaseData> extends TableColumns<T> {constructor(public key: string,public title: string,public sortAble?: boolean | string,public width?: number,public customCell?: (data: T, index?: number) => React.ReactNode | string,public keepWidth?: boolean) {super(key, title, width, customCell, keepWidth, sortAble);}
}
// 操作按钮的tableColumns
export class TableOperationColumns<T extends BaseData<string | Id> = BaseData> extends TableColumns<T> {constructor(public key: string,public title: string = "common.operation",public sticky?: StickyType,public width?: number,public customCell?: (data: T, index?: number) => React.ReactNode | string,public keepWidth?: boolean,public sortAble?: boolean) {super(key, title, width, customCell, keepWidth, sortAble);}public padding = "none";
}
// 解析时间的tableColumns
export class TableDateColumns<T extends BaseData<string | Id> = BaseData> extends TableColumns<T> {constructor(public key: string,public title: string,public width: number = 175,public customCell?: (data: T, index?: number) => React.ReactNode | string,public keepWidth?: boolean,public sortAble?: boolean) {super(key, title, width, customCell, keepWidth, sortAble);if (!customCell) {this.customCell = (data) => <Typography>{timeFormat(data[key]) || "-"}</Typography>;}}public padding = "normal";
}
// “描述”字段的tableColumns
export class TableDescriptionColumns<T extends BaseData<string | Id> = BaseData> extends TableColumns<T> {constructor(private onOk: (data: T) => Promise<any>,public width?: number,public customCell?: (data: T, index?: number) => React.ReactNode | string,public key: string = "description",public title: string = "common.description",public keepWidth?: boolean,public sortAble?: boolean) {super(key, title, width, customCell, keepWidth, sortAble);if (!customCell) {this.customCell = (data) => {const value = data[key];const onAddDescOk = (description: string) => {const newData: T = { ...data, description };// 这里直接修改data里面的description字段return this.onOk(newData);};return (<Box sx={{ pl: 2 }}><AddDescription value={value} onOk={onAddDescOk}></AddDescription></Box>);};}this.padding = "none";}
}export class TableStickyColumns<T extends BaseData<string | Id> = BaseData> extends TableColumns<T> {constructor(public sticky: StickyType,public key: string,public title: string,public width?: number,public customCell?: (data: T, index?: number) => React.ReactNode | string,public keepWidth?: boolean,public sortAble?: boolean) {super(key, title, width, customCell, keepWidth, sortAble);}
}export function getColumnStickyStatus<T extends BaseData<Id | string>>(column: TableColumns<T>, showSelect: boolean) {if (column instanceof TableStickyColumns || column instanceof TableOperationColumns) {if (column.sticky === "left") {if (showSelect) {return "sticky-cell sticky-left-with-select";} else {return "sticky-cell sticky-left";}} else if (column.sticky === "right") {return "sticky-cell sticky-right";}return column.sticky;} else {return null;}
}// 这里的泛型需要约束为BaseData类型  为了兼容性BaseData类型的泛型参数可能是Id或者string  因为整个系统中的
export interface CommonTableProps<T extends BaseData<string | Id> = BaseData> extends React.PropsWithChildren<any> {rows: T[];totalPages?: number;columns: TableColumns<T>[];orderAble?: boolean;sortProperty?: string;showSelect?: boolean;selected?: T[];width?: number;height?: number | string;minHeight?: number | string;pageSize?: number;hideBoxShadow?: boolean;pageChange?: (page: number) => void | Promise<any>;onSelectChange?(selected: T[]): void;onSortChange?(order: SortOrder, property: string): void;// 选择框是否可选的规则selectDisableRule?(data: T): boolean;children?: ReactNode;footerPy?: number;pagination?: boolean;loading?: boolean;page?: number;clearSelectOnPageChange?: boolean;
}/***** @export* @template T* @param {CommonTableProps<T>} props* @description table的分页有两种模式 一种是父级组件控制,通常是服务端分页的情况,另一种是本组件自己维护分页,通过是否传入pageChange方法这个prop来确定* @returns*/
export default function CommonTable<T extends BaseData<string | Id> = BaseData>(props: CommonTableProps<T>) {let { rows = [], orderAble, totalPages, columns, children, width, height, pageSize = 10, pagination = true, pageChange, onSortChange, clearSelectOnPageChange, selectDisableRule } = props;// 如果没有传入totalPages就自己计算页数if (totalPages === undefined) {totalPages = rows.length === 0 ? 0 : Math.ceil(rows.length / pageSize);}const [order, setOrder] = useState<Order>("desc");// const [orderBy, setOrderBy] = useState<string>(columns ? columns.find((v) => v.sortAble)?.key : null);const [orderBy, setOrderBy] = useState<string>(props.sortProperty || null);const [selected, setSelected] = useState<T[]>(props.selected || []);const [page, setPage] = useState(props.page || 0);const isOb = useObSelector();const computedShowSelect = useMemo(() => {return props.showSelect && !isOb;}, [isOb, props.showSelect]);// 监听页码变化const onPageChange = useCallback((event: any, page: number) => {if (pageChange) {// 如果是有自定义pageChange事件则代表数据是来自分页查询数据 因此在pageChange的时候需要重置selectedif (props.page !== page - 1) {pageChange(page - 1);if (clearSelectOnPageChange) {setSelected([]);}}} else {setPage(page - 1);}},[pageChange, props.page, clearSelectOnPageChange]);// 如果有传入pageChange  rows就接直接使用  否则需要利用page进行分页const displayedRows = useMemo(() => {if (pageChange) {return rows;} else {return rows.slice(page * pageSize, page * pageSize + pageSize);}}, [page, pageSize, rows, pageChange]);useEffect(() => {if (displayedRows.length <= 0 && rows.length) {onPageChange(null, 1);}}, [displayedRows, onPageChange, rows]);// 随时注意同步props传过来的selected数组useEffect(() => {if (props.selected) {setSelected(props.selected);}}, [props.selected]);// 排序const handleRequestSort = (event: React.MouseEvent<unknown>, property: string) => {const isDesc = orderBy === property && order === "desc";const sortOrder = isDesc ? "asc" : "desc";setOrder(sortOrder);setOrderBy(property);console.log(property, isDesc);onSortChange && onSortChange(sortOrder, property);};const isSelected = (data: T) => selected.some((v) => parseIdObject(v) === parseIdObject(data));// 被选中的属于当前页的const selectedOfThisPage = useMemo(() => {const selectedIds = selected.map((v) => parseIdObject(v));return displayedRows.filter((v) => selectedIds.includes(parseIdObject(v)));}, [selected, displayedRows]);//const selectedIdsOfThisPage = useMemo<string[]>(() => selectedOfThisPage.map((v) => parseIdObject(v)), [selectedOfThisPage]);// 点击全选CheckBox的时候的操作const handleSelectAllClick = (event: React.ChangeEvent<HTMLInputElement>) => {if (event.target.checked) {const newSelecteds = displayedRows.map((n: T) => n).filter((sn) => !selectDisableRule || !selectDisableRule(sn));setSelected(Array.from(new Set([...newSelecteds, ...selected])));return;}setSelected(selected.filter((item) => !selectedIdsOfThisPage.includes(parseIdObject(item))));};// 如果props里面有pageChange参数 就由父组件自己控制分页  否则由该组件自己控制const currentPage = pageChange ? props.page || 0 : page;const handleSelect = (event: React.ChangeEvent<HTMLInputElement>, data: T) => {if (event.target.checked) {setSelected([data, ...selected]);} else {setSelected(selected.filter((v) => parseIdObject(v) !== parseIdObject(data)));}};// 监听selected的变化useEffect(() => {if (props.onSelectChange) {props.onSelectChange(selected);}// eslint-disable-next-line}, [selected]);// Avoid a layout jump when reaching the last page with empty rows.// const emptyRows = page > 0 ? Math.max(0, (1 + page) * pageSize - rows.length) : 0;const maxTableHeight = useMemo(() => {if (height) {return height;} else if (pagination) {return "calc(100% - 68px)";} else {return "100%";}}, [height, pagination]);const ableToSelectRows = useMemo(() => {return displayedRows.filter((item) => !selectDisableRule || !selectDisableRule(item));}, [displayedRows, selectDisableRule]);return (<Box sx={{ width: "100%", height: 1 / 1 }}><Papersx={{width: "100%",height: 1 / 1,boxShadow: props.hideBoxShadow ? "none !important" : commonBoxShadow,position: "relative",// overflow: "hidden",boxSizing: "border-box",minHeight: props.minHeight,}}><TableContainer sx={{ maxHeight: maxTableHeight, height: 1 / 1, minHeight: props.minHeight }}><Table stickyHeader={true} sx={{ minWidth: width }} aria-labelledby="tableTitle" size="medium">{rows.length >= 0 && (<EnhancedTableHead<T>numSelected={selectedOfThisPage.length}order={order}orderBy={orderBy}onSelectAllClick={handleSelectAllClick}onRequestSort={handleRequestSort}rowCount={ableToSelectRows.length}headerCells={columns}orderAble={orderAble}showSelect={computedShowSelect}/>)}<TableBody>{displayedRows?.map((row, index) => {const isItemSelected = isSelected(row);return (<StyledRowhover// onClick={(event) => handleSelect(event, row)}role="checkbox"aria-checked={isItemSelected}tabIndex={-1}key={parseIdObject(row)}selected={isItemSelected}sx={{ pb: 2 }}>{computedShowSelect ? (<TableCell className="sticky-cell" sx={{ border: "none" }} onClick={() => {}} padding="checkbox"><Checkbox disabled={selectDisableRule && selectDisableRule(row)} onChange={(event) => handleSelect(event, row)} color="primary" checked={isItemSelected} /></TableCell>) : null}{columns?.map((column) => (<TableCellclassName={getColumnStickyStatus<T>(column, computedShowSelect)}sx={{ border: "none" }}width={column.width}padding={column.padding as TableCellProps["padding"]}key={column.key}><Box sx={{ width: column.width, maxWidth: column.width }}>{!isNull(column.customCell) ? column.customCell(row, index) || "-" : <Typography className="word-ellipsis">{isNull(row[column.key]) ? "-" : row[column.key]}</Typography>}</Box></TableCell>))}</StyledRow>);})}</TableBody></Table>{rows.length <= 0 && !props.loading && (children || <EmptyData pt={4} />)}</TableContainer>{pagination && (<Box sx={{ p: 1.5, py: props.footerPy || 2, opacity: rows.length ? 1 : 0, pt: props.footerPy || 2.5 }}><Pagination sx={{ display: "flex", justifyContent: "flex-end" }} count={totalPages} color="primary" onChange={onPageChange} page={currentPage + 1} /></Box>)}{props.loading && <TableLoading></TableLoading>}</Paper></Box>);
}
单选组(radio-group)
import Radio from "@mui/material/Radio";
import RadioGroup from "@mui/material/RadioGroup";
import FormControlLabel from "@mui/material/FormControlLabel";
import FormControl from "@mui/material/FormControl";
import { Box } from "@mui/system";
import { MatFormItemProps } from "../../../models/base.model";
import { useTranslation } from "react-i18next";
import { FormLabel } from "@mui/material";
import MessageTip from "../../regist-template/options/MessageTip";export interface MatRadioOption<T> {label: string;value: T;tip?: string;
}export interface MatRadioProps<T> extends MatFormItemProps<T> {options: MatRadioOption<T>[];row?: boolean;labelwidth?: number;optionlabelwidth?: number;
}export default function MatRadioGroup<T>(props: MatRadioProps<T>) {const { t } = useTranslation();const { row = true } = props;return (<FormControlsx={{display: "flex",flexDirection: "row",alignItems: row ? "center" : "",flexWrap: "wrap",}}><Box sx={{ width: props.labelwidth || 160 }}><FormLabel sx={{ lineHeight: "36px" }}>{t(props.label)}</FormLabel></Box><RadioGroup {...props} value={props.value} row={row} name={props.name}>{props.options.map((option, index) => (<Box key={index} className="flex-box-start flex-wrap"><FormControlLabelsx={{ width: props.optionlabelwidth || 250 }}value={option.value}control={<Radio disabled={props.disabled} />}label={t(option.label) as string}/>{option.tip && <MessageTip content={option.tip}></MessageTip>}</Box>))}</RadioGroup></FormControl>);
}
搜索框(search-input)
import {alpha,Icon,InputBaseProps,styled,TextField,SxProps,
} from "@mui/material";
import { t } from "i18next";
import debounce from "debounce";
import { ChangeEvent, Fragment, memo, useMemo, useState } from "react";
import Iconfont from "../tools/Iconfont";const SytledInput = styled(TextField)(({ theme }) => {return {"& .MuiOutlinedInput-root": {paddingRight: 4,},backgroundColor: alpha(theme.palette.primary.main, 0.04),"&:hover fieldset": {borderColor: theme.palette.primary.main + " !important",},};
});interface MatSearchInputProps {value: string;onChange(text: string): void;placeholder?: string;width?: number;fullWidth?: boolean;inputProps?: InputBaseProps["inputProps"];sx?: SxProps;
}export default memo(function MatSearchInput(props: MatSearchInputProps) {const [value, setValue] = useState(props.value || "");const onChange = (event: ChangeEvent<HTMLInputElement>) => {setValue(event.target.value);emitChangeByProps(event);};// 这里用debounce做防抖  做性能优化const emitChangeByProps = useMemo(() => {return debounce(function (event: ChangeEvent<HTMLInputElement>) {if (props.onChange) props.onChange(event.target.value);}, 300);}, [props]);const clearContent = () => {setValue("");props.onChange("");};return (<Fragment><SytledInput{...props}style={{ width: props.fullWidth ? "100%" : props.width || 250 }}InputProps={{startAdornment: (<Icon sx={{ mr: 1, color: (theme) => theme.palette.action.active }}>search</Icon>),endAdornment: !!value && (<IconfontonClick={clearContent}icon="ic_chip_close"mr={0.5}fontSize={16}style={{ cursor: "pointer" }}></Iconfont>),}}placeholder={props.placeholder ? t(props.placeholder) : "Search..."}color="primary"size="small"value={value}onChange={onChange}></SytledInput><input type="text" className="not-to-show" /></Fragment>);
});
选择框(select)
import {Box,FormControl,InputLabel,MenuItem,Select,SxProps,Typography,
} from "@mui/material";
import { nanoid } from "nanoid";
import React, { memo } from "react";
import { useTranslation } from "react-i18next";
import { MatFormItemProps } from "../../../models/base.model";
import { isNull } from "../../../utils";
import { selectGreyColor } from "../../../utils/selectors";const MenuProps = {PaperProps: {style: {maxHeight: 280,// width: 250,},},
};export interface MatSelectOption<T = string | number | readonly string[] | any
> {value: T;label: string;
}export class MatSelectOptionFactory implements MatSelectOption {constructor(public value: MatSelectOption["value"], public label: string) {}
}export interface MatSelectPropsextends MatFormItemProps<MatSelectOption["value"]> {options: MatSelectOption[];size?: "small" | "medium";sx?: SxProps;
}const initNoneValue = "@@INIT" + nanoid();export default memo(function MatSelect(props: MatSelectProps) {const { t } = useTranslation();// 初始化默认值  根据是否传入placeholder来判断const defaultValue = props.placeholder ? initNoneValue : "";const value = isNull(props.value) ? defaultValue : props.value;const optionHeight = props.size === "medium" ? 48 : 40;return (<FormControlsize={props.size || "small"}fullWidthsx={{ maxWidth: props.width || 1 / 1, ...(props.sx || {}) }}><InputLabel>{t(props.label)}</InputLabel><SelectonChange={props.onChange}disabled={props.disabled}onBlur={props.onBlur}name={props.name}variant="outlined"error={props.error}MenuProps={MenuProps}labelId="demo-simple-select-label"value={value}label={props.label ? t(props.label) : undefined}>{props.placeholder && (<MenuItem sx={{ display: "none" }} disabled value={initNoneValue}><Typography color={selectGreyColor}>{t(props.placeholder)}</Typography></MenuItem>)}{!(props.options?.length > 0) && (<Box sx={{ px: 2, py: 1 }}>{t("common.noDataFound")}</Box>)}{props.options?.map((option, index) => (<MenuItemkey={index}sx={{ height: optionHeight }}value={option.value}>{t(option.label)}</MenuItem>))}</Select></FormControl>);
});
滑动输入框(slider)
import {alpha,Box,FormControl,FormControlLabel,Slider,styled,Theme,
} from "@mui/material";
import { makeStyles } from "@mui/styles";
import { t } from "i18next";
import { memo } from "react";
import { MatFormItemProps } from "../../../models/base.model";export interface MatSliderProps extends MatFormItemProps<number> {}const StyledFormControlLabel = styled(FormControlLabel)(({ theme }) => ({"   .MuiFormControlLabel-label": {whiteSpace: "nowrap",display: "inline-block",marginRight: 16,},
}));const useStyles = makeStyles((theme: Theme) => {return {valueContainer: {width: 45,fontSize: 14,height: 32,borderRadius: 16,backgroundColor: alpha(theme.palette.primary.main, 0.08),color: theme.palette.primary.main,},};
});export default memo(function MatSlider(props: MatSliderProps) {const classes = useStyles();return (<FormControlfullWidthsx={{flexDirection: "row",maxWidth: props.width || 1 / 1,height: 1 / 1,alignItems: "center",pl: 0,}}>{/* <Slider size="small" defaultValue={70} valueLabelDisplay="auto" /> */}<StyledFormControlLabelsx={{ whiteSpace: "nowrap", mr: 1, flex: 1, ml: 0 }}labelPlacement="start"label={t(props.label) as string}control={<Slider size="small" {...props} valueLabelDisplay="auto" />}></StyledFormControlLabel><Box className={classes.valueContainer + " flex-box"}>{props.value + "%"}</Box></FormControl>);
});
信息框(message)
import { Alert, AlertProps, Snackbar } from "@mui/material";
import { t } from "i18next";
import { useSelector, useDispatch } from "react-redux";
import { closeSnackBar } from "../../../store/actions/tools.action";
import { selectSnackBarConfig } from "../../../store/selectors";export interface MatSnackBarProps {duration: number;type: AlertProps["severity"];
}export default function MatSnackBar() {const config = useSelector(selectSnackBarConfig);const dispatch = useDispatch();const onSnackbarClose = () => {dispatch(closeSnackBar());};return (<SnackbaranchorOrigin={{ vertical: "top", horizontal: "center" }}open={config.open}onClose={onSnackbarClose}autoHideDuration={config.duration * 1000}><Alert severity={config.type} sx={{ width: "100%" }}>{t(config.content)}</Alert></Snackbar>);
}// 使用 一开始就挂载在App组件 然后通过store控制显隐以及内容 以及type等等
export function message(type = "info" as AlertProps["severity"],content = "",duration = 3
) {store.dispatch(openSnackBar({ open: true, type, content, duration }));
}
/**** @param {string} content 显示的内容 可以直接写翻译参数* @param {number} [duration=3] 显示的时间 默认3秒*/
export const $message = {success(content: string, duration = 3) {message("success", content, duration);},info(content: string, duration = 3) {message("info", content, duration);},warning(content: string, duration = 3) {message("warning", content, duration);},error(content: string, duration = 3) {message("error", content, duration);},
};
开关(switch)
import * as React from "react";
import FormControlLabel, {FormControlLabelProps,
} from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch";
import { MatFormItemProps } from "../../../models/base.model";
import { useTranslation } from "react-i18next";
import { Box, Tooltip, Typography } from "@mui/material";
import Iconfont from "../tools/Iconfont";
// interface MatSwiyexport interface MatSwitchProps extends MatFormItemProps<boolean> {labelplacement?: FormControlLabelProps["labelPlacement"];tip?: string;
}export default React.memo(function MatSwitch(props: MatSwitchProps) {const { t } = useTranslation();const renderLabel = () => {if (props.tip) {return (<Typography className="flex-box">{t(props.label)}<Tooltip title={t(props.tip)} sx={{ ml: 2, mt: 0.5 }}><Box><Iconfontcolor="#f3a15d"icon={"ic_alert"}fontSize={16}mr={1}></Iconfont></Box></Tooltip></Typography>);} else {return t(props.label);}};return (<FormControlLabellabelPlacement={props.labelplacement || "end"}control={<Switch checked={props.value} {...props} />}label={renderLabel()}/>);
});
自定义按钮(button)
import { alpha, Button, styled } from "@mui/material";const TextButton = styled(Button)(({ theme }) => {const color = alpha(theme.palette.primary.main, 0.08);const hoveredColor = alpha(theme.palette.primary.main, 0.16);const disabledColor = alpha(theme.palette.action.disabled, 0.11);return {backgroundColor: color,"&:hover": {backgroundColor: hoveredColor,},"&.btn-disabled": {backgroundColor: disabledColor,cursor: "not-allowed",opacity: 0.4,},"&.btn-disabled:active": {backgroundColor: disabledColor,cursor: "not-allowed",opacity: 0.4,},};
});export default TextButton;

基于 Mui 封装常用 React 组件相关推荐

  1. 基于jquery的php分页,基于jQuery封装的分页组件

    前言: 由于项目需要实现分页效果,上jQuery插件库找了下,但是木有找到自己想要的效果,于是自己封装了个分页组件. 思路: 主要是初始化时基于原型建立的分页模板然后绑定动态事件并实现刷新DOM的分页 ...

  2. android 按钮点击动画效果_如何用纯css打造类materialUI的按钮点击动画并封装成react组件...

    作为一个前端框架的重度使用者,在技术选型上也会非常注意其生态和完整性.笔者先后开发过基于vue,react,angular等框架的项目,碧如vue生态的elementUI, ant-design-vu ...

  3. 一个基于dumi搭建的react组件库,特别的开源组件项目,主要用于学习

    前言 在日常开发中,肯定会积累许许多多的业务组件在项目内,部分可以作为公共组件被抽离到公共组件库,但大部分或许与业务强相关,或许带有接口请求,并不适合抽离为公共组件,但仍需要有一个地方去展示这些组件的 ...

  4. Echarts 封装常用图表组件

    npm 安装 echarts npm install echarts 在项目根目录下的 src/components 中新建一个Chart.vue组件. 在 src 下新建一个 charts 文件夹, ...

  5. 【Web技术】1518- 抛弃 moment.js,基于 date-fns 封装日期相关 utils

    作者:jjjona0215 https://juejin.cn/post/7151050708094189582 前言 本文将简要介绍前端常用日期处理库:官方停止维护的moment.js,无缝代替mo ...

  6. AngularJS指令封装高德地图组件

    2019独角兽企业重金招聘Python工程师标准>>> 1 概述 公司移动门户原来是基于AngularJS指令封装的百度地图组件,用于签到.签退.定位等功能,在使用过程中发现百度地图 ...

  7. monaco-editor基本使用以及monaco-editor封装成vue组件

    文章目录 一.monaco-editor基本使用 二.monaco-editor封装成vue组件 一.monaco-editor基本使用 以vue2项目为例 安装依赖 npm i monaco-edi ...

  8. 抛弃 moment.js,基于 date-fns 封装日期相关 utils

    大厂技术  高级前端  Node进阶 点击上方 程序员成长指北,关注公众号 回复1,加入高级Node交流群 作者:jjjona0215 https://juejin.cn/post/715105070 ...

  9. React组件常用设计模式之Render Props

    自己在总结最近半年的React开发最佳实践时,提到了Render Props,想好好写写,但感觉篇幅又太长,所以就有了此文.愿你看完,能有所收获,如果有什么不足或错误之处还请指正.文中所提到的所有代码 ...

最新文章

  1. mysql repo_centos7下使用wget命令安装mysql
  2. ffmpeg加入libass
  3. 只开窗不镀锡_翡翠为什么要开窗??————开窗有哪几种?
  4. docker安装php怎么修改配置,怎么给docker配置内存大小?
  5. [Leedcode][JAVA][第125题][验证回文串][双指针][String]
  6. 提高HTML5 canvas性能的几种方法
  7. Android扩展类方法,Android 扩展 uni小程序SDK 原生能力
  8. 业界分享 | 阿里达摩院:超大规模预训练语言模型落地实践
  9. 电子信息工程这个专业学的是什么内容,就业怎么样?
  10. iBATIS In Action(六)执行非查询语句
  11. 5款内容超赞的微信小程序,每一个都是深藏!
  12. [转载]JTree 编辑、删除、添加节点_-Chaz-_新浪博客
  13. trymyapps下载_Incentivized Application Starts Up-Trymyapps
  14. tp5shop tp5商城 WSTMart B2B2C开源商城系统
  15. 小米捧红氮化镓快充?看完此文让你秒懂氮化镓!
  16. Web应用开发技术笔记
  17. python计算机语言基础_PYTHON之计算机语言基础知识 —— 编程语言的分类
  18. k8s证书过期怎么办?
  19. git merge的三种操作merge, squash merge, 和rebase merge
  20. 日语二级语法汇总(part11/16)

热门文章

  1. linux归档文件是否影响使用,再Linux下使用Tar工具归档文件的教程
  2. 大工计算机学硕多少分能上,大连理工大学考研难考吗?(大连理工大学考研难度分析)...
  3. java 保存数据到数据库_保存数据到数据库成功
  4. django多个html模板,django二、模板详解(templates)——页面视图
  5. iOS 常用的一些第三方库下载地址
  6. AMD Ryzen 处理器 使用 VMWare 安装 Mac OS 虚拟机详细记录
  7. CentOS7.9 虚拟机挂载exFAT格式U盘
  8. Ubuntu,Linux下实现划词翻译之goldendict词典安装及配置(转)
  9. HTML5小游戏动手做(一):简单的连连看
  10. [Python Tips]去除 Trivial 赋值语句