@hzab/form-render 0.0.2 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,41 @@
1
+ # Uploader
2
+
3
+ 上传组件——使用 antd upload 组件封装而成
4
+
5
+ - 支持 oss 上传
6
+ - 支持限制文件大小
7
+ - 支持预览
8
+ - 支持配置下载模板
9
+
10
+ ### InfoPanel Attributes
11
+
12
+ - 参数参考 antd upload 组件
13
+
14
+ | 参数 | 类型 | 必填 | 默认值 | 说明 |
15
+ | -------------------- | -------- | ---- | -------- | ------------------------------------------------------------------ |
16
+ | name | string | 否 | file | 文件名 |
17
+ | accept | string | 否 | - | 接受上传的文件类型, 详见 input accept Attribute |
18
+ | listType | string | 否 | text | 上传列表的内建样式,支持三种基本样式 text, picture 和 picture-card |
19
+ | maxCount | number | 否 | 1 | 限制上传数量。当为 1 时,始终用最新上传的文件代替当前文件 |
20
+ | maxSize | number | 否 | 1 | 限制上传的文件大小。 |
21
+ | multiple | boolean | 否 | - | 是否支持多选文件,ie10+ 支持。开启后按住 ctrl 可选择多个文件 |
22
+ | disabled | boolean | 否 | false | 禁用状态 |
23
+ | readOnly | boolean | 否 | false | 只读状态 |
24
+ | isResRemoveArr | boolean | 否 | false | maxCount === 1 时,结果是否自动去除数组嵌套 |
25
+ | isOssUpload | boolean | 否 | false | 是否使用 oss 上传 |
26
+ | ossUrl | string | 否 | - | 获取 oss 参数的接口,默认 /api/v1/user/oss/getWebOssConfig |
27
+ | ossOpt | Object | 否 | - | oss 上传的配置参数 |
28
+ | isStrRes | boolean | 否 | false | 是否使用字符串结果(表单中得到的一直是 oss URl 字符串) |
29
+ | onCountExceed | Function | 否 | - | 数量超出 maxCount 时的回调 |
30
+ | uploadProps | Object | 否 | - | antd upload 组件的 props 参数,可覆盖组件已有配置项 |
31
+ | templateUrl | string | 否 | - | 模板下载的地址 |
32
+ | templateDownloadText | string | 否 | 模板下载 | 模板下载按钮的文案 |
33
+
34
+ ### ossOpt
35
+
36
+ | 参数 | 类型 | 必填 | 默认值 | 说明 |
37
+ | --------------- | ------ | ---- | ------ | -------------------------------------------------------------- |
38
+ | axios | Object | 否 | - | 发起请求的 axios |
39
+ | axiosConf | Object | 否 | - | axios 配置 |
40
+ | signatureParams | Object | 否 | - | 请求 oss 上传地址接口的入参 |
41
+ | serverUrl | string | 否 | - | 请求 oss 上传地址的接口,默认 /api/v1/user/oss/getWebOssConfig |
@@ -0,0 +1,39 @@
1
+ import { checkImageUrl, checkVideoUrl, checkAudioUrl } from "./utils";
2
+
3
+ export const TYPE_VIDEO = "video";
4
+ export const TYPE_IMG = "img";
5
+ export const TYPE_AUDIO = "audio";
6
+
7
+ export function checkFileType(file) {
8
+ const { type, url = file?.ossUrl } = file || {};
9
+ let fileType = "";
10
+ // 判断文件类型,获取对应展示的数据
11
+ if (url) {
12
+ // 图片
13
+ if (url.startsWith("data:image/") || checkImageUrl(url)) {
14
+ fileType = TYPE_IMG;
15
+ } else if (checkVideoUrl(url)) {
16
+ // 视频
17
+ fileType = TYPE_VIDEO;
18
+ } else if (checkAudioUrl(url)) {
19
+ // 音频
20
+ fileType = TYPE_AUDIO;
21
+ }
22
+ } else if (type) {
23
+ // 图片
24
+ if (type?.startsWith("image/")) {
25
+ fileType = TYPE_IMG;
26
+ }
27
+
28
+ // 视频
29
+ if (type?.startsWith("video/")) {
30
+ fileType = TYPE_VIDEO;
31
+ }
32
+
33
+ // 音频
34
+ if (type?.startsWith("audio/")) {
35
+ fileType = TYPE_AUDIO;
36
+ }
37
+ }
38
+ return fileType;
39
+ }
@@ -0,0 +1,155 @@
1
+ import { axios } from "@hzab/data-model";
2
+ import { nanoid } from "nanoid";
3
+
4
+ export function getSignature(opt = {}) {
5
+ const { serverUrl = "/api/v1/user/oss/getWebOssConfig" } = opt;
6
+ // 减 10 秒,避免发起请求时 刚好过期的情况
7
+ if (
8
+ window.__ossSignatureRes &&
9
+ serverUrl === window.__ossSignatureRes.serverUrl &&
10
+ Date.now() - window.__ossSignatureRes.__saveTime < window.__ossSignatureRes.expireTimeMillis - 10000
11
+ ) {
12
+ return Promise.resolve(window.__ossSignatureRes);
13
+ }
14
+ const { axios: _ax = axios, params = {}, axiosConf } = opt;
15
+ return _ax
16
+ .get(serverUrl, {
17
+ ...axiosConf,
18
+ params: {
19
+ isPublic: 1,
20
+ ...params,
21
+ },
22
+ })
23
+ .then((res) => {
24
+ window.__ossSignatureRes = res?.data?.data;
25
+ if (window.__ossSignatureRes) {
26
+ window.__ossSignatureRes.__saveTime = Date.now();
27
+ window.__ossSignatureRes.serverUrl = serverUrl;
28
+ }
29
+ return window.__ossSignatureRes;
30
+ });
31
+ }
32
+
33
+ class OssUpload {
34
+ constructor(props = {}) {
35
+ this.axios = props.axios || axios;
36
+ this.axiosConf = props.axiosConf || {};
37
+ this.serverUrl = props.serverUrl || "/api/v1/user/oss/getWebOssConfig";
38
+ this.signatureParams = props.signatureParams || {};
39
+ }
40
+
41
+ getSignature(serverUrl = this.serverUrl, opt) {
42
+ return getSignature({
43
+ ...opt,
44
+ serverUrl,
45
+ axios: opt?.axios || this.axios,
46
+ axiosConf: { ...this.axiosConf, ...opt?.axiosConf },
47
+ });
48
+ }
49
+
50
+ upload(file, opt = {}) {
51
+ return new Promise(async (resolve, reject) => {
52
+ const ossParams = await this.getSignature(opt.serverUrl || this.serverUrl, {
53
+ ...opt,
54
+ params: { ...this.signatureParams, ...opt.signatureParams },
55
+ });
56
+
57
+ const { ossParams: propOssParams } = opt || {};
58
+ const formData = new FormData();
59
+ // key 表示上传到 Bucket 内的 Object 的完整路径,例如 exampledir/exampleobject.txtObject,完整路径中不能包含 Bucket 名称。
60
+ // filename 表示待上传的本地文件名称。
61
+ let filename = file?.name;
62
+ if (file?.name) {
63
+ const nameArr = file?.name.match(/^(.+)\.(.+)$/);
64
+ if (nameArr && nameArr.length > 2) {
65
+ filename = `${nameArr[1]}_${Date.now()}_${nanoid()}.${nameArr[2]}`;
66
+ }
67
+ }
68
+ if (!filename) {
69
+ filename = `${Date.now()}_${nanoid()}.${file.type?.replace(/\w+\/, ''/)}`;
70
+ }
71
+ const key = `${ossParams?.dir}${filename}`;
72
+ formData.set("key", key);
73
+ formData.set("OSSAccessKeyId", ossParams.accessid);
74
+ formData.set("policy", ossParams.policy);
75
+ formData.set("Signature", ossParams.signature);
76
+ if (ossParams.callback) {
77
+ formData.set("callback", ossParams.callback);
78
+ }
79
+ formData.set("success_action_status", 200);
80
+ formData.set("file", file);
81
+
82
+ if (propOssParams) {
83
+ for (const key in propOssParams) {
84
+ if (Object.hasOwnProperty.call(propOssParams, key)) {
85
+ formData.set(key, propOssParams[key]);
86
+ }
87
+ }
88
+ }
89
+
90
+ const _axios = opt?.axios || this.axios;
91
+
92
+ return _axios
93
+ .post(ossParams.host, formData, { ...this.axiosConf, ...opt?.axiosConf })
94
+ .then((res) => {
95
+ resolve(res);
96
+ return res;
97
+ })
98
+ .catch((err) => {
99
+ console.error("oss upload err", err);
100
+ reject(err);
101
+ return Promise.reject(err);
102
+ });
103
+ });
104
+ }
105
+ }
106
+
107
+ /**
108
+ * 处理文件上传逻辑
109
+ * @param {Array} files
110
+ * @param {Object} opt
111
+ * @returns
112
+ */
113
+ export async function handleOssUpload(files, opt) {
114
+ const _files = files;
115
+ const { ossUrl, signatureParams, ossParams, axiosConf } = opt || {};
116
+ const ossUpload = new OssUpload({
117
+ axios: opt.axios,
118
+ axiosConf: axiosConf,
119
+ serverUrl: ossUrl || "/api/v1/user/oss/getWebOssConfig",
120
+ });
121
+
122
+ const promise = [];
123
+ _files?.forEach((file) => {
124
+ // 数据已经是 url 的情况
125
+ if (typeof file === "string" || file.ossUrl) {
126
+ promise.push(Promise.resolve(file));
127
+ } else {
128
+ promise.push(
129
+ ossUpload
130
+ .upload(file, {
131
+ signatureParams: {
132
+ isPublic: 1,
133
+ ...(signatureParams || {}),
134
+ },
135
+ ossParams,
136
+ axiosConf,
137
+ })
138
+ .then((res) => {
139
+ return Promise.resolve(res?.data?.data?.fileUrl);
140
+ }),
141
+ );
142
+ }
143
+ });
144
+
145
+ return Promise.all(promise).then((filePromises) => {
146
+ filePromises?.forEach((fileUrl, idx) => {
147
+ _files[idx].ossUrl = fileUrl;
148
+ });
149
+ return Promise.resolve(_files);
150
+ });
151
+ }
152
+
153
+ export { axios };
154
+
155
+ export default OssUpload;
@@ -0,0 +1,177 @@
1
+ import { nanoid } from "nanoid";
2
+
3
+ /**
4
+ * 建立一个可以存取该 file 的 url
5
+ * @param {Object} file 文件
6
+ * @returns {string} url
7
+ * blob:http://localhost:8000/c9950644-5118-4231-9be7-8183bde1fdc7
8
+ */
9
+ export function getFileURL(file) {
10
+ let url = file.url || null;
11
+
12
+ try {
13
+ // 下面函数执行的效果是一样的,只是需要针对不同的浏览器执行不同的 js 函数而已
14
+ if (window.createObjectURL != undefined) {
15
+ // basic
16
+ url = window.createObjectURL(file);
17
+ } else if (window.URL != undefined) {
18
+ // mozilla(firefox)
19
+ url = window.URL.createObjectURL(file);
20
+ } else if (window.webkitURL != undefined) {
21
+ // webkit or chrome
22
+ url = window.webkitURL.createObjectURL(file);
23
+ }
24
+ } catch (error) {
25
+ console.warn("getFileURL Error: ", error);
26
+ }
27
+
28
+ return url;
29
+ }
30
+
31
+ /**
32
+ * 判断 url 是否带有指定图片后缀
33
+ * @param {string} url
34
+ * @returns
35
+ */
36
+ export function checkImageUrl(url) {
37
+ const imgTypes = [
38
+ "apng",
39
+ "avif",
40
+ "bmp",
41
+ "gif",
42
+ "ico",
43
+ "cur",
44
+ "jpg",
45
+ "jpeg",
46
+ "jfif",
47
+ "pjpeg",
48
+ "pjp",
49
+ "png",
50
+ "svg",
51
+ "tif",
52
+ "tiff",
53
+ "webp",
54
+ ];
55
+ return checkUrlSuffix(url, imgTypes);
56
+ }
57
+
58
+ /**
59
+ * 判断 url 是否带有指定视频后缀
60
+ * @param {string} url
61
+ * @returns
62
+ */
63
+ export function checkVideoUrl(url) {
64
+ const imgTypes = ["3gp", "mpg", "mpeg", "mp4", "m4v", "m4p", "ogv", "ogg", "mov", "webm"];
65
+ return checkUrlSuffix(url, imgTypes);
66
+ }
67
+
68
+ /**
69
+ * 判断 url 是否带有指定音频后缀
70
+ * @param {string} url
71
+ * @returns
72
+ */
73
+ export function checkAudioUrl(url) {
74
+ const imgTypes = ["3gp", "adts", "mpeg", "mp3", "mp4", "ogg", "mov", "webm", "rtp", "amr", "wav"];
75
+ return checkUrlSuffix(url, imgTypes);
76
+ }
77
+
78
+ /**
79
+ * 检查 url 是否带有指定后缀
80
+ * @param {string} url url 地址
81
+ * @param {Array} types 后缀数组
82
+ * @returns
83
+ */
84
+ export function checkUrlSuffix(url, types = [], caseSensitive) {
85
+ if (!url) {
86
+ return false;
87
+ }
88
+ let _url = url?.replace(/\?.+/, "");
89
+ const reg = new RegExp(`\.(${types.join("|")})$`, caseSensitive ? undefined : "i");
90
+ if (reg.test(_url)) {
91
+ return true;
92
+ }
93
+ }
94
+
95
+ export function getFileName(fileUrl) {
96
+ const res = fileUrl?.match(/[^\/]+?\.[^\/]+$/);
97
+ if (res && res.length > 0) {
98
+ return res[0];
99
+ }
100
+ return fileUrl;
101
+ }
102
+
103
+ export function getFileType(file) {
104
+ if (typeof file === "object" && file?.type) {
105
+ return file?.type;
106
+ }
107
+ if (typeof file === "string") {
108
+ let type = null;
109
+ if (checkVideoUrl(file)) {
110
+ type = "video/mp4";
111
+ } else if (checkImageUrl(file)) {
112
+ type = "image/jpeg";
113
+ } else if (checkAudioUrl(file)) {
114
+ type = "audio/3gpp";
115
+ } else if (checkUrlSuffix(file, ["pdf"])) {
116
+ type = "application/pdf";
117
+ } else if (checkUrlSuffix(file, ["pdf"])) {
118
+ type = "application/pdf";
119
+ } else if (checkUrlSuffix(file, ["xlsx", "xls", "csv", "xlsm", "xlsb"])) {
120
+ type = "application/vnd.ms-excel";
121
+ } else if (checkUrlSuffix(file, ["doc", "docx"])) {
122
+ type = "application/msword";
123
+ } else if (checkUrlSuffix(file, ["ppt", "pptx"])) {
124
+ type = "application/vnd.ms-powerpoint";
125
+ }
126
+ return type;
127
+ }
128
+ }
129
+
130
+ function getArr(value) {
131
+ return Array.isArray(value) ? value : (value && [value]) || [];
132
+ }
133
+
134
+ export function handleMaxCount(fileList, maxCount) {
135
+ let list = getArr(fileList);
136
+ if (maxCount > 0 && list.length > maxCount) {
137
+ list = list.slice(0, maxCount);
138
+ }
139
+ return list;
140
+ }
141
+
142
+ /**
143
+ * 处理传入的数据,数据格式统一转成 Array<fileObject>
144
+ * @param {Array} fileList Array<fileObject|string>
145
+ * @param {number} maxCount
146
+ * @returns Array<fileObject>
147
+ */
148
+ export function handleInputFileList(_fileList, maxCount = 1) {
149
+ const fileList = handleMaxCount(getArr(_fileList), maxCount);
150
+ if (fileList.length === 0) {
151
+ return fileList;
152
+ }
153
+ let resList = fileList;
154
+ if (!Array.isArray(resList)) {
155
+ resList = [resList];
156
+ }
157
+ resList = fileList.map((file, i) => {
158
+ if (typeof file === "string") {
159
+ const uid = nanoid();
160
+ return {
161
+ name: uid,
162
+ uid: uid,
163
+ ossUrl: file,
164
+ url: file,
165
+ // size: Infinity,
166
+ };
167
+ }
168
+ if (file && !file.uid) {
169
+ file.uid = nanoid();
170
+ }
171
+ if (file && !file.name) {
172
+ file.name = file.uid;
173
+ }
174
+ return file;
175
+ });
176
+ return resList;
177
+ }
@@ -0,0 +1,16 @@
1
+ .uploader-image-previewer {
2
+ display: flex;
3
+ justify-content: center;
4
+ align-items: center;
5
+ width: 100%;
6
+ height: 100%;
7
+ border: 1px solid #eee;
8
+ color: #fff;
9
+ border-radius: 4px;
10
+ cursor: pointer;
11
+
12
+ .uploader-image-previewer-img {
13
+ max-width: 100%;
14
+ max-height: 100%;
15
+ }
16
+ }
@@ -0,0 +1,33 @@
1
+ import { useState } from "react";
2
+ import { Modal } from "antd";
3
+
4
+ import "./index.less";
5
+
6
+ function ImagePreviewer(props) {
7
+ const [modalVisible, setMaskVisible] = useState(false);
8
+
9
+ function onPreview() {
10
+ setMaskVisible(true);
11
+ }
12
+
13
+ return (
14
+ <>
15
+ <div className={`uploader-image-previewer ${props.className || ""}`} onClick={onPreview}>
16
+ <img className="uploader-image-previewer-img" src={props.src} alt={props.alt} />
17
+ </div>
18
+ <Modal
19
+ className="formily-uploader-previewer-modal"
20
+ title="预览"
21
+ width={800}
22
+ visible={modalVisible}
23
+ destroyOnClose
24
+ onCancel={(e) => setMaskVisible(false)}
25
+ footer={null}
26
+ >
27
+ <img className="previewer-content" src={props.src} alt={props.alt} />
28
+ </Modal>
29
+ </>
30
+ );
31
+ }
32
+
33
+ export default ImagePreviewer;
@@ -0,0 +1,22 @@
1
+ import { CloseCircleOutlined } from "@ant-design/icons";
2
+
3
+ export const ItemRender = (props) => {
4
+ const { index, disabled, readOnly, onItemDel } = props;
5
+ return (
6
+ <div className="file-item">
7
+ {props.children}
8
+ {disabled || readOnly ? null : (
9
+ <div
10
+ className="file-item-del"
11
+ onClick={() => {
12
+ onItemDel(index);
13
+ }}
14
+ >
15
+ <CloseCircleOutlined />
16
+ </div>
17
+ )}
18
+ </div>
19
+ );
20
+ };
21
+
22
+ export default ItemRender;
@@ -0,0 +1,53 @@
1
+ import { getFileURL } from "../../common/utils";
2
+
3
+ import { TYPE_VIDEO, TYPE_IMG, TYPE_AUDIO, checkFileType } from "../../common/checkFileType";
4
+
5
+ import Video from "../video";
6
+ import Image from "../Image";
7
+ import ItemRender from "./ItemRender";
8
+
9
+ export const ItemList = (props) => {
10
+ const { fileList, baseUrl, disabled, readOnly, onItemDel } = props || {};
11
+
12
+ return (
13
+ <>
14
+ {fileList.map((it, idx) => {
15
+ if (!it) {
16
+ return null;
17
+ }
18
+ const { name, url = it?.ossUrl } = it || {};
19
+
20
+ let src = url || getFileURL(it);
21
+
22
+ if (baseUrl) {
23
+ src = baseUrl + src;
24
+ }
25
+
26
+ let content = (
27
+ <div className="file-item-view" title={it?.name}>
28
+ {it?.name}
29
+ </div>
30
+ );
31
+
32
+ const fileType = checkFileType(it);
33
+ if (fileType === TYPE_IMG) {
34
+ content = <Image src={src} alt={name} />;
35
+ }
36
+ if (fileType === TYPE_VIDEO) {
37
+ content = <Video src={src} href={src} />;
38
+ }
39
+ if (fileType === TYPE_AUDIO) {
40
+ content = <audio src={src} controls></audio>;
41
+ }
42
+
43
+ return (
44
+ <ItemRender index={idx} key={name + "_" + idx} disabled={disabled} readOnly={readOnly} onItemDel={onItemDel}>
45
+ {content}
46
+ </ItemRender>
47
+ );
48
+ })}
49
+ </>
50
+ );
51
+ };
52
+
53
+ export default ItemList;
@@ -0,0 +1,39 @@
1
+ import { useState } from "react";
2
+ import { Modal } from "antd";
3
+ import { PlayCircleOutlined } from "@ant-design/icons";
4
+
5
+ import "./index.less";
6
+
7
+ function Video({ src, href }) {
8
+ const [modalVisible, setMaskVisible] = useState(false);
9
+
10
+ function onPreview() {
11
+ setMaskVisible(true);
12
+ }
13
+
14
+ return (
15
+ <>
16
+ <div className="uploader-video-wrap" onClick={onPreview}>
17
+ <PlayCircleOutlined className="uploader-video-play-icon" />
18
+ </div>
19
+
20
+ <Modal
21
+ className="formily-uploader-previewer-modal"
22
+ title="预览"
23
+ width={800}
24
+ visible={modalVisible}
25
+ destroyOnClose
26
+ onCancel={() => setMaskVisible(false)}
27
+ footer={null}
28
+ >
29
+ <video className="previewer-content" src={src} controls>
30
+ 抱歉,您的浏览器不支持内嵌视频,不过不用担心,你可以
31
+ <a href={href || src}>下载</a>
32
+ 并用你喜欢的播放器观看!
33
+ </video>
34
+ </Modal>
35
+ </>
36
+ );
37
+ }
38
+
39
+ export default Video;
@@ -0,0 +1,14 @@
1
+ .uploader-video-wrap {
2
+ display: flex;
3
+ justify-content: center;
4
+ align-items: center;
5
+ width: 100%;
6
+ height: 100%;
7
+ border: 1px solid #eee;
8
+ color: #999;
9
+ border-radius: 4px;
10
+ cursor: pointer;
11
+ .uploader-video-play-icon {
12
+ font-size: 30px;
13
+ }
14
+ }
@@ -0,0 +1,38 @@
1
+ import UploaderCom from "./uploader";
2
+ import { useGlobalPropsContext } from "../../common/global-props-context";
3
+
4
+ export const Upload = (props) => {
5
+ // 组件外部传入的 props
6
+ const globalProps = useGlobalPropsContext() || {};
7
+ const { field = {}, onChange, value } = props;
8
+ const { name, mode, componentProps = {} } = field;
9
+ const { multiple } = componentProps;
10
+
11
+ async function onUploadChange(files) {
12
+ let _files = files;
13
+ if (field?.autoUpload && props.fieldsConf[name]?.onUpload) {
14
+ _files = await props.fieldsConf[name]?.onUpload(files);
15
+ if (!_files) {
16
+ return;
17
+ }
18
+ }
19
+ // 若单选模式,默认只返回对应数据,而非嵌套一层数组
20
+ if (!multiple && _files && _files.length === 1) {
21
+ _files = _files[0];
22
+ }
23
+ onChange && onChange(_files);
24
+ }
25
+
26
+ const _props = {
27
+ mode: mode,
28
+ ...props,
29
+ value: typeof value === "string" ? [value] : value,
30
+ axios: globalProps?.axios,
31
+ axiosConf: globalProps?.axiosConf,
32
+ ...componentProps,
33
+ };
34
+
35
+ return <UploaderCom {..._props} onChange={onUploadChange} />;
36
+ };
37
+
38
+ export default Upload;