Mui 是基于google
的Materia
设计风格开发的基于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,下面记录一些常用的
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;
这段代码中有几种写法:
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 属性是 Mui 非常优秀的设计,所有的 Mui 组件均可以使用 sx 属性,该属性用来定义 Mui 组件的样式,并且该属性的值中的 css 属性有很多种简写形式,非常好用, 比如:
// padding、margin等值都和主题中的间距(spacing)有关,比如{p: 2}代表padding的值为2*theme.spacing
// 下面以spacing默认为4处理
{*/ padding: 1 * 4 = 4px /*}
{*/ padding-left: 1 * 4 = 4px, padding-right: 1 * 4 = 4px /*}
{*/ padding-left: 1 * 4 = 4px /*}
{*/ padding-top: 1 * 4 = 4px, padding-bottom: 1 * 4 = 4px /*}
{*/ padding-right: 1 * 4 = 4px, background-color: 主题色 /*}
theme.palette.primary.main}}>
上面大概介绍了 Mui 组件样式设置的方法,下面是一些常用的组件的封装
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 (
<FormControl
error={props.error}
sx={{ maxWidth: width || 1 / 1 }}
fullWidth={fullWidth}
>
<TextField
{...props}
size={size}
label={props.label && t(props.label)}
value={value}
></TextField>
</FormControl>
);
});
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 (
<FormControl
sx={{
height: 1 / 1,
display: "flex",
justifyContent: "center",
...(props.sx || {}),
}}
>
<FormControlLabel
control={
<Checkbox
disabled={props.disabled}
onBlur={props.onBlur}
name={props.name}
onChange={props.onChange}
checked={props.value || false}
/>
}
label={t(props.label) as string}
/>
</FormControl>
);
}
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;
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>
<Dialog
open={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>
)}
<LoadingButton
disabled={config.okBtnDisabled}
variant={okBtnVariant}
loading={loading}
onClick={handleOk}
autoFocus
color={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",
})
);
}
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} 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
<TextButton
className={props.disabled ? "btn-disabled" : ""}
onClick={handleClick}
sx={{ minWidth: "auto", borderRadius: 2 }}
>
<MoreVertIcon
sx={{
transform: "rotate(90deg)",
color: (theme) => theme.palette.action.active,
}}
/>
</TextButton>
)}
{!props.disabled && (
<StyledMenu
sx={{ maxHeight: 450 }}
MenuListProps={{
"aria-labelledby": "demo-customized-button",
}}
anchorEl={anchorEl}
open={open}
onClose={handleClose}
>
{props.children}
{menus?.map((menu, index) => (
<Box key={index}>
<MenuItem
disabled={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>
);
}
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 (
<FormControl
size={props.size || "small"}
sx={{ maxWidth: props.width || 1 / 1 }}
fullWidth
>
<p>{props.error}</p>
<InputLabel error={props.error}>{t(props.label)}</InputLabel>
<OutlinedInput
disabled={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">
<IconButton
size="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>
);
});
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 }}>
<LinearProgress
color={color}
sx={{ height, borderRadius: "5px", mr: 1 }}
variant="determinate"
value={props.progress}
/>
</Box>
<Typography color={selectGreyColor}>{label || defaultLabel}</Typography>
</Box>
);
}
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} 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(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的时候需要重置selected
if (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 }}>
<Paper
sx={{
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 (
<StyledRow
hover
// 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) => (
<TableCell
className={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>
);
}
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 (
<FormControl
sx={{
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">
<FormControlLabel
sx={{ 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>
);
}
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 && (
<Iconfont
onClick={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>
);
});
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 MatSelectProps
extends 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 (
<FormControl
size={props.size || "small"}
fullWidth
sx={{ maxWidth: props.width || 1 / 1, ...(props.sx || {}) }}
>
<InputLabel>{t(props.label)}</InputLabel>
<Select
onChange={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) => (
<MenuItem
key={index}
sx={{ height: optionHeight }}
value={option.value}
>
{t(option.label)}
</MenuItem>
))}
</Select>
</FormControl>
);
});
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 (
<FormControl
fullWidth
sx={{
flexDirection: "row",
maxWidth: props.width || 1 / 1,
height: 1 / 1,
alignItems: "center",
pl: 0,
}}
>
{/* */}
<StyledFormControlLabel
sx={{ 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>
);
});
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 (
<Snackbar
anchorOrigin={{ 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);
},
};
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 MatSwiy
export 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>
<Iconfont
color="#f3a15d"
icon={"ic_alert"}
fontSize={16}
mr={1}
></Iconfont>
</Box>
</Tooltip>
</Typography>
);
} else {
return t(props.label);
}
};
return (
<FormControlLabel
labelPlacement={props.labelplacement || "end"}
control={<Switch checked={props.value} {...props} />}
label={renderLabel()}
/>
);
});
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;