feat: InputImages 多选图片列表支持拖拽排序 Close: #6826 (#6923)

This commit is contained in:
liaoxuezhi 2023-05-19 12:28:19 +08:00 committed by GitHub
parent c73239b60b
commit 20212a4d28
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 188 additions and 46 deletions

View File

@ -420,38 +420,67 @@ app.listen(8080, function () {});
}
```
## 拖拽排序
可配置 `draggable``true` 启动拖拽排序。
```schema: scope="body"
{
"type": "form",
"title": "表单",
data: {
image: [
'http://www.sortablejs.com/assets/img/npm.png',
'http://www.sortablejs.com/assets/img/bower.png',
'http://www.sortablejs.com/assets/img/js.png'
]
},
"body": [
'Images: <br />${image|split|join:"<br />"}',
{
type: 'input-image',
name: 'image',
multiple: true,
draggable: true
}
]
}
```
## 属性表
除了支持 [普通表单项属性表](./formitem#%E5%B1%9E%E6%80%A7%E8%A1%A8) 中的配置以外,还支持下面一些配置
| 属性名 | 类型 | 默认值 | 说明 |
| ------------------ | ------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| receiver | [API](../../../docs/types/api) | | 上传文件接口 |
| accept | `string` | `.jpeg,.jpg,.png,.gif` | 支持的图片类型格式,请配置此属性为图片后缀,例如`.jpg,.png` |
| maxSize | `number` | | 默认没有限制,当设置后,文件大小大于此值将不允许上传。单位为`B` |
| maxLength | `number` | | 默认没有限制,当设置后,一次只允许上传指定数量文件。 |
| multiple | `boolean` | `false` | 是否多选。 |
| joinValues | `boolean` | `true` | [拼接值](./options#%E6%8B%BC%E6%8E%A5%E5%80%BC-joinvalues) |
| extractValue | `boolean` | `false` | [提取值](./options#%E6%8F%90%E5%8F%96%E5%A4%9A%E9%80%89%E5%80%BC-extractvalue) |
| delimiter | `string` | `,` | [拼接符](./options#%E6%8B%BC%E6%8E%A5%E7%AC%A6-delimiter) |
| autoUpload | `boolean` | `true` | 否选择完就自动开始上传 |
| hideUploadButton | `boolean` | `false` | 隐藏上传按钮 |
| fileField | `string` | `file` | 如果你不想自己存储,则可以忽略此属性。 |
| crop | `boolean`或`{"aspectRatio":""}` | | 用来设置是否支持裁剪。 |
| crop.aspectRatio | `number` | | 裁剪比例。浮点型,默认 `1``1:1`,如果要设置 `16:9` 请设置 `1.7777777777777777``16 / 9`。。 |
| crop.rotatable | `boolean` | `false` | 裁剪时是否可旋转 |
| crop.scalable | `boolean` | `false` | 裁剪时是否可缩放 |
| crop.viewMode | `number` | `1` | 裁剪时的查看模式0 是无限制 |
| cropFormat | `string` | `image/png` | 裁剪文件格式 |
| cropQuality | `number` | `1` | 裁剪文件格式的质量,用于 jpeg/webp取值在 0 和 1 之间 |
| limit | Limit | | 限制图片大小,超出不让上传。 |
| frameImage | `string` | | 默认占位图地址 |
| fixedSize | `boolean` | | 是否开启固定尺寸,若开启,需同时设置 fixedSizeClassName |
| fixedSizeClassName | `string` | | 开启固定尺寸时,根据此值控制展示尺寸。例如`h-30`,即图片框高为 h-30,AMIS 将自动缩放比率设置默认图所占位置的宽度,最终上传图片根据此尺寸对应缩放。 |
| initAutoFill | `boolean` | `false` | 表单反显时是否执行 autoFill |
| uploadBtnText | `string` \| [SchemaNode](../../docs/types/schemanode) | | 上传按钮文案。支持tpl、schema形式配置。 |
| dropCrop | `boolean` | `true` | 图片上传后是否进入裁剪模式 |
| initCrop | `boolean` | `false` | 图片选择器初始化后是否立即进入裁剪模式 |
| 属性名 | 类型 | 默认值 | 说明 |
| ------------------ | ----------------------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| receiver | [API](../../../docs/types/api) | | 上传文件接口 |
| accept | `string` | `.jpeg,.jpg,.png,.gif` | 支持的图片类型格式,请配置此属性为图片后缀,例如`.jpg,.png` |
| maxSize | `number` | | 默认没有限制,当设置后,文件大小大于此值将不允许上传。单位为`B` |
| maxLength | `number` | | 默认没有限制,当设置后,一次只允许上传指定数量文件。 |
| multiple | `boolean` | `false` | 是否多选。 |
| joinValues | `boolean` | `true` | [拼接值](./options#%E6%8B%BC%E6%8E%A5%E5%80%BC-joinvalues) |
| extractValue | `boolean` | `false` | [提取值](./options#%E6%8F%90%E5%8F%96%E5%A4%9A%E9%80%89%E5%80%BC-extractvalue) |
| delimiter | `string` | `,` | [拼接符](./options#%E6%8B%BC%E6%8E%A5%E7%AC%A6-delimiter) |
| autoUpload | `boolean` | `true` | 否选择完就自动开始上传 |
| hideUploadButton | `boolean` | `false` | 隐藏上传按钮 |
| fileField | `string` | `file` | 如果你不想自己存储,则可以忽略此属性。 |
| crop | `boolean`或`{"aspectRatio":""}` | | 用来设置是否支持裁剪。 |
| crop.aspectRatio | `number` | | 裁剪比例。浮点型,默认 `1``1:1`,如果要设置 `16:9` 请设置 `1.7777777777777777``16 / 9`。。 |
| crop.rotatable | `boolean` | `false` | 裁剪时是否可旋转 |
| crop.scalable | `boolean` | `false` | 裁剪时是否可缩放 |
| crop.viewMode | `number` | `1` | 裁剪时的查看模式0 是无限制 |
| cropFormat | `string` | `image/png` | 裁剪文件格式 |
| cropQuality | `number` | `1` | 裁剪文件格式的质量,用于 jpeg/webp取值在 0 和 1 之间 |
| limit | Limit | | 限制图片大小,超出不让上传。 |
| frameImage | `string` | | 默认占位图地址 |
| fixedSize | `boolean` | | 是否开启固定尺寸,若开启,需同时设置 fixedSizeClassName |
| fixedSizeClassName | `string` | | 开启固定尺寸时,根据此值控制展示尺寸。例如`h-30`,即图片框高为 h-30,AMIS 将自动缩放比率设置默认图所占位置的宽度,最终上传图片根据此尺寸对应缩放。 |
| initAutoFill | `boolean` | `false` | 表单反显时是否执行 autoFill |
| uploadBtnText | `string` \| [SchemaNode](../../docs/types/schemanode) | | 上传按钮文案。支持 tpl、schema 形式配置。 |
| dropCrop | `boolean` | `true` | 图片上传后是否进入裁剪模式 |
| initCrop | `boolean` | `false` | 图片选择器初始化后是否立即进入裁剪模式 |
| draggable | `boolean` | false | 开启后支持拖拽排序改变图片值顺序 |
| draggableTip | `string` | '拖拽排序' | 拖拽提示文案 |
### Limit 属性表

View File

@ -25,7 +25,7 @@
var(--inputImage-base-default-bottom-left-border-radius);
cursor: pointer;
margin-right: var(--gap-base);
&-bg {
position: absolute;
z-index: 0;
@ -119,11 +119,10 @@
}
&:not(:disabled):not(.is-disabled) {
&.is-invalid:hover {
border-color: var(--FileControl-danger-color);
}
&:hover {
svg {
color: var(--inputImage-base-hover-icon-color);
@ -198,6 +197,10 @@
}
}
&-itemList {
display: inline;
}
&-item {
border: var(--borderWidth) solid var(--borderColor);
border-radius: var(--ImageControl-addBtn-borderRadius);
@ -221,6 +224,10 @@
svg.icon-refresh {
transform: rotate(180deg);
}
&--dragging {
border: var(--borderWidth) solid var(--colors-brand-5);
}
}
&-filename {

View File

@ -156,6 +156,7 @@ register('de-DE', {
'Iframe.invalid': 'Ungültige Iframe-URL',
'Iframe.invalidProtocol':
'HTTP-URL-Iframe kann nicht in https verwendet werden',
'Image.dragTip': 'Zum Sortieren ziehen',
'Image.upload': 'Bild hochladen',
'Image.configError':
'Es können nur eine Beschneidung oder mehrere festgelegt werden',

View File

@ -151,6 +151,7 @@ register('en-US', {
'Form.nestedError': 'Form cannot appear as a descendant of form',
'Iframe.invalid': 'Invalid iframe url',
'Iframe.invalidProtocol': 'Can not use http url iframe in https',
'Image.dragTip': 'Drag to sort',
'Image.upload': 'Upload image',
'Image.errorRetry': 'upload failed, please try again',
'Image.configError': 'Can only set one of crop or multiple',

View File

@ -155,6 +155,7 @@ register('zh-CN', {
'Form.nestedError': '表单不要直接嵌套在表单下面',
'Iframe.invalid': 'iframe 地址不合法',
'Iframe.invalidProtocol': '无法加载 http 协议的 iframe',
'Image.dragTip': '拖拽排序',
'Image.upload': '图片上传',
'Image.errorRetry': '上传失败,请重试',
'Image.configError': '图片多选配置和裁剪配置只能设置一个',

View File

@ -40,6 +40,7 @@ import isPlainObject from 'lodash/isPlainObject';
import merge from 'lodash/merge';
import omit from 'lodash/omit';
import {TplSchema} from '../Tpl';
import Sortable from 'sortablejs';
/**
* Image
@ -284,6 +285,18 @@ export interface ImageControlSchema extends FormBaseControlSchema {
* CSS类名
*/
fixedSizeClassName?: SchemaClassName;
/**
*
*/
draggable?: boolean;
/**
*
*
* @default
*/
draggableTip?: string;
}
let preventEvent = (e: any) => e.stopPropagation();
@ -403,6 +416,7 @@ export default class ImageControl extends React.Component<
};
files: Array<FileValue | FileX> = [];
fileKeys: WeakMap<FileValue | FileX, string> = new WeakMap();
fileUploadCancelExecutors: Array<{
file: any;
executor: () => void;
@ -470,6 +484,7 @@ export default class ImageControl extends React.Component<
this.syncAutoFill = this.syncAutoFill.bind(this);
this.handleReSelect = this.handleReSelect.bind(this);
this.handleFileCancel = this.handleFileCancel.bind(this);
this.dragTipRef = this.dragTipRef.bind(this);
}
componentDidMount() {
@ -549,6 +564,17 @@ export default class ImageControl extends React.Component<
componentWillUnmount() {
this.unmounted = true;
this.fileKeys = new WeakMap();
}
getFileKey(file: FileValue | FileX) {
if (this.fileKeys.has(file)) {
return this.fileKeys.get(file);
}
const key = guid();
this.fileKeys.set(file, key);
return key;
}
buildCrop(props: ImageProps) {
@ -1438,6 +1464,56 @@ export default class ImageControl extends React.Component<
);
}
dragTip?: HTMLElement;
sortable?: Sortable;
id: string = guid();
dragTipRef(ref: any) {
if (!this.dragTip && ref) {
this.initDragging(ref.parentNode as HTMLElement);
} else if (this.dragTip && !ref) {
this.destroyDragging();
}
this.dragTip = ref;
}
initDragging(dom: HTMLElement) {
const ns = this.props.classPrefix;
this.sortable = new Sortable(dom, {
group: `inputimages-${this.id}`,
animation: 150,
handle: `.${ns}ImageControl-item [data-role="dragBar"]`,
ghostClass: `${ns}ImageControl-item--dragging`,
onEnd: (e: any) => {
// 没有移动
if (e.newIndex === e.oldIndex) {
return;
}
// 换回来
const parent = e.to as HTMLElement;
if (e.oldIndex < parent.childNodes.length - 1) {
parent.insertBefore(e.item, parent.childNodes[e.oldIndex]);
} else {
parent.appendChild(e.item);
}
const files = this.files.concat();
files.splice(e.newIndex, 0, files.splice(e.oldIndex, 1)[0]);
this.setState(
{
files: (this.files = files)
},
() => this.onChange(true)
);
}
});
}
destroyDragging() {
this.sortable && this.sortable.destroy();
}
render() {
const {
className,
@ -1462,7 +1538,9 @@ export default class ImageControl extends React.Component<
addBtnControlClassName,
iconControlClassName,
id,
translate: __
translate: __,
draggable,
draggableTip
} = this.props;
insertCustomStyle(
@ -1529,6 +1607,10 @@ export default class ImageControl extends React.Component<
}
const filterFrameImage = filter(frameImage, this.props.data, '| raw');
const hasPending = files.some(file => file.state == 'pending');
const enableDraggable =
!!multiple && draggable && !disabled && !hasPending && files.length > 1;
return (
<div
className={cx(`ImageControl`, className, inputImageControlClassName)}
@ -1616,10 +1698,11 @@ export default class ImageControl extends React.Component<
</div>
) : (
<>
{files && files.length
? files.map((file, key) => (
{files && files.length ? (
<div className={cx('ImageControl-itemList')}>
{files.map((file, key) => (
<div
key={file.id || key}
key={this.getFileKey(file)}
className={cx(
'ImageControl-item',
{
@ -1755,6 +1838,22 @@ export default class ImageControl extends React.Component<
thumbRatio={thumbRatio}
overlays={
<>
{enableDraggable ? (
<a
data-role="dragBar"
data-tooltip={__(
draggableTip || 'Image.dragTip'
)}
data-position="bottom"
target="_blank"
rel="noopener"
>
<Icon
icon="drag-bar"
className="icon"
/>
</a>
) : null}
<a
data-tooltip={__('Image.zoomIn')}
data-position="bottom"
@ -1820,23 +1919,27 @@ export default class ImageControl extends React.Component<
</a>
) : null}
{/* <a
data-tooltip={
file.name ||
getNameFromUrl(file.value || file.url)
}
data-position="bottom"
target="_blank"
>
<Icon icon="info" className="icon" />
</a> */}
data-tooltip={
file.name ||
getNameFromUrl(file.value || file.url)
}
data-position="bottom"
target="_blank"
>
<Icon icon="info" className="icon" />
</a> */}
</>
}
/>
</>
)}
</div>
))
: null}
))}
{enableDraggable ? (
<span ref={this.dragTipRef} />
) : null}
</div>
) : null}
{(multiple && (!maxLength || files.length < maxLength)) ||
(!multiple && !files.length) ? (