diff --git a/docs/zh-CN/components/form/input-range.md b/docs/zh-CN/components/form/input-range.md index e93d7d7aaeed..87e4bf2fc060 100755 --- a/docs/zh-CN/components/form/input-range.md +++ b/docs/zh-CN/components/form/input-range.md @@ -230,6 +230,29 @@ order: 38 } ``` +## 显示单位 + +在打开`showInput`输入框且设置了`unit`单位的前提下,开启`showInputUnit`可在input框中显示已配置的单位。 + +```schema: scope="body" +{ + "type": "form", + "debug": true, + "api": "/api/mock2/form/saveForm", + "body": [ + { + "type": "input-range", + "name": "range", + "label": "range", + "value": 20, + "unit": "个", + "showInput": true, + "showInputUnit": true + } + ] +} +``` + ## 显示标签 标签默认在 hover 和拖拽过程中展示,通过`tooltipVisible`或者`tipFormatter`可指定标签是否展示。标签默认展示在滑块上方,通过`tooltipPlacement`可指定标签展示的位置。 @@ -288,17 +311,18 @@ order: 38 | showSteps | `boolean` | `false` | 是否显示步长 | | parts | `number` or `number[]` | `1` | 分割的块数
主持数组传入分块的节点 | | marks | { [number | string]: string | number | SchemaObject } or { [number | string]: { style: CSSProperties, label: string } } | | 刻度标记
- 支持自定义样式
- 设置百分比 | -| tooltipVisible | `boolean` | `false` | 是否显示滑块标签 | -| tooltipPlacement | `auto` or `bottom` or `left` or `right` | `top` | 滑块标签的位置,默认`auto`,方向自适应
前置条件:tooltipVisible 不为 false 时有效 | -| tipFormatter | `function` | | 控制滑块标签显隐函数
前置条件:tooltipVisible 不为 false 时有效 | -| multiple | `boolean` | `false` | 支持选择范围 | -| joinValues | `boolean` | `true` | 默认为 `true`,选择的 `value` 会通过 `delimiter` 连接起来,否则直接将以`{min: 1, max: 100}`的形式提交
前置条件:开启`multiple`时有效 | -| delimiter | `string` | `,` | 分隔符 | -| unit | `string` | | 单位 | -| clearable | `boolean` | `false` | 是否可清除
前置条件:开启`showInput`时有效 | -| showInput | `boolean` | `false` | 是否显示输入框 | -| onChange | `function` | | 当 组件 的值发生改变时,会触发 onChange 事件,并把改变后的值作为参数传入 | -| onAfterChange | `function` | | 与 `onmouseup` 触发时机一致,把当前值作为参数传入 | +| tooltipVisible | `boolean` | `false` | 是否显示滑块标签 | +| tooltipPlacement | `auto` or `bottom` or `left` or `right` | `top` | 滑块标签的位置,默认`auto`,方向自适应
前置条件:tooltipVisible 不为 false 时有效 | +| tipFormatter | `function` | | 控制滑块标签显隐函数
前置条件:tooltipVisible 不为 false 时有效 | +| multiple | `boolean` | `false` | 支持选择范围 | +| joinValues | `boolean` | `true` | 默认为 `true`,选择的 `value` 会通过 `delimiter` 连接起来,否则直接将以`{min: 1, max: 100}`的形式提交
前置条件:开启`multiple`时有效 | +| delimiter | `string` | `,` | 分隔符 | +| unit | `string` | | 单位 | +| clearable | `boolean` | `false` | 是否可清除
前置条件:开启`showInput`时有效 | +| showInput | `boolean` | `false` | 是否显示输入框 | +| showInputUnit | `boolean` | `false` | 是否显示输入框单位
前置条件:开启`showInput`且配置了`unit`单位时有效 |`6.0.0`后支持变量 +| onChange | `function` | | 当 组件 的值发生改变时,会触发 onChange 事件,并把改变后的值作为参数传入 | +| onAfterChange | `function` | | 与 `onmouseup` 触发时机一致,把当前值作为参数传入 | ## 事件表 diff --git a/docs/zh-CN/components/progress.md b/docs/zh-CN/components/progress.md index b4c055c2d5ac..ebe753908fbb 100755 --- a/docs/zh-CN/components/progress.md +++ b/docs/zh-CN/components/progress.md @@ -316,3 +316,75 @@ List 的内容、Card 卡片的内容配置同上 | strokeWidth | `number` | line 类型为`10`,circle、dashboard 类型为`6` | 进度条线宽度 | | gapDegree | `number` | `75` | 仪表盘缺角角度,可取值 0 ~ 295 | | gapPosition | `string` | `bottom` | 仪表盘进度条缺口位置,可选`top bottom left right` | + +## 动作表 + +当前组件对外暴露以下特性动作,其他组件可以通过指定`actionType: 动作名称`、`componentId: 该组件id`来触发这些动作,动作配置可以通过`args: {动作配置项名称: xxx}`来配置具体的参数,详细请查看[事件动作](../../docs/concepts/event-action#触发其他组件的动作)。 + +| 动作名称 | 动作配置 | 说明 | +| -------- | ------------------------------------ | ------------ | +| reset | - | 将值重置为 0 | +| setValue | `value: string` \| `number` 更新的值 | 更新数据 | + +### reset + +```schema: scope="body" +{ + "type": "page", + "body": [ + { + "type": "progress", + "name": "progress", + "id": "progress", + "value": 67 + }, + { + "type": "button", + "label": "重置值", + "onEvent": { + "click": { + "actions": [ + { + "actionType": "reset", + "componentId": "progress" + } + ] + } + } + } + ] +} +``` + +### setValue + +```schema: scope="body" +{ + "type": "page", + "body": [ + { + "type": "progress", + "name": "progress", + "id": "progress", + "value": 67 + }, + { + "type": "button", + "label": "设置值", + "onEvent": { + "click": { + "actions": [ + { + "actionType": "setValue", + "componentId": "progress", + "args": { + "value": 20 + } + } + ] + } + } + } + ] +} +``` diff --git a/packages/amis-editor-core/scss/editor.scss b/packages/amis-editor-core/scss/editor.scss index 3363e3824ce7..dce5ee479ff8 100644 --- a/packages/amis-editor-core/scss/editor.scss +++ b/packages/amis-editor-core/scss/editor.scss @@ -229,8 +229,12 @@ z-index: 0; } - > .cxd-Page > .cxd-Page-content { - height: auto; + > .cxd-Page { + // 这里主要是为了确保能撑开画布区 + display: flex; + > .cxd-Page-content { + height: auto; + } } } } diff --git a/packages/amis-editor/src/plugin/Progress.tsx b/packages/amis-editor/src/plugin/Progress.tsx index 5fe38087ec34..46ff7cdd523b 100644 --- a/packages/amis-editor/src/plugin/Progress.tsx +++ b/packages/amis-editor/src/plugin/Progress.tsx @@ -1,4 +1,4 @@ -import {registerEditorPlugin} from 'amis-editor-core'; +import {registerEditorPlugin, RendererPluginAction} from 'amis-editor-core'; import {BaseEventContext, BasePlugin} from 'amis-editor-core'; import {defaultValue, getSchemaTpl} from 'amis-editor-core'; import {tipedLabel} from 'amis-editor-core'; @@ -32,6 +32,20 @@ export class ProgressPlugin extends BasePlugin { ...this.scaffold }; + // 动作定义 + actions: RendererPluginAction[] = [ + { + actionType: 'reset', + actionLabel: '重置', + description: '重置为默认值' + }, + { + actionType: 'setValue', + actionLabel: '赋值', + description: '触发组件数据更新' + } + ]; + panelTitle = '进度'; panelJustify = true; diff --git a/packages/amis-ui/scss/components/form/_range.scss b/packages/amis-ui/scss/components/form/_range.scss index c235ccac764e..b0b0d5378f30 100644 --- a/packages/amis-ui/scss/components/form/_range.scss +++ b/packages/amis-ui/scss/components/form/_range.scss @@ -40,6 +40,68 @@ } } + &-input-with-unit { + display: flex; + width: auto; + + &:hover { + .#{$ns}Number, + .#{$ns}InputRange-unit { + border-width: var(--inputNumber-base-hover-top-border-width) + var(--inputNumber-base-hover-right-border-width) + var(--inputNumber-base-hover-bottom-border-width) + var(--inputNumber-base-hover-left-border-width); + border-style: var(--inputNumber-base-hover-top-border-style) + var(--inputNumber-base-hover-right-border-style) + var(--inputNumber-base-hover-bottom-border-style) + var(--inputNumber-base-hover-left-border-style); + border-color: var(--inputNumber-base-hover-top-border-color) + var(--inputNumber-base-hover-right-border-color) + var(--inputNumber-base-hover-bottom-border-color) + var(--inputNumber-base-hover-left-border-color); + border-radius: var(--inputNumber-base-hover-top-left-border-radius) + var(--inputNumber-base-hover-top-right-border-radius) + var(--inputNumber-base-hover-bottom-right-border-radius) + var(--inputNumber-base-hover-bottom-left-border-radius); + } + } + + .#{$ns}Number-focused + .#{$ns}InputRange-unit { + border-width: var(--inputNumber-base-active-top-border-width) + var(--inputNumber-base-active-right-border-width) + var(--inputNumber-base-active-bottom-border-width) + var(--inputNumber-base-active-left-border-width); + border-style: var(--inputNumber-base-active-top-border-style) + var(--inputNumber-base-active-right-border-style) + var(--inputNumber-base-active-bottom-border-style) + var(--inputNumber-base-active-left-border-style); + border-color: var(--inputNumber-base-active-top-border-color) + var(--inputNumber-base-active-right-border-color) + var(--inputNumber-base-active-bottom-border-color) + var(--inputNumber-base-active-left-border-color); + border-radius: var(--inputNumber-base-active-top-left-border-radius) + var(--inputNumber-base-active-top-right-border-radius) + var(--inputNumber-base-active-bottom-right-border-radius) + var(--inputNumber-base-active-bottom-left-border-radius); + } + + .#{$ns}Number { + width: px2rem(80px); + border-top-right-radius: 0 !important; + border-bottom-right-radius: 0 !important; + border-right: none !important; + } + + .#{$ns}InputRange-unit { + cursor: default; + text-align: center; + min-width: unset; + border-top-left-radius: 0 !important; + border-bottom-left-radius: 0 !important; + background-color: var(--inputNumber-base-unit-bg-color); + } + } + &.is-mobile { .#{$ns}InputRange-input { width: var(--InputRange-input-mobile-width); @@ -234,6 +296,10 @@ border-radius: var(--InputRange-label-border-radius); visibility: hidden; + span { + word-break: keep-all; + } + &-visible { visibility: visible; } @@ -330,6 +396,22 @@ top: 0; transform: translateX(-50%); + &:first-child { + transform: translateX(-10%); + span { + left: 0; + transform: translateX(0%); + } + } + + &:last-child { + transform: translateX(-90%); + span { + left: 100%; + transform: translateX(-100%); + } + } + span { position: absolute; left: 50%; diff --git a/packages/amis-ui/src/components/Progress.tsx b/packages/amis-ui/src/components/Progress.tsx index c0e3d900f55e..ab9da76763c7 100644 --- a/packages/amis-ui/src/components/Progress.tsx +++ b/packages/amis-ui/src/components/Progress.tsx @@ -209,7 +209,11 @@ export class Progress extends React.Component { ]; } - return
{viewValue}
; + return ( +
+ {viewValue} +
+ ); } } diff --git a/packages/amis/__tests__/renderers/Form/__snapshots__/range.test.tsx.snap b/packages/amis/__tests__/renderers/Form/__snapshots__/range.test.tsx.snap index f6dee72b42dc..88a042df4ecd 100644 --- a/packages/amis/__tests__/renderers/Form/__snapshots__/range.test.tsx.snap +++ b/packages/amis/__tests__/renderers/Form/__snapshots__/range.test.tsx.snap @@ -1236,7 +1236,7 @@ exports[`Renderer:range with multiple & clearable & delimiter 1`] = ` `; -exports[`Renderer:range with showInput 1`] = ` +exports[`Renderer:range with showInput & showInputUnit 1`] = `
- 7 + 7个
+
+ 个 +
{ +test('Renderer:range with showInput & showInputUnit', async () => { const {container} = render( amisRender( { @@ -27,7 +27,9 @@ test('Renderer:range with showInput', async () => { type: 'input-range', name: 'range', value: 10, - showInput: true + showInput: true, + showInputUnit: true, + unit: '个' } ], title: 'The form', @@ -50,6 +52,7 @@ test('Renderer:range with showInput', async () => { container.querySelector('.cxd-InputRange-input') as Element ).toBeInTheDocument(); + const inputWrapper = container.querySelector('.cxd-InputRange-input'); const input = container.querySelector('.cxd-InputRange-input input'); fireEvent.change(input!, { target: { @@ -64,6 +67,7 @@ test('Renderer:range with showInput', async () => { ).getAttribute('style') ).toContain('width: 7%'); + expect(inputWrapper).toHaveClass('cxd-InputRange-input-with-unit'); expect(container).toMatchSnapshot(); }); diff --git a/packages/amis/__tests__/renderers/Progress.test.tsx b/packages/amis/__tests__/renderers/Progress.test.tsx index 0c601b2f9da9..059e6cfae793 100644 --- a/packages/amis/__tests__/renderers/Progress.test.tsx +++ b/packages/amis/__tests__/renderers/Progress.test.tsx @@ -9,10 +9,12 @@ * 6. 环形模式和仪表盘样式 * 7. 线条宽度 strokeWidth * 8. 自定义 value 显示 valueTpl + * 9. 事件动作 */ import React from 'react'; -import {render} from '@testing-library/react'; +import {fireEvent, render} from '@testing-library/react'; +import {act} from 'react-test-renderer'; import '../../src'; import {render as amisRender} from '../../src'; import {makeEnv, wait} from '../helper'; @@ -249,3 +251,61 @@ test('Renderer:Progress with valueTpl', async () => { expect(container).toHaveTextContent('67个'); expect(container).toMatchSnapshot(); }); + +test('9.Renderer:Process reset and setValue actions', async () => { + const {container, getByText, rerender} = render( + amisRender({ + type: 'page', + body: [ + { + type: 'progress', + name: 'progress', + id: 'progress', + value: 67 + }, + { + type: 'button', + label: '重置值', + onEvent: { + click: { + actions: [ + { + actionType: 'reset', + componentId: 'progress' + } + ] + } + } + }, + { + type: 'button', + label: '设置值', + onEvent: { + click: { + actions: [ + { + actionType: 'setValue', + componentId: 'progress', + args: { + value: 20 + } + } + ] + } + } + } + ] + }) + ); + + fireEvent.click(getByText('重置值')); + expect(container.querySelector('.cxd-Progress-line-bar')).toHaveStyle({ + width: '0%' + }); + + await wait(200); + fireEvent.click(getByText('设置值')); + expect(container.querySelector('.cxd-Progress-line-bar')).toHaveStyle({ + width: '20%' + }); +}); diff --git a/packages/amis/src/renderers/Form/InputNumber.tsx b/packages/amis/src/renderers/Form/InputNumber.tsx index 6f2910ba4a62..8e98a3e85b22 100644 --- a/packages/amis/src/renderers/Form/InputNumber.tsx +++ b/packages/amis/src/renderers/Form/InputNumber.tsx @@ -187,7 +187,7 @@ export default class NumberControl extends React.Component< this.handleChangeUnit = this.handleChangeUnit.bind(this); const unit = this.getUnit(); const unitOptions = normalizeOptions(props.unitOptions); - const {formItem, setPrinstineValue, precision, step, value} = props; + const {formItem, setPrinstineValue, precision, step, value, big} = props; const normalizedPrecision = NumberInput.normalizePrecision( this.filterNum(precision), this.filterNum(step) @@ -201,7 +201,9 @@ export default class NumberControl extends React.Component< formItem && value != null && normalizedPrecision != null && - (!unit || unitOptions.length === 0) + (!unit || unitOptions.length === 0) && + // 大数下不需要进行精度处理,因为输入输出都是字符串 + big !== true ) { const normalizedValue = parseFloat( toFixed(value.toString(), '.', normalizedPrecision) @@ -381,14 +383,17 @@ export default class NumberControl extends React.Component< componentDidUpdate(prevProps: NumberProps) { const unit = this.getUnit(); - const value = this.props.value; + const {value, formInited, onChange, setPrinstineValue} = this.props; if ( value != null && (typeof value === 'string' || typeof value === 'number') && unit && !String(value).endsWith(unit) ) { - this.props.setPrinstineValue(this.getValue(value)); + const finalValue = this.getValue(value); + formInited === false + ? setPrinstineValue?.(finalValue) + : onChange?.(finalValue); } // 匹配 数字 + ?字符 const reg = /^([-+]?(([1-9]\d*\.?\d*)|(0\.\d*[1-9]))[^\d\.]*)$/; diff --git a/packages/amis/src/renderers/Form/InputRange.tsx b/packages/amis/src/renderers/Form/InputRange.tsx index f861b12d2dea..4d2f14f5d85f 100644 --- a/packages/amis/src/renderers/Form/InputRange.tsx +++ b/packages/amis/src/renderers/Form/InputRange.tsx @@ -203,6 +203,11 @@ export interface RangeProps extends FormControlProps { */ showInput: boolean; + /** + * 输入框是否显示单位 + */ + showInputUnit?: boolean; + /** * 是否禁用 */ @@ -238,6 +243,7 @@ export interface DefaultProps { clearable: boolean; disabled: boolean; showInput: boolean; + showInputUnit: boolean; multiple: boolean; joinValues: boolean; delimiter: string; @@ -472,7 +478,9 @@ export class Input extends React.Component { disabled, max, min, - mobileUI + mobileUI, + unit, + showInputUnit } = this.props; const _value = multiple ? type === 'min' @@ -480,7 +488,11 @@ export class Input extends React.Component { : Math.max((value as MultipleValue).min, (value as MultipleValue).max) : value; return ( -
+
{ onFocus={this.onFocus} mobileUI={mobileUI} /> + {unit && showInputUnit && ( +
+ {unit} +
+ )}
); } @@ -512,6 +529,7 @@ export default class RangeControl extends React.PureComponent< clearable: true, disabled: false, showInput: false, + showInputUnit: false, multiple: false, joinValues: true, delimiter: ',', diff --git a/packages/amis/src/renderers/Progress.tsx b/packages/amis/src/renderers/Progress.tsx index d333617e341e..f7dc681baa5f 100644 --- a/packages/amis/src/renderers/Progress.tsx +++ b/packages/amis/src/renderers/Progress.tsx @@ -1,5 +1,15 @@ import React from 'react'; -import {Renderer, RendererProps, filter} from 'amis-core'; +import { + Renderer, + RendererProps, + filter, + ActionObject, + ScopedContext, + IScopedContext, + ScopedComponentType +} from 'amis-core'; +import isEqual from 'lodash/isEqual'; +import pick from 'lodash/pick'; import cx from 'classnames'; import {BaseSchema, SchemaClassName, SchemaTpl} from '../Schema'; import {autobind, getPropValue, createObject} from 'amis-core'; @@ -96,7 +106,16 @@ export interface ProgressProps extends RendererProps, Omit {} -export class ProgressField extends React.Component { +interface ProgressState { + value: number; +} + +const COMPARE_KEYS = ['name', 'value', 'data', 'defaultValue']; + +export class ProgressField extends React.Component< + ProgressProps, + ProgressState +> { static defaultProps = { placeholder: '-', progressClassName: '', @@ -108,6 +127,32 @@ export class ProgressField extends React.Component { animate: false }; + constructor(props: ProgressProps) { + super(props); + + this.state = { + value: this.getValue() + }; + } + + componentDidUpdate(prevProps: Readonly): void { + if ( + !isEqual(pick(prevProps, COMPARE_KEYS), pick(this.props, COMPARE_KEYS)) + ) { + this.setState({value: this.getValue()}); + } + } + + getValue() { + let value = getPropValue(this.props); + value = typeof value === 'number' ? value : filter(value, this.props.data); + + if (/^\d*\.?\d+$/.test(value)) { + value = parseFloat(value); + } + return value; + } + @autobind format(value: number) { const {valueTpl, render, data} = this.props; @@ -135,13 +180,7 @@ export class ProgressField extends React.Component { threshold, showThresholdText } = this.props; - - let value = getPropValue(this.props); - value = typeof value === 'number' ? value : filter(value, data); - - if (/^\d*\.?\d+$/.test(value)) { - value = parseFloat(value); - } + const {value} = this.state; if (threshold) { if (Array.isArray(threshold)) { @@ -184,4 +223,32 @@ export class ProgressField extends React.Component { @Renderer({ type: 'progress' }) -export class ProgressFieldRenderer extends ProgressField {} +export class ProgressFieldRenderer extends ProgressField { + static contextType = ScopedContext; + + constructor(props: ProgressProps, context: IScopedContext) { + super(props); + const scoped = context; + scoped.registerComponent(this as unknown as ScopedComponentType); + } + + componentWillUnmount() { + super.componentWillUnmount?.(); + const scoped = this.context as IScopedContext; + scoped.unRegisterComponent(this as unknown as ScopedComponentType); + } + + doAction(action: ActionObject, args: any, throwErrors: boolean): any { + const actionType = action?.actionType as string; + + if (actionType === 'reset') { + this.setState({value: 0}); + } + } + + setData(value: number) { + if (typeof value === 'number' || typeof +value === 'number') { + this.setState({value: +value}); + } + } +} diff --git a/packages/amis/src/renderers/Table/exportExcel.ts b/packages/amis/src/renderers/Table/exportExcel.ts index e01c4dd8d3c3..cd9d4c32fdf4 100644 --- a/packages/amis/src/renderers/Table/exportExcel.ts +++ b/packages/amis/src/renderers/Table/exportExcel.ts @@ -2,7 +2,7 @@ * 导出 Excel 功能 */ -import {filter, isEffectiveApi, arraySlice} from 'amis-core'; +import {filter, isEffectiveApi, arraySlice, isObject} from 'amis-core'; import './ColumnToggler'; import {TableStore} from 'amis-core'; import {saveAs} from 'file-saver'; @@ -425,6 +425,7 @@ export async function exportExcel( } } else if (type == 'link' || (type as any) === 'static-link') { const href = column.pristine.href; + const linkURL = (typeof href === 'string' && href ? filter(href, rowData, '| raw') @@ -443,6 +444,8 @@ export async function exportExcel( }; } else if (type === 'mapping' || (type as any) === 'static-mapping') { let map = column.pristine.map; + const valueField = column.pristine.valueField || 'value'; + const labelField = column.pristine.labelField || 'label'; const source = column.pristine.source; if (source) { let sourceValue = source; @@ -462,6 +465,29 @@ export async function exportExcel( } } + if (Array.isArray(map)) { + map = map.reduce((res, now) => { + if (now == null) { + return res; + } else if (isObject(now)) { + let keys = Object.keys(now); + if ( + keys.length === 1 || + (keys.length == 2 && keys.includes('$$id')) + ) { + // 针对amis-editor的特殊处理 + keys = keys.filter(key => key !== '$$id'); + // 单key 数组对象 + res[keys[0]] = now[keys[0]]; + } else if (keys.length > 1) { + // 多key 数组对象 + res[now[valueField]] = now; + } + } + return res; + }, {}); + } + if (typeof value !== 'undefined' && map && (map[value] ?? map['*'])) { const viewValue = map[value] ?? @@ -470,7 +496,23 @@ export async function exportExcel( : value === false && map['0'] ? map['0'] : map['*']); // 兼容平台旧用法:即 value 为 true 时映射 1 ,为 false 时映射 0 - let text = removeHTMLTag(viewValue); + + let label = viewValue; + if (isObject(viewValue)) { + if (labelField === undefined || labelField === '') { + if (!viewValue.hasOwnProperty('type')) { + // 映射值是object + // 没配置labelField + // object 也没有 type,不能作为schema渲染 + // 默认取 label 字段 + label = viewValue['label']; + } + } else { + label = viewValue[labelField || 'label']; + } + } + + let text = removeHTMLTag(label); /** map可能会使用比较复杂的html结构,富文本也无法完全支持,直接把里面的变量解析出来即可 */ if (isPureVariable(text)) {