Wiki源代码线性布局组件与占位组件
由 Qiongpan Ke 于 2024-12-02 最后修改
显示最后作者
author | version | line-number | content |
---|---|---|---|
1 | 第一次开发前端项目,也是第一次使用 React 开发,虽说都是 flex 布局,但依然调样式调到想吐。 | ||
2 | |||
3 | 没办法,根据以往开发 APP 的经验,参考 Android 和 HarmonyOS 的线性布局概念,用 React 实现了一个线性布局组件 LinearLayout ,配合占位组件 Placeholder ,可以在不写任行一行样式代码的情况下编排出想要的布局。 | ||
4 | |||
5 | = 使用示例 = | ||
6 | |||
7 | 以编排卡片布局为例,左边一个居中的头像,右边上下为主标题与副标题,中间带有分隔空间。 | ||
8 | |||
9 | [[image:demo.png]] | ||
10 | |||
11 | 代码如下: | ||
12 | |||
13 | {{code language="html"}} | ||
14 | <Placeholder height="128px" color="lightgray"> | ||
15 | <Row> | ||
16 | <Compact align="center"><Placeholder alt="头像" width="48px" height="48px" /></Compact> | ||
17 | <Blank/> | ||
18 | <Stretch> | ||
19 | <Column> | ||
20 | <Stretch><Placeholder alt="标题" /></Stretch> | ||
21 | <Blank/> | ||
22 | <Stretch><Placeholder alt="副标题" /></Stretch> | ||
23 | </Column> | ||
24 | </Stretch> | ||
25 | </Row> | ||
26 | </Placeholder> | ||
27 | {{/code}} | ||
28 | |||
29 | = 组件源码 = | ||
30 | |||
31 | linearLayout/index.tsx | ||
32 | |||
33 | {{code language="ts"}} | ||
34 | import React from "react"; | ||
35 | import styles from "./index.module.less"; | ||
36 | |||
37 | /** | ||
38 | * 线性布局,以行 Row 和列 Column 的嵌套组合来布局界面,行/列内则以紧凑 Compact 和延展 Stretch 来划分空间。 | ||
39 | * 可以以比较简单直接的方式来编排界面元素,甚至配合 Placeholder 组件,可以在不写任何一行 CSS 样式的情况下, | ||
40 | * 快速编排出需要的界面布局,再在此基础上进行精细的 CSS 样式设置,所以对新手较为友好,老手也可以降低心智负担。 | ||
41 | * | ||
42 | * // 以编排卡片布局为例,左边一个居中的头像,右边上下为主标题与副标题,中间带有分隔空间。 | ||
43 | * <Placeholder height="128px" color="lightgray"> | ||
44 | * <Row> | ||
45 | * <Compact align="center"><Placeholder alt="头像" width="48px" height="48px" /></Compact> | ||
46 | * <Blank/> | ||
47 | * <Stretch> | ||
48 | * <Column> | ||
49 | * <Stretch><Placeholder alt="标题" /></Stretch> | ||
50 | * <Blank/> | ||
51 | * <Stretch><Placeholder alt="副标题" /></Stretch> | ||
52 | * </Column> | ||
53 | * </Stretch> | ||
54 | * </Row> | ||
55 | * </Placeholder> | ||
56 | * | ||
57 | * 上述的 Row/Column/Compact/Stretch 等组件,透传了 children/className/style 属性,除了其自身拥有的特性, | ||
58 | * 可以看作是普通的 <div> 标签,甚至通过 className/sytle 也可以修改这些组件的特性。 | ||
59 | */ | ||
60 | export namespace LinearLayout { | ||
61 | type TProps = { | ||
62 | children?: React.ReactNode; | ||
63 | className?: string; | ||
64 | style?: React.CSSProperties; | ||
65 | }; | ||
66 | |||
67 | const joinClasses = (...classNames: (string | undefined)[]) => { | ||
68 | return (classNames ?? []) | ||
69 | .map(it => it ?? '') | ||
70 | .filter(it => it.length > 0) | ||
71 | .join(' ') | ||
72 | .trim(); | ||
73 | }; | ||
74 | |||
75 | const joinStyles = (...styleObjects: (React.CSSProperties | undefined)[]) => { | ||
76 | return Object.assign({}, ...styleObjects); | ||
77 | } | ||
78 | |||
79 | type TRowProps = TProps & { | ||
80 | justify?: string; // 水平方向的对齐方式,相当于投映 justify-content 属性。 | ||
81 | } | ||
82 | |||
83 | /** | ||
84 | * 单行容器,配合 Compact/Stretch 等子元素,在水平方向上编排界面布局。 | ||
85 | */ | ||
86 | export const Row: React.FC<TRowProps> = (props) => { | ||
87 | return <div className={ | ||
88 | joinClasses(styles.base, styles.row, props.className) | ||
89 | } style={ | ||
90 | joinStyles({ | ||
91 | justifyContent: props.justify | ||
92 | }, props.style) | ||
93 | }> | ||
94 | {props.children} | ||
95 | </div>; | ||
96 | }; | ||
97 | |||
98 | type TColumnProps = TProps & { | ||
99 | justify?: string; // 垂直方向的对齐方式,相当于投映 justify-content 属性。 | ||
100 | } | ||
101 | |||
102 | /** | ||
103 | * 单列容器,配合 Compact/Stretch 等子元素,在垂直方向上编排界面布局。 | ||
104 | */ | ||
105 | export const Column: React.FC<TColumnProps> = (props) => { | ||
106 | return <div className={ | ||
107 | joinClasses(styles.base, styles.column, props.className) | ||
108 | } style={props.style}> | ||
109 | {props.children} | ||
110 | </div>; | ||
111 | }; | ||
112 | |||
113 | type TCompactProps = TProps & { | ||
114 | align?: string; // 当前紧凑容器在所处线性布局方向上的对齐方式,相当于投映 align-self 属性。 | ||
115 | } | ||
116 | |||
117 | /** | ||
118 | * 紧凑容器,在水平和垂直方向均紧凑粘合子元素内容。 | ||
119 | */ | ||
120 | export const Compact: React.FC<TCompactProps> = (props) => { | ||
121 | return <div className={ | ||
122 | joinClasses(styles.base, styles.compact, props.className) | ||
123 | } style={ | ||
124 | joinStyles({ | ||
125 | alignSelf: props.align | ||
126 | }, props.style) | ||
127 | }>{props.children}</div>; | ||
128 | }; | ||
129 | |||
130 | type TStretchProps = TProps & { | ||
131 | weight?: string; // 延伸比重(类似 Android 中的 layoutWeight 属性,默认值为 1 ),相当于投映 flex-grow 属性。 | ||
132 | align?: string; // 各子元素(默认为水平方向排布)在垂直方向的对齐方式(上中下等),相当于投映 align-items 属性。 | ||
133 | justify?: string; // 各子元素(默认为水平方向排布)在水平方向的堆叠方式(左中右等),相当于投映 justify-content 属性。 | ||
134 | }; | ||
135 | |||
136 | /** | ||
137 | * 延展容器,在水平和垂直方向均贪婪延展,多个 Stretch 容器按比重 weight 占用父节点的剩余空间,默认为平分。 | ||
138 | */ | ||
139 | export const Stretch: React.FC<TStretchProps> = (props) => { | ||
140 | return <div className={ | ||
141 | joinClasses(styles.base, styles.stretch, props.className) | ||
142 | } style={ | ||
143 | joinStyles({ | ||
144 | flexGrow: props.weight, | ||
145 | alignItems: props.align, | ||
146 | justifyContent: props.justify, | ||
147 | }, props.style) | ||
148 | }> | ||
149 | {props.children} | ||
150 | </div>; | ||
151 | }; | ||
152 | |||
153 | type TBlankProps = TProps & { | ||
154 | length?: string; // 所处线性布局方向上的长度,默认为 8px 。 | ||
155 | }; | ||
156 | |||
157 | /** | ||
158 | * 空白内容,在所处线性布局方向上的占用空间长度的空间,默认占用 8px 。 | ||
159 | */ | ||
160 | export const Blank: React.FC<TBlankProps> = (props) => { | ||
161 | return <div className={ | ||
162 | joinClasses(styles.base, styles.blank, props.className) | ||
163 | } style={ | ||
164 | joinStyles({ | ||
165 | flexBasis: props.length | ||
166 | }, props.style) | ||
167 | } />; | ||
168 | } | ||
169 | }; | ||
170 | |||
171 | // 与命名空间 LinearLayout 一并导出,方便不带命名空间直接使用。 | ||
172 | export const Row = LinearLayout.Row; | ||
173 | export const Column = LinearLayout.Column; | ||
174 | export const Compact = LinearLayout.Compact; | ||
175 | export const Stretch = LinearLayout.Stretch; | ||
176 | export const Blank = LinearLayout.Blank; | ||
177 | {{/code}} | ||
178 | |||
179 | linearLayout/index.module.less | ||
180 | |||
181 | {{code language="less"}} | ||
182 | .base { | ||
183 | display: flex; | ||
184 | } | ||
185 | |||
186 | .row { | ||
187 | flex-direction: row; | ||
188 | flex: 1 1 0px; | ||
189 | align-self: stretch; | ||
190 | } | ||
191 | |||
192 | .column { | ||
193 | flex-direction: column; | ||
194 | flex: 1 1 0px; | ||
195 | align-self: stretch; | ||
196 | } | ||
197 | |||
198 | .compact { | ||
199 | flex: unset 0 0px; | ||
200 | align-self: flex-start; | ||
201 | } | ||
202 | |||
203 | .stretch { | ||
204 | flex: 1 1 0px; | ||
205 | align-self: stretch; | ||
206 | } | ||
207 | |||
208 | .blank { | ||
209 | flex: 0 0 8px; | ||
210 | } | ||
211 | {{/code}} | ||
212 | |||
213 | placeholder/index.tsx | ||
214 | |||
215 | {{code language="ts"}} | ||
216 | import React from "react"; | ||
217 | import styles from "./index.module.less"; | ||
218 | |||
219 | enum EArea { | ||
220 | NONE = 'none', | ||
221 | WIDE = 'wide', | ||
222 | FULL = 'full' | ||
223 | } | ||
224 | |||
225 | type TProps = { | ||
226 | children?: React.ReactNode, | ||
227 | className?: string; | ||
228 | style?: React.CSSProperties; | ||
229 | width?: string, | ||
230 | height?: string, | ||
231 | area?: string | EArea, | ||
232 | align?: string, | ||
233 | color?: string, | ||
234 | borderd?: boolean, | ||
235 | alt?: string | ||
236 | } | ||
237 | |||
238 | export const Placeholder: React.FC<TProps> = (props) => { | ||
239 | return ( | ||
240 | <div className={ | ||
241 | (props.className ?? '') + ' ' + [ | ||
242 | styles.component, | ||
243 | styles[`area-${props.area ?? 'full'}`] | ||
244 | ].join(' ') | ||
245 | } style={{ | ||
246 | width: props.width, | ||
247 | height: props.height, | ||
248 | backgroundColor: props.color, | ||
249 | alignSelf: props.align, | ||
250 | ...props.style | ||
251 | }}> | ||
252 | {props.children ?? props.alt ?? 'Placeholder'} | ||
253 | </div> | ||
254 | ) | ||
255 | }; | ||
256 | {{/code}} | ||
257 | |||
258 | placeholder/index.module.less | ||
259 | |||
260 | {{code language="less"}} | ||
261 | .component { | ||
262 | display: flex; | ||
263 | border: 1px dashed black; | ||
264 | } | ||
265 | |||
266 | .area-none { | ||
267 | flex: unset 0 0px; | ||
268 | align-self: flex-start; | ||
269 | } | ||
270 | |||
271 | .area-wide { | ||
272 | flex: 1 0 0px; | ||
273 | align-self: flex-start; | ||
274 | } | ||
275 | |||
276 | .area-full { | ||
277 | flex: 1 0 0px; | ||
278 | align-self: stretch; | ||
279 | } | ||
280 | {{/code}} |