mirror of
https://gitee.com/baidu/amis.git
synced 2024-11-30 02:48:55 +08:00
feat: 增加打印事件, 支持打印容器组件 Closes #9475
This commit is contained in:
parent
4c3321e059
commit
5072f74c34
@ -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'`和邮件属性实现发送邮件操作。
|
||||
|
51
packages/amis-core/src/actions/PrintAction.ts
Normal file
51
packages/amis-core/src/actions/PrintAction.ts
Normal 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());
|
@ -19,5 +19,6 @@ import './EmailAction';
|
||||
import './LinkAction';
|
||||
import './ToastAction';
|
||||
import './PageAction';
|
||||
import './PrintAction';
|
||||
|
||||
export * from './Action';
|
||||
|
@ -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'}} />
|
||||
|
76
packages/amis-core/src/utils/printElement.ts
Normal file
76
packages/amis-core/src/utils/printElement.ts
Normal 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);
|
||||
}
|
||||
}
|
13
packages/amis-ui/scss/components/_print.scss
Normal file
13
packages/amis-ui/scss/components/_print.scss
Normal 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;
|
||||
}
|
||||
}
|
@ -142,3 +142,5 @@
|
||||
@import '../components/debug';
|
||||
@import '../components/menu';
|
||||
@import '../components/overflow-tpl';
|
||||
|
||||
@import '../components/print';
|
||||
|
@ -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(
|
||||
|
@ -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)}
|
||||
|
@ -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
|
||||
|
@ -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) =>
|
||||
|
@ -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} />
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)}
|
||||
|
@ -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()}
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user