mirror of
https://gitee.com/baidu/amis.git
synced 2024-11-29 18:48:45 +08:00
feat: 宫格导航 (#3044)
* feat: 宫格导航 * feat: 添加直接页面跳转配置 * feat: 宫格导航兼容url与link格式
This commit is contained in:
parent
0d3606fb14
commit
729924cc53
373
docs/zh-CN/components/grid-nav.md
Normal file
373
docs/zh-CN/components/grid-nav.md
Normal file
@ -0,0 +1,373 @@
|
||||
---
|
||||
title: GridNav 宫格导航
|
||||
description:
|
||||
type: 0
|
||||
group: ⚙ 组件
|
||||
menuName: GridNav 宫格导航
|
||||
icon:
|
||||
order: 54
|
||||
---
|
||||
|
||||
宫格菜单导航,不支持配置初始化接口初始化数据域,所以需要搭配类似像`Service`、`Form`或`CRUD`这样的,具有配置接口初始化数据域功能的组件,或者手动进行数据域初始化,然后通过`source`属性,获取数据链中的数据,完成菜单展示。
|
||||
|
||||
## 基本用法
|
||||
|
||||
通过 source 关联上下文数据,或者通过 name 关联。
|
||||
|
||||
```schema
|
||||
{
|
||||
"type": "page",
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"icon": "https://internal-amis-res.cdn.bcebos.com/images/icon-1.png",
|
||||
"text": "导航1"
|
||||
},
|
||||
{
|
||||
"icon": "https://internal-amis-res.cdn.bcebos.com/images/icon-1.png",
|
||||
"text": "导航2"
|
||||
},
|
||||
{
|
||||
"icon": "https://internal-amis-res.cdn.bcebos.com/images/icon-1.png",
|
||||
"text": "导航3"
|
||||
},
|
||||
{
|
||||
"icon": "https://internal-amis-res.cdn.bcebos.com/images/icon-1.png",
|
||||
"text": "导航4"
|
||||
}
|
||||
]
|
||||
},
|
||||
"body": [
|
||||
{
|
||||
"type": "grid-nav",
|
||||
"source": "${items}"
|
||||
},
|
||||
{
|
||||
"type": "divider"
|
||||
},
|
||||
{
|
||||
"type": "grid-nav",
|
||||
"name": "items"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
也可以静态展示,即不关联数据固定显示。
|
||||
|
||||
```schema
|
||||
{
|
||||
"type": "page",
|
||||
"body": {
|
||||
"type": "grid-nav",
|
||||
"options": [
|
||||
{
|
||||
"icon": "https://internal-amis-res.cdn.bcebos.com/images/icon-1.png",
|
||||
"text": "导航1"
|
||||
},
|
||||
{
|
||||
"icon": "https://internal-amis-res.cdn.bcebos.com/images/icon-1.png",
|
||||
"text": "导航2"
|
||||
},
|
||||
{
|
||||
"icon": "https://internal-amis-res.cdn.bcebos.com/images/icon-1.png",
|
||||
"text": "导航3"
|
||||
},
|
||||
{
|
||||
"icon": "https://internal-amis-res.cdn.bcebos.com/images/icon-1.png",
|
||||
"text": "导航4"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 自定义列数
|
||||
|
||||
默认一行展示四个格子,可以通过 `columnNum` 自定义列数
|
||||
|
||||
```schema
|
||||
{
|
||||
"type": "page",
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"icon": "https://internal-amis-res.cdn.bcebos.com/images/icon-1.png",
|
||||
"text": "导航1"
|
||||
},
|
||||
{
|
||||
"icon": "https://internal-amis-res.cdn.bcebos.com/images/icon-1.png",
|
||||
"text": "导航2"
|
||||
},
|
||||
{
|
||||
"icon": "https://internal-amis-res.cdn.bcebos.com/images/icon-1.png",
|
||||
"text": "导航3"
|
||||
}
|
||||
]
|
||||
},
|
||||
"body": {
|
||||
"type": "grid-nav",
|
||||
"columnNum": 3,
|
||||
"source": "${items}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 正方形格子
|
||||
|
||||
设置 `square` 属性后,格子的高度会和宽度保持一致。
|
||||
|
||||
```schema
|
||||
{
|
||||
"type": "page",
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"icon": "https://internal-amis-res.cdn.bcebos.com/images/icon-1.png",
|
||||
"text": "导航1"
|
||||
},
|
||||
{
|
||||
"icon": "https://internal-amis-res.cdn.bcebos.com/images/icon-1.png",
|
||||
"text": "导航2"
|
||||
},
|
||||
{
|
||||
"icon": "https://internal-amis-res.cdn.bcebos.com/images/icon-1.png",
|
||||
"text": "导航3"
|
||||
},
|
||||
{
|
||||
"icon": "https://internal-amis-res.cdn.bcebos.com/images/icon-1.png",
|
||||
"text": "导航4"
|
||||
}
|
||||
]
|
||||
},
|
||||
"body": {
|
||||
"type": "grid-nav",
|
||||
"source": "${items}",
|
||||
"square": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 格子间距
|
||||
|
||||
通过 `gutter` 属性设置格子之间的距离。
|
||||
|
||||
```schema
|
||||
{
|
||||
"type": "page",
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"icon": "https://internal-amis-res.cdn.bcebos.com/images/icon-1.png",
|
||||
"text": "导航1"
|
||||
},
|
||||
{
|
||||
"icon": "https://internal-amis-res.cdn.bcebos.com/images/icon-1.png",
|
||||
"text": "导航2"
|
||||
},
|
||||
{
|
||||
"icon": "https://internal-amis-res.cdn.bcebos.com/images/icon-1.png",
|
||||
"text": "导航3"
|
||||
},
|
||||
{
|
||||
"icon": "https://internal-amis-res.cdn.bcebos.com/images/icon-1.png",
|
||||
"text": "导航4"
|
||||
}
|
||||
]
|
||||
},
|
||||
"body": {
|
||||
"type": "grid-nav",
|
||||
"source": "${items}",
|
||||
"gutter": 20
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 内容横排
|
||||
|
||||
将 `direction` 属性设置为 `horizontal`,可以让宫格的内容呈横向排列。
|
||||
|
||||
```schema
|
||||
{
|
||||
"type": "page",
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"icon": "https://internal-amis-res.cdn.bcebos.com/images/icon-1.png",
|
||||
"text": "导航1"
|
||||
},
|
||||
{
|
||||
"icon": "https://internal-amis-res.cdn.bcebos.com/images/icon-1.png",
|
||||
"text": "导航2"
|
||||
},
|
||||
{
|
||||
"icon": "https://internal-amis-res.cdn.bcebos.com/images/icon-1.png",
|
||||
"text": "导航3"
|
||||
},
|
||||
{
|
||||
"icon": "https://internal-amis-res.cdn.bcebos.com/images/icon-1.png",
|
||||
"text": "导航4"
|
||||
}
|
||||
]
|
||||
},
|
||||
"body": {
|
||||
"type": "grid-nav",
|
||||
"direction": "horizontal",
|
||||
"source": "${items}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 图标占比
|
||||
|
||||
设置 `iconRatio` 可以控制图标宽度占比,默认 60%,设置 1-100 的数字。
|
||||
|
||||
```schema
|
||||
{
|
||||
"type": "page",
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"icon": "https://internal-amis-res.cdn.bcebos.com/images/icon-1.png",
|
||||
"text": "导航1"
|
||||
},
|
||||
{
|
||||
"icon": "https://internal-amis-res.cdn.bcebos.com/images/icon-1.png",
|
||||
"text": "导航2"
|
||||
},
|
||||
{
|
||||
"icon": "https://internal-amis-res.cdn.bcebos.com/images/icon-1.png",
|
||||
"text": "导航3"
|
||||
},
|
||||
{
|
||||
"icon": "https://internal-amis-res.cdn.bcebos.com/images/icon-1.png",
|
||||
"text": "导航4"
|
||||
}
|
||||
]
|
||||
},
|
||||
"body": {
|
||||
"type": "grid-nav",
|
||||
"iconRatio": "40",
|
||||
"source": "${items}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 角标提示
|
||||
|
||||
设置 badge 属性后,会在图标右上角展示相应的角标,支持红点、数字、彩带模式。
|
||||
|
||||
```schema
|
||||
{
|
||||
"type": "page",
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"icon": "https://internal-amis-res.cdn.bcebos.com/images/icon-1.png",
|
||||
"text": "导航1",
|
||||
"badge": {
|
||||
"mode": "text",
|
||||
"text": "10"
|
||||
}
|
||||
},
|
||||
{
|
||||
"icon": "https://internal-amis-res.cdn.bcebos.com/images/icon-1.png",
|
||||
"text": "导航2",
|
||||
"badge": {
|
||||
"mode": "dot"
|
||||
}
|
||||
},
|
||||
{
|
||||
"icon": "https://internal-amis-res.cdn.bcebos.com/images/icon-1.png",
|
||||
"text": "导航3",
|
||||
"badge": {
|
||||
"mode": "ribbon",
|
||||
"text": "热销"
|
||||
}
|
||||
},
|
||||
{
|
||||
"icon": "https://internal-amis-res.cdn.bcebos.com/images/icon-1.png",
|
||||
"text": "导航4"
|
||||
}
|
||||
]
|
||||
},
|
||||
"body": {
|
||||
"type": "grid-nav",
|
||||
"source": "${items}",
|
||||
"border": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 点击交互
|
||||
|
||||
设置 clickAction 属性支持通用点击交互,详见 [Action](./action) 配置
|
||||
|
||||
```schema
|
||||
{
|
||||
"type": "page",
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"icon": "https://internal-amis-res.cdn.bcebos.com/images/icon-1.png",
|
||||
"text": "外部跳转",
|
||||
"link": "https://www.baidu.com",
|
||||
"blank": true
|
||||
},
|
||||
{
|
||||
"icon": "https://internal-amis-res.cdn.bcebos.com/images/icon-1.png",
|
||||
"text": "弹框",
|
||||
"clickAction": {
|
||||
"actionType": "dialog",
|
||||
"dialog": {
|
||||
"title": "弹框",
|
||||
"body": "这是个简单的弹框。"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"icon": "https://internal-amis-res.cdn.bcebos.com/images/icon-1.png",
|
||||
"text": "内部跳转",
|
||||
"link": "/docs/index"
|
||||
},
|
||||
{
|
||||
"icon": "https://internal-amis-res.cdn.bcebos.com/images/icon-1.png",
|
||||
"text": "导航4"
|
||||
}
|
||||
]
|
||||
},
|
||||
"body": {
|
||||
"type": "grid-nav",
|
||||
"source": "${items}",
|
||||
"border": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 属性表
|
||||
|
||||
| 属性名 | 类型 | 默认值 | 说明 |
|
||||
| ------------------- | --------------- | ---------- | -------------------------------------------------------- |
|
||||
| type | `string` | `grid-nav` | |
|
||||
| className | `string` | | 外层 CSS 类名 |
|
||||
| itemClassName | `string` | | 列表项 css 类名 |
|
||||
| value | `Array<object>` | | 图片数组 |
|
||||
| source | `string` | | 数据源 |
|
||||
| square | `boolean` | | 是否将列表项固定为正方形 |
|
||||
| center | `boolean` | `true` | 是否将列表项内容居中显示 |
|
||||
| border | `boolean` | `true` | 是否显示列表项边框 |
|
||||
| gutter | `number` | | 列表项之间的间距,默认单位为`px` |
|
||||
| reverse | `boolean` | | 是否调换图标和文本的位置 |
|
||||
| iconRatio | `number` | 60 | 图标宽度占比,单位% |
|
||||
| direction | `string` | `vertical` | 列表项内容排列的方向,可选值为 `horizontal` 、`vertical` |
|
||||
| columnNum | `number` | 4 | 列数 |
|
||||
| options.icon | `string` | | 列表项图标 |
|
||||
| options.text | `string` | | 列表项文案 |
|
||||
| options.badge | `BadgeSchema` | | 列表项角标,详见 [Badge](./badge) |
|
||||
| options.link | `string` | | 内部页面路径或外部跳转 URL 地址,优先级高于 clickAction |
|
||||
| options.blank | `boolean` | | 是否新页面打开,link 为 url 时有效 |
|
||||
| options.clickAction | `ActionSchema` | | 列表项点击交互 详见 [Action](./action) |
|
||||
|
||||
```
|
||||
|
||||
```
|
@ -854,6 +854,14 @@ export const components = [
|
||||
makeMarkdownRenderer
|
||||
)
|
||||
},
|
||||
{
|
||||
label: 'GridNav 宫格导航',
|
||||
path: '/zh-CN/components/grid-nav',
|
||||
getComponent: () =>
|
||||
import('../../docs/zh-CN/components/grid-nav.md').then(
|
||||
makeMarkdownRenderer
|
||||
)
|
||||
},
|
||||
{
|
||||
label: 'Json',
|
||||
path: '/zh-CN/components/json',
|
||||
|
128
scss/components/_grid-nav.scss
Normal file
128
scss/components/_grid-nav.scss
Normal file
@ -0,0 +1,128 @@
|
||||
.u-hairline::after {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
content: ' ';
|
||||
pointer-events: none;
|
||||
top: -50%;
|
||||
right: -50%;
|
||||
bottom: -50%;
|
||||
left: -50%;
|
||||
border: 0 solid var(--borderColorLight);
|
||||
z-index: 1;
|
||||
transform: scale(0.5);
|
||||
}
|
||||
|
||||
.#{$ns}GridNav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&-top {
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
border-top-width: px2rem(1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.#{$ns}GridNavItem {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
|
||||
&--square {
|
||||
height: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
width: var(--rv-grid-item-icon-size);
|
||||
}
|
||||
|
||||
&-text {
|
||||
color: var(--text-color);
|
||||
font-size: var(--fontSizeSm);
|
||||
line-height: 1.5;
|
||||
word-break: break-all;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&-icon + &-text {
|
||||
margin-top: px2rem(8px);
|
||||
}
|
||||
|
||||
&-image {
|
||||
display: inline-block;
|
||||
|
||||
svg,
|
||||
img {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
width: 60%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
padding: var(--gap-md) var(--gap-sm);
|
||||
background-color: var(--white);
|
||||
position: relative;
|
||||
|
||||
.#{$ns}Badge-text {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
&--border::after {
|
||||
border-width: 0 var(--borderWidth) var(--borderWidth) 0;
|
||||
}
|
||||
|
||||
&--square {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&--center {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&--horizontal {
|
||||
flex-direction: row;
|
||||
|
||||
.#{$ns}GridNavItem-text {
|
||||
margin: 0 0 0 var(--gap-sm);
|
||||
}
|
||||
}
|
||||
|
||||
&--reverse {
|
||||
flex-direction: column-reverse;
|
||||
|
||||
.#{$ns}GridNavItem-text {
|
||||
margin: 0 0 var(--gap-sm);
|
||||
}
|
||||
}
|
||||
|
||||
&--horizontal &--reverse {
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.#{$ns}GridNavItem-text {
|
||||
margin: 0 var(--gap-sm) 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
&--surround {
|
||||
&::after {
|
||||
border-width: var(--borderWidth);
|
||||
}
|
||||
}
|
||||
|
||||
&--clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
@ -70,6 +70,7 @@
|
||||
@import '../components/icon';
|
||||
@import '../components/steps';
|
||||
@import '../components/portlet';
|
||||
@import '../components/grid-nav';
|
||||
|
||||
@import '../components/form/fieldset';
|
||||
@import '../components/form/group';
|
||||
|
@ -319,6 +319,7 @@ export type SchemaType =
|
||||
| 'tree-select'
|
||||
| 'table-view'
|
||||
| 'portlet'
|
||||
| 'grid-nav'
|
||||
|
||||
// 原生 input 类型
|
||||
| 'native-date'
|
||||
|
233
src/components/GridNav.tsx
Normal file
233
src/components/GridNav.tsx
Normal file
@ -0,0 +1,233 @@
|
||||
/**
|
||||
* @file GridNav
|
||||
* @description 金刚位宫格导航 参考react-vant
|
||||
*/
|
||||
|
||||
import React, {useMemo} from 'react';
|
||||
import {ClassNamesFn} from '../theme';
|
||||
import {Badge, BadgeProps} from './Badge';
|
||||
|
||||
export type GridNavDirection = 'horizontal' | 'vertical';
|
||||
|
||||
export interface GridNavProps {
|
||||
/** 是否将格子固定为正方形 */
|
||||
square?: boolean;
|
||||
/** 是否将格子内容居中显示 */
|
||||
center?: boolean;
|
||||
/** 是否显示边框 */
|
||||
border?: boolean;
|
||||
/** 格子之间的间距,默认单位为`px` */
|
||||
gutter?: number;
|
||||
/** 是否调换图标和文本的位置 */
|
||||
reverse?: boolean;
|
||||
/** 图标占比,默认单位为`%` */
|
||||
iconRatio?: number;
|
||||
/** 格子内容排列的方向,可选值为 `horizontal` */
|
||||
direction?: GridNavDirection;
|
||||
/** 列数 */
|
||||
columnNum?: number;
|
||||
className?: string;
|
||||
itemClassName?: string;
|
||||
classnames: ClassNamesFn;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export interface GridNavItemProps {
|
||||
/** 图标右上角徽标 */
|
||||
badge?: BadgeProps;
|
||||
/** 文字 */
|
||||
text?: string | React.ReactNode;
|
||||
/** 图标名称或图片链接 */
|
||||
icon?: string | React.ReactNode;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
contentClassName?: string;
|
||||
contentStyle?: React.CSSProperties;
|
||||
children?: React.ReactNode;
|
||||
classnames: ClassNamesFn;
|
||||
onClick?: (event: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
type InternalProps = {
|
||||
parent?: GridNavProps;
|
||||
index?: number;
|
||||
};
|
||||
|
||||
function addUnit(value?: string | number): string | undefined {
|
||||
if (value === undefined || value === null) {
|
||||
return undefined;
|
||||
}
|
||||
value = String(value);
|
||||
return /^\d+(\.\d+)?$/.test(value) ? `${value}px` : value;
|
||||
}
|
||||
|
||||
export const GridNavItem: React.FC<GridNavItemProps & InternalProps> = ({
|
||||
children,
|
||||
classnames: cx,
|
||||
className,
|
||||
style,
|
||||
...props
|
||||
}) => {
|
||||
const {index = 0, parent} = props;
|
||||
if (!parent) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
'[React Vant] <GridNavItem> must be a child component of <GridNav>.'
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const rootStyle = useMemo(() => {
|
||||
const {square, gutter, columnNum = 4} = parent;
|
||||
const percent = `${100 / +columnNum}%`;
|
||||
const internalStyle: React.CSSProperties = {
|
||||
...style,
|
||||
flexBasis: percent
|
||||
};
|
||||
|
||||
if (square) {
|
||||
internalStyle.paddingTop = percent;
|
||||
} else if (gutter) {
|
||||
const gutterValue = addUnit(gutter);
|
||||
internalStyle.paddingRight = gutterValue;
|
||||
|
||||
if (index >= columnNum) {
|
||||
internalStyle.marginTop = gutterValue;
|
||||
}
|
||||
}
|
||||
|
||||
return internalStyle;
|
||||
}, [parent.style, parent.gutter, parent.columnNum]);
|
||||
|
||||
const contentStyle = useMemo(() => {
|
||||
const {square, gutter} = parent;
|
||||
|
||||
if (square && gutter) {
|
||||
const gutterValue = addUnit(gutter);
|
||||
return {
|
||||
...props.contentStyle,
|
||||
right: gutterValue,
|
||||
bottom: gutterValue,
|
||||
height: 'auto'
|
||||
};
|
||||
}
|
||||
return props.contentStyle;
|
||||
}, [parent.gutter, parent.columnNum, props.contentStyle]);
|
||||
|
||||
const renderIcon = () => {
|
||||
const ratio = parent.iconRatio || 60;
|
||||
if (typeof props.icon === 'string') {
|
||||
if (props.badge) {
|
||||
return (
|
||||
<Badge {...props.badge}>
|
||||
<div className={cx('GridNavItem-image')}>
|
||||
<img src={props.icon} style={{width: ratio + '%'}} />
|
||||
</div>
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={cx('GridNavItem-image')}>
|
||||
<img src={props.icon} style={{width: ratio + '%'}} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (React.isValidElement(props.icon)) {
|
||||
return <Badge {...(props.badge as BadgeProps)}>{props.icon}</Badge>;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderText = () => {
|
||||
if (React.isValidElement(props.text)) {
|
||||
return props.text;
|
||||
}
|
||||
if (props.text) {
|
||||
return <span className={cx('GridNavItem-text')}>{props.text}</span>;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
if (children) {
|
||||
return children;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{renderIcon()}
|
||||
{renderText()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const {center, border, square, gutter, reverse, direction} = parent;
|
||||
|
||||
const prefix = 'GridNavItem-content';
|
||||
const classes = cx(`${prefix} ${props.contentClassName || ''}`, {
|
||||
[`${prefix}--${direction}`]: !!direction,
|
||||
[`${prefix}--center`]: center,
|
||||
[`${prefix}--square`]: square,
|
||||
[`${prefix}--reverse`]: reverse,
|
||||
[`${prefix}--clickable`]: !!props.onClick,
|
||||
[`${prefix}--surround`]: border && gutter,
|
||||
[`${prefix}--border u-hairline`]: border
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(className, {'GridNavItem--square': square})}
|
||||
style={rootStyle}
|
||||
>
|
||||
<div
|
||||
role={props.onClick ? 'button' : undefined}
|
||||
className={classes}
|
||||
style={contentStyle}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
{renderContent()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const GridNav: React.FC<GridNavProps> = ({
|
||||
children,
|
||||
className,
|
||||
classnames: cx,
|
||||
itemClassName,
|
||||
style,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
style={{paddingLeft: addUnit(props.gutter), ...style}}
|
||||
className={cx(`GridNav ${className || ''}`, {
|
||||
'GridNav-top u-hairline': props.border && !props.gutter
|
||||
})}
|
||||
>
|
||||
{React.Children.toArray(children)
|
||||
.filter(Boolean)
|
||||
.map((child: React.ReactElement, index: number) =>
|
||||
React.cloneElement(child, {
|
||||
index,
|
||||
parent: props,
|
||||
className: itemClassName,
|
||||
classnames: cx
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
GridNav.defaultProps = {
|
||||
direction: 'vertical',
|
||||
center: true,
|
||||
border: true,
|
||||
columnNum: 4
|
||||
};
|
||||
|
||||
export default GridNav;
|
@ -168,6 +168,7 @@ import './renderers/Markdown';
|
||||
import './renderers/TableView';
|
||||
import './renderers/Code';
|
||||
import './renderers/WebComponent';
|
||||
import './renderers/GridNav';
|
||||
|
||||
import Scoped, {ScopedContext} from './Scoped';
|
||||
|
||||
|
204
src/renderers/GridNav.tsx
Normal file
204
src/renderers/GridNav.tsx
Normal file
@ -0,0 +1,204 @@
|
||||
import React from 'react';
|
||||
import {Renderer, RendererProps} from '../factory';
|
||||
import {autobind, getPropValue} from '../utils/helper';
|
||||
import {isPureVariable, resolveVariableAndFilter} from '../utils/tpl-builtin';
|
||||
import {
|
||||
BaseSchema,
|
||||
SchemaTokenizeableString,
|
||||
SchemaTpl,
|
||||
SchemaUrlPath
|
||||
} from '../Schema';
|
||||
import {ActionSchema} from './Action';
|
||||
import GridNav, {GridNavDirection, GridNavItem} from '../components/GridNav';
|
||||
import {BadgeSchema} from '../components/Badge';
|
||||
import handleAction from '../utils/handleAction';
|
||||
import {validations} from '../utils/validations';
|
||||
|
||||
export interface ListItemSchema extends Omit<BaseSchema, 'type'> {
|
||||
/**
|
||||
* 单项点击事件
|
||||
*/
|
||||
clickAction?: ActionSchema;
|
||||
|
||||
/**
|
||||
* 跳转地址
|
||||
*/
|
||||
link?: string;
|
||||
|
||||
/**
|
||||
* 打开方式
|
||||
*/
|
||||
blank?: string;
|
||||
|
||||
/**
|
||||
* 图片地址
|
||||
*/
|
||||
icon?: SchemaUrlPath;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
text?: SchemaTpl;
|
||||
|
||||
/**
|
||||
* 图标最大宽度比例 0-100
|
||||
*/
|
||||
iconRatio?: number;
|
||||
|
||||
/**
|
||||
* 角标
|
||||
*/
|
||||
badge?: BadgeSchema;
|
||||
}
|
||||
|
||||
/**
|
||||
* List 列表展示控件。
|
||||
* 文档:https://baidu.gitee.io/amis/docs/components/card
|
||||
*/
|
||||
export interface ListSchema extends BaseSchema {
|
||||
/**
|
||||
* 指定为 List 列表展示控件。
|
||||
*/
|
||||
type: 'grid-nav';
|
||||
|
||||
/**
|
||||
* 列表项类名
|
||||
*/
|
||||
itemClassName?: string;
|
||||
|
||||
/**
|
||||
* 静态图片列表配置
|
||||
*/
|
||||
options?: Array<ListItemSchema>;
|
||||
|
||||
/**
|
||||
* 是否将列表项固定为正方形
|
||||
*/
|
||||
square?: boolean;
|
||||
|
||||
/**
|
||||
* 是否将列表项内容居中显示
|
||||
*/
|
||||
center?: boolean;
|
||||
|
||||
/**
|
||||
* 是否显示列表项边框
|
||||
*/
|
||||
border?: boolean;
|
||||
|
||||
/**
|
||||
* 列表项之间的间距,默认单位为px
|
||||
*/
|
||||
gutter?: number;
|
||||
|
||||
/**
|
||||
* 图标宽度占比, 1-100
|
||||
*/
|
||||
iconRatio?: number;
|
||||
|
||||
/**
|
||||
* 列表项内容排列的方向,可选值为 horizontal 、vertical
|
||||
*/
|
||||
direction?: GridNavDirection;
|
||||
|
||||
/**
|
||||
* 列数
|
||||
*/
|
||||
columnNum?: number;
|
||||
|
||||
/**
|
||||
* 数据源: 绑定当前环境变量
|
||||
*
|
||||
* @default ${items}
|
||||
*/
|
||||
source?: SchemaTokenizeableString;
|
||||
}
|
||||
|
||||
export interface Column {
|
||||
type: string;
|
||||
[propName: string]: any;
|
||||
}
|
||||
|
||||
export interface ListProps
|
||||
extends RendererProps,
|
||||
Omit<ListSchema, 'type' | 'className'> {
|
||||
handleClick: (item?: ListItemSchema) => void;
|
||||
}
|
||||
|
||||
@Renderer({
|
||||
type: 'grid-nav'
|
||||
})
|
||||
export default class List extends React.Component<ListProps, object> {
|
||||
@autobind
|
||||
handleClick(item: ListItemSchema) {
|
||||
return (e: React.MouseEvent) => {
|
||||
let action;
|
||||
if (item.link) {
|
||||
action = validations.isUrl({}, item.link)
|
||||
? {
|
||||
type: 'button',
|
||||
actionType: 'url',
|
||||
url: item.link,
|
||||
blank: item.blank
|
||||
}
|
||||
: {
|
||||
type: 'button',
|
||||
actionType: 'link',
|
||||
link: item.link
|
||||
};
|
||||
} else {
|
||||
action = item.clickAction!;
|
||||
}
|
||||
handleAction(e, action as ActionSchema, this.props);
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const {itemClassName, source, data, options, classnames} = this.props;
|
||||
|
||||
let value = getPropValue(this.props);
|
||||
let list: any = [];
|
||||
|
||||
if (typeof source === 'string' && isPureVariable(source)) {
|
||||
list = resolveVariableAndFilter(source, data, '| raw') || undefined;
|
||||
} else if (Array.isArray(value)) {
|
||||
list = value;
|
||||
} else if (Array.isArray(options)) {
|
||||
list = options;
|
||||
}
|
||||
|
||||
if (list && !Array.isArray(list)) {
|
||||
list = [list];
|
||||
}
|
||||
|
||||
if (!list?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<GridNav {...this.props}>
|
||||
{list.map((item: ListItemSchema, index: number) => (
|
||||
<GridNavItem
|
||||
key={index}
|
||||
onClick={
|
||||
item.clickAction || item.link ? this.handleClick(item) : undefined
|
||||
}
|
||||
className={itemClassName}
|
||||
text={item.text}
|
||||
icon={item.icon}
|
||||
classnames={classnames}
|
||||
badge={
|
||||
item.badge
|
||||
? {
|
||||
badge: item.badge,
|
||||
data: data,
|
||||
classnames
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</GridNav>
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user