tachybase_todo/packages/app/src/components/views/Field/index.tsx

357 lines
9.2 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import moment from 'moment';
import { Tag, Popover, Table, Drawer, Modal } from 'antd';
import Icon from '@/components/icons';
import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty';
import findIndex from 'lodash/findIndex';
import { fields2columns } from '../SortableTable';
import ViewFactory from '..';
import './style.less';
import { getImageByUrl, testUrl } from '@/components/form.fields';
import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
import marked from 'marked';
import Lightbox from 'react-image-lightbox';
import 'react-image-lightbox/style.css';
marked.setOptions({
gfm: true,
breaks: true,
});
const InterfaceTypes = new Map<string, any>();
function registerFieldComponent(type, Component) {
InterfaceTypes.set(type, Component);
}
function registerFieldComponents(components) {
Object.keys(components).forEach(key => {
registerFieldComponent(key, components[key]);
});
}
function getFieldComponent(type) {
if (InterfaceTypes.has(type)) {
return InterfaceTypes.get(type);
}
return InterfaceTypes.get('string');
}
export function StringField(props: any) {
const { value, viewType } = props;
if (!value) {
return null;
}
if (typeof value === 'object') {
return JSON.stringify(value);
}
if (viewType === 'table' && value.length > 20) {
return (
<Popover content={<div onClick={(e) => {
e.stopPropagation();
}} style={{maxWidth: 300}}>{value}</div>}>{value.substring(0, 15)}...</Popover>
);
}
return (
<>{value}</>
);
}
export function TextareaField(props: any) {
const { value, viewType } = props;
if (!value) {
return null;
}
if (viewType !== 'table') {
return <div className={'textarea-field-content'} style={{maxWidth: 300}} dangerouslySetInnerHTML={{__html: marked(value)}}/>;
}
if (value.length > 20) {
return (
<Popover content={(
<div onClick={(e) => e.stopPropagation()} className={'textarea-field-content'} style={{maxWidth: 300}} dangerouslySetInnerHTML={{__html: marked(value)}}/>
)}>{value.substring(0, 15)}...</Popover>
);
}
return (
<>{value}</>
);
}
export function BooleanField(props: any) {
const { value } = props;
return (
<>{value ? <CheckOutlined style={{color: '#52c41a'}}/> : <CloseOutlined style={{color: '#f5222d'}}/>}</>
);
}
export function NumberField(props: any) {
const { schema: { precision = 0 }, value } = props;
if (!isNumber(value)) {
return null;
}
return (
<div className={'number-field'}>{new Intl.NumberFormat().format(value)}</div>
);
}
export function isNumber(num) {
if (typeof num === 'number') {
return num - num === 0;
}
if (typeof num === 'string' && num.trim() !== '') {
return Number.isFinite ? Number.isFinite(+num) : isFinite(+num);
}
return false;
};
export function PercentField(props: any) {
const { schema: { precision = 0 }, value } = props;
if (!isNumber(value)) {
return null;
}
return (
<div className={'percent-field'}>{new Intl.NumberFormat().format(value)}%</div>
);
}
export function DateTimeField(props: any) {
const { schema: { dateFormat, showTime, timeFormat }, value } = props;
const m = moment(value);
if (!m.isValid()) {
return null;
}
let format = dateFormat;
if (showTime) {
format += ` ${timeFormat}`;
}
return (
<>{m.format(`${format}`)}</>
);
}
export function IconField(props) {
const { value } = props;
return <Icon type={value}/>;
}
function toFlat(items = []): Array<any> {
let flat = [];
items.forEach(item => {
flat.push(item);
if (Array.isArray(item.children) && item.children.length) {
flat = flat.concat(toFlat(item.children));
}
});
return flat;
}
export function DataSourceField(props: any) {
const { schema: { dataSource = [] }, value } = props;
const items = toFlat(dataSource);
// console.log(items);
if (isEmpty(value)) {
return null;
}
if (Array.isArray(value)) {
return value.map(val => {
const item = items.find(item => item.value === val);
return (
<Tag color={item.color}>
{item ? item.label : val}
</Tag>
)
});
}
const item = items.find(item => item.value === value);
return (
<Tag color={item.color}>
{item ? item.label : value}
</Tag>
)
}
export function RealtionField(props: any) {
const { schema: { labelField }, value } = props;
if (!value) {
return null;
}
const items = Array.isArray(value) ? value : [value];
return (
<>
{items.map(item => (
<span>{get(item, labelField)}</span>
))}
</>
);
}
export function SubTableField(props: any) {
const { schema: { children }, value } = props;
console.log(value);
if (!Array.isArray(value)) {
return null;
}
return (
<div className={'sub-table-field'}>
<Table size={'small'} columns={fields2columns(children)} dataSource={value} pagination={false}/>
</div>
);
}
export function LinkToField(props: any) {
const { schema, value } = props;
if (!value) {
return null;
}
const values = Array.isArray(value) ? value : [value];
return (
<div className={'link-to-field'}>
{values.map(item => <LinkToFieldLink data={item} schema={schema}/>)}
</div>
);
}
export function LinkToFieldLink(props) {
const { data, schema, schema: { title, labelField } } = props;
const [visible, setVisible] = useState(false);
// console.log(schema);
return (
<span className={'link-to-field-tag'}>
<a onClick={(e) => {
e.stopPropagation();
setVisible(true);
}}>{data[labelField]}</a>
<Drawer
// @ts-ignore
onClick={(e) => {
e.stopPropagation();
}}
className={'noco-drawer'}
bodyStyle={{padding: 0}}
width={'40%'}
title={`查看${title}详情`}
visible={visible}
onClose={() => setVisible(false)}
>
<ViewFactory
resourceName={schema.target}
viewName={'details'}
resourceKey={data.id}
/>
</Drawer>
</span>
);
}
function getImgUrls(value) {
const values = Array.isArray(value) ? value : [value];
return values.filter(item => testUrl(item.url, {
exclude: ['.png', '.jpg', '.jpeg', '.gif']
})).map(item => item);
}
export function AttachmentField(props: any) {
const { value, schema } = props;
const [imgIndex, setImgIndex] = useState(0);
const [visible, setVisible] = useState(false);
if (!value) {
return null;
}
const values = Array.isArray(value) ? value : [value];
const images = getImgUrls(values);
console.log(images);
return (
<div onClick={(e) => {
e.stopPropagation();
}} className={'attachment-field'}>
{values.map(item => <AttachmentFieldItem onClick={() => {
setVisible(true);
const index = findIndex(images, img => item.id === img.id);
setImgIndex(index);
}} data={item} schema={schema}/>)}
{visible && <Lightbox
mainSrc={get(images, [imgIndex, 'url'])}
nextSrc={get(images, [imgIndex + 1, 'url'])}
prevSrc={get(images, [imgIndex - 1, 'url'])}
onCloseRequest={() => setVisible(false)}
onMovePrevRequest={() => {
setImgIndex((imgIndex + images.length - 1) % images.length);
}}
onMoveNextRequest={() => {
setImgIndex((imgIndex + 1) % images.length);
}}
/>}
</div>
);
}
export function AttachmentFieldItem(props: any) {
const { title, url } = props.data || {};
const img = getImageByUrl(url, {
exclude: ['.png', '.jpg', '.jpeg', '.gif']
})
// const [visible, setVisible] = useState(false);
return (
<>
<a onClick={(e) => {
e.stopPropagation();
if (testUrl(url, {
exclude: ['.png', '.jpg', '.jpeg', '.gif']
})) {
props.onClick && props.onClick();
// setVisible(true);
e.preventDefault();
}
}} className={'attachment-field-item'} target={'_blank'} href={url}>
<img height={20} alt={title} title={title} src={`${img}?x-oss-process=style/thumbnail`}/>
</a>
{/* <Modal
className={'attachment-modal'}
onCancel={(e) => {
e.stopPropagation();
setVisible(false);
}}
// @ts-ignore
onClick={(e) => {
e.stopPropagation();
}}
bodyStyle={{padding: 0}}
footer={null}
visible={visible}>
<img onClick={(e) => {
e.stopPropagation();
}} style={{height: 'auto', width: '100%'}} alt={title} title={title} src={url}/>
</Modal> */}
</>
);
}
registerFieldComponents({
string: StringField,
textarea: TextareaField,
boolean: BooleanField,
select: DataSourceField,
multipleSelect: DataSourceField,
radio: DataSourceField,
checkboxes: DataSourceField,
number: NumberField,
percent: PercentField,
datetime: DateTimeField,
createdAt: DateTimeField,
updatedAt: DateTimeField,
icon: IconField,
createdBy: RealtionField,
updatedBy: RealtionField,
subTable: SubTableField,
linkTo: LinkToField,
attachment: AttachmentField,
});
export default function Field(props: any) {
const { schema = {} } = props;
const Component = getFieldComponent(schema.interface);
return <Component {...props}/>;
}