我们在做大屏项目的时候,通常会遇到这些问题
- 如何布局,包括常规布局,是px 还是vw,还是rem,还是%
- 是否支持页面响应式,每次要浪费大量时间编码,调试
- 样式能否复用,如果封装成主题,还可以支持主题切换的功能
综合多个大屏项目的实际开发和落地,我封装了大屏响应式布局组件(React),经过几个版本的迭代和优化,现已开源和发布到npm
本文给大家介绍下这个组件的功能和设计,希望大家可以找到大屏组件开发的灵感
源码地址
1.栅格系统/布局系统
tsx
<DashboardAppResponsive
cols={12}
resource={{
A1: TestChildA1
}}
layout={[
{
i: "A1",
w: 3,
h: 4,
x: 0,
y: 0,
},
]}
/>
- 这是一个最简单的布局案例:
cols
属性指定栅格列数(默认12); resource
配置格子组件,key是id,TestChildA1
是函数组件;layout
配置布局信息,i
是id,w h
分别是宽高,x y
分别是坐标,左上角是0,0开始算,单位都是栅格- 参考
https://github.com/react-grid-layout/react-grid-layout?tab=readme-ov-file#usage
的设计
maxRows是什么意思呢?由于大屏一般有两种模式,一种是在pc/电视设备,一屏展示,不需要滚动条,另一种是移动端设备,需要流式滚动展示,所以需要区别计算
部分源码
tsx
// 计算出一共多少行,最底下的格子组件在什么位置
const computedMaxRow = (layout: Partial<IDashboardItemProps>[]) => {
return layout.map((l) => (l.y || 0) + (l.h || 0)).sort((a, b) => b - a)[0];
};
const rows = computedMaxRow(layout);
// pc设备,移动端设备分别计算
const rowHeight = breakpoint === 'desktop' || breakpoint === 'showroom'
? sizeFormat((contentHeight) / rows)
: typeof rowHeightParam === "function"
? rowHeightParam({ breakpoint, themeMode, themeName })
: rowHeightParam;
后续扩展
resource前端控制, layout存储在服务端,可以进行位置配置
2.响应式系统
tsx
<DashboardAppResponsive
breakpoints={{
showroom: 2600,
desktop: 1300,
tablet: 500,
mobile: 0,
}}
cols={({ breakpoint })=>{
if (breakpoint === "tablet" || breakpoint == "mobile") {
return 6
}
return 12;
}}
layout={({ breakpoint }) => {
if (breakpoint === "tablet" || breakpoint == "mobile") {
return tabletLayout
}
return pcLayout;
}}
/>
设计说明
-
内置断点系统,
layout
,cols
等属性可以写成函数的形式 -
组件只有一套,布局有n套
-
参考antd-table的components属性
默认断点值说明
名称 | 值(px) |
---|---|
showroom 展厅 | 大于2600 |
desktop pc端 | 大于1300,小于2600 |
tablet 平板 | 大于500,小于1300 |
mobile 手机 | 大于0,小于500 |
覆盖默认组件
tsx
<DashboardAppResponsive
components={{
content: ({ children, contentRef, breakpoint }: any) => {
return (
<div
ref={contentRef}
style={{
height: "calc( 100% - 120px)",
overflowX: "hidden",
overflowY:
breakpoint === "tablet" || breakpoint === "mobile"
? "auto"
: "hidden",
position: "relative",
}}
>
{children}
</div>
);
},
headerInner: () => (
<HeaderWrap>
<Header
data={{
activeMenu: [2, 1],
projectId: "",
projectName: "",
...props.data,
}}
emit={props?.emit}
/>
</HeaderWrap>
),
}}
/>
自定义组件说明
layout : 最外面的容器组件
content : 除头部以外的内容区域
headerWrapper: 头部外部组件,一般定义背景图片
headerInner : 头部内部组件(由于带业务需求,往往需要子系统自定义)
containerWrapper: 卡片容器组件,一般由主题提供
3.主题预设系统
设计思路
-
每个主题可以提供组件的任意属性,和样式文件,将会与用户传递给组件的属性进行合并
-
用户属性大于主题属性
-
参考设计
https://github.com/nosferatu500/react-sortable-tree/blob/stable/src/react-sortable-tree.tsx#L35C7-L35C17
优势
- 快速开发
- UI验收过的主题不会出错
使用一个主题
这部分文档尚未完善,先以内部主题为样例
tsx
import './App.css'
import { DashboardAppResponsive } from 'rc-dashboard'
import { TestChildBase } from './components/TestChildBase'
import { tabletLayout } from './utils/tabletLayout'
import { pcLayout } from './utils/pcLayout'
import { jfDarkTheme } from 'rc-dashboard/dist/themes'
import 'rc-dashboard/dist/themes/index.css'
function App() {
return (
<DashboardAppResponsive
minHeight={555}
resource={{
A1: TestChildBase,
}}
layout={({ breakpoint }) => {
if (breakpoint === "tablet" || breakpoint == "mobile") {
return tabletLayout
}
return pcLayout;
}}
themeProvider={jfDarkTheme}
/>
)
}
export default App
我们可以看到 themeProvider
传入了 jfDarkTheme ,同时还引用了一个css,接下来来看下主题内部源码是怎么样的
tsx
// 一般会提供一份主题样式
import "./jfDarkThemeGlobal.less";
export const jfDarkTheme: DashBoardTheme = {
// themeName会做为className
themeName: "jfDarkTheme",
wrapperStyle: {
backgroundColor: "#052B54",
},
components: {
containerWrapper: NodeContentRenderer,
headerWrapper: TitleNodeRenderer,
content: JfDarkThemeContent
}
};
export * from "../interface";
jfDarkThemeGlobal.less , 主要是提供css变量和基础样式
less
@text-common: #c3d4e5;
@text-primary: #3b78ef;
@text-num-light: #f4a52e;
@text-num: #ffdc2f;
@text-light: #34daff;
html[data-theme="dark"] body ,
html[data-theme-name="jfDarkTheme"] body
{
/* // 交发黑暗色 */
/* 下拉框的背景颜色 */
--popover: 209 96% 18%;
}
.jfDarkTheme {
background-image: url("@/assets/jfDarkTheme/main-bg.jpg");
background-size: cover;
background-position: center;
background-repeat: no-repeat;
font-size: 14px;
color: #c3d4e5;
// 有用的
--text-num: @text-num;
--text-num-light: @text-num-light;
--thumb-color: rgba(59, 120, 239, 0.5);
/* // 交发黑暗色 */
/* 下拉框的背景颜色 */
--popover: 209 96% 18%;
&.mobile {
background-repeat: repeat;
background-size: contain;
background-position: unset;
background-image: url("@/assets/jfDarkTheme/main-bg-m.png");
}
}
4.主题切换系统
本质上是动态修改themeProvider属性
模式1: 亮色/暗色切换
提供hooks
tsx
const { themeMode, setThemeMode } = useThemeMode()
type ThemeMode = "dark" | "light" | "system";
使用hooks
tsx
export const TitleNodeChildRenderer = (props: any) => {
const { themeMode, setThemeMode } = useThemeMode()
return <div style={{
display: 'flex',
}}>
<p>当前模式{themeMode}</p>
<div>
<Button onClick={() => {
setThemeMode('light')
}}>明色</Button>
<Button
onClick={() => {
setThemeMode('dark')
}}
>暗色</Button>
</div>
</div>;
};
模式2,主题切换
提供hooks
scss
const { themeName, setThemeName } = useThemeMode()
使用hooks
typescript
export const TitleNodeChangeTheme = (props: any) => {
const { themeName, setThemeName } = useThemeMode()
return <div style={{
display: 'flex',
}}>
<p>当前主题{themeName}</p>
<div>
<Button onClick={() => {
setThemeName('jfDarkTheme')
}}>蓝深色</Button>
<Button
onClick={() => {
setThemeName('jfLightTheme')
}}
>蓝亮色</Button>
</div>
</div>;
};
模式2下,外层需要判断当前主题
tsx
export const DashboardAppResponsiveThemeDemo = () => {
const [themeName, setThemeName] = useState(localStorage.getItem("vite-ui-theme-name") || 'jfDarkTheme')
return (
<DashboardAppResponsive
onThemeNameChange={(name = '') => {
name && setThemeName(name)
}}
themeProvider={() => {
return themesMap[themeName] || jfDarkTheme
}}
/>
);
};