feat: 增加打印事件, 支持打印容器组件 Closes #9475

This commit is contained in:
wuduoyi 2024-02-25 10:46:50 +08:00
parent 4c3321e059
commit 5072f74c34
16 changed files with 265 additions and 17 deletions

View File

@ -1444,6 +1444,75 @@ run action ajax
| copyFormat | `string` | `text/html` | 复制格式 |
| content | [模板](../../docs/concepts/template) | - | 指定复制的内容。可用 `${xxx}` 取值 |
### 打印
> 6.2.0 及以后版本
打印页面中的某个组件,对应的组件需要配置 `testid`,如果要打印多个,可以使用 `"testids": ["x", "y"]` 来打印多个组件
```schema
{
type: 'page',
body: [
{
type: 'button',
label: '打印',
level: 'primary',
className: 'mr-2',
onEvent: {
click: {
actions: [
{
actionType: 'print',
args: {
testid: 'mycrud'
}
}
]
}
}
},
{
"type": "crud",
"api": "/api/mock2/sample",
"testid": "mycrud",
"syncLocation": false,
"columns": [
{
"name": "id",
"label": "ID"
},
{
"name": "engine",
"label": "Rendering engine"
},
{
"name": "browser",
"label": "Browser"
},
{
"name": "platform",
"label": "Platform(s)"
},
{
"name": "version",
"label": "Engine version"
},
{
"name": "grade",
"label": "CSS grade"
}
]
}
]
}
```
| 属性名 | 类型 | 默认值 | 说明 |
| ------- | ---------- | ------ | ----------------- |
| testid | `string` | | 组件的 testid |
| testids | `string[]` | - | 多个组件的 testid |
### 发送邮件
通过配置`actionType: 'email'`和邮件属性实现发送邮件操作。

View File

@ -0,0 +1,51 @@
import {printElements} from '../utils/printElement';
import {RendererEvent} from '../utils/renderer-event';
import {
RendererAction,
ListenerAction,
ListenerContext,
registerAction
} from './Action';
export interface IPrintAction extends ListenerAction {
actionType: 'copy';
args: {
testid?: string;
testids?: string[];
};
}
/**
*
*
* @export
* @class PrintAction
* @implements {Action}
*/
export class PrintAction implements RendererAction {
async run(
action: IPrintAction,
renderer: ListenerContext,
event: RendererEvent<any>
) {
if (action.args?.testid) {
const element = document.querySelector(
`[data-testid='${action.args.testid}']`
);
if (element) {
printElements([element]);
}
} else if (action.args?.testids) {
const elements: Element[] = [];
action.args.testids.forEach(testid => {
const element = document.querySelector(`[data-testid='${testid}']`);
if (element) {
elements.push(element);
}
});
printElements(elements);
}
}
}
registerAction('print', new PrintAction());

View File

@ -19,5 +19,6 @@ import './EmailAction';
import './LinkAction';
import './ToastAction';
import './PageAction';
import './PrintAction';
export * from './Action';

View File

@ -49,7 +49,7 @@ import LazyComponent from '../components/LazyComponent';
import {isAlive} from 'mobx-state-tree';
import type {LabelAlign} from './Item';
import {injectObjectChain} from '../utils';
import {buildTestId, injectObjectChain} from '../utils';
import {reaction} from 'mobx';
export interface FormHorizontal {
@ -1808,7 +1808,8 @@ export default class Form extends React.Component<FormProps, object> {
render,
staticClassName,
static: isStatic = false,
loadingConfig
loadingConfig,
testid
} = this.props;
const {restError} = store;
@ -1840,6 +1841,7 @@ export default class Form extends React.Component<FormProps, object> {
)}
onSubmit={this.handleFormSubmit}
noValidate
{...buildTestId(testid)}
>
{/* 实现回车自动提交 */}
<input type="submit" style={{display: 'none'}} />

View File

@ -0,0 +1,76 @@
/**
* https://github.com/szepeshazi/print-elements 里的实现
*
*
*/
const hideFromPrintClass = 'pe-no-print';
const preservePrintClass = 'pe-preserve-print';
const preserveAncestorClass = 'pe-preserve-ancestor';
const bodyElementName = 'BODY';
function hide(element: Element) {
if (!element.classList.contains(preservePrintClass)) {
element.classList.add(hideFromPrintClass);
}
}
function preserve(element: Element, isStartingElement: boolean) {
element.classList.remove(hideFromPrintClass);
element.classList.add(preservePrintClass);
if (!isStartingElement) {
element.classList.add(preserveAncestorClass);
}
}
function clean(element: Element) {
element.classList.remove(hideFromPrintClass);
element.classList.remove(preservePrintClass);
element.classList.remove(preserveAncestorClass);
}
function walkSiblings(element: Element, callback: (element: Element) => void) {
let sibling = element.previousElementSibling;
while (sibling) {
callback(sibling);
sibling = sibling.previousElementSibling;
}
sibling = element.nextElementSibling;
while (sibling) {
callback(sibling);
sibling = sibling.nextElementSibling;
}
}
function attachPrintClasses(element: Element, isStartingElement: boolean) {
preserve(element, isStartingElement);
walkSiblings(element, hide);
}
function cleanup(element: Element, isStartingElement: boolean) {
clean(element);
walkSiblings(element, clean);
}
function walkTree(
element: Element,
callback: (element: Element, isStartingElement: boolean) => void
) {
let currentElement: Element | null = element;
callback(currentElement, true);
currentElement = currentElement.parentElement;
while (currentElement && currentElement.nodeName !== bodyElementName) {
callback(currentElement, false);
currentElement = currentElement.parentElement;
}
}
export function printElements(elements: Element[]) {
for (let i = 0; i < elements.length; i++) {
walkTree(elements[i], attachPrintClasses);
}
window.print();
for (let i = 0; i < elements.length; i++) {
walkTree(elements[i], cleanup);
}
}

View File

@ -0,0 +1,13 @@
@media print {
.pe-no-print {
display: none !important;
}
.pe-preserve-ancestor {
display: block !important;
margin: 0 !important;
padding: 0 !important;
border: none !important;
box-shadow: none !important;
}
}

View File

@ -142,3 +142,5 @@
@import '../components/debug';
@import '../components/menu';
@import '../components/overflow-tpl';
@import '../components/print';

View File

@ -7,7 +7,8 @@ import {
RendererProps,
evalExpressionWithConditionBuilder,
filterTarget,
mapTree
mapTree,
buildTestId
} from 'amis-core';
import {SchemaNode, Schema, ActionObject, PlainObject} from 'amis-core';
import {CRUDStore, ICRUDStore} from 'amis-core';
@ -2516,6 +2517,7 @@ export default class CRUD extends React.Component<CRUDProps, any> {
onSearchableFromInit,
headerToolbarRender,
footerToolbarRender,
testid,
...rest
} = this.props;
@ -2526,6 +2528,7 @@ export default class CRUD extends React.Component<CRUDProps, any> {
'is-mobile': isMobile()
})}
style={style}
{...buildTestId(testid)}
>
{filter && (!store.filterTogggable || store.filterVisible)
? render(

View File

@ -30,7 +30,8 @@ import {
isApiOutdated,
isPureVariable,
resolveVariableAndFilter,
parsePrimitiveQueryString
parsePrimitiveQueryString,
buildTestId
} from 'amis-core';
import {Html, SpinnerExtraProps} from 'amis-ui';
import {
@ -1308,6 +1309,7 @@ export default class CRUD2 extends React.Component<CRUD2Props, any> {
columnsTogglable,
headerToolbarClassName,
footerToolbarClassName,
testid,
...rest
} = this.props;
@ -1317,6 +1319,7 @@ export default class CRUD2 extends React.Component<CRUD2Props, any> {
'is-loading': store.loading
})}
style={style}
{...buildTestId(testid)}
>
<div className={cx('Crud2-filter')}>
{this.renderFilter(filterSchema)}

View File

@ -195,7 +195,8 @@ export default class Container<T> extends React.Component<
wrapperCustomStyle,
env,
themeCss,
baseControlClassName
baseControlClassName,
testid
} = this.props;
const finalDraggable: boolean = isPureVariable(draggable)
? resolveVariableAndFilter(draggable, data, '| raw')
@ -231,6 +232,7 @@ export default class Container<T> extends React.Component<
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
style={buildStyle(style, data)}
{...buildTestId(testid)}
>
{this.renderBody()}
<CustomStyle

View File

@ -8,7 +8,8 @@ import {
Renderer,
RendererProps,
CustomStyle,
setThemeClassName
setThemeClassName,
buildTestId
} from 'amis-core';
import {Schema} from 'amis-core';
import {BaseSchema, SchemaCollection, SchemaObject} from '../Schema';
@ -111,7 +112,8 @@ export default class Flex extends React.Component<FlexProps, object> {
wrapperCustomStyle,
env,
themeCss,
classnames: cx
classnames: cx,
testid
} = this.props;
const styleVar = buildStyle(style, data);
const flexStyle = {
@ -150,6 +152,7 @@ export default class Flex extends React.Component<FlexProps, object> {
themeCss: wrapperCustomStyle
})
)}
{...buildTestId(testid)}
>
{(Array.isArray(items) ? items : items ? [items] : []).map(
(item, key) =>

View File

@ -5,7 +5,8 @@ import {
RendererProps,
buildStyle,
CustomStyle,
setThemeClassName
setThemeClassName,
buildTestId
} from 'amis-core';
import pick from 'lodash/pick';
import {BaseSchema, SchemaClassName, SchemaCollection} from '../Schema';
@ -212,7 +213,8 @@ export default class Grid<T> extends React.Component<GridProps & T, object> {
id,
wrapperCustomStyle,
env,
themeCss
themeCss,
testid
} = this.props;
const styleVar = buildStyle(style, data);
return (
@ -239,6 +241,7 @@ export default class Grid<T> extends React.Component<GridProps & T, object> {
})
)}
style={styleVar}
{...buildTestId(testid)}
>
{this.renderColumns(this.props.columns)}
<Spinner loadingConfig={loadingConfig} overlay show={loading} />

View File

@ -1,5 +1,5 @@
import React from 'react';
import {Renderer, RendererProps} from 'amis-core';
import {buildTestId, Renderer, RendererProps} from 'amis-core';
import {Api, SchemaNode, Schema, ActionObject} from 'amis-core';
import {isVisible} from 'amis-core';
import {BaseSchema, SchemaObject} from '../Schema';
@ -172,7 +172,8 @@ export default class Grid2D extends React.Component<Grid2DProps, object> {
}
render() {
const {grids, cols, gap, gapRow, width, rowHeight, style} = this.props;
const {grids, cols, gap, gapRow, width, rowHeight, style, testid} =
this.props;
const templateColumns = new Array(cols);
templateColumns.fill('1fr');
@ -214,7 +215,11 @@ export default class Grid2D extends React.Component<Grid2DProps, object> {
gridTemplateRows: templateRows.join(' ')
};
return <div style={curStyle}>{this.renderGrids()}</div>;
return (
<div style={curStyle} {...buildTestId(testid)}>
{this.renderGrids()}
</div>
);
}
}

View File

@ -41,7 +41,8 @@ import {
resizeSensor,
offset,
getStyleNumber,
getPropValue
getPropValue,
buildTestId
} from 'amis-core';
import {
Button,
@ -2802,7 +2803,8 @@ export default class Table extends React.Component<TableProps, object> {
affixHeader,
autoFillHeight,
autoGenerateFilter,
mobileUI
mobileUI,
testid
} = this.props;
this.renderedToolbars = []; // 用来记录哪些 toolbar 已经渲染了,已经渲染了就不重复渲染了。
@ -2821,6 +2823,7 @@ export default class Table extends React.Component<TableProps, object> {
'Table--autoFillHeight': autoFillHeight
})}
style={store.buildStyles(style)}
{...buildTestId(testid)}
>
{autoGenerateFilter ? this.renderAutoFilterForm() : null}
{this.renderAffixHeader(tableClassName)}

View File

@ -8,7 +8,8 @@ import {
RendererProps,
resolveMappingObject,
CustomStyle,
setThemeClassName
setThemeClassName,
buildTestId
} from 'amis-core';
import {BaseSchema, SchemaObject} from '../Schema';
@ -276,6 +277,7 @@ export default class TableView extends React.Component<TableViewProps, object> {
wrapperCustomStyle,
env,
themeCss,
testid,
baseControlClassName
} = this.props;
@ -298,6 +300,7 @@ export default class TableView extends React.Component<TableViewProps, object> {
})
)}
style={{width: width, borderCollapse: 'collapse'}}
{...buildTestId(testid)}
>
{this.renderCaption()}
{this.renderCols()}

View File

@ -1,5 +1,5 @@
import React from 'react';
import {Renderer, RendererProps} from 'amis-core';
import {buildTestId, Renderer, RendererProps} from 'amis-core';
import {BaseSchema, SchemaCollection} from '../Schema';
import {resolveVariable} from 'amis-core';
import {SchemaNode} from 'amis-core';
@ -59,7 +59,15 @@ export default class Wrapper extends React.Component<WrapperProps, object> {
}
render() {
const {className, size, classnames: cx, style, data, wrap} = this.props;
const {
className,
size,
classnames: cx,
style,
data,
wrap,
testid
} = this.props;
// 期望不要使用,给 form controls 用法自动转换时使用的。
if (wrap === false) {
@ -74,6 +82,7 @@ export default class Wrapper extends React.Component<WrapperProps, object> {
className
)}
style={buildStyle(style, data)}
{...buildTestId(testid)}
>
{this.renderBody()}
</div>