This commit is contained in:
ggchinazhangwei 2025-04-09 14:01:08 +08:00
parent 81b9f69953
commit fb1964ecdc
54 changed files with 5108 additions and 0 deletions

View File

@ -0,0 +1,12 @@
.calibration {
width: calc(200% - 50px);
height: 200%;
position: relative;
white-space: nowrap;
pointer-events: none;
user-select: none;
:global(.calibrationNumber) {
font-size: 12px;
color: #888;
}
}

View File

@ -0,0 +1,123 @@
import React, { useState, useEffect, useRef, useCallback } from "react";
import styles from "./index.less";
export interface calibrationTypes {
width: number;
height: number;
}
export type CalibrationTypes = {
direction: "up" | "left" | "right";
multiple: number;
id: string;
};
export default function Calibration(props: CalibrationTypes) {
const { direction, multiple } = props;
const [calibrationLength, setCalibration] = useState<calibrationTypes>({
width: 0,
height: 0
});
const calibrationRef = useRef<HTMLDivElement>(null);
const generateElement = useCallback(
(item?: boolean, num?: number) => {
if (calibrationRef.current) {
let createSpan = document.createElement("div");
createSpan.className = "calibrationLine";
createSpan.style.backgroundColor = "#ccc";
calibrationRef.current.style.display = "flex";
calibrationRef.current.style.justifyContent = "space-between";
if (direction === "up") {
calibrationRef.current.style.marginLeft = "50px";
createSpan.style.width = "1px";
createSpan.style.height = "6px";
createSpan.style.display = "inline-block";
} else {
calibrationRef.current.style.flexDirection = "column";
createSpan.style.height = "1px";
createSpan.style.width = "6px";
}
if (item) {
let createSpanContent = document.createElement("span");
if (direction === "up") {
createSpan.style.height = "12px";
createSpanContent.style.transform = "translate3d(-4px, 20px, 0px)";
createSpan.style.transform = "translateY(0px)";
} else {
createSpan.style.width = "12px";
createSpanContent.style.paddingLeft = "20px";
}
createSpanContent.style.display = "block";
createSpanContent.className = "calibrationNumber";
createSpanContent.innerHTML = num! * 5 + "";
createSpan.appendChild(createSpanContent);
}
calibrationRef.current.appendChild(createSpan);
}
},
[direction]
);
useEffect(() => {
if (calibrationRef.current) {
let calibration = calibrationRef.current.getBoundingClientRect();
setCalibration({ width: calibration.width, height: calibration.height });
let length = direction === "up" ? calibration.width : calibration.height;
for (let i = 0; i < length / 5; i++) {
if (i % 10 === 0) {
generateElement(true, i);
} else {
generateElement();
}
}
}
}, [direction, generateElement]);
useEffect(() => {
if (calibrationRef.current) {
let width = calibrationLength.width
? calibrationLength.width
: calibrationRef.current.getBoundingClientRect().width;
let height = calibrationLength.height
? calibrationLength.height
: calibrationRef.current.getBoundingClientRect().height;
let arr = [
...Array.from(
calibrationRef.current.querySelectorAll(".calibrationLine")
)
];
if (arr.length) {
if (direction === "up") {
calibrationRef.current.style.width =
parseFloat(multiple.toFixed(1)) * width + "px";
arr.forEach(el => {
let dom = [
...Array.from(el.querySelectorAll(".calibrationNumber"))
][0] as HTMLElement;
if (dom) {
dom.style.transform = `translate3d(-4px, 16px, 0px) scale(${(
multiple + 0.1
).toFixed(1)})`;
}
});
} else {
calibrationRef.current.style.height =
parseFloat(multiple.toFixed(1)) * height + "px";
arr.forEach(el => {
let dom = [
...Array.from(el.querySelectorAll(".calibrationNumber"))
][0] as HTMLElement;
if (dom) {
dom.style.transform = `translate3d(-4px, -8px, 0px) scale(${(
multiple + 0.1
).toFixed(1)})`;
}
});
}
}
}
}, [calibrationLength.height, calibrationLength.width, direction, multiple]);
return <div className={styles.calibration} ref={calibrationRef}></div>;
}

View File

@ -0,0 +1,12 @@
.calibration {
width: calc(200% - 50px);
height: 200%;
position: relative;
white-space: nowrap;
pointer-events: none;
user-select: none;
:global(.calibrationNumber) {
font-size: 12px;
color: #888;
}
}

View File

@ -0,0 +1,123 @@
import React, { useState, useEffect, useRef, useCallback } from "react";
import styles from "./index.less";
export interface calibrationTypes {
width: number;
height: number;
}
export type CalibrationTypes = {
direction: "up" | "left" | "right";
multiple: number;
id: string;
};
export default function Calibration(props: CalibrationTypes) {
const { direction, multiple } = props;
const [calibrationLength, setCalibration] = useState<calibrationTypes>({
width: 0,
height: 0
});
const calibrationRef = useRef<HTMLDivElement>(null);
const generateElement = useCallback(
(item?: boolean, num?: number) => {
if (calibrationRef.current) {
let createSpan = document.createElement("div");
createSpan.className = "calibrationLine";
createSpan.style.backgroundColor = "#ccc";
calibrationRef.current.style.display = "flex";
calibrationRef.current.style.justifyContent = "space-between";
if (direction === "up") {
calibrationRef.current.style.marginLeft = "50px";
createSpan.style.width = "1px";
createSpan.style.height = "6px";
createSpan.style.display = "inline-block";
} else {
calibrationRef.current.style.flexDirection = "column";
createSpan.style.height = "1px";
createSpan.style.width = "6px";
}
if (item) {
let createSpanContent = document.createElement("span");
if (direction === "up") {
createSpan.style.height = "12px";
createSpanContent.style.transform = "translate3d(-4px, 20px, 0px)";
createSpan.style.transform = "translateY(0px)";
} else {
createSpan.style.width = "12px";
createSpanContent.style.paddingLeft = "20px";
}
createSpanContent.style.display = "block";
createSpanContent.className = "calibrationNumber";
createSpanContent.innerHTML = num! * 5 + "";
createSpan.appendChild(createSpanContent);
}
calibrationRef.current.appendChild(createSpan);
}
},
[direction]
);
useEffect(() => {
if (calibrationRef.current) {
let calibration = calibrationRef.current.getBoundingClientRect();
setCalibration({ width: calibration.width, height: calibration.height });
let length = direction === "up" ? calibration.width : calibration.height;
for (let i = 0; i < length / 5; i++) {
if (i % 10 === 0) {
generateElement(true, i);
} else {
generateElement();
}
}
}
}, [direction, generateElement]);
useEffect(() => {
if (calibrationRef.current) {
let width = calibrationLength.width
? calibrationLength.width
: calibrationRef.current.getBoundingClientRect().width;
let height = calibrationLength.height
? calibrationLength.height
: calibrationRef.current.getBoundingClientRect().height;
let arr = [
...Array.from(
calibrationRef.current.querySelectorAll(".calibrationLine")
)
];
if (arr.length) {
if (direction === "up") {
calibrationRef.current.style.width =
parseFloat(multiple.toFixed(1)) * width + "px";
arr.forEach(el => {
let dom = [
...Array.from(el.querySelectorAll(".calibrationNumber"))
][0] as HTMLElement;
if (dom) {
dom.style.transform = `translate3d(-4px, 16px, 0px) scale(${(
multiple + 0.1
).toFixed(1)})`;
}
});
} else {
calibrationRef.current.style.height =
parseFloat(multiple.toFixed(1)) * height + "px";
arr.forEach(el => {
let dom = [
...Array.from(el.querySelectorAll(".calibrationNumber"))
][0] as HTMLElement;
if (dom) {
dom.style.transform = `translate3d(-4px, -8px, 0px) scale(${(
multiple + 0.1
).toFixed(1)})`;
}
});
}
}
}
}, [calibrationLength.height, calibrationLength.width, direction, multiple]);
return <div className={styles.calibration} ref={calibrationRef}></div>;
}

View File

@ -0,0 +1,32 @@
import React, { ErrorInfo, PropsWithChildren } from "react";
interface ErrorBoundaryState {
hasError: boolean;
}
class ErrorBoundary extends React.Component<
PropsWithChildren<{}>,
ErrorBoundaryState
> {
constructor(props: PropsWithChildren<{}>) {
super(props);
this.state = { hasError: false };
}
componentDidCatch(_error: Error, _info: ErrorInfo) {
// Display fallback UI
this.setState({ hasError: true });
// You can also log the error to an error reporting service
//logErrorToMyService(error, info);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
export default ErrorBoundary;

View File

@ -0,0 +1,32 @@
import React, { ErrorInfo, PropsWithChildren } from "react";
interface ErrorBoundaryState {
hasError: boolean;
}
class ErrorBoundary extends React.Component<
PropsWithChildren<{}>,
ErrorBoundaryState
> {
constructor(props: PropsWithChildren<{}>) {
super(props);
this.state = { hasError: false };
}
componentDidCatch(_error: Error, _info: ErrorInfo) {
// Display fallback UI
this.setState({ hasError: true });
// You can also log the error to an error reporting service
//logErrorToMyService(error, info);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
export default ErrorBoundary;

View File

@ -0,0 +1,16 @@
.pickerWrap {
display: flex;
flex-wrap: wrap;
.picker {
display: inline-block;
padding: 10px;
border: 2px solid transparent;
cursor: pointer;
&:hover {
border-color: #4091f7;
}
&.selected {
border-color: #4091f7;
}
}
}

View File

@ -0,0 +1,49 @@
import { useState, useEffect, memo } from "react";
import classnames from "classnames";
import Icon from "@/materials/base/Icon";
import styles from "./index.less";
import React from "react";
import { IconTypes } from "@/materials/base/Icon/schema";
import { ICardPickerConfigType } from "../types";
interface CardPickerType
extends Omit<ICardPickerConfigType<IconTypes>, "type" | "key" | "name"> {
onChange?: (v: string) => void;
type: IconTypes;
}
export default memo((props: CardPickerType) => {
const { type, icons, onChange } = props;
const [selected, setSelected] = useState<IconTypes>(type);
const handlePicker = (v: IconTypes) => {
if (onChange) {
onChange(v);
return;
}
setSelected(v);
};
useEffect(() => {
setSelected(type);
}, [type]);
return (
<div className={styles.pickerWrap}>
{icons.map((item, i) => {
return (
<span
className={classnames(
styles.picker,
selected === item ? styles.selected : ""
)}
onClick={() => handlePicker(item)}
key={i}
>
<Icon type={item} size={20} color={"#4091f7"} spin={false} />
</span>
);
})}
</div>
);
});

View File

@ -0,0 +1,89 @@
import React from "react";
import { SketchPicker, ColorResult } from "react-color";
import { rgba2Obj } from "@/utils/tool";
export type ColorConfigType = string;
//value 初始值传来onchange item给的回调
interface ColorProps {
value?: ColorConfigType;
onChange?: (v: ColorConfigType) => void;
}
class colorPicker extends React.Component<ColorProps> {
state = {
displayColorPicker: false,
color: rgba2Obj(this.props.value)
};
handleClick = () => {
this.setState({ displayColorPicker: !this.state.displayColorPicker });
};
handleClose = () => {
this.setState({ displayColorPicker: false });
};
handleChange = (color: ColorResult) => {
this.setState({ color: color.rgb });
this.props.onChange &&
this.props.onChange(
`rgba(${color.rgb.r},${color.rgb.g},${color.rgb.b},${color.rgb.a})`
);
};
render() {
return (
<div>
<div
style={{
// padding: '5px',
background: "#fff",
borderRadius: "1px",
boxShadow: "0 0 0 1px rgba(0,0,0,.1)",
display: "inline-block",
cursor: "pointer"
}}
onClick={this.handleClick}
>
<div
style={{
width: "20px",
height: "20px",
borderRadius: "2px",
background: `rgba(${this.state.color.r}, ${this.state.color.g}, ${this.state.color.b}, ${this.state.color.a})`
}}
/>
</div>
{this.state.displayColorPicker ? (
<React.Fragment>
<div
style={{
position: "absolute",
zIndex: 2000
}}
>
<SketchPicker
color={this.state.color}
onChange={this.handleChange}
/>
</div>
<div
style={{
position: "fixed",
top: "0px",
right: "0px",
bottom: "0px",
left: "0px",
zIndex: 1000
}}
onClick={this.handleClose}
/>
</React.Fragment>
) : null}
</div>
);
}
}
export default colorPicker;

View File

@ -0,0 +1,127 @@
import React, { memo, useEffect, FC } from "react";
import { Form, Select, Input, Modal, Button } from "antd";
import Upload from "../Upload";
import { Store } from "antd/lib/form/interface";
import { TDataListDefaultTypeItem } from "../FormEditor/types";
// import styles from './index.less';
const normFile = (e: any) => {
if (Array.isArray(e)) {
return e;
}
return e && e.fileList;
};
const { Option } = Select;
const formItemLayout = {
labelCol: { span: 6 },
wrapperCol: { span: 14 }
};
export type EditorModalProps = {
visible: boolean;
onCancel:
| ((e: React.MouseEvent<HTMLElement, MouseEvent>) => void)
| undefined;
item?: TDataListDefaultTypeItem;
onSave: Function;
cropRate: number;
};
const EditorModal: FC<EditorModalProps> = props => {
const { item, onSave, visible, onCancel, cropRate } = props;
const onFinish = (values: Store) => {
console.log(values);
onSave && onSave(values);
};
const handleOk = () => {
form
.validateFields()
.then(values => {
if (item) {
values.id = item.id;
onSave && onSave(values);
}
})
.catch(err => {
console.log(err);
});
};
const [form] = Form.useForm();
useEffect(() => {
if (form && item && visible) {
form.resetFields();
}
}, [form, item, visible]);
return (
<>
{!!item && (
<Modal
title="编辑数据源"
closable={false}
visible={visible}
onOk={handleOk}
okText="确定"
forceRender
footer={
<Button type={"primary"} onClick={() => handleOk()}>
</Button>
}
>
<Form
form={form}
name={`form_editor_modal`}
{...formItemLayout}
onFinish={onFinish}
initialValues={item}
>
<Form.Item
label="标题"
name="title"
rules={[{ required: true, message: "请输入标题!" }]}
>
<Input />
</Form.Item>
<Form.Item label="描述" name="desc">
<Input />
</Form.Item>
<Form.Item label="链接地址" name="link">
<Input />
</Form.Item>
{!!window["currentCates"] && (
<Form.Item
label="分类"
name="type"
rules={[{ required: true, message: "请选择分类!" }]}
>
<Select placeholder="请选择">
{window["currentCates"].map((v, i) => {
return (
<Option value={i} key={i}>
{v}
</Option>
);
})}
</Select>
</Form.Item>
)}
<Form.Item
label="上传图片"
name="imgUrl"
valuePropName="fileList"
getValueFromEvent={normFile}
>
<Upload cropRate={cropRate} isCrop />
</Form.Item>
</Form>
</Modal>
)}
</>
);
};
export default memo(EditorModal);

View File

@ -0,0 +1,46 @@
.dataList {
padding: 6px 10px;
border: 1px solid #f0f0f0;
text-align: justify;
padding-left: 10px;
padding-top: 10px;
}
.listItem {
position: relative;
padding-bottom: 6px;
margin-bottom: 6px;
border-bottom: 1px solid #f0f0f0;
&:hover {
.actionBar {
display: block;
}
}
&:last-child {
border-bottom: none;
margin-bottom: 0;
}
.tit {
font-weight: bold;
padding-bottom: 5px;
}
.desc {
font-size: 12px;
color: #ccc;
}
.actionBar {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
display: none;
background: #fff;
box-shadow: -20px 0 10px 10px #fff;
.action {
margin-right: 18px;
cursor: pointer;
&:last-child {
cursor: move;
}
}
}
}

View File

@ -0,0 +1,263 @@
import React, { memo, useState, useEffect, useCallback } from "react";
import {
EditOutlined,
MinusCircleOutlined,
MenuOutlined
} from "@ant-design/icons";
import { Button } from "antd";
import {
DragSource,
DropTarget,
DndProvider,
ConnectDropTarget,
DragSourceSpec,
DropTargetConnector,
DragSourceMonitor,
DragSourceConnector,
DropTargetSpec,
ConnectDragSource,
ConnectDragPreview
} from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import EditorModal from "./editorModal";
import { uuid } from "@/utils/tool";
import styles from "./index.less";
import { TDataListDefaultType, TDataListDefaultTypeItem } from "../types";
type ListItemProps = DndItemProps & {
isDragging: boolean;
connectDragSource: ConnectDragSource;
connectDragPreview: ConnectDragPreview;
connectDropTarget: ConnectDropTarget;
};
function ListItem(props: ListItemProps) {
const {
title,
desc,
onDel,
onEdit,
// 这些 props 由 React DnD注入参考`collect`函数定义
isDragging,
connectDragSource,
connectDragPreview,
connectDropTarget
} = props;
const opacity = isDragging ? 0.5 : 1;
return connectDropTarget(
// 列表项本身作为 Drop 对象
connectDragPreview(
// 整个列表项作为跟随拖动的影像
<div className={styles.listItem} style={Object.assign({}, { opacity })}>
<div className={styles.tit}>{title}</div>
<div className={styles.desc}>{desc}</div>
<div className={styles.actionBar}>
<span className={styles.action} onClick={() => onEdit()}>
<EditOutlined />
</span>
<span className={styles.action} onClick={() => onDel()}>
<MinusCircleOutlined />
</span>
{connectDragSource(
<span className={styles.action}>
<MenuOutlined />
</span>
) // 拖动图标作为 Drag 对象
}
</div>
</div>
)
);
}
type DndItemProps = TDataListDefaultTypeItem & {
onDel: Function;
onEdit: Function;
key: number;
find: Function;
move: Function;
type?: number;
};
const type = "item";
type DragObject = {
id: string;
originalIndex: number;
};
const dragSpec: DragSourceSpec<DndItemProps, DragObject> = {
// 拖动开始时,返回描述 source 数据。后续通过 monitor.getItem() 获得
beginDrag: props => ({
id: props.id,
originalIndex: props.find(props.id).index
}),
// 拖动停止时,处理 source 数据
endDrag(props, monitor) {
const { id: droppedId, originalIndex } = monitor.getItem();
const didDrop = monitor.didDrop();
// source 是否已经放置在 target
if (!didDrop) {
return props.move(droppedId, originalIndex);
}
}
};
const dragCollect = (
connect: DragSourceConnector,
monitor: DragSourceMonitor
) => ({
connectDragSource: connect.dragSource(), // 用于包装需要拖动的组件
connectDragPreview: connect.dragPreview(), // 用于包装需要拖动跟随预览的组件
isDragging: monitor.isDragging() // 用于判断是否处于拖动状态
});
const dropSpec: DropTargetSpec<DndItemProps> = {
canDrop: () => false, // item 不处理 drop
hover(props, monitor) {
const { id: draggedId } = monitor.getItem();
const { id: overId } = props;
// 如果 source item 与 target item 不同,则交换位置并重新排序
if (draggedId !== overId) {
const { index: overIndex } = props.find(overId);
props.move(draggedId, overIndex);
}
}
};
const dropCollect = (connect: DropTargetConnector) => ({
connectDropTarget: connect.dropTarget() // 用于包装需接收拖拽的组件
});
const DndItem = DropTarget(
type,
dropSpec,
dropCollect
)(DragSource(type, dragSpec, dragCollect)(ListItem));
export type DataListMemo = {
onChange?: (v: TDataListDefaultType) => void;
value?: TDataListDefaultType;
cropRate: number;
};
export type DataListType = DataListMemo & {
connectDropTarget: ConnectDropTarget;
};
const List = function(props: DataListType) {
const { onChange, value, connectDropTarget, cropRate } = props;
const [list, setList] = useState(value);
const [visible, setVisible] = useState(false);
const [curItem, setCurItem] = useState<TDataListDefaultTypeItem>();
const handleDel = (id: string) => {
if (value && onChange) {
let newVal = value.filter(item => id !== item.id);
onChange(newVal);
}
};
const find = (id: string) => {
const item = list!.find(c => `${c.id}` === id)!;
return {
item,
index: list!.indexOf(item!)
};
};
const move = (id: string, toIndex: number) => {
const { item, index } = find(id);
const oldList = [...list!];
oldList.splice(index, 1);
oldList.splice(toIndex, 0, item);
if (onChange) {
onChange(oldList);
return;
}
setList(oldList);
};
const handleCancel = useCallback(() => {
console.log("a");
setVisible(false);
}, []);
const handleEdit = useCallback((item: TDataListDefaultTypeItem) => {
console.log("b");
setVisible(true);
setCurItem(item);
}, []);
const handleSave = useCallback(
(item: TDataListDefaultTypeItem) => {
console.log("c");
setVisible(false);
if (onChange) {
onChange(list!.map(p => (p.id === item.id ? item : p)));
return;
}
setList(prev => prev!.map(p => (p.id === item.id ? item : p)));
},
[list, onChange]
);
const handleAdd = () => {
const item = {
title: "新增项标题",
desc: "新增项描述",
id: uuid(8, 10),
imgUrl: [],
link: ""
};
if (onChange) {
onChange([...list!, item]);
return;
}
setList([...list!, item]);
};
useEffect(() => {
setList(value);
}, [value]);
return connectDropTarget(
<div className={styles.dataList}>
{!!(list && list.length) &&
list.map((item, i) => (
<DndItem
{...item}
onDel={() => handleDel(item.id)}
onEdit={() => handleEdit(item)}
key={i}
id={`${item.id}`}
find={find}
move={move}
/>
))}
<div style={{ marginTop: "10px" }}>
<Button onClick={handleAdd} block>
</Button>
</div>
<EditorModal
visible={visible}
onCancel={handleCancel}
item={curItem}
onSave={handleSave}
cropRate={cropRate}
/>
</div>
);
};
const DndList = DropTarget(type, {}, connect => ({
connectDropTarget: connect.dropTarget()
}))(List);
// 将 HTMLBackend 作为参数传给 DragDropContext
export default memo((props: DataListMemo) => {
return (
<DndProvider backend={HTML5Backend}>
<DndList {...props} />
</DndProvider>
);
});

View File

@ -0,0 +1,137 @@
import React, { FC, memo, useEffect } from "react";
import { Form, Select, Input, Modal, Button, InputNumber } from "antd";
import { baseFormOptionsType } from "../types";
import Color from "../Color";
const { Option } = Select;
const formItemLayout = {
labelCol: { span: 6 },
wrapperCol: { span: 14 }
};
interface EditorModalProps {
item: any;
onSave: (data: any) => void;
visible: boolean;
}
const EditorModal: FC<EditorModalProps> = props => {
const { item, onSave, visible } = props;
const onFinish = (values: any) => {
onSave && onSave(values);
};
const handleOk = () => {
form
.validateFields()
.then(values => {
values.id = item.id;
onSave && onSave(values);
})
.catch(err => {
console.log(err);
});
};
const [form] = Form.useForm();
useEffect(() => {
if (form && item && visible) {
form.resetFields();
}
}, [form, item, visible]);
return (
<>
{!!item && (
<Modal
title="编辑表单组件"
footer={
<div>
<Button type="primary" onClick={() => handleOk()}>
</Button>
</div>
}
forceRender
visible={visible}
onOk={handleOk}
closable={false}
>
<Form
form={form}
name={`formItem_editor_modal`}
{...formItemLayout}
onFinish={onFinish}
initialValues={item}
>
{
<Form.Item label="类型" name="type" hidden>
<Input />
</Form.Item>
}
{!!item.label && (
<Form.Item
label="字段名"
name="label"
rules={[{ required: true, message: "请输入字段名!" }]}
>
<Input />
</Form.Item>
)}
{!!item.fontSize && (
<Form.Item
label="字体大小"
name="fontSize"
rules={[{ required: true, message: "请输入字体大小!" }]}
>
<InputNumber min={12} max={30} defaultValue={14} />
</Form.Item>
)}
{!!item.color && (
<Form.Item
label="文字颜色"
name="color"
rules={[{ required: true, message: "请输入文字颜色!" }]}
>
<Color />
</Form.Item>
)}
{!!item.placeholder && (
<Form.Item label="提示文本" name="placeholder">
<Input placeholder="请输入提示文本" />
</Form.Item>
)}
{!!item.options && (
<Form.Item
label="选项源"
name="options"
rules={[{ required: true, message: "选项不能为空!" }]}
>
<Select
placeholder="请输入"
mode="tags"
labelInValue
maxTagCount={39}
maxTagTextLength={16}
>
{item.options.map((v: baseFormOptionsType, i: number) => {
return (
<Option value={v.value} key={i}>
{v.label}
</Option>
);
})}
</Select>
</Form.Item>
)}
</Form>
</Modal>
)}
</>
);
};
export default memo(EditorModal);

View File

@ -0,0 +1,215 @@
import React, {
memo,
RefObject,
useCallback,
useEffect,
useState
} from "react";
import BaseForm from "@/materials/base/Form/BaseForm";
import BasePopoverForm from "@/materials/base/Form/BasePopoverForm";
import EditorModal from "./EditorModal";
import { MinusCircleFilled, EditFilled, PlusOutlined } from "@ant-design/icons";
import styles from "./formItems.less";
import { baseFormUnion, TFormItemsDefaultType } from "../types";
import { uuid } from "@/utils/tool";
import { Button } from "antd";
import MyPopover from "yh-react-popover";
// import { Popconfirm } from 'antd';
const formTpl: TFormItemsDefaultType = [
{
id: "1",
type: "Text",
label: "文本框",
placeholder: "请输入文本"
},
{
id: "2",
type: "Textarea",
label: "长文本框",
placeholder: "请输入长文本请输入长文本"
},
{
id: "3",
type: "Number",
label: "数值",
placeholder: " 请输入数值"
},
{
id: "4",
type: "MyRadio",
label: "单选框",
options: [
{ label: "选项一", value: "1" },
{ label: "选项二", value: "2" }
]
},
{
id: "5",
type: "MySelect",
label: "下拉选择框",
options: [
{ label: "选项一", value: "1" },
{ label: "选项二", value: "2" },
{ label: "选项三", value: "3" }
]
},
{
id: "6",
type: "Date",
label: "日期框",
placeholder: ""
},
{
id: "7",
type: "MyTextTip",
label: "纯文本",
fontSize: 12,
color: "rgba(0,0,0,1)"
}
];
interface FormItemsProps {
formList?: TFormItemsDefaultType;
onChange?: (v: TFormItemsDefaultType) => void;
data: any;
rightPannelRef: RefObject<HTMLDivElement>;
}
const FormItems = (props: FormItemsProps) => {
const { formList, onChange, rightPannelRef } = props;
const [formData, setFormData] = useState<TFormItemsDefaultType>(
formList || []
);
const [visible, setVisible] = useState(false);
const [curItem, setCurItem] = useState<baseFormUnion>();
const [force, setforce] = useState<{ force: Function }>({
force: () => {}
});
const handleAddItem = (item: baseFormUnion) => {
let tpl = formTpl.find(v => v.type === item.type);
let newData = [...formData, { ...tpl!, id: uuid(6, 10) }];
setFormData(newData);
onChange && onChange(newData);
force.force();
};
const handleEditItem = (item: baseFormUnion) => {
setVisible(true);
setCurItem(item);
};
const handleDelItem = (item: baseFormUnion) => {
let newData = formData.filter(v => v.id !== item.id);
setFormData(newData);
onChange && onChange(newData);
};
const handleSaveItem = (data: baseFormUnion) => {
let newData = formData.map(v => (v.id === data.id ? data : v));
setFormData(newData);
onChange && onChange(newData);
setVisible(false);
};
const callback = useCallback((v: Function) => {
console.log(v);
setforce({ force: v });
}, []);
useEffect(() => {
let listenner: (e: Event) => void;
if (rightPannelRef.current) {
listenner = () => {
force.force();
};
rightPannelRef.current.addEventListener("scroll", listenner);
}
return () => {
if (rightPannelRef.current) {
// eslint-disable-next-line react-hooks/exhaustive-deps
rightPannelRef.current.removeEventListener("scroll", listenner);
}
};
}, [force, rightPannelRef]);
return (
<div className={styles.formItemWrap}>
<div className={styles.formTitle}></div>
<div className={styles.editForm}>
{formData.map((item: baseFormUnion, i: number) => {
let FormItem = BaseForm[item.type];
return (
<div className={styles.formItem} key={i}>
<div className={styles.disClick}>
<FormItem {...item} />
</div>
<div className={styles.deleteWrap}>
<span
className={styles.operationBtn}
onClick={() => handleDelItem(item)}
>
<MinusCircleFilled />
</span>
</div>
<div className={styles.editWrap}>
<span
className={styles.operationBtn}
onClick={() => handleEditItem(item)}
>
<EditFilled />
</span>
</div>
</div>
);
})}
<div className={styles.formAddWrap}>
<MyPopover
content={
<>
<div className={styles.formTpl} style={{ color: "red" }}>
{formTpl.map((item, i) => {
let FormItem = BasePopoverForm[item.type];
return (
<div
className={styles.formItem}
key={i}
onClick={() => handleAddItem(item)}
>
<div
className={styles.disClick}
style={{
display: "flex",
flexDirection: "column",
overflow: "row",
marginTop: "10px"
}}
>
<FormItem {...item} />
</div>
</div>
);
})}
</div>
{/* <a style={{color: 'red'}} onClick={() => setFormTplVisible(false)}>Close</a> */}
</>
}
directions={"LB"}
innerConstDomStyle={{ display: "block" }}
constDomStyle={{ display: "block" }}
callback={callback}
>
<Button style={{ width: "100%" }} block icon={<PlusOutlined />}>
</Button>
</MyPopover>
</div>
</div>
<EditorModal item={curItem} onSave={handleSaveItem} visible={visible} />
</div>
);
};
export default memo(FormItems);

View File

@ -0,0 +1,88 @@
.formItemWrap {
.formTitle {
width: 56px;
height: 20px;
font-size: 14px;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: bold;
color: #000000;
line-height: 20px;
}
.editForm {
text-align: left;
width: 251px;
.formItem {
position: relative;
padding-left: 2px;
.common {
position: absolute;
top: 19px;
box-shadow: 0 0 20px #fff;
.operationBtn {
margin-right: 15px;
display: inline-block;
cursor: pointer;
}
}
.deleteWrap {
.common;
left: 0;
}
.editWrap {
.common;
right: -18px;
}
}
.formAddWrap {
font-size: 14px;
font-weight: 400;
color: #4a4a4a;
line-height: 20px;
background-color: #2f54eb;
}
}
.formAddWrap {
.formTpl {
margin-top: 12px;
border-top: 1px dashed #ccc;
padding-top: 16px;
background-color: #4a4a4a;
.formItem {
button,
[type="button"] {
color: #fff;
background-color: #4a4a4a;
border: 1px solid #fff;
border-radius: 4px 0px 0px 0px;
}
position: relative;
border: 1px solid #ccc;
margin-bottom: 2px;
background-color: #4a4a4a;
cursor: pointer;
.disClick {
pointer-events: none;
color: #fff;
}
&:hover {
border-color: #2f54eb;
.addBtn {
display: inline-block;
}
}
.addBtn {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
display: none;
padding: 3px 6px;
color: #fff;
border-radius: 3px;
background-color: #2f54eb;
cursor: pointer;
}
}
}
}
}

View File

@ -0,0 +1,2 @@
import FormItems from "./FormItems";
export default FormItems;

View File

@ -0,0 +1,12 @@
.mutiText {
.iptWrap {
margin-bottom: 12px;
display: flex;
.delBtn {
// font-size: 14px;
margin-left: 12px;
cursor: pointer;
align-self: center;
}
}
}

View File

@ -0,0 +1,74 @@
import React, { memo, useEffect } from "react";
import { Input, Button, Popconfirm } from "antd";
import { MinusCircleFilled } from "@ant-design/icons";
import styles from "./index.less";
import { TMutiTextDefaultType } from "../types";
type MultiTextProps = {
onChange?: (v: TMutiTextDefaultType) => void;
value?: TMutiTextDefaultType;
};
export default memo(function MutiText(props: MultiTextProps) {
const { value, onChange } = props;
const handleAdd = () => {
onChange && onChange([...value!, "新增项目"]);
};
const handleDel = (index: number) => {
let newList = value!.filter((_item, i) => i !== index);
onChange && onChange(newList);
};
const handleChange = (
index: number,
e: React.ChangeEvent<HTMLInputElement>
) => {
let newList = value!.map((item, i) =>
i === index ? e.target.value : item
);
onChange && onChange(newList);
};
useEffect(() => {
window["currentCates"] = value!;
return () => {
window["currentCates"] = null;
};
}, [value]);
return (
<div className={styles.mutiText}>
{value && value.length ? (
value!.map((item, i) => {
return (
<div className={styles.iptWrap} key={i}>
<Input defaultValue={item} onChange={e => handleChange(i, e)} />
<Popconfirm
title="确定要删除吗?"
onConfirm={() => handleDel(i)}
placement="leftTop"
okText="确定"
cancelText="取消"
>
<span className={styles.delBtn}>
<MinusCircleFilled />
</span>
</Popconfirm>
</div>
);
})
) : (
<div className={styles.iptWrap}>
<Input />
</div>
)}
{value && value.length < 3 && (
<div className={styles.iptWrap}>
<Button block onClick={handleAdd}>
</Button>
</div>
)}
</div>
);
});

View File

@ -0,0 +1,11 @@
.posIpt {
display: flex;
justify-content: flex-end;
margin-right: -10px;
.posItem {
margin-right: 10px;
span {
margin-right: 3px;
}
}
}

View File

@ -0,0 +1,39 @@
import React, { memo, useState, useEffect } from "react";
import { InputNumber } from "antd";
import styles from "./index.less";
import { TPosDefaultType, TPosItem } from "../types";
type PosProps = {
value?: TPosDefaultType;
onChange?: (v: TPosItem | string) => void;
};
export default memo(function Pos(props: PosProps) {
const { value, onChange } = props;
let _this: typeof Pos = Pos;
const handleChange = (index: number, v: TPosItem | string) => {
let arr: any = value || [];
arr[index] = v;
onChange && onChange(arr);
};
return (
<div className={styles.posIpt}>
<div className={styles.posItem}>
<span>x: </span>
<InputNumber
defaultValue={value && value[0]}
onChange={handleChange.bind(_this, 0)}
/>
</div>
<div className={styles.posItem}>
<span>y: </span>
<InputNumber
defaultValue={value && value[1]}
onChange={handleChange.bind(_this, 1)}
/>
</div>
</div>
);
});

View File

@ -0,0 +1,32 @@
:global(.editable-cell) {
position: relative;
}
:global(.editable-cell-value-wrap) {
padding: 5px 12px;
cursor: pointer;
}
:global(.editable-row) {
&:hover :global(.editable-cell-value-wrap) {
border: 1px solid #d9d9d9;
border-radius: 4px;
padding: 4px 11px;
}
}
:global([data-theme="dark"]) {
:global(.editable-row) {
&:hover {
:global(.editable-cell-value-wrap) {
border: 1px solid #434343;
}
}
}
}
.apiForm {
.formItem {
margin-bottom: 16px;
}
}

View File

@ -0,0 +1,449 @@
import React, {
useContext,
useState,
useEffect,
useRef,
memo,
RefObject
} from "react";
import { Table, Input, Button, Popconfirm, Form, Modal, Upload } from "antd";
import { ColumnsType } from "antd/lib/table";
import { uuid } from "@/utils/tool";
import XLSX from "xlsx";
// 下方样式主要为全局样式,暂时不可删
import styles from "./index.less";
const EditableContext = React.createContext<any>(null);
interface Item {
key: string;
name: string;
age: string;
address: string;
}
interface EditableRowProps {
index: number;
}
const EditableRow: React.FC<EditableRowProps> = ({ index, ...props }) => {
const [form] = Form.useForm();
return (
<Form form={form} component={false}>
<EditableContext.Provider value={form}>
<tr {...props} />
</EditableContext.Provider>
</Form>
);
};
interface EditableCellProps {
title: React.ReactNode;
editable: boolean;
children: React.ReactNode;
dataIndex: string;
record: any;
handleSave: (record: Item) => void;
}
const EditableCell: React.FC<EditableCellProps> = ({
title,
editable,
children,
dataIndex,
record,
handleSave,
...restProps
}) => {
const [editing, setEditing] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const form = useContext(EditableContext);
useEffect(() => {
if (editing) {
inputRef.current?.focus();
}
}, [editing]);
const toggleEdit = () => {
setEditing(!editing);
form.setFieldsValue({ [dataIndex]: record[dataIndex] });
};
const save = async () => {
try {
const values = await form.validateFields();
toggleEdit();
handleSave({ ...record, ...values });
} catch (errInfo) {
console.log("Save failed:", errInfo);
}
};
let childNode = children;
if (editable) {
childNode = editing ? (
<Form.Item
style={{ margin: 0 }}
name={dataIndex}
rules={[
{
required: true,
message: `${title} 是必填的.`
}
]}
>
<Input
ref={(inputRef as unknown) as () => RefObject<HTMLInputElement>}
onPressEnter={save}
onBlur={save}
/>
</Form.Item>
) : (
<div
className="editable-cell-value-wrap"
style={{ paddingRight: 24 }}
onClick={toggleEdit}
>
{children}
</div>
);
}
return <td {...restProps}>{childNode}</td>;
};
class EditableTable extends React.Component<any, any> {
columns: (
| {
title: string;
dataIndex: string;
width: string;
editable: boolean;
render?: undefined;
}
| {
title: string;
dataIndex: string;
render: (text: string, record: any) => JSX.Element | null;
width?: undefined;
editable?: undefined;
}
)[];
apiForm: {
api: string;
header: string;
dataField: string;
};
constructor(props: any) {
super(props);
this.columns = [
{
title: "名字",
dataIndex: "name",
width: "180px",
editable: true
},
{
title: "值",
dataIndex: "value",
width: "120px",
editable: true
},
{
title: "操作",
dataIndex: "operation",
render: (text: string, record) =>
this.state.dataSource.length >= 1 ? (
<Popconfirm
title="Sure to delete?"
onConfirm={() => this.handleDelete(record.key)}
>
<Button type="link"></Button>
</Popconfirm>
) : null
}
];
this.apiForm = {
api: "",
header: "",
dataField: ""
};
const dataSource =
props.data &&
props.data.map((item: any, i: number) => ({ key: i + "", ...item }));
this.state = {
dataSource: dataSource,
visible: false,
apiVisible: false,
apiResult: ""
};
}
handleDelete = (key: string) => {
const dataSource = [...this.state.dataSource];
const newDataSource = dataSource.filter(item => item.key !== key);
this.setState({ dataSource: newDataSource });
this.props.onChange && this.props.onChange(newDataSource);
};
handleAdd = () => {
const { dataSource } = this.state;
const uid = uuid(8, 10);
const newData = {
key: uid,
name: `dooring ${dataSource.length + 1}`,
value: 32
};
const newDataSource = [...dataSource, newData];
this.setState({
dataSource: newDataSource
});
this.props.onChange && this.props.onChange(newDataSource);
};
handleSave = (row: any) => {
const newData = [...this.state.dataSource];
const index = newData.findIndex(item => row.key === item.key);
const item = newData[index];
newData.splice(index, 1, {
...item,
...row
});
this.setState({ dataSource: newData });
this.props.onChange && this.props.onChange(newData);
};
showModal = () => {
this.setState({
visible: true
});
};
handleOk = (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
this.setState({
visible: false
});
};
handleCancel = (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
this.setState({
visible: false
});
};
showApiModal = () => {
this.setState({
apiVisible: true
});
};
handleAPIOk = () => {
const { dataField } = this.apiForm;
if (dataField) {
let data = this.state.apiResult[dataField];
if (data && data instanceof Array) {
data = data.map((item, i) => ({ key: i + "", ...item }));
this.setState({
dataSource: data
});
this.props.onChange && this.props.onChange(data);
}
this.setState({
apiVisible: false
});
}
};
handleAPICancel = () => {
this.setState({
apiVisible: false
});
};
handleApiField = (type: "api" | "header" | "dataField", v: string) => {
this.apiForm[type] = v;
};
getApiFn = () => {
console.log(this.apiForm);
const { api, header } = this.apiForm;
fetch(api, {
cache: "no-cache",
headers: Object.assign(
{ "content-type": "application/json" },
header ? JSON.parse(header) : {}
),
method: "GET",
mode: "cors"
})
.then(res => res.json())
.then(res => {
this.setState({
apiResult: res
});
});
};
render() {
const { dataSource } = this.state;
const components = {
body: {
row: EditableRow,
cell: EditableCell
}
};
const columns: ColumnsType<any> = this.columns.map(col => {
if (!col.editable) {
return col;
}
return {
...col,
onCell: record => ({
record,
editable: col.editable,
dataIndex: col.dataIndex,
title: col.title,
handleSave: this.handleSave
})
};
});
const _this = this;
const props = {
name: "file",
// action: '',
showUploadList: false,
beforeUpload(file: File, fileList: Array<File>) {
// 解析并提取excel数据
let reader = new FileReader();
reader.onload = function(e: any) {
let data = e.target.result;
let workbook = XLSX.read(data, { type: "binary" });
let sheetNames = workbook.SheetNames; // 工作表名称集合
let draftArr: any = {};
sheetNames.forEach(name => {
let worksheet = workbook.Sheets[name]; // 只能通过工作表名称来获取指定工作表
for (let key in worksheet) {
// v是读取单元格的原始值
if (key[0] !== "!") {
if (draftArr[key[0]]) {
draftArr[key[0]].push(worksheet[key].v);
} else {
draftArr[key[0]] = [worksheet[key].v];
}
}
}
});
let sourceData = Object.values(draftArr).map((item: any, i) => ({
key: i + "",
name: item[0],
value: item[1]
}));
_this.setState({
dataSource: sourceData
});
_this.props.onChange && _this.props.onChange(sourceData);
};
reader.readAsBinaryString(file);
}
};
return (
<div>
<Button type="primary" onClick={this.showModal}>
</Button>
<Modal
title="编辑数据源"
visible={this.state.visible}
onOk={this.handleOk}
onCancel={this.handleCancel}
okText="确定"
cancelText="取消"
>
<Button
onClick={this.handleAdd}
type="primary"
style={{ marginBottom: 16, marginRight: 16 }}
>
</Button>
<Upload {...props}>
<Button type="primary" ghost style={{ marginRight: 16 }}>
Excel
</Button>
</Upload>
<Button type="primary" ghost onClick={this.showApiModal}>
API
</Button>
<Table
components={components}
rowClassName={() => "editable-row"}
bordered
dataSource={dataSource}
columns={columns}
pagination={{ pageSize: 50 }}
scroll={{ y: 240 }}
/>
</Modal>
<Modal
title="配置api"
visible={this.state.apiVisible}
onOk={this.handleAPIOk}
onCancel={this.handleAPICancel}
okText="确定"
cancelText="取消"
>
<div className={styles.apiForm}>
<div className={styles.formItem}>
<Input
placeholder="请输入api地址"
onChange={e => this.handleApiField("api", e.target.value)}
/>
</div>
<div className={styles.formItem}>
<Input.TextArea
placeholder="请输入头信息, 如{token: 123456}, 格式必须为json对象"
rows={4}
onChange={e => this.handleApiField("header", e.target.value)}
/>
</div>
<div className={styles.formItem}>
<Button type="primary" onClick={this.getApiFn}>
</Button>
</div>
{this.state.apiResult && (
<>
<div className={styles.formItem}>
<Input.TextArea
rows={6}
value={JSON.stringify(this.state.apiResult, null, 4)}
/>
</div>
<div className={styles.formItem}>
<Input
placeholder="设置数据源字段"
onChange={e =>
this.handleApiField("dataField", e.target.value)
}
/>
<p style={{ color: "red" }}>
, ,
</p>
</div>
</>
)}
</div>
</Modal>
</div>
);
}
}
export default memo(EditableTable);

View File

@ -0,0 +1,58 @@
:global(.ant-upload-select-picture-card i) {
color: #999;
font-size: 14px;
}
:global(.ant-upload-select-picture-card .ant-upload-text) {
margin-top: 8px;
color: #666;
}
.avatarUploader {
display: inline-block;
text-align: left;
}
.wallBtn {
position: absolute;
left: 140px;
bottom: 56px;
display: inline-block;
color: #2f54eb;
cursor: pointer;
border-bottom: 1px solid #2f54eb;
}
.imgBox {
display: flex;
flex-wrap: wrap;
max-height: 520px;
overflow: auto;
.imgItem {
position: relative;
margin-right: 16px;
margin-bottom: 16px;
width: 320px;
max-height: 220px;
overflow: hidden;
cursor: pointer;
img {
width: 100%;
}
&:hover,
&.seleted {
.iconBtn {
visibility: visible;
}
}
.iconBtn {
position: absolute;
visibility: hidden;
top: 6px;
right: 10px;
font-size: 18px;
color: rgb(8, 156, 8);
}
}
}

View File

@ -0,0 +1,288 @@
import React from "react";
import { Upload, Modal, message, Tabs, Result } from "antd";
import { PlusOutlined, CheckCircleFilled } from "@ant-design/icons";
import ImgCrop from "antd-img-crop";
import classnames from "classnames";
import {
UploadFile,
UploadChangeParam,
RcFile
} from "antd/lib/upload/interface";
import { isDev, unParams, uuid } from "@/utils/tool";
import req from "@/utils/req";
import styles from "./index.less";
const { TabPane } = Tabs;
// 维护图片分类映射
const wallCateName: any = {
photo: "照片",
bg: "背景",
chahua: "插画"
};
function getBase64(file: File | Blob) {
return new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result as string);
reader.onerror = error => reject(error);
});
}
interface PicturesWallType {
fileList?: UploadFile<any>[];
action?: string;
headers?: any;
withCredentials?: boolean;
maxLen?: number;
onChange?: (v: any) => void;
cropRate?: number | boolean;
isCrop?: boolean;
}
class PicturesWall extends React.Component<PicturesWallType> {
state = {
previewVisible: false,
previewImage: "",
wallModalVisible: false,
previewTitle: "",
imgBed: {
photo: [],
bg: [],
chahua: []
},
curSelectedImg: "",
fileList: this.props.fileList || []
};
handleCancel = () => this.setState({ previewVisible: false });
handleModalCancel = () => this.setState({ wallModalVisible: false });
handlePreview = async (file: UploadFile<any>) => {
if (!file.url && !file.preview) {
file.preview = await getBase64(file.originFileObj!);
}
this.setState({
previewImage: file.url || file.preview,
previewVisible: true,
previewTitle:
file.name || file.url!.substring(file.url!.lastIndexOf("/") + 1)
});
};
handleWallSelect = (url: string) => {
this.setState({
wallModalVisible: true
});
};
handleImgSelected = (url: string) => {
this.setState({
curSelectedImg: url
});
};
handleWallShow = () => {
this.setState({
wallModalVisible: true
});
};
handleModalOk = () => {
const fileList = [
{
uid: uuid(8, 16),
name: "h5-dooring图片库",
status: "done",
url: this.state.curSelectedImg
}
];
this.props.onChange && this.props.onChange(fileList);
this.setState({ fileList, wallModalVisible: false });
};
handleChange = ({ file, fileList }: UploadChangeParam<UploadFile<any>>) => {
this.setState({ fileList });
if (file.status === "done") {
const files = fileList.map(item => {
const { uid, name, status } = item;
const url = item.url || item.response.result.url;
return { uid, name, status, url };
});
this.props.onChange && this.props.onChange(files);
}
};
handleBeforeUpload = (file: RcFile) => {
const isJpgOrPng =
file.type === "image/jpeg" ||
file.type === "image/png" ||
file.type === "image/jpg" ||
file.type === "image/gif";
if (!isJpgOrPng) {
message.error("只能上传格式为jpeg/png/gif的图片");
}
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isLt2M) {
message.error("图片必须小于2MB!");
}
return isJpgOrPng && isLt2M;
};
componentDidMount() {
// req.get(`/visible/bed/get?tid=${unParams(location.search)!.tid}`).then(res => {
// res &&
// this.setState({
// imgBed: res,
// });
// });
}
render() {
const {
previewVisible,
previewImage,
fileList,
previewTitle,
wallModalVisible,
imgBed,
curSelectedImg
} = this.state;
const {
action = isDev
? "http://192.168.1.8:3000/api/v0/files/upload/free"
: "你的服务器地址",
headers,
withCredentials = true,
maxLen = 1,
cropRate = 375 / 158,
isCrop
} = this.props;
const uploadButton = (
<div>
<PlusOutlined />
<div className="ant-upload-text"></div>
</div>
);
const cates = Object.keys(imgBed);
return (
<>
{isCrop ? (
<ImgCrop
modalTitle="裁剪图片"
modalOk="确定"
modalCancel="取消"
rotate={true}
aspect={cropRate}
>
<Upload
fileList={fileList}
onPreview={this.handlePreview}
onChange={this.handleChange}
name="file"
listType="picture-card"
className={styles.avatarUploader}
action={action}
withCredentials={withCredentials}
headers={{
"x-requested-with": localStorage.getItem("user") || "",
authorization: localStorage.getItem("token") || "",
...headers
}}
beforeUpload={this.handleBeforeUpload}
>
{fileList.length >= maxLen ? null : uploadButton}
</Upload>
</ImgCrop>
) : (
<Upload
fileList={fileList}
onPreview={this.handlePreview}
onChange={this.handleChange}
name="file"
listType="picture-card"
className={styles.avatarUploader}
action={action}
withCredentials={withCredentials}
headers={{
"x-requested-with": localStorage.getItem("user") || "",
authorization: localStorage.getItem("token") || "",
...headers
}}
beforeUpload={this.handleBeforeUpload}
>
{fileList.length >= maxLen ? null : uploadButton}
</Upload>
)}
<div className={styles.wallBtn} onClick={this.handleWallShow}>
</div>
<Modal
visible={previewVisible}
title={previewTitle}
footer={null}
onCancel={this.handleCancel}
>
<img alt="预览图片" style={{ width: "100%" }} src={previewImage} />
</Modal>
<Modal
visible={wallModalVisible}
title="图片库"
okText="确定"
cancelText="取消"
width={860}
onCancel={this.handleModalCancel}
onOk={this.handleModalOk}
>
<Tabs
defaultActiveKey={cates[0]}
tabPosition="left"
style={{ height: 520 }}
>
{cates.map((item, i) => {
return (
<TabPane tab={wallCateName[item]} key={item}>
<div className={styles.imgBox}>
{(imgBed as any)[item] &&
(imgBed as any)[item].map((item: string, i: number) => {
return (
<div
className={classnames(
styles.imgItem,
curSelectedImg === item ? styles.seleted : ""
)}
key={i}
onClick={() => this.handleImgSelected(item)}
>
<img src={item} alt="趣谈前端-h5-dooring" />
<span className={styles.iconBtn}>
<CheckCircleFilled />
</span>
</div>
);
})}
</div>
</TabPane>
);
})}
<TabPane tab="更多" key="more">
<Result
status="500"
title="Dooring温馨提示"
subTitle="更多素材, 正在筹备中..."
/>
</TabPane>
</Tabs>
</Modal>
</>
);
}
}
export default PicturesWall;

View File

@ -0,0 +1,4 @@
.avatarUploader > :global(.ant-upload) {
width: 128px;
height: 128px;
}

View File

@ -0,0 +1,94 @@
import React, { useState, useEffect, memo } from "react";
import req from "@/utils/req";
import BraftEditor from "braft-editor";
import "braft-editor/dist/index.css";
import styles from "./index.less";
const controls = [
{
key: "bold",
text: <b></b>
},
"undo",
"redo",
"emoji",
"list-ul",
"list-ol",
"blockquote",
"text-align",
"font-size",
"line-height",
"letter-spacing",
"text-color",
"italic",
"underline",
"link",
"media"
];
export default memo(function XEditor(props: any) {
const { value, onChange } = props;
const [editorState, setEditorState] = useState(
BraftEditor.createEditorState(value)
);
const myUploadFn = (param: any) => {
const fd = new FormData();
fd.append("file", param.file);
req
.post("xxxx", fd, {
headers: {
"Content-Type": "multipart/form-data"
},
onUploadProgress: function(event) {
// 上传进度发生变化时调用param.progress
console.log((event.loaded / event.total) * 100);
param.progress((event.loaded / event.total) * 100);
}
})
.then((res: any) => {
// 上传成功后调用param.success并传入上传后的文件地址
param.success({
url: res.url,
meta: {
id: Date.now(),
title: res.filename,
alt: "趣谈前端"
}
});
})
.catch(err => {
param.error({
msg: "上传失败."
});
});
};
const submitContent = () => {
const htmlContent = editorState.toHTML();
onChange && onChange(htmlContent);
};
const handleEditorChange = editorState => {
setEditorState(editorState);
if (onChange) {
const htmlContent = editorState.toHTML();
onChange(htmlContent);
}
};
useEffect(() => {
const htmlContent = value || "";
setEditorState(BraftEditor.createEditorState(htmlContent));
}, []);
return (
<BraftEditor
value={editorState}
controls={controls}
onChange={handleEditorChange}
onSave={submitContent}
media={{ uploadFn: myUploadFn }}
/>
);
});

View File

@ -0,0 +1,243 @@
////////////////////
export interface IUploadConfigType {
key: string;
name: string;
type: "Upload";
isCrop?: boolean;
cropRate?: number;
}
export type TUploadDefaultType = Array<{
uid: string;
name: string;
status: string;
url: string;
}>;
/////////////////
export interface ITextConfigType {
key: string;
name: string;
type: "Text";
}
export type TTextDefaultType = string;
////////////////////////
export interface ITextAreaConfigType {
key: string;
name: string;
type: "TextArea";
}
export type TTextAreaDefaultType = string;
////////////////////////////
export interface INumberConfigType {
key: string;
name: string;
type: "Number";
range?: [number, number];
step?: number;
}
export type TNumberDefaultType = number;
///////////////////
export interface IDataListConfigType {
key: string;
name: string;
type: "DataList";
cropRate: number;
}
export type TDataListDefaultTypeItem = {
id: string;
title: string;
desc: string;
link: string;
type?: number;
imgUrl: TUploadDefaultType;
};
export type TDataListDefaultType = Array<TDataListDefaultTypeItem>;
////////////////////
export interface IColorConfigType {
key: string;
name: string;
type: "Color";
}
export type TColorDefaultType = string;
/////////////////
export interface IRichTextConfigType {
key: string;
name: string;
type: "RichText";
}
export type TRichTextDefaultType = string;
export interface IMutiTextConfigType {
key: string;
name: string;
type: "MutiText";
}
export type TMutiTextDefaultType = Array<string>;
/////////////////////////////////
export interface ISelectConfigType<KeyType> {
key: string;
name: string;
type: "Select";
range: Array<{
key: KeyType;
text: string;
}>;
}
export type TSelectDefaultType<KeyType> = KeyType;
/////////////////////////
export interface IRadioConfigType<KeyType> {
key: string;
name: string;
type: "Radio";
range: Array<{
key: KeyType;
text: string;
}>;
}
export type TRadioDefaultType<KeyType> = KeyType;
///////////////
export interface ISwitchConfigType {
key: string;
name: string;
type: "Switch";
}
export type TSwitchDefaultType = boolean;
/////////////////////////////
export interface ICardPickerConfigType<T> {
key: string;
name: string;
type: "CardPicker";
icons: Array<T>;
}
export type TCardPickerDefaultType<T> = T;
/////////////
export interface ITableConfigType {
key: string;
name: string;
type: "Table";
}
export type TTableDefaultType = Array<{
name: string;
value: number;
}>;
// position input control
export interface IPosConfigType {
key: string;
name: string;
type: "Pos";
placeObj: {
text: string;
link: string;
};
}
export type TPosItem = number | undefined;
export type TPosDefaultType = [TPosItem, TPosItem];
//////////////////
export interface IFormItemsConfigType {
key: string;
name: string;
type: "FormItems";
}
//0---------baseform
export type baseFormOptionsType = {
label: string;
value: string;
};
export type baseFormTextTpl = {
id: string;
type: "Text";
label: string;
placeholder: string;
};
export type baseFormTextTipTpl = {
id: string;
type: "MyTextTip";
label: string;
color: string;
fontSize: number;
};
export type baseFormNumberTpl = {
id: string;
type: "Number";
label: string;
placeholder: string;
};
export type baseFormTextAreaTpl = {
id: string;
type: "Textarea";
label: string;
placeholder: string;
};
export type baseFormMyRadioTpl = {
id: string;
type: "MyRadio";
label: string;
options: baseFormOptionsType[];
};
export type baseFormMyCheckboxTpl = {
id: string;
type: "MyCheckbox";
label: string;
options: baseFormOptionsType[];
};
export type baseFormMySelectTpl = {
id: string;
type: "MySelect";
label: string;
options: baseFormOptionsType[];
};
export type baseFormDateTpl = {
id: string;
type: "Date";
label: string;
placeholder: string;
};
export type baseFormUnion =
| baseFormTextTpl
| baseFormTextTipTpl
| baseFormNumberTpl
| baseFormTextAreaTpl
| baseFormMyRadioTpl
| baseFormMyCheckboxTpl
| baseFormMySelectTpl
| baseFormDateTpl;
export type baseFormUnionType =
| baseFormTextTpl["type"]
| baseFormTextTipTpl["type"]
| baseFormNumberTpl["type"]
| baseFormTextAreaTpl["type"]
| baseFormMyRadioTpl["type"]
| baseFormMyCheckboxTpl["type"]
| baseFormMySelectTpl["type"]
| baseFormDateTpl["type"];
export type TFormItemsDefaultType = Array<baseFormUnion>;

View File

@ -0,0 +1,16 @@
.pickerWrap {
display: flex;
flex-wrap: wrap;
.picker {
display: inline-block;
padding: 10px;
border: 2px solid transparent;
cursor: pointer;
&:hover {
border-color: #4091f7;
}
&.selected {
border-color: #4091f7;
}
}
}

View File

@ -0,0 +1,49 @@
import { useState, useEffect, memo } from "react";
import classnames from "classnames";
import Icon from "@/materials/base/Icon";
import styles from "./index.less";
import React from "react";
import { IconTypes } from "@/materials/base/Icon/schema";
import { ICardPickerConfigType } from "../types";
interface CardPickerType
extends Omit<ICardPickerConfigType<IconTypes>, "type" | "key" | "name"> {
onChange?: (v: string) => void;
type: IconTypes;
}
export default memo((props: CardPickerType) => {
const { type, icons, onChange } = props;
const [selected, setSelected] = useState<IconTypes>(type);
const handlePicker = (v: IconTypes) => {
if (onChange) {
onChange(v);
return;
}
setSelected(v);
};
useEffect(() => {
setSelected(type);
}, [type]);
return (
<div className={styles.pickerWrap}>
{icons.map((item, i) => {
return (
<span
className={classnames(
styles.picker,
selected === item ? styles.selected : ""
)}
onClick={() => handlePicker(item)}
key={i}
>
<Icon type={item} size={20} color={"#4091f7"} spin={false} />
</span>
);
})}
</div>
);
});

View File

@ -0,0 +1,89 @@
import React from "react";
import { SketchPicker, ColorResult } from "react-color";
import { rgba2Obj } from "@/utils/tool";
export type ColorConfigType = string;
//value 初始值传来onchange item给的回调
interface ColorProps {
value?: ColorConfigType;
onChange?: (v: ColorConfigType) => void;
}
class colorPicker extends React.Component<ColorProps> {
state = {
displayColorPicker: false,
color: rgba2Obj(this.props.value)
};
handleClick = () => {
this.setState({ displayColorPicker: !this.state.displayColorPicker });
};
handleClose = () => {
this.setState({ displayColorPicker: false });
};
handleChange = (color: ColorResult) => {
this.setState({ color: color.rgb });
this.props.onChange &&
this.props.onChange(
`rgba(${color.rgb.r},${color.rgb.g},${color.rgb.b},${color.rgb.a})`
);
};
render() {
return (
<div>
<div
style={{
// padding: '5px',
background: "#fff",
borderRadius: "1px",
boxShadow: "0 0 0 1px rgba(0,0,0,.1)",
display: "inline-block",
cursor: "pointer"
}}
onClick={this.handleClick}
>
<div
style={{
width: "20px",
height: "20px",
borderRadius: "2px",
background: `rgba(${this.state.color.r}, ${this.state.color.g}, ${this.state.color.b}, ${this.state.color.a})`
}}
/>
</div>
{this.state.displayColorPicker ? (
<React.Fragment>
<div
style={{
position: "absolute",
zIndex: 2000
}}
>
<SketchPicker
color={this.state.color}
onChange={this.handleChange}
/>
</div>
<div
style={{
position: "fixed",
top: "0px",
right: "0px",
bottom: "0px",
left: "0px",
zIndex: 1000
}}
onClick={this.handleClose}
/>
</React.Fragment>
) : null}
</div>
);
}
}
export default colorPicker;

View File

@ -0,0 +1,127 @@
import React, { memo, useEffect, FC } from "react";
import { Form, Select, Input, Modal, Button } from "antd";
import Upload from "../Upload";
import { Store } from "antd/lib/form/interface";
import { TDataListDefaultTypeItem } from "../FormEditor/types";
// import styles from './index.less';
const normFile = (e: any) => {
if (Array.isArray(e)) {
return e;
}
return e && e.fileList;
};
const { Option } = Select;
const formItemLayout = {
labelCol: { span: 6 },
wrapperCol: { span: 14 }
};
export type EditorModalProps = {
visible: boolean;
onCancel:
| ((e: React.MouseEvent<HTMLElement, MouseEvent>) => void)
| undefined;
item?: TDataListDefaultTypeItem;
onSave: Function;
cropRate: number;
};
const EditorModal: FC<EditorModalProps> = props => {
const { item, onSave, visible, onCancel, cropRate } = props;
const onFinish = (values: Store) => {
console.log(values);
onSave && onSave(values);
};
const handleOk = () => {
form
.validateFields()
.then(values => {
if (item) {
values.id = item.id;
onSave && onSave(values);
}
})
.catch(err => {
console.log(err);
});
};
const [form] = Form.useForm();
useEffect(() => {
if (form && item && visible) {
form.resetFields();
}
}, [form, item, visible]);
return (
<>
{!!item && (
<Modal
title="编辑数据源"
closable={false}
visible={visible}
onOk={handleOk}
okText="确定"
forceRender
footer={
<Button type={"primary"} onClick={() => handleOk()}>
</Button>
}
>
<Form
form={form}
name={`form_editor_modal`}
{...formItemLayout}
onFinish={onFinish}
initialValues={item}
>
<Form.Item
label="标题"
name="title"
rules={[{ required: true, message: "请输入标题!" }]}
>
<Input />
</Form.Item>
<Form.Item label="描述" name="desc">
<Input />
</Form.Item>
<Form.Item label="链接地址" name="link">
<Input />
</Form.Item>
{!!window["currentCates"] && (
<Form.Item
label="分类"
name="type"
rules={[{ required: true, message: "请选择分类!" }]}
>
<Select placeholder="请选择">
{window["currentCates"].map((v, i) => {
return (
<Option value={i} key={i}>
{v}
</Option>
);
})}
</Select>
</Form.Item>
)}
<Form.Item
label="上传图片"
name="imgUrl"
valuePropName="fileList"
getValueFromEvent={normFile}
>
<Upload cropRate={cropRate} isCrop />
</Form.Item>
</Form>
</Modal>
)}
</>
);
};
export default memo(EditorModal);

View File

@ -0,0 +1,46 @@
.dataList {
padding: 6px 10px;
border: 1px solid #f0f0f0;
text-align: justify;
padding-left: 10px;
padding-top: 10px;
}
.listItem {
position: relative;
padding-bottom: 6px;
margin-bottom: 6px;
border-bottom: 1px solid #f0f0f0;
&:hover {
.actionBar {
display: block;
}
}
&:last-child {
border-bottom: none;
margin-bottom: 0;
}
.tit {
font-weight: bold;
padding-bottom: 5px;
}
.desc {
font-size: 12px;
color: #ccc;
}
.actionBar {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
display: none;
background: #fff;
box-shadow: -20px 0 10px 10px #fff;
.action {
margin-right: 18px;
cursor: pointer;
&:last-child {
cursor: move;
}
}
}
}

View File

@ -0,0 +1,263 @@
import React, { memo, useState, useEffect, useCallback } from "react";
import {
EditOutlined,
MinusCircleOutlined,
MenuOutlined
} from "@ant-design/icons";
import { Button } from "antd";
import {
DragSource,
DropTarget,
DndProvider,
ConnectDropTarget,
DragSourceSpec,
DropTargetConnector,
DragSourceMonitor,
DragSourceConnector,
DropTargetSpec,
ConnectDragSource,
ConnectDragPreview
} from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import EditorModal from "./editorModal";
import { uuid } from "@/utils/tool";
import styles from "./index.less";
import { TDataListDefaultType, TDataListDefaultTypeItem } from "../types";
type ListItemProps = DndItemProps & {
isDragging: boolean;
connectDragSource: ConnectDragSource;
connectDragPreview: ConnectDragPreview;
connectDropTarget: ConnectDropTarget;
};
function ListItem(props: ListItemProps) {
const {
title,
desc,
onDel,
onEdit,
// 这些 props 由 React DnD注入参考`collect`函数定义
isDragging,
connectDragSource,
connectDragPreview,
connectDropTarget
} = props;
const opacity = isDragging ? 0.5 : 1;
return connectDropTarget(
// 列表项本身作为 Drop 对象
connectDragPreview(
// 整个列表项作为跟随拖动的影像
<div className={styles.listItem} style={Object.assign({}, { opacity })}>
<div className={styles.tit}>{title}</div>
<div className={styles.desc}>{desc}</div>
<div className={styles.actionBar}>
<span className={styles.action} onClick={() => onEdit()}>
<EditOutlined />
</span>
<span className={styles.action} onClick={() => onDel()}>
<MinusCircleOutlined />
</span>
{connectDragSource(
<span className={styles.action}>
<MenuOutlined />
</span>
) // 拖动图标作为 Drag 对象
}
</div>
</div>
)
);
}
type DndItemProps = TDataListDefaultTypeItem & {
onDel: Function;
onEdit: Function;
key: number;
find: Function;
move: Function;
type?: number;
};
const type = "item";
type DragObject = {
id: string;
originalIndex: number;
};
const dragSpec: DragSourceSpec<DndItemProps, DragObject> = {
// 拖动开始时,返回描述 source 数据。后续通过 monitor.getItem() 获得
beginDrag: props => ({
id: props.id,
originalIndex: props.find(props.id).index
}),
// 拖动停止时,处理 source 数据
endDrag(props, monitor) {
const { id: droppedId, originalIndex } = monitor.getItem();
const didDrop = monitor.didDrop();
// source 是否已经放置在 target
if (!didDrop) {
return props.move(droppedId, originalIndex);
}
}
};
const dragCollect = (
connect: DragSourceConnector,
monitor: DragSourceMonitor
) => ({
connectDragSource: connect.dragSource(), // 用于包装需要拖动的组件
connectDragPreview: connect.dragPreview(), // 用于包装需要拖动跟随预览的组件
isDragging: monitor.isDragging() // 用于判断是否处于拖动状态
});
const dropSpec: DropTargetSpec<DndItemProps> = {
canDrop: () => false, // item 不处理 drop
hover(props, monitor) {
const { id: draggedId } = monitor.getItem();
const { id: overId } = props;
// 如果 source item 与 target item 不同,则交换位置并重新排序
if (draggedId !== overId) {
const { index: overIndex } = props.find(overId);
props.move(draggedId, overIndex);
}
}
};
const dropCollect = (connect: DropTargetConnector) => ({
connectDropTarget: connect.dropTarget() // 用于包装需接收拖拽的组件
});
const DndItem = DropTarget(
type,
dropSpec,
dropCollect
)(DragSource(type, dragSpec, dragCollect)(ListItem));
export type DataListMemo = {
onChange?: (v: TDataListDefaultType) => void;
value?: TDataListDefaultType;
cropRate: number;
};
export type DataListType = DataListMemo & {
connectDropTarget: ConnectDropTarget;
};
const List = function(props: DataListType) {
const { onChange, value, connectDropTarget, cropRate } = props;
const [list, setList] = useState(value);
const [visible, setVisible] = useState(false);
const [curItem, setCurItem] = useState<TDataListDefaultTypeItem>();
const handleDel = (id: string) => {
if (value && onChange) {
let newVal = value.filter(item => id !== item.id);
onChange(newVal);
}
};
const find = (id: string) => {
const item = list!.find(c => `${c.id}` === id)!;
return {
item,
index: list!.indexOf(item!)
};
};
const move = (id: string, toIndex: number) => {
const { item, index } = find(id);
const oldList = [...list!];
oldList.splice(index, 1);
oldList.splice(toIndex, 0, item);
if (onChange) {
onChange(oldList);
return;
}
setList(oldList);
};
const handleCancel = useCallback(() => {
console.log("a");
setVisible(false);
}, []);
const handleEdit = useCallback((item: TDataListDefaultTypeItem) => {
console.log("b");
setVisible(true);
setCurItem(item);
}, []);
const handleSave = useCallback(
(item: TDataListDefaultTypeItem) => {
console.log("c");
setVisible(false);
if (onChange) {
onChange(list!.map(p => (p.id === item.id ? item : p)));
return;
}
setList(prev => prev!.map(p => (p.id === item.id ? item : p)));
},
[list, onChange]
);
const handleAdd = () => {
const item = {
title: "新增项标题",
desc: "新增项描述",
id: uuid(8, 10),
imgUrl: [],
link: ""
};
if (onChange) {
onChange([...list!, item]);
return;
}
setList([...list!, item]);
};
useEffect(() => {
setList(value);
}, [value]);
return connectDropTarget(
<div className={styles.dataList}>
{!!(list && list.length) &&
list.map((item, i) => (
<DndItem
{...item}
onDel={() => handleDel(item.id)}
onEdit={() => handleEdit(item)}
key={i}
id={`${item.id}`}
find={find}
move={move}
/>
))}
<div style={{ marginTop: "10px" }}>
<Button onClick={handleAdd} block>
</Button>
</div>
<EditorModal
visible={visible}
onCancel={handleCancel}
item={curItem}
onSave={handleSave}
cropRate={cropRate}
/>
</div>
);
};
const DndList = DropTarget(type, {}, connect => ({
connectDropTarget: connect.dropTarget()
}))(List);
// 将 HTMLBackend 作为参数传给 DragDropContext
export default memo((props: DataListMemo) => {
return (
<DndProvider backend={HTML5Backend}>
<DndList {...props} />
</DndProvider>
);
});

View File

@ -0,0 +1,137 @@
import React, { FC, memo, useEffect } from "react";
import { Form, Select, Input, Modal, Button, InputNumber } from "antd";
import { baseFormOptionsType } from "../types";
import Color from "../Color";
const { Option } = Select;
const formItemLayout = {
labelCol: { span: 6 },
wrapperCol: { span: 14 }
};
interface EditorModalProps {
item: any;
onSave: (data: any) => void;
visible: boolean;
}
const EditorModal: FC<EditorModalProps> = props => {
const { item, onSave, visible } = props;
const onFinish = (values: any) => {
onSave && onSave(values);
};
const handleOk = () => {
form
.validateFields()
.then(values => {
values.id = item.id;
onSave && onSave(values);
})
.catch(err => {
console.log(err);
});
};
const [form] = Form.useForm();
useEffect(() => {
if (form && item && visible) {
form.resetFields();
}
}, [form, item, visible]);
return (
<>
{!!item && (
<Modal
title="编辑表单组件"
footer={
<div>
<Button type="primary" onClick={() => handleOk()}>
</Button>
</div>
}
forceRender
visible={visible}
onOk={handleOk}
closable={false}
>
<Form
form={form}
name={`formItem_editor_modal`}
{...formItemLayout}
onFinish={onFinish}
initialValues={item}
>
{
<Form.Item label="类型" name="type" hidden>
<Input />
</Form.Item>
}
{!!item.label && (
<Form.Item
label="字段名"
name="label"
rules={[{ required: true, message: "请输入字段名!" }]}
>
<Input />
</Form.Item>
)}
{!!item.fontSize && (
<Form.Item
label="字体大小"
name="fontSize"
rules={[{ required: true, message: "请输入字体大小!" }]}
>
<InputNumber min={12} max={30} defaultValue={14} />
</Form.Item>
)}
{!!item.color && (
<Form.Item
label="文字颜色"
name="color"
rules={[{ required: true, message: "请输入文字颜色!" }]}
>
<Color />
</Form.Item>
)}
{!!item.placeholder && (
<Form.Item label="提示文本" name="placeholder">
<Input placeholder="请输入提示文本" />
</Form.Item>
)}
{!!item.options && (
<Form.Item
label="选项源"
name="options"
rules={[{ required: true, message: "选项不能为空!" }]}
>
<Select
placeholder="请输入"
mode="tags"
labelInValue
maxTagCount={39}
maxTagTextLength={16}
>
{item.options.map((v: baseFormOptionsType, i: number) => {
return (
<Option value={v.value} key={i}>
{v.label}
</Option>
);
})}
</Select>
</Form.Item>
)}
</Form>
</Modal>
)}
</>
);
};
export default memo(EditorModal);

View File

@ -0,0 +1,215 @@
import React, {
memo,
RefObject,
useCallback,
useEffect,
useState
} from "react";
import BaseForm from "@/materials/base/Form/BaseForm";
import BasePopoverForm from "@/materials/base/Form/BasePopoverForm";
import EditorModal from "./EditorModal";
import { MinusCircleFilled, EditFilled, PlusOutlined } from "@ant-design/icons";
import styles from "./formItems.less";
import { baseFormUnion, TFormItemsDefaultType } from "../types";
import { uuid } from "@/utils/tool";
import { Button } from "antd";
import MyPopover from "yh-react-popover";
// import { Popconfirm } from 'antd';
const formTpl: TFormItemsDefaultType = [
{
id: "1",
type: "Text",
label: "文本框",
placeholder: "请输入文本"
},
{
id: "2",
type: "Textarea",
label: "长文本框",
placeholder: "请输入长文本请输入长文本"
},
{
id: "3",
type: "Number",
label: "数值",
placeholder: " 请输入数值"
},
{
id: "4",
type: "MyRadio",
label: "单选框",
options: [
{ label: "选项一", value: "1" },
{ label: "选项二", value: "2" }
]
},
{
id: "5",
type: "MySelect",
label: "下拉选择框",
options: [
{ label: "选项一", value: "1" },
{ label: "选项二", value: "2" },
{ label: "选项三", value: "3" }
]
},
{
id: "6",
type: "Date",
label: "日期框",
placeholder: ""
},
{
id: "7",
type: "MyTextTip",
label: "纯文本",
fontSize: 12,
color: "rgba(0,0,0,1)"
}
];
interface FormItemsProps {
formList?: TFormItemsDefaultType;
onChange?: (v: TFormItemsDefaultType) => void;
data: any;
rightPannelRef: RefObject<HTMLDivElement>;
}
const FormItems = (props: FormItemsProps) => {
const { formList, onChange, rightPannelRef } = props;
const [formData, setFormData] = useState<TFormItemsDefaultType>(
formList || []
);
const [visible, setVisible] = useState(false);
const [curItem, setCurItem] = useState<baseFormUnion>();
const [force, setforce] = useState<{ force: Function }>({
force: () => {}
});
const handleAddItem = (item: baseFormUnion) => {
let tpl = formTpl.find(v => v.type === item.type);
let newData = [...formData, { ...tpl!, id: uuid(6, 10) }];
setFormData(newData);
onChange && onChange(newData);
force.force();
};
const handleEditItem = (item: baseFormUnion) => {
setVisible(true);
setCurItem(item);
};
const handleDelItem = (item: baseFormUnion) => {
let newData = formData.filter(v => v.id !== item.id);
setFormData(newData);
onChange && onChange(newData);
};
const handleSaveItem = (data: baseFormUnion) => {
let newData = formData.map(v => (v.id === data.id ? data : v));
setFormData(newData);
onChange && onChange(newData);
setVisible(false);
};
const callback = useCallback((v: Function) => {
console.log(v);
setforce({ force: v });
}, []);
useEffect(() => {
let listenner: (e: Event) => void;
if (rightPannelRef.current) {
listenner = () => {
force.force();
};
rightPannelRef.current.addEventListener("scroll", listenner);
}
return () => {
if (rightPannelRef.current) {
// eslint-disable-next-line react-hooks/exhaustive-deps
rightPannelRef.current.removeEventListener("scroll", listenner);
}
};
}, [force, rightPannelRef]);
return (
<div className={styles.formItemWrap}>
<div className={styles.formTitle}></div>
<div className={styles.editForm}>
{formData.map((item: baseFormUnion, i: number) => {
let FormItem = BaseForm[item.type];
return (
<div className={styles.formItem} key={i}>
<div className={styles.disClick}>
<FormItem {...item} />
</div>
<div className={styles.deleteWrap}>
<span
className={styles.operationBtn}
onClick={() => handleDelItem(item)}
>
<MinusCircleFilled />
</span>
</div>
<div className={styles.editWrap}>
<span
className={styles.operationBtn}
onClick={() => handleEditItem(item)}
>
<EditFilled />
</span>
</div>
</div>
);
})}
<div className={styles.formAddWrap}>
<MyPopover
content={
<>
<div className={styles.formTpl} style={{ color: "red" }}>
{formTpl.map((item, i) => {
let FormItem = BasePopoverForm[item.type];
return (
<div
className={styles.formItem}
key={i}
onClick={() => handleAddItem(item)}
>
<div
className={styles.disClick}
style={{
display: "flex",
flexDirection: "column",
overflow: "row",
marginTop: "10px"
}}
>
<FormItem {...item} />
</div>
</div>
);
})}
</div>
{/* <a style={{color: 'red'}} onClick={() => setFormTplVisible(false)}>Close</a> */}
</>
}
directions={"LB"}
innerConstDomStyle={{ display: "block" }}
constDomStyle={{ display: "block" }}
callback={callback}
>
<Button style={{ width: "100%" }} block icon={<PlusOutlined />}>
</Button>
</MyPopover>
</div>
</div>
<EditorModal item={curItem} onSave={handleSaveItem} visible={visible} />
</div>
);
};
export default memo(FormItems);

View File

@ -0,0 +1,88 @@
.formItemWrap {
.formTitle {
width: 56px;
height: 20px;
font-size: 14px;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: bold;
color: #000000;
line-height: 20px;
}
.editForm {
text-align: left;
width: 251px;
.formItem {
position: relative;
padding-left: 2px;
.common {
position: absolute;
top: 19px;
box-shadow: 0 0 20px #fff;
.operationBtn {
margin-right: 15px;
display: inline-block;
cursor: pointer;
}
}
.deleteWrap {
.common;
left: 0;
}
.editWrap {
.common;
right: -18px;
}
}
.formAddWrap {
font-size: 14px;
font-weight: 400;
color: #4a4a4a;
line-height: 20px;
background-color: #2f54eb;
}
}
.formAddWrap {
.formTpl {
margin-top: 12px;
border-top: 1px dashed #ccc;
padding-top: 16px;
background-color: #4a4a4a;
.formItem {
button,
[type="button"] {
color: #fff;
background-color: #4a4a4a;
border: 1px solid #fff;
border-radius: 4px 0px 0px 0px;
}
position: relative;
border: 1px solid #ccc;
margin-bottom: 2px;
background-color: #4a4a4a;
cursor: pointer;
.disClick {
pointer-events: none;
color: #fff;
}
&:hover {
border-color: #2f54eb;
.addBtn {
display: inline-block;
}
}
.addBtn {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
display: none;
padding: 3px 6px;
color: #fff;
border-radius: 3px;
background-color: #2f54eb;
cursor: pointer;
}
}
}
}
}

View File

@ -0,0 +1,2 @@
import FormItems from "./FormItems";
export default FormItems;

View File

@ -0,0 +1,12 @@
.mutiText {
.iptWrap {
margin-bottom: 12px;
display: flex;
.delBtn {
// font-size: 14px;
margin-left: 12px;
cursor: pointer;
align-self: center;
}
}
}

View File

@ -0,0 +1,74 @@
import React, { memo, useEffect } from "react";
import { Input, Button, Popconfirm } from "antd";
import { MinusCircleFilled } from "@ant-design/icons";
import styles from "./index.less";
import { TMutiTextDefaultType } from "../types";
type MultiTextProps = {
onChange?: (v: TMutiTextDefaultType) => void;
value?: TMutiTextDefaultType;
};
export default memo(function MutiText(props: MultiTextProps) {
const { value, onChange } = props;
const handleAdd = () => {
onChange && onChange([...value!, "新增项目"]);
};
const handleDel = (index: number) => {
let newList = value!.filter((_item, i) => i !== index);
onChange && onChange(newList);
};
const handleChange = (
index: number,
e: React.ChangeEvent<HTMLInputElement>
) => {
let newList = value!.map((item, i) =>
i === index ? e.target.value : item
);
onChange && onChange(newList);
};
useEffect(() => {
window["currentCates"] = value!;
return () => {
window["currentCates"] = null;
};
}, [value]);
return (
<div className={styles.mutiText}>
{value && value.length ? (
value!.map((item, i) => {
return (
<div className={styles.iptWrap} key={i}>
<Input defaultValue={item} onChange={e => handleChange(i, e)} />
<Popconfirm
title="确定要删除吗?"
onConfirm={() => handleDel(i)}
placement="leftTop"
okText="确定"
cancelText="取消"
>
<span className={styles.delBtn}>
<MinusCircleFilled />
</span>
</Popconfirm>
</div>
);
})
) : (
<div className={styles.iptWrap}>
<Input />
</div>
)}
{value && value.length < 3 && (
<div className={styles.iptWrap}>
<Button block onClick={handleAdd}>
</Button>
</div>
)}
</div>
);
});

View File

@ -0,0 +1,11 @@
.posIpt {
display: flex;
justify-content: flex-end;
margin-right: -10px;
.posItem {
margin-right: 10px;
span {
margin-right: 3px;
}
}
}

View File

@ -0,0 +1,39 @@
import React, { memo, useState, useEffect } from "react";
import { InputNumber } from "antd";
import styles from "./index.less";
import { TPosDefaultType, TPosItem } from "../types";
type PosProps = {
value?: TPosDefaultType;
onChange?: (v: TPosItem | string) => void;
};
export default memo(function Pos(props: PosProps) {
const { value, onChange } = props;
let _this: typeof Pos = Pos;
const handleChange = (index: number, v: TPosItem | string) => {
let arr: any = value || [];
arr[index] = v;
onChange && onChange(arr);
};
return (
<div className={styles.posIpt}>
<div className={styles.posItem}>
<span>x: </span>
<InputNumber
defaultValue={value && value[0]}
onChange={handleChange.bind(_this, 0)}
/>
</div>
<div className={styles.posItem}>
<span>y: </span>
<InputNumber
defaultValue={value && value[1]}
onChange={handleChange.bind(_this, 1)}
/>
</div>
</div>
);
});

View File

@ -0,0 +1,32 @@
:global(.editable-cell) {
position: relative;
}
:global(.editable-cell-value-wrap) {
padding: 5px 12px;
cursor: pointer;
}
:global(.editable-row) {
&:hover :global(.editable-cell-value-wrap) {
border: 1px solid #d9d9d9;
border-radius: 4px;
padding: 4px 11px;
}
}
:global([data-theme="dark"]) {
:global(.editable-row) {
&:hover {
:global(.editable-cell-value-wrap) {
border: 1px solid #434343;
}
}
}
}
.apiForm {
.formItem {
margin-bottom: 16px;
}
}

View File

@ -0,0 +1,449 @@
import React, {
useContext,
useState,
useEffect,
useRef,
memo,
RefObject
} from "react";
import { Table, Input, Button, Popconfirm, Form, Modal, Upload } from "antd";
import { ColumnsType } from "antd/lib/table";
import { uuid } from "@/utils/tool";
import XLSX from "xlsx";
// 下方样式主要为全局样式,暂时不可删
import styles from "./index.less";
const EditableContext = React.createContext<any>(null);
interface Item {
key: string;
name: string;
age: string;
address: string;
}
interface EditableRowProps {
index: number;
}
const EditableRow: React.FC<EditableRowProps> = ({ index, ...props }) => {
const [form] = Form.useForm();
return (
<Form form={form} component={false}>
<EditableContext.Provider value={form}>
<tr {...props} />
</EditableContext.Provider>
</Form>
);
};
interface EditableCellProps {
title: React.ReactNode;
editable: boolean;
children: React.ReactNode;
dataIndex: string;
record: any;
handleSave: (record: Item) => void;
}
const EditableCell: React.FC<EditableCellProps> = ({
title,
editable,
children,
dataIndex,
record,
handleSave,
...restProps
}) => {
const [editing, setEditing] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const form = useContext(EditableContext);
useEffect(() => {
if (editing) {
inputRef.current?.focus();
}
}, [editing]);
const toggleEdit = () => {
setEditing(!editing);
form.setFieldsValue({ [dataIndex]: record[dataIndex] });
};
const save = async () => {
try {
const values = await form.validateFields();
toggleEdit();
handleSave({ ...record, ...values });
} catch (errInfo) {
console.log("Save failed:", errInfo);
}
};
let childNode = children;
if (editable) {
childNode = editing ? (
<Form.Item
style={{ margin: 0 }}
name={dataIndex}
rules={[
{
required: true,
message: `${title} 是必填的.`
}
]}
>
<Input
ref={(inputRef as unknown) as () => RefObject<HTMLInputElement>}
onPressEnter={save}
onBlur={save}
/>
</Form.Item>
) : (
<div
className="editable-cell-value-wrap"
style={{ paddingRight: 24 }}
onClick={toggleEdit}
>
{children}
</div>
);
}
return <td {...restProps}>{childNode}</td>;
};
class EditableTable extends React.Component<any, any> {
columns: (
| {
title: string;
dataIndex: string;
width: string;
editable: boolean;
render?: undefined;
}
| {
title: string;
dataIndex: string;
render: (text: string, record: any) => JSX.Element | null;
width?: undefined;
editable?: undefined;
}
)[];
apiForm: {
api: string;
header: string;
dataField: string;
};
constructor(props: any) {
super(props);
this.columns = [
{
title: "名字",
dataIndex: "name",
width: "180px",
editable: true
},
{
title: "值",
dataIndex: "value",
width: "120px",
editable: true
},
{
title: "操作",
dataIndex: "operation",
render: (text: string, record) =>
this.state.dataSource.length >= 1 ? (
<Popconfirm
title="Sure to delete?"
onConfirm={() => this.handleDelete(record.key)}
>
<Button type="link"></Button>
</Popconfirm>
) : null
}
];
this.apiForm = {
api: "",
header: "",
dataField: ""
};
const dataSource =
props.data &&
props.data.map((item: any, i: number) => ({ key: i + "", ...item }));
this.state = {
dataSource: dataSource,
visible: false,
apiVisible: false,
apiResult: ""
};
}
handleDelete = (key: string) => {
const dataSource = [...this.state.dataSource];
const newDataSource = dataSource.filter(item => item.key !== key);
this.setState({ dataSource: newDataSource });
this.props.onChange && this.props.onChange(newDataSource);
};
handleAdd = () => {
const { dataSource } = this.state;
const uid = uuid(8, 10);
const newData = {
key: uid,
name: `dooring ${dataSource.length + 1}`,
value: 32
};
const newDataSource = [...dataSource, newData];
this.setState({
dataSource: newDataSource
});
this.props.onChange && this.props.onChange(newDataSource);
};
handleSave = (row: any) => {
const newData = [...this.state.dataSource];
const index = newData.findIndex(item => row.key === item.key);
const item = newData[index];
newData.splice(index, 1, {
...item,
...row
});
this.setState({ dataSource: newData });
this.props.onChange && this.props.onChange(newData);
};
showModal = () => {
this.setState({
visible: true
});
};
handleOk = (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
this.setState({
visible: false
});
};
handleCancel = (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
this.setState({
visible: false
});
};
showApiModal = () => {
this.setState({
apiVisible: true
});
};
handleAPIOk = () => {
const { dataField } = this.apiForm;
if (dataField) {
let data = this.state.apiResult[dataField];
if (data && data instanceof Array) {
data = data.map((item, i) => ({ key: i + "", ...item }));
this.setState({
dataSource: data
});
this.props.onChange && this.props.onChange(data);
}
this.setState({
apiVisible: false
});
}
};
handleAPICancel = () => {
this.setState({
apiVisible: false
});
};
handleApiField = (type: "api" | "header" | "dataField", v: string) => {
this.apiForm[type] = v;
};
getApiFn = () => {
console.log(this.apiForm);
const { api, header } = this.apiForm;
fetch(api, {
cache: "no-cache",
headers: Object.assign(
{ "content-type": "application/json" },
header ? JSON.parse(header) : {}
),
method: "GET",
mode: "cors"
})
.then(res => res.json())
.then(res => {
this.setState({
apiResult: res
});
});
};
render() {
const { dataSource } = this.state;
const components = {
body: {
row: EditableRow,
cell: EditableCell
}
};
const columns: ColumnsType<any> = this.columns.map(col => {
if (!col.editable) {
return col;
}
return {
...col,
onCell: record => ({
record,
editable: col.editable,
dataIndex: col.dataIndex,
title: col.title,
handleSave: this.handleSave
})
};
});
const _this = this;
const props = {
name: "file",
// action: '',
showUploadList: false,
beforeUpload(file: File, fileList: Array<File>) {
// 解析并提取excel数据
let reader = new FileReader();
reader.onload = function(e: any) {
let data = e.target.result;
let workbook = XLSX.read(data, { type: "binary" });
let sheetNames = workbook.SheetNames; // 工作表名称集合
let draftArr: any = {};
sheetNames.forEach(name => {
let worksheet = workbook.Sheets[name]; // 只能通过工作表名称来获取指定工作表
for (let key in worksheet) {
// v是读取单元格的原始值
if (key[0] !== "!") {
if (draftArr[key[0]]) {
draftArr[key[0]].push(worksheet[key].v);
} else {
draftArr[key[0]] = [worksheet[key].v];
}
}
}
});
let sourceData = Object.values(draftArr).map((item: any, i) => ({
key: i + "",
name: item[0],
value: item[1]
}));
_this.setState({
dataSource: sourceData
});
_this.props.onChange && _this.props.onChange(sourceData);
};
reader.readAsBinaryString(file);
}
};
return (
<div>
<Button type="primary" onClick={this.showModal}>
</Button>
<Modal
title="编辑数据源"
visible={this.state.visible}
onOk={this.handleOk}
onCancel={this.handleCancel}
okText="确定"
cancelText="取消"
>
<Button
onClick={this.handleAdd}
type="primary"
style={{ marginBottom: 16, marginRight: 16 }}
>
</Button>
<Upload {...props}>
<Button type="primary" ghost style={{ marginRight: 16 }}>
Excel
</Button>
</Upload>
<Button type="primary" ghost onClick={this.showApiModal}>
API
</Button>
<Table
components={components}
rowClassName={() => "editable-row"}
bordered
dataSource={dataSource}
columns={columns}
pagination={{ pageSize: 50 }}
scroll={{ y: 240 }}
/>
</Modal>
<Modal
title="配置api"
visible={this.state.apiVisible}
onOk={this.handleAPIOk}
onCancel={this.handleAPICancel}
okText="确定"
cancelText="取消"
>
<div className={styles.apiForm}>
<div className={styles.formItem}>
<Input
placeholder="请输入api地址"
onChange={e => this.handleApiField("api", e.target.value)}
/>
</div>
<div className={styles.formItem}>
<Input.TextArea
placeholder="请输入头信息, 如{token: 123456}, 格式必须为json对象"
rows={4}
onChange={e => this.handleApiField("header", e.target.value)}
/>
</div>
<div className={styles.formItem}>
<Button type="primary" onClick={this.getApiFn}>
</Button>
</div>
{this.state.apiResult && (
<>
<div className={styles.formItem}>
<Input.TextArea
rows={6}
value={JSON.stringify(this.state.apiResult, null, 4)}
/>
</div>
<div className={styles.formItem}>
<Input
placeholder="设置数据源字段"
onChange={e =>
this.handleApiField("dataField", e.target.value)
}
/>
<p style={{ color: "red" }}>
, ,
</p>
</div>
</>
)}
</div>
</Modal>
</div>
);
}
}
export default memo(EditableTable);

View File

@ -0,0 +1,58 @@
:global(.ant-upload-select-picture-card i) {
color: #999;
font-size: 14px;
}
:global(.ant-upload-select-picture-card .ant-upload-text) {
margin-top: 8px;
color: #666;
}
.avatarUploader {
display: inline-block;
text-align: left;
}
.wallBtn {
position: absolute;
left: 140px;
bottom: 56px;
display: inline-block;
color: #2f54eb;
cursor: pointer;
border-bottom: 1px solid #2f54eb;
}
.imgBox {
display: flex;
flex-wrap: wrap;
max-height: 520px;
overflow: auto;
.imgItem {
position: relative;
margin-right: 16px;
margin-bottom: 16px;
width: 320px;
max-height: 220px;
overflow: hidden;
cursor: pointer;
img {
width: 100%;
}
&:hover,
&.seleted {
.iconBtn {
visibility: visible;
}
}
.iconBtn {
position: absolute;
visibility: hidden;
top: 6px;
right: 10px;
font-size: 18px;
color: rgb(8, 156, 8);
}
}
}

View File

@ -0,0 +1,288 @@
import React from "react";
import { Upload, Modal, message, Tabs, Result } from "antd";
import { PlusOutlined, CheckCircleFilled } from "@ant-design/icons";
import ImgCrop from "antd-img-crop";
import classnames from "classnames";
import {
UploadFile,
UploadChangeParam,
RcFile
} from "antd/lib/upload/interface";
import { isDev, unParams, uuid } from "@/utils/tool";
import req from "@/utils/req";
import styles from "./index.less";
const { TabPane } = Tabs;
// 维护图片分类映射
const wallCateName: any = {
photo: "照片",
bg: "背景",
chahua: "插画"
};
function getBase64(file: File | Blob) {
return new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result as string);
reader.onerror = error => reject(error);
});
}
interface PicturesWallType {
fileList?: UploadFile<any>[];
action?: string;
headers?: any;
withCredentials?: boolean;
maxLen?: number;
onChange?: (v: any) => void;
cropRate?: number | boolean;
isCrop?: boolean;
}
class PicturesWall extends React.Component<PicturesWallType> {
state = {
previewVisible: false,
previewImage: "",
wallModalVisible: false,
previewTitle: "",
imgBed: {
photo: [],
bg: [],
chahua: []
},
curSelectedImg: "",
fileList: this.props.fileList || []
};
handleCancel = () => this.setState({ previewVisible: false });
handleModalCancel = () => this.setState({ wallModalVisible: false });
handlePreview = async (file: UploadFile<any>) => {
if (!file.url && !file.preview) {
file.preview = await getBase64(file.originFileObj!);
}
this.setState({
previewImage: file.url || file.preview,
previewVisible: true,
previewTitle:
file.name || file.url!.substring(file.url!.lastIndexOf("/") + 1)
});
};
handleWallSelect = (url: string) => {
this.setState({
wallModalVisible: true
});
};
handleImgSelected = (url: string) => {
this.setState({
curSelectedImg: url
});
};
handleWallShow = () => {
this.setState({
wallModalVisible: true
});
};
handleModalOk = () => {
const fileList = [
{
uid: uuid(8, 16),
name: "h5-dooring图片库",
status: "done",
url: this.state.curSelectedImg
}
];
this.props.onChange && this.props.onChange(fileList);
this.setState({ fileList, wallModalVisible: false });
};
handleChange = ({ file, fileList }: UploadChangeParam<UploadFile<any>>) => {
this.setState({ fileList });
if (file.status === "done") {
const files = fileList.map(item => {
const { uid, name, status } = item;
const url = item.url || item.response.result.url;
return { uid, name, status, url };
});
this.props.onChange && this.props.onChange(files);
}
};
handleBeforeUpload = (file: RcFile) => {
const isJpgOrPng =
file.type === "image/jpeg" ||
file.type === "image/png" ||
file.type === "image/jpg" ||
file.type === "image/gif";
if (!isJpgOrPng) {
message.error("只能上传格式为jpeg/png/gif的图片");
}
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isLt2M) {
message.error("图片必须小于2MB!");
}
return isJpgOrPng && isLt2M;
};
componentDidMount() {
// req.get(`/visible/bed/get?tid=${unParams(location.search)!.tid}`).then(res => {
// res &&
// this.setState({
// imgBed: res,
// });
// });
}
render() {
const {
previewVisible,
previewImage,
fileList,
previewTitle,
wallModalVisible,
imgBed,
curSelectedImg
} = this.state;
const {
action = isDev
? "http://192.168.1.8:3000/api/v0/files/upload/free"
: "你的服务器地址",
headers,
withCredentials = true,
maxLen = 1,
cropRate = 375 / 158,
isCrop
} = this.props;
const uploadButton = (
<div>
<PlusOutlined />
<div className="ant-upload-text"></div>
</div>
);
const cates = Object.keys(imgBed);
return (
<>
{isCrop ? (
<ImgCrop
modalTitle="裁剪图片"
modalOk="确定"
modalCancel="取消"
rotate={true}
aspect={cropRate}
>
<Upload
fileList={fileList}
onPreview={this.handlePreview}
onChange={this.handleChange}
name="file"
listType="picture-card"
className={styles.avatarUploader}
action={action}
withCredentials={withCredentials}
headers={{
"x-requested-with": localStorage.getItem("user") || "",
authorization: localStorage.getItem("token") || "",
...headers
}}
beforeUpload={this.handleBeforeUpload}
>
{fileList.length >= maxLen ? null : uploadButton}
</Upload>
</ImgCrop>
) : (
<Upload
fileList={fileList}
onPreview={this.handlePreview}
onChange={this.handleChange}
name="file"
listType="picture-card"
className={styles.avatarUploader}
action={action}
withCredentials={withCredentials}
headers={{
"x-requested-with": localStorage.getItem("user") || "",
authorization: localStorage.getItem("token") || "",
...headers
}}
beforeUpload={this.handleBeforeUpload}
>
{fileList.length >= maxLen ? null : uploadButton}
</Upload>
)}
<div className={styles.wallBtn} onClick={this.handleWallShow}>
</div>
<Modal
visible={previewVisible}
title={previewTitle}
footer={null}
onCancel={this.handleCancel}
>
<img alt="预览图片" style={{ width: "100%" }} src={previewImage} />
</Modal>
<Modal
visible={wallModalVisible}
title="图片库"
okText="确定"
cancelText="取消"
width={860}
onCancel={this.handleModalCancel}
onOk={this.handleModalOk}
>
<Tabs
defaultActiveKey={cates[0]}
tabPosition="left"
style={{ height: 520 }}
>
{cates.map((item, i) => {
return (
<TabPane tab={wallCateName[item]} key={item}>
<div className={styles.imgBox}>
{(imgBed as any)[item] &&
(imgBed as any)[item].map((item: string, i: number) => {
return (
<div
className={classnames(
styles.imgItem,
curSelectedImg === item ? styles.seleted : ""
)}
key={i}
onClick={() => this.handleImgSelected(item)}
>
<img src={item} alt="趣谈前端-h5-dooring" />
<span className={styles.iconBtn}>
<CheckCircleFilled />
</span>
</div>
);
})}
</div>
</TabPane>
);
})}
<TabPane tab="更多" key="more">
<Result
status="500"
title="Dooring温馨提示"
subTitle="更多素材, 正在筹备中..."
/>
</TabPane>
</Tabs>
</Modal>
</>
);
}
}
export default PicturesWall;

View File

@ -0,0 +1,4 @@
.avatarUploader > :global(.ant-upload) {
width: 128px;
height: 128px;
}

View File

@ -0,0 +1,94 @@
import React, { useState, useEffect, memo } from "react";
import req from "@/utils/req";
import BraftEditor from "braft-editor";
import "braft-editor/dist/index.css";
import styles from "./index.less";
const controls = [
{
key: "bold",
text: <b></b>
},
"undo",
"redo",
"emoji",
"list-ul",
"list-ol",
"blockquote",
"text-align",
"font-size",
"line-height",
"letter-spacing",
"text-color",
"italic",
"underline",
"link",
"media"
];
export default memo(function XEditor(props: any) {
const { value, onChange } = props;
const [editorState, setEditorState] = useState(
BraftEditor.createEditorState(value)
);
const myUploadFn = (param: any) => {
const fd = new FormData();
fd.append("file", param.file);
req
.post("xxxx", fd, {
headers: {
"Content-Type": "multipart/form-data"
},
onUploadProgress: function(event) {
// 上传进度发生变化时调用param.progress
console.log((event.loaded / event.total) * 100);
param.progress((event.loaded / event.total) * 100);
}
})
.then((res: any) => {
// 上传成功后调用param.success并传入上传后的文件地址
param.success({
url: res.url,
meta: {
id: Date.now(),
title: res.filename,
alt: "趣谈前端"
}
});
})
.catch(err => {
param.error({
msg: "上传失败."
});
});
};
const submitContent = () => {
const htmlContent = editorState.toHTML();
onChange && onChange(htmlContent);
};
const handleEditorChange = editorState => {
setEditorState(editorState);
if (onChange) {
const htmlContent = editorState.toHTML();
onChange(htmlContent);
}
};
useEffect(() => {
const htmlContent = value || "";
setEditorState(BraftEditor.createEditorState(htmlContent));
}, []);
return (
<BraftEditor
value={editorState}
controls={controls}
onChange={handleEditorChange}
onSave={submitContent}
media={{ uploadFn: myUploadFn }}
/>
);
});

View File

@ -0,0 +1,243 @@
////////////////////
export interface IUploadConfigType {
key: string;
name: string;
type: "Upload";
isCrop?: boolean;
cropRate?: number;
}
export type TUploadDefaultType = Array<{
uid: string;
name: string;
status: string;
url: string;
}>;
/////////////////
export interface ITextConfigType {
key: string;
name: string;
type: "Text";
}
export type TTextDefaultType = string;
////////////////////////
export interface ITextAreaConfigType {
key: string;
name: string;
type: "TextArea";
}
export type TTextAreaDefaultType = string;
////////////////////////////
export interface INumberConfigType {
key: string;
name: string;
type: "Number";
range?: [number, number];
step?: number;
}
export type TNumberDefaultType = number;
///////////////////
export interface IDataListConfigType {
key: string;
name: string;
type: "DataList";
cropRate: number;
}
export type TDataListDefaultTypeItem = {
id: string;
title: string;
desc: string;
link: string;
type?: number;
imgUrl: TUploadDefaultType;
};
export type TDataListDefaultType = Array<TDataListDefaultTypeItem>;
////////////////////
export interface IColorConfigType {
key: string;
name: string;
type: "Color";
}
export type TColorDefaultType = string;
/////////////////
export interface IRichTextConfigType {
key: string;
name: string;
type: "RichText";
}
export type TRichTextDefaultType = string;
export interface IMutiTextConfigType {
key: string;
name: string;
type: "MutiText";
}
export type TMutiTextDefaultType = Array<string>;
/////////////////////////////////
export interface ISelectConfigType<KeyType> {
key: string;
name: string;
type: "Select";
range: Array<{
key: KeyType;
text: string;
}>;
}
export type TSelectDefaultType<KeyType> = KeyType;
/////////////////////////
export interface IRadioConfigType<KeyType> {
key: string;
name: string;
type: "Radio";
range: Array<{
key: KeyType;
text: string;
}>;
}
export type TRadioDefaultType<KeyType> = KeyType;
///////////////
export interface ISwitchConfigType {
key: string;
name: string;
type: "Switch";
}
export type TSwitchDefaultType = boolean;
/////////////////////////////
export interface ICardPickerConfigType<T> {
key: string;
name: string;
type: "CardPicker";
icons: Array<T>;
}
export type TCardPickerDefaultType<T> = T;
/////////////
export interface ITableConfigType {
key: string;
name: string;
type: "Table";
}
export type TTableDefaultType = Array<{
name: string;
value: number;
}>;
// position input control
export interface IPosConfigType {
key: string;
name: string;
type: "Pos";
placeObj: {
text: string;
link: string;
};
}
export type TPosItem = number | undefined;
export type TPosDefaultType = [TPosItem, TPosItem];
//////////////////
export interface IFormItemsConfigType {
key: string;
name: string;
type: "FormItems";
}
//0---------baseform
export type baseFormOptionsType = {
label: string;
value: string;
};
export type baseFormTextTpl = {
id: string;
type: "Text";
label: string;
placeholder: string;
};
export type baseFormTextTipTpl = {
id: string;
type: "MyTextTip";
label: string;
color: string;
fontSize: number;
};
export type baseFormNumberTpl = {
id: string;
type: "Number";
label: string;
placeholder: string;
};
export type baseFormTextAreaTpl = {
id: string;
type: "Textarea";
label: string;
placeholder: string;
};
export type baseFormMyRadioTpl = {
id: string;
type: "MyRadio";
label: string;
options: baseFormOptionsType[];
};
export type baseFormMyCheckboxTpl = {
id: string;
type: "MyCheckbox";
label: string;
options: baseFormOptionsType[];
};
export type baseFormMySelectTpl = {
id: string;
type: "MySelect";
label: string;
options: baseFormOptionsType[];
};
export type baseFormDateTpl = {
id: string;
type: "Date";
label: string;
placeholder: string;
};
export type baseFormUnion =
| baseFormTextTpl
| baseFormTextTipTpl
| baseFormNumberTpl
| baseFormTextAreaTpl
| baseFormMyRadioTpl
| baseFormMyCheckboxTpl
| baseFormMySelectTpl
| baseFormDateTpl;
export type baseFormUnionType =
| baseFormTextTpl["type"]
| baseFormTextTipTpl["type"]
| baseFormNumberTpl["type"]
| baseFormTextAreaTpl["type"]
| baseFormMyRadioTpl["type"]
| baseFormMyCheckboxTpl["type"]
| baseFormMySelectTpl["type"]
| baseFormDateTpl["type"];
export type TFormItemsDefaultType = Array<baseFormUnion>;

View File

@ -0,0 +1,3 @@
import React from "react";
export default () => <div className="rotate-animate">Dooring</div>;

View File

@ -0,0 +1,3 @@
import React from "react";
export default () => <div className="rotate-animate">Dooring</div>;

View File

@ -0,0 +1,9 @@
.takeCat {
display: inline-block;
}
.imgWrap {
width: 160px;
img {
width: 100%;
}
}

View File

@ -0,0 +1,39 @@
import React, { memo } from "react";
import { Button, Popover } from "antd";
import styles from "./index.less";
interface IProps {
text: any;
}
///这组件写的有问题 popover会重定位
const content = (
<div className={styles.imgWrap}>
<img
src={`http://h5.dooring.cn/uploads/WechatIMG2_17969ccfe40.jpeg`}
alt="sponsorship"
/>
</div>
);
export default memo(function ZanPao(props: IProps) {
const {
text = (
<Button
type="primary"
danger
style={{ background: "red !important" }}
size="large"
>
, ~
</Button>
)
} = props;
return (
<div className={styles.takeCat}>
<Popover placement="top" title={null} content={content} trigger="hover">
{text}
</Popover>
</div>
);
});

View File

@ -0,0 +1,9 @@
.takeCat {
display: inline-block;
}
.imgWrap {
width: 160px;
img {
width: 100%;
}
}

View File

@ -0,0 +1,39 @@
import React, { memo } from "react";
import { Button, Popover } from "antd";
import styles from "./index.less";
interface IProps {
text: any;
}
///这组件写的有问题 popover会重定位
const content = (
<div className={styles.imgWrap}>
<img
src={`http://h5.dooring.cn/uploads/WechatIMG2_17969ccfe40.jpeg`}
alt="sponsorship"
/>
</div>
);
export default memo(function ZanPao(props: IProps) {
const {
text = (
<Button
type="primary"
danger
style={{ background: "red !important" }}
size="large"
>
, ~
</Button>
)
} = props;
return (
<div className={styles.takeCat}>
<Popover placement="top" title={null} content={content} trigger="hover">
{text}
</Popover>
</div>
);
});