feat: 美化crud2移动端的卡片样式,支持查询表单折叠

This commit is contained in:
jinye 2024-10-14 17:32:08 +08:00 committed by lmaomaoz
parent 46a2dd0ad4
commit c7039f7376
9 changed files with 307 additions and 94 deletions

View File

@ -878,4 +878,8 @@ $Table-strip-bg: transparent;
:root {
--fontSizeBase: var(--fontSizeLg);
}
:root,
.AMISCSSWrapper {
--Page-body-padding: var(--gap-md);
}
}

View File

@ -30,21 +30,21 @@
font-weight: var(--Pick-base-value-fontWeight);
background: var(--Pick-base-value-bgColor);
border-width: var(--Pick-base-value-top-border-width)
var(--Pick-base-value-right-border-width)
var(--Pick-base-value-bottom-border-width)
var(--Pick-base-value-left-border-width);
var(--Pick-base-value-right-border-width)
var(--Pick-base-value-bottom-border-width)
var(--Pick-base-value-left-border-width);
border-style: var(--Pick-base-value-top-border-style)
var(--Pick-base-value-right-border-style)
var(--Pick-base-value-bottom-border-style)
var(--Pick-base-value-left-border-style);
var(--Pick-base-value-right-border-style)
var(--Pick-base-value-bottom-border-style)
var(--Pick-base-value-left-border-style);
border-color: var(--Pick-base-value-top-border-color)
var(--Pick-base-value-right-border-color)
var(--Pick-base-value-bottom-border-color)
var(--Pick-base-value-left-border-color);
var(--Pick-base-value-right-border-color)
var(--Pick-base-value-bottom-border-color)
var(--Pick-base-value-left-border-color);
border-radius: var(--Pick-base-top-left-border-radius)
var(--Pick-base-top-right-border-radius)
var(--Pick-base-bottom-right-border-radius)
var(--Pick-base-bottom-left-border-radius);
var(--Pick-base-top-right-border-radius)
var(--Pick-base-bottom-right-border-radius)
var(--Pick-base-bottom-left-border-radius);
margin-right: var(--gap-xs);
margin-top: var(--gap-xs);
@ -133,6 +133,111 @@
&-filter {
margin-bottom: var(--gap-base);
}
&.is-mobile-cards {
// 移动端卡片模式不需要列选择器
.#{$ns}ColumnToggler {
display: none;
}
.#{$ns}SearchBox {
border-radius: var(--Form-input-borderRadius);
width: 100%;
}
.#{$ns}Card {
--Card-borderRadius: var(--sizes-size-5);
--gap-base: var(--sizes-size-9);
--fontSizeBase: var(--fonts-size-7);
--body-lineHeight: var(--sizes-base-11);
--Card-actions-borderColor: #f2f2f4;
--Card-actions-fontSize: var(--fontSizeBase);
font-size: var(--fontSizeBase);
border: 0;
box-shadow: var(--boxShadowSm);
&-field {
margin-bottom: var(--sizes-size-3);
}
&-fieldLabel {
color: var(--colors-neutral-text-5);
font-size: var(--fontSizeBase);
flex-basis: var(--sizes-base-28);
line-height: var(--body-lineHeight);
margin-right: var(--sizes-size-6);
}
&-fieldValue {
color: var(--colors-neutral-text-2);
}
&-actions {
&-wrapper {
padding-left: var(--gap-md);
padding-right: var(--gap-md);
}
& > a {
height: var(--sizes-base-12);
line-height: var(--sizes-base-12);
margin-top: var(--sizes-size-4);
margin-bottom: var(--sizes-size-4);
}
}
.#{$ns}Form-item {
.#{$ns}Form-value,
.#{$ns}Form-control {
font-size: var(--fontSizeBase);
}
}
&-multiMedia {
&--right {
align-items: flex-start;
}
&-img {
width: var(--sizes-base-45);
height: var(--sizes-base-45);
}
}
.#{$ns}Image {
border: 0;
&--thumb {
padding-left: 0;
img {
border-radius: var(--sizes-size-5);
}
}
}
}
.#{$ns}Panel {
--Panel-bodyPadding: var(--gap-md);
--Panel-headingPadding: var(--gap-sm) var(--gap-md);
--Panel-body-paddingTop: var(--gap-md);
--Panel-body-paddingBottom: var(--gap-md);
--Panel-body-paddingLeft: var(--gap-md);
--Panel-body-paddingRight: var(--gap-md);
&--form {
margin: 0;
margin-bottom: var(--gap-md);
border-radius: var(--sizes-size-5);
}
&-body {
padding-top: 0;
}
.#{$ns}Form {
&--column {
margin-left: 0;
margin-right: 0;
}
&-item {
padding-left: 0;
padding-right: 0;
}
}
}
}
}
@include media-breakpoint-up(sm) {

View File

@ -256,6 +256,7 @@
min-height: 0;
flex: 1;
height: px2rem(44px);
font-size: var(--fontSizeMd);
&:first-child {
margin-left: 0;

View File

@ -145,6 +145,18 @@
var(--Panel-heading-bottom-border-width)
var(--Panel-heading-left-border-width);
border-radius: var(--Panel-headingBorderRadius);
&.is-collapsible {
display: flex;
justify-content: space-between;
}
}
&-arrow {
transition: transform 0.1s ease-in;
&.is-collapsed {
transform: rotate(180deg);
}
}
&-title {
@ -219,8 +231,10 @@
border-radius: 0;
.#{$ns}Panel-title {
padding-left: var(--Panel-body-paddingLeft);
border-left: px2rem(3px) solid var(--primary);
.icon {
width: var(--sizes-base-7);
height: var(--sizes-base-7);
}
font-size: var(--fontSizeLg);
}
}

View File

@ -20,6 +20,7 @@
text-align: center;
color: #999;
margin-bottom: 12px;
font-size: var(--fontSizeMd);
}
.loading-icon {

View File

@ -655,7 +655,6 @@
overflow-wrap: break-word;
margin-right: 0;
margin-bottom: 0;
font-size: var(--fontSizeLg);
& + .#{$ns}Form-item-controlBox {
max-width: calc(100% - 28%);
@ -672,10 +671,8 @@
}
.#{$ns}TextControl-input {
font-size: var(--fontSizeLg);
input {
height: calc(var(--Form-input-lineHeight) * var(--fontSizeLg));
height: calc(var(--Form-input-lineHeight) * var(--Form-item-fontSize));
}
}
@ -744,7 +741,6 @@
.#{$ns}Form-control {
flex: 1;
flex-wrap: wrap;
font-size: var(--fontSizeLg);
min-width: 0;
.#{$ns}ColorPicker {
@ -865,6 +861,7 @@
.#{$ns}TextareaControl > textarea,
.#{$ns}Form-control > .#{$ns}TextControl-input,
.#{$ns}Number-input,
.#{$ns}TextControl.is-focused > .#{$ns}TextControl-input {
border: none;
padding: 0 var(--Form-input-paddingX) 0 0;

View File

@ -17,6 +17,7 @@ export interface CardProps extends ThemeProps {
footerClassName?: string;
media?: React.ReactNode;
mediaPosition?: 'top' | 'left' | 'right' | 'bottom';
mediaActionPosition?: 'outside';
toolbar?: React.ReactNode;
children?: React.ReactNode;
actions?: React.ReactNode;
@ -80,6 +81,7 @@ export class Card extends React.Component<CardProps> {
footerClassName,
media,
mediaPosition,
mediaActionPosition,
actions,
children,
onClick,
@ -150,6 +152,20 @@ export class Card extends React.Component<CardProps> {
const body = children;
const actionView =
secondary || actions ? (
<div className={cx('Card-footer-wrapper', footerClassName)}>
{secondary ? (
<div className={cx('Card-secondary', secondaryClassName)}>
{secondary}
</div>
) : null}
{actions ? (
<div className={cx('Card-actions-wrapper')}>{actions}</div>
) : null}
</div>
) : null;
return (
<div
onClick={this.handleClick}
@ -159,45 +175,26 @@ export class Card extends React.Component<CardProps> {
style={style}
>
{media ? (
<div className={cx(`Card-multiMedia--${mediaPosition}`)}>
{media}
<div className={cx('Card-multiMedia-flex')}>
{heading}
{body ? (
<div className={cx('Card-body', bodyClassName)}>{body}</div>
) : null}
{secondary || actions ? (
<div className={cx('Card-footer-wrapper', footerClassName)}>
{secondary ? (
<div className={cx('Card-secondary', secondaryClassName)}>
{secondary}
</div>
) : null}
{actions ? (
<div className={cx('Card-actions-wrapper')}>{actions}</div>
) : null}
</div>
) : null}
<>
<div className={cx(`Card-multiMedia--${mediaPosition}`)}>
{media}
<div className={cx('Card-multiMedia-flex')}>
{heading}
{body ? (
<div className={cx('Card-body', bodyClassName)}>{body}</div>
) : null}
{!mediaActionPosition ? actionView : null}
</div>
</div>
</div>
{mediaActionPosition === 'outside' ? actionView : null}
</>
) : (
<>
{heading}
{body ? (
<div className={cx('Card-body', bodyClassName)}>{body}</div>
) : null}
{secondary || actions ? (
<div className={cx('Card-footer-wrapper', footerClassName)}>
{secondary ? (
<div className={cx('Card-secondary', secondaryClassName)}>
{secondary}
</div>
) : null}
{actions ? (
<div className={cx('Card-actions-wrapper')}>{actions}</div>
) : null}
</div>
) : null}
{actionView}
</>
)}
</div>

View File

@ -1276,7 +1276,26 @@ export default class CRUD2 extends React.Component<CRUD2Props, any> {
replaceQuery: true,
resetPage: true
});
}
},
// 移动端的查询表单支持折叠
...(this.props.mobileUI
? {
columnCount: 1,
mode: 'normal',
collapsible: true,
title: {
type: 'container',
body: [
{
type: 'icon',
icon: 'column-filter',
className: 'icon mr-2'
},
(item as any).title || ''
]
}
}
: {})
})
);
}
@ -1335,14 +1354,20 @@ export default class CRUD2 extends React.Component<CRUD2Props, any> {
}
transformTable2cards() {
const {store, columns, card} = this.props;
const {store, columns: propsColumns, card, mobileMode} = this.props;
const body: any[] = [];
const fieldCount = mobileMode.fieldCount || 4;
const actions: any[] = [];
let cover: string = '';
((store.columns ?? columns) || []).forEach((item: any) => {
const columns = (store.columns ?? propsColumns) || [];
for (let index = 0; index < columns.length; index++) {
const item = columns[index];
if (!isPlainObject(item)) {
return;
} else if (item.type === 'operation') {
continue;
}
if (item.type === 'operation') {
actions.push(...(item?.buttons || []));
} else if (item.type === 'button' && item.name === 'operation') {
actions.push(item);
@ -1350,9 +1375,20 @@ export default class CRUD2 extends React.Component<CRUD2Props, any> {
if (!item.label && item.title) {
item.label = item.title;
}
body.push(item);
if (item.type === 'static-image' && !cover) {
cover = `\${${item.name}}`;
continue;
}
if (body.length < fieldCount) {
if (item.type === 'static-image' && item.title) {
delete item.title;
}
body.push(item);
}
}
});
}
if (!body.length) {
return null;
@ -1364,7 +1400,18 @@ export default class CRUD2 extends React.Component<CRUD2Props, any> {
card: {
...card,
body,
actions
actions,
...(cover
? {
media: {
type: 'image',
url: cover,
position: 'right',
className: ''
},
mediaActionPosition: 'outside'
}
: {})
}
};
}
@ -1420,13 +1467,20 @@ export default class CRUD2 extends React.Component<CRUD2Props, any> {
let mobileModeProps: any = {};
if (mobileMode && mobileUI && mode.includes('table')) {
const cardsSchema = this.transformTable2cards();
if (typeof mobileMode === 'string' && mobileMode === 'cards') {
const cardsSchema = this.transformTable2cards();
if (cardsSchema) {
mobileModeProps = cardsSchema;
}
} else if (typeof mobileMode === 'object') {
mobileModeProps = {...mobileMode};
mobileModeProps = {
...cardsSchema,
...mobileMode,
card: {
...cardsSchema?.card,
...mobileMode.card
}
};
}
// 移动端模式,默认开启上拉刷新
if (mobileModeProps && !_pullRefresh?.disabled) {
@ -1500,7 +1554,10 @@ export default class CRUD2 extends React.Component<CRUD2Props, any> {
return (
<div
className={cx('Crud2', className, {
'is-loading': store.loading
'is-loading': store.loading,
'is-mobile': mobileUI,
'is-mobile-cards':
mobileMode === 'cards' || mobileModeProps.type === 'cards'
})}
style={style}
data-id={id}

View File

@ -14,6 +14,7 @@ import {
import {ActionSchema} from './Action';
import {FormHorizontal} from 'amis-core';
import omit from 'lodash/omit';
import {Icon} from 'amis-ui';
/**
* Panel渲染器
@ -80,6 +81,11 @@ export interface PanelSchema extends BaseSchema {
*/
affixFooter?: boolean | 'always';
/**\
*
*/
collapsible?: boolean;
/**
*
*/
@ -123,6 +129,10 @@ export default class Panel extends React.Component<PanelProps> {
// bodyClassName: 'Panel-body'
};
state = {
collapsed: false
};
renderBody(): JSX.Element | null {
const {
type,
@ -205,6 +215,7 @@ export default class Panel extends React.Component<PanelProps> {
classPrefix: ns,
classnames: cx,
id,
collapsible,
...rest
} = this.props;
@ -214,33 +225,37 @@ export default class Panel extends React.Component<PanelProps> {
};
const footerDoms = [];
const actions = this.renderActions();
actions &&
footerDoms.push(
<div
key="actions"
className={cx(
`Panel-btnToolbar`,
actionsClassName || `Panel-footer`,
actionsControlClassName
)}
>
{actions}
</div>
);
const collapsed = this.state.collapsed;
footer &&
footerDoms.push(
<div
key="footer"
className={cx(
footerClassName || `Panel-footer`,
actionsControlClassName
)}
>
{render('footer', footer, subProps)}
</div>
);
if (!collapsed) {
const actions = this.renderActions();
actions &&
footerDoms.push(
<div
key="actions"
className={cx(
`Panel-btnToolbar`,
actionsClassName || `Panel-footer`,
actionsControlClassName
)}
>
{actions}
</div>
);
footer &&
footerDoms.push(
<div
key="footer"
className={cx(
footerClassName || `Panel-footer`,
actionsControlClassName
)}
>
{render('footer', footer, subProps)}
</div>
);
}
let footerDom = footerDoms.length ? (
<div
@ -273,20 +288,42 @@ export default class Panel extends React.Component<PanelProps> {
<div
className={cx(
headerClassName || `Panel-heading`,
headerControlClassName
headerControlClassName,
{
'is-collapsible': collapsible
}
)}
>
<h3 className={cx(`Panel-title`, headerTitleControlClassName)}>
{render('title', title, subProps)}
</h3>
{collapsible ? (
<span
className={cx('Panel-arrow-wrap')}
onClick={() => {
this.setState({
collapsed: !collapsed
});
}}
>
<Icon
icon="down-arrow-bold"
className={cx('Panel-arrow', 'icon', {
'is-collapsed': collapsed
})}
/>
</span>
) : null}
</div>
) : null}
<div
className={cx(bodyClassName || `Panel-body`, bodyControlClassName)}
>
{this.renderBody()}
</div>
{!collapsed ? (
<div
className={cx(bodyClassName || `Panel-body`, bodyControlClassName)}
>
{this.renderBody()}
</div>
) : null}
{footerDom}
</div>