@hzab/form-render 1.7.16-alpha.0 → 1.7.17

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hzab/form-render",
3
- "version": "1.7.16-alpha.0",
3
+ "version": "1.7.17",
4
4
  "description": "",
5
5
  "main": "src",
6
6
  "scripts": {
@@ -1,57 +1,57 @@
1
- import { useState } from "react";
2
- import { Input, AutoComplete } from "antd";
3
-
4
- import { getSearchTips } from "../../servers";
5
-
6
- import "./index.less";
7
-
8
- const cityCode = "0571";
9
-
10
- function Search(props) {
11
- const [loading, setLoading] = useState(false);
12
- const [tips, setTips] = useState([]);
13
-
14
- function onSelect(value, option) {
15
- const { lng: _lng, lat: _lat } = option || {};
16
- props.setPoint && props.setPoint(_lng, _lat, { changeCenter: true });
17
- }
18
-
19
- function onSearch(search) {
20
- if (!search) {
21
- setTips([]);
22
- return;
23
- }
24
- setLoading(true);
25
- getSearchTips(search, props.cityCode || cityCode)
26
- .then((res) => {
27
- setTips(
28
- res?.map((it) => ({
29
- ...it,
30
- label: it.name,
31
- // 解决回显为 id 的问题
32
- value: it.name,
33
- })),
34
- );
35
- setLoading(false);
36
- })
37
- .catch((err) => {
38
- setLoading(false);
39
- });
40
- }
41
-
42
- return (
43
- <AutoComplete
44
- className="location-map-search"
45
- options={tips}
46
- onSelect={onSelect}
47
- onSearch={_.debounce(onSearch, 500)}
48
- onClear={() => {
49
- setTips([]);
50
- }}
51
- >
52
- <Input.Search allowClear loading={loading} placeholder="请输入" />
53
- </AutoComplete>
54
- );
55
- }
56
-
57
- export default Search;
1
+ import { useState } from "react";
2
+ import { Input, AutoComplete } from "antd";
3
+
4
+ import { getSearchTips } from "../../servers";
5
+
6
+ import "./index.less";
7
+
8
+ const cityCode = "0571";
9
+
10
+ function Search(props) {
11
+ const [loading, setLoading] = useState(false);
12
+ const [tips, setTips] = useState([]);
13
+
14
+ function onSelect(value, option) {
15
+ const { lng: _lng, lat: _lat } = option || {};
16
+ props.setPoint && props.setPoint(_lng, _lat, { changeCenter: true });
17
+ }
18
+
19
+ function onSearch(search) {
20
+ if (!search) {
21
+ setTips([]);
22
+ return;
23
+ }
24
+ setLoading(true);
25
+ getSearchTips(search, props.cityCode || cityCode)
26
+ .then((res) => {
27
+ setTips(
28
+ res?.map((it) => ({
29
+ ...it,
30
+ label: it.name,
31
+ // 解决回显为 id 的问题
32
+ value: it.name,
33
+ })),
34
+ );
35
+ setLoading(false);
36
+ })
37
+ .catch((err) => {
38
+ setLoading(false);
39
+ });
40
+ }
41
+
42
+ return (
43
+ <AutoComplete
44
+ className="location-map-search"
45
+ options={tips}
46
+ onSelect={onSelect}
47
+ onSearch={_.debounce(onSearch, 500)}
48
+ onClear={() => {
49
+ setTips([]);
50
+ }}
51
+ >
52
+ <Input.Search allowClear loading={loading} placeholder="请输入" />
53
+ </AutoComplete>
54
+ );
55
+ }
56
+
57
+ export default Search;
@@ -1,221 +1,221 @@
1
- import _ from "lodash";
2
-
3
- export interface ITabTableDataOpt extends optT {
4
- /**
5
- * 展示 tab 的层级
6
- */
7
- tabLevel?: number;
8
- /**
9
- * 当前处理的层级
10
- */
11
- _level?: number;
12
- fieldNames?: {
13
- label?: string;
14
- value?: string;
15
- children?: string;
16
- };
17
- }
18
- /**
19
- * 处理数据,按需设置 tab、table 数据
20
- * @param treeData 树数据数组
21
- * @param opt 配置项
22
- * @returns
23
- */
24
- export function getTabTableData(_treeData = [], opt: ITabTableDataOpt = {}) {
25
- const treeData = _.cloneDeep(_treeData);
26
- const { tabLevel = 0, _level = 0, fieldNames } = opt || {};
27
- const { value, children } = fieldNames || {};
28
- let resList = [];
29
- // 当前层级小于 tabLevel,视为当前层级渲染为 tab
30
- const isTab = _level < tabLevel;
31
-
32
- // 当前层级渲染为 tab,直接 push 到 resList 中
33
- if (isTab) {
34
- resList.push({
35
- level: _level,
36
- defaultActiveKey: treeData[0]?.[value],
37
- tabList: treeData,
38
- });
39
- // 继续处理子集数据
40
- treeData?.forEach((it) => {
41
- if (it[children]?.length > 0) {
42
- it[children] = getTabTableData(it[children], {
43
- ...opt,
44
- _level: _level + 1,
45
- });
46
- }
47
- });
48
- } else {
49
- // 非 tab 层级,处理 table 渲染数据(handleTreeToArray 自动处理子集)
50
- resList.push({
51
- level: _level,
52
- tableData: handleTreeToArray(treeData, {
53
- ...opt,
54
- level: undefined,
55
- }),
56
- });
57
- }
58
- return resList;
59
- }
60
-
61
- /**
62
- * 处理树形数据,把树形数据方向转为多维数组,便于渲染
63
- * @param params
64
- * @returns
65
- 如:
66
- [{key:1, children:[{key:11},{key:12}]}, {key:2, children:[{key:21, children:[{key:211}]},{key:22},{key:23}]}]
67
- 结果数组:
68
- [1, 11, ]
69
- [ , 12, ]
70
- [2, 21, 211]
71
- [ , 22, ]
72
- [ , 23, ]
73
- */
74
- export function handleTreeToArray(treeData = [], opt: any = {}) {
75
- const { resList, maxCol } = transformTreeData(treeData, opt);
76
- resList?.forEach((row) => {
77
- fillArrByPush(maxCol, row);
78
- });
79
- return resList;
80
- }
81
-
82
- export type optT = {
83
- resList?: Array<any>;
84
- preCol?: number;
85
- //是否合并叶子节点
86
- mergeLeaf?: boolean;
87
- // 最后一列是否合并到一个单元格
88
- mergeFullLeaf?: boolean;
89
- // 当前递归的层级
90
- level?: number;
91
- fieldNames?: {
92
- value?: string;
93
- label?: string;
94
- children?: string;
95
- };
96
- };
97
-
98
- /**
99
- * 处理树形数据,把树形数据方向转为多维数组,便于渲染
100
- * @param params
101
- * @returns
102
- 如:
103
- [{key:1, children:[{key:11},{key:12}]}, {key:2, children:[{key:21, children:[{key:211}]},{key:22},{key:23}]}]
104
- 结果数组:
105
- [1, 11, ]
106
- [ , 12, ]
107
- [2, 21, 211]
108
- [ , 22, ]
109
- [ , 23, ]
110
- */
111
- export function transformTreeData(treeData = [], opt: optT = {}) {
112
- const { resList = [], preCol = 0, mergeLeaf, mergeFullLeaf, level = 0, fieldNames } = opt || {};
113
- const { value = "value", children = "children", label = "label" } = fieldNames || {};
114
- let maxCol = preCol + 1;
115
- // 统计当前项包含的最大行数(当前项的行数等于所有子项行数之和),用于合并行的值
116
- let maxRow = 0;
117
-
118
- const leafIndexList = [];
119
- const leafItemList = [];
120
-
121
- // 是否有子项
122
- let hasChildren = false;
123
- treeData?.forEach((it, i) => {
124
- if (it && Array.isArray(it[children]) && it[children].length > 0) {
125
- hasChildren = true;
126
- } else {
127
- // 保存当前层级的叶子节点,用于叶子节点合并
128
- leafIndexList.push(i);
129
- leafItemList.push(it);
130
- }
131
- });
132
-
133
- let lastRowIdx = resList.length > 0 ? resList.length - 1 : 0;
134
- // 合并不存在 children 的层级,mergeFullLeaf === true && hasChildren === false && level > 0 时,合并当前数据
135
- if (mergeFullLeaf === true && !hasChildren && level > 0) {
136
- resList[lastRowIdx].push([...treeData?.map((it) => ({ ...it, label: it[label], value: it[value] }))]);
137
- return {
138
- resList,
139
- maxCol,
140
- // 合并之后视为 1 行
141
- maxRow: 1,
142
- };
143
- }
144
-
145
- // 合并当前层级的叶子节点,非叶子节点忽略
146
- if (mergeLeaf === true && leafIndexList.length > 0) {
147
- // treeData 中删除当前层级其他叶子节点
148
- for (let i = leafIndexList.length - 1; i >= 0; i--) {
149
- const idx = leafIndexList[i];
150
- treeData.splice(idx, 1);
151
- }
152
- // 所有叶子节点放在尾部
153
- treeData.push({
154
- _type_: "leafList",
155
- leafList: leafItemList?.map((it) => ({ ...it, label: it[label], value: it[value] })),
156
- });
157
- }
158
-
159
- treeData?.forEach((it, i) => {
160
- let item = null;
161
- if (it._type_ === "leafList") {
162
- // 合并当前层级的叶子节点
163
- item = it.leafList;
164
- } else {
165
- item = { ...it, value: it[value], label: it[label] };
166
- }
167
- const childList = item[children] || [];
168
- const childLen = childList?.length;
169
- // 当 mergeFullLeaf !== true && children 长度大于1时,子项下标大于 0 的项需要另起一行,同时补充之前空的列。
170
- if (i > 0) {
171
- resList.push(fillArrByPush(preCol, [], { content: false }));
172
- }
173
- lastRowIdx = resList.length > 0 ? resList.length - 1 : 0;
174
- // 解决首次进来第一行数组空的问题
175
- if (!resList[lastRowIdx]) {
176
- resList[lastRowIdx] = [];
177
- }
178
- resList[lastRowIdx].push(item);
179
- if (childLen > 0) {
180
- const res = transformTreeData(childList, {
181
- ...opt,
182
- resList,
183
- preCol: preCol + 1,
184
- level: level + 1,
185
- });
186
- if (maxCol < res.maxCol) {
187
- maxCol = res.maxCol;
188
- }
189
- // 当前的最大行数加上子项的行数
190
- maxRow = maxRow + res.maxRow;
191
- // 每项的最大行数为子项的最大行数之和
192
- item._maxRow = res.maxRow;
193
- } else {
194
- item._maxRow = 1;
195
- maxRow += 1;
196
- }
197
- });
198
-
199
- return {
200
- resList,
201
- maxCol,
202
- maxRow,
203
- };
204
- }
205
-
206
- /**
207
- * 填充数组
208
- * @param len
209
- * @param arr
210
- * @param opt
211
- * @returns
212
- */
213
- export function fillArrByPush(len, arr = [], opt: any = {}) {
214
- const { content } = opt || {};
215
- if (arr.length < len) {
216
- for (let i = arr.length; i < len; i++) {
217
- arr.push(content);
218
- }
219
- }
220
- return arr;
221
- }
1
+ import _ from "lodash";
2
+
3
+ export interface ITabTableDataOpt extends optT {
4
+ /**
5
+ * 展示 tab 的层级
6
+ */
7
+ tabLevel?: number;
8
+ /**
9
+ * 当前处理的层级
10
+ */
11
+ _level?: number;
12
+ fieldNames?: {
13
+ label?: string;
14
+ value?: string;
15
+ children?: string;
16
+ };
17
+ }
18
+ /**
19
+ * 处理数据,按需设置 tab、table 数据
20
+ * @param treeData 树数据数组
21
+ * @param opt 配置项
22
+ * @returns
23
+ */
24
+ export function getTabTableData(_treeData = [], opt: ITabTableDataOpt = {}) {
25
+ const treeData = _.cloneDeep(_treeData);
26
+ const { tabLevel = 0, _level = 0, fieldNames } = opt || {};
27
+ const { value, children } = fieldNames || {};
28
+ let resList = [];
29
+ // 当前层级小于 tabLevel,视为当前层级渲染为 tab
30
+ const isTab = _level < tabLevel;
31
+
32
+ // 当前层级渲染为 tab,直接 push 到 resList 中
33
+ if (isTab) {
34
+ resList.push({
35
+ level: _level,
36
+ defaultActiveKey: treeData[0]?.[value],
37
+ tabList: treeData,
38
+ });
39
+ // 继续处理子集数据
40
+ treeData?.forEach((it) => {
41
+ if (it[children]?.length > 0) {
42
+ it[children] = getTabTableData(it[children], {
43
+ ...opt,
44
+ _level: _level + 1,
45
+ });
46
+ }
47
+ });
48
+ } else {
49
+ // 非 tab 层级,处理 table 渲染数据(handleTreeToArray 自动处理子集)
50
+ resList.push({
51
+ level: _level,
52
+ tableData: handleTreeToArray(treeData, {
53
+ ...opt,
54
+ level: undefined,
55
+ }),
56
+ });
57
+ }
58
+ return resList;
59
+ }
60
+
61
+ /**
62
+ * 处理树形数据,把树形数据方向转为多维数组,便于渲染
63
+ * @param params
64
+ * @returns
65
+ 如:
66
+ [{key:1, children:[{key:11},{key:12}]}, {key:2, children:[{key:21, children:[{key:211}]},{key:22},{key:23}]}]
67
+ 结果数组:
68
+ [1, 11, ]
69
+ [ , 12, ]
70
+ [2, 21, 211]
71
+ [ , 22, ]
72
+ [ , 23, ]
73
+ */
74
+ export function handleTreeToArray(treeData = [], opt: any = {}) {
75
+ const { resList, maxCol } = transformTreeData(treeData, opt);
76
+ resList?.forEach((row) => {
77
+ fillArrByPush(maxCol, row);
78
+ });
79
+ return resList;
80
+ }
81
+
82
+ export type optT = {
83
+ resList?: Array<any>;
84
+ preCol?: number;
85
+ //是否合并叶子节点
86
+ mergeLeaf?: boolean;
87
+ // 最后一列是否合并到一个单元格
88
+ mergeFullLeaf?: boolean;
89
+ // 当前递归的层级
90
+ level?: number;
91
+ fieldNames?: {
92
+ value?: string;
93
+ label?: string;
94
+ children?: string;
95
+ };
96
+ };
97
+
98
+ /**
99
+ * 处理树形数据,把树形数据方向转为多维数组,便于渲染
100
+ * @param params
101
+ * @returns
102
+ 如:
103
+ [{key:1, children:[{key:11},{key:12}]}, {key:2, children:[{key:21, children:[{key:211}]},{key:22},{key:23}]}]
104
+ 结果数组:
105
+ [1, 11, ]
106
+ [ , 12, ]
107
+ [2, 21, 211]
108
+ [ , 22, ]
109
+ [ , 23, ]
110
+ */
111
+ export function transformTreeData(treeData = [], opt: optT = {}) {
112
+ const { resList = [], preCol = 0, mergeLeaf, mergeFullLeaf, level = 0, fieldNames } = opt || {};
113
+ const { value = "value", children = "children", label = "label" } = fieldNames || {};
114
+ let maxCol = preCol + 1;
115
+ // 统计当前项包含的最大行数(当前项的行数等于所有子项行数之和),用于合并行的值
116
+ let maxRow = 0;
117
+
118
+ const leafIndexList = [];
119
+ const leafItemList = [];
120
+
121
+ // 是否有子项
122
+ let hasChildren = false;
123
+ treeData?.forEach((it, i) => {
124
+ if (it && Array.isArray(it[children]) && it[children].length > 0) {
125
+ hasChildren = true;
126
+ } else {
127
+ // 保存当前层级的叶子节点,用于叶子节点合并
128
+ leafIndexList.push(i);
129
+ leafItemList.push(it);
130
+ }
131
+ });
132
+
133
+ let lastRowIdx = resList.length > 0 ? resList.length - 1 : 0;
134
+ // 合并不存在 children 的层级,mergeFullLeaf === true && hasChildren === false && level > 0 时,合并当前数据
135
+ if (mergeFullLeaf === true && !hasChildren && level > 0) {
136
+ resList[lastRowIdx].push([...treeData?.map((it) => ({ ...it, label: it[label], value: it[value] }))]);
137
+ return {
138
+ resList,
139
+ maxCol,
140
+ // 合并之后视为 1 行
141
+ maxRow: 1,
142
+ };
143
+ }
144
+
145
+ // 合并当前层级的叶子节点,非叶子节点忽略
146
+ if (mergeLeaf === true && leafIndexList.length > 0) {
147
+ // treeData 中删除当前层级其他叶子节点
148
+ for (let i = leafIndexList.length - 1; i >= 0; i--) {
149
+ const idx = leafIndexList[i];
150
+ treeData.splice(idx, 1);
151
+ }
152
+ // 所有叶子节点放在尾部
153
+ treeData.push({
154
+ _type_: "leafList",
155
+ leafList: leafItemList?.map((it) => ({ ...it, label: it[label], value: it[value] })),
156
+ });
157
+ }
158
+
159
+ treeData?.forEach((it, i) => {
160
+ let item = null;
161
+ if (it._type_ === "leafList") {
162
+ // 合并当前层级的叶子节点
163
+ item = it.leafList;
164
+ } else {
165
+ item = { ...it, value: it[value], label: it[label] };
166
+ }
167
+ const childList = item[children] || [];
168
+ const childLen = childList?.length;
169
+ // 当 mergeFullLeaf !== true && children 长度大于1时,子项下标大于 0 的项需要另起一行,同时补充之前空的列。
170
+ if (i > 0) {
171
+ resList.push(fillArrByPush(preCol, [], { content: false }));
172
+ }
173
+ lastRowIdx = resList.length > 0 ? resList.length - 1 : 0;
174
+ // 解决首次进来第一行数组空的问题
175
+ if (!resList[lastRowIdx]) {
176
+ resList[lastRowIdx] = [];
177
+ }
178
+ resList[lastRowIdx].push(item);
179
+ if (childLen > 0) {
180
+ const res = transformTreeData(childList, {
181
+ ...opt,
182
+ resList,
183
+ preCol: preCol + 1,
184
+ level: level + 1,
185
+ });
186
+ if (maxCol < res.maxCol) {
187
+ maxCol = res.maxCol;
188
+ }
189
+ // 当前的最大行数加上子项的行数
190
+ maxRow = maxRow + res.maxRow;
191
+ // 每项的最大行数为子项的最大行数之和
192
+ item._maxRow = res.maxRow;
193
+ } else {
194
+ item._maxRow = 1;
195
+ maxRow += 1;
196
+ }
197
+ });
198
+
199
+ return {
200
+ resList,
201
+ maxCol,
202
+ maxRow,
203
+ };
204
+ }
205
+
206
+ /**
207
+ * 填充数组
208
+ * @param len
209
+ * @param arr
210
+ * @param opt
211
+ * @returns
212
+ */
213
+ export function fillArrByPush(len, arr = [], opt: any = {}) {
214
+ const { content } = opt || {};
215
+ if (arr.length < len) {
216
+ for (let i = arr.length; i < len; i++) {
217
+ arr.push(content);
218
+ }
219
+ }
220
+ return arr;
221
+ }
@@ -27,13 +27,26 @@
27
27
  | ossServerUrl | string | 否 | - | 获取 oss 参数的接口,默认 /api/v1/user/oss/getWebOssConfig |
28
28
  | ossOpt | Object | 否 | - | oss 上传的配置参数 |
29
29
  | fileListConf | Object | 否 | {} | 结果数据配置 |
30
- | previewConfig | Object | 否 | {} | 预览场景的配置 |
30
+ | previewConfig | Object | 否 | {} | 预览场景配置(见下表)。仅影响列表/弹窗预览,不进入 onChange / 表单提交 |
31
+ | previewBaseUrl | string | 否 | - | 仅预览使用的域名/路径前缀(糖衣)。与 `previewConfig.url` 并存时,以 `previewConfig.url` 为准;不进入表单提交 |
31
32
  | onCountExceed | Function | 否 | - | 数量超出 maxCount 时的回调 |
32
33
  | beforeUploadCheck | Function | 否 | - | 追加的 beforeUpload 检查,返回 Promise.reject() 进行拦截 |
33
34
  | templateUrl | string | 否 | - | 模板下载的地址 |
34
35
  | templateDownloadText | string | 否 | 模板下载 | 模板下载按钮的文案 |
35
36
  | uploadParams | Object | 否 | {} | 文件上传接口入参 |
36
37
 
38
+ ### previewConfig 预览配置
39
+
40
+ | 参数 | 类型 | 必填 | 默认值 | 说明 |
41
+ | ---- | ---- | ---- | ------ | ---- |
42
+ | url | string | 否 | - | 预览域名或路径前缀,如 `https://cdn.example.com`。相对路径会拼到此前缀后用于列表缩略图与弹窗预览 |
43
+ | query | object | 否 | - | 追加到预览地址的 query 对象,推荐写法,如 `{ loookType: 1 }` |
44
+ | search | string | 否 | - | `?` 后的原始字符串(可不含 `?`),会先解析再与 `query` 合并;同名键以 `query` 为准 |
45
+
46
+ 兼容:把 `query` 写成字符串(如 `"loookType=1"`)时,组件会解析为 `search`,无需立刻改业务 schema。
47
+
48
+ > **边界**:`previewConfig.url` / `previewBaseUrl` 只用于上传列表视图与弹窗预览;`onChange` 与表单提交仍使用后端返回的相对/原始路径,不会提交拼接后的完整地址。
49
+
37
50
  ### fileListConf 结果数据配置
38
51
 
39
52
  | 参数 | 类型 | 必填 | 默认值 | 说明 |
@@ -1,11 +1,12 @@
1
1
  import { getFileURL } from "@hzab/utils/src/file/file";
2
2
  import { getFileType } from "@hzab/utils/src/file/fileType";
3
- import { isBase64Str } from "@hzab/utils/src/base64Str";
4
3
  import { getUFileName, getFileName, getFullFileName, getFileExt } from "@hzab/utils/src/file/fileName";
5
- import { getFileNameObj } from "@hzab/utils/src/upload/uploadUtils";
4
+ import { getFileNameObj, mergePreviewConfig } from "@hzab/utils/src/upload/uploadUtils";
6
5
  import { nanoidNumALetters } from "@hzab/utils/src/nanoid";
7
6
  import { getFormRenderPCGlobalServeConfig } from "../../../globalConfig";
8
- import { addQuery } from "@hzab/utils/src/url"
7
+ import { addQuery } from "@hzab/utils/src/url";
8
+
9
+ export { mergePreviewConfig };
9
10
 
10
11
 
11
12
  /**
@@ -139,43 +140,6 @@ export const objStrToObj = function (objStr) {
139
140
  return _objStr;
140
141
  };
141
142
 
142
- /**
143
- * 预览 url 添加前缀
144
- * @param {*} uri
145
- * @param {*} previewConfig
146
- * @returns
147
- */
148
- export function mergePreviewConfig(uri, previewConfig) {
149
- let _uri = uri;
150
- let basePath = "";
151
- if (typeof _uri === "object") {
152
- basePath = _uri.basePath;
153
- _uri = _uri.previewUrl || _uri.url;
154
- }
155
- if (typeof _uri !== "string") {
156
- return _uri;
157
- }
158
- _uri = _uri?.trim();
159
- // base64 直接返回
160
- if (isBase64Str(_uri)) {
161
- return _uri;
162
- }
163
- const isUriPattern = /http[s]?:\/\/[^\s]+/.test(_uri);
164
- const query = previewConfig?.query?.trim() ? "?" + previewConfig?.query?.trim() : "";
165
-
166
- if (isUriPattern) {
167
- // 源地址为 http 地址,直接拼接 query 参数
168
- return `${_uri}${query}`;
169
- }
170
-
171
- if (previewConfig) {
172
- // 拼接前缀
173
- return `${previewConfig?.url?.trim()}/${_uri}${query}`?.trim();
174
- }
175
-
176
- return _uri;
177
- }
178
-
179
143
  /**
180
144
  * 子项数据归一化
181
145
  * 字符串:url、base64
@@ -1,5 +1,5 @@
1
- import { formatDirStr, mergeDirStr, handleMaxCount, mergePreviewConfig } from "@hzab/utils/upload/uploadUtils";
2
- import { getFileURL } from "@hzab/utils/file/file";
1
+ import { formatDirStr, mergeDirStr, handleMaxCount, mergePreviewConfig } from "@hzab/utils/src/upload/uploadUtils";
2
+ import { getFileURL } from "@hzab/utils/src/file/file";
3
3
  import { isBase64Str } from "@hzab/utils/src/base64Str";
4
4
  import {
5
5
  checkImageUrl,
@@ -7,6 +7,7 @@ import {
7
7
  checkAudioUrl,
8
8
  isValidMediaType,
9
9
  checkUrlSuffix,
10
+ getFileType,
10
11
  } from "@hzab/utils/src/file/fileType";
11
12
  import { getArr } from "@hzab/utils/src/array";
12
13
 
@@ -10,6 +10,7 @@ import { Button, Upload as AUpload, message } from "antd";
10
10
  import React, { useEffect, useState, useMemo } from "react";
11
11
 
12
12
  import { handleInputFileList, handleOutputFileList } from "./common/handleIOFileList";
13
+ import { mergePreviewConfig } from "@hzab/utils/src/upload/uploadUtils";
13
14
 
14
15
  import PreviewModal, { hasPreviewRender, hasPreviewMedium } from "./components/PreviewModal";
15
16
  import { getOssUploadRequest, getOfflineUploadRequest } from "./common/customRequest";
@@ -57,6 +58,8 @@ export function Uploader({ onChange, ...props }) {
57
58
  isResRemoveArr = true,
58
59
  /** 预览配置参数 */
59
60
  previewConfig = {},
61
+ /** 仅预览使用的域名/路径前缀,不参与 onChange / 表单提交;优先级低于 previewConfig.url */
62
+ previewBaseUrl = getFormRenderPCGlobalServeConfig()?.previewBaseUrl || "",
60
63
  onCountExceed,
61
64
  templateUrl,
62
65
  beforeUploadCheck,
@@ -69,6 +72,26 @@ export function Uploader({ onChange, ...props }) {
69
72
  const isPrivateStore =
70
73
  props.uploadParams?.fileAcl === "private" || props.ossOpt?.signatureParams?.fileAcl === "private";
71
74
 
75
+ /** 预览前缀配置:previewConfig.url 优先,其次 previewBaseUrl;*/
76
+ const resolvedPreviewConfig = useMemo(() => {
77
+ const url = previewConfig?.url || previewBaseUrl;
78
+ const { query, search, ...rest } = previewConfig || {};
79
+ const isQueryObject = query && typeof query === "object";
80
+ const legacyQueryStr = typeof query === "string" ? query.trim() : "";
81
+ const resolvedSearch = (search || legacyQueryStr || "").replace(/^\?/, "") || undefined;
82
+ const resolvedQuery = isQueryObject ? query : undefined;
83
+
84
+ if (!url && !resolvedQuery && !resolvedSearch) {
85
+ return previewConfig || {};
86
+ }
87
+ return {
88
+ ...rest,
89
+ url,
90
+ ...(resolvedQuery ? { query: resolvedQuery } : {}),
91
+ ...(resolvedSearch ? { search: resolvedSearch } : {}),
92
+ };
93
+ }, [previewConfig.url, previewConfig.query, previewConfig.search, previewBaseUrl]);
94
+
72
95
  /**
73
96
  * 数据格式配置
74
97
  */
@@ -135,7 +158,12 @@ export function Uploader({ onChange, ...props }) {
135
158
  };
136
159
 
137
160
  useEffect(() => {
138
- const list = handleInputFileList(value, { ...fileListConf, maxCount, previewConfig, thumbnailParams });
161
+ const list = handleInputFileList(value, {
162
+ ...fileListConf,
163
+ maxCount,
164
+ previewConfig: resolvedPreviewConfig,
165
+ thumbnailParams,
166
+ });
139
167
 
140
168
  async function handlePreview() {
141
169
  await handlePreviewUrls(list, {
@@ -150,7 +178,27 @@ export function Uploader({ onChange, ...props }) {
150
178
  } else {
151
179
  setFileList(list);
152
180
  }
153
- }, [value]);
181
+ }, [value, resolvedPreviewConfig]);
182
+
183
+ /** 仅用于列表展示:补齐 thumbUrl/previewUrl,不改动 url(提交仍用相对路径) */
184
+ const displayFileList = useMemo(() => {
185
+ if (!resolvedPreviewConfig?.url || !fileList?.length) {
186
+ return fileList;
187
+ }
188
+ return fileList.map((file) => {
189
+ if (!file || typeof file !== "object") {
190
+ return file;
191
+ }
192
+ // 用存储路径 url 拼前缀,避免已是完整 previewUrl 时语义混乱;无 url 时回退对象自身
193
+ const previewUrl = mergePreviewConfig(file.url || file, resolvedPreviewConfig);
194
+ const thumbUrl = file.thumbUrl ? mergePreviewConfig(file.thumbUrl, resolvedPreviewConfig) : previewUrl;
195
+ return {
196
+ ...file,
197
+ previewUrl,
198
+ thumbUrl,
199
+ };
200
+ });
201
+ }, [fileList, resolvedPreviewConfig]);
154
202
 
155
203
  const onRemove = (file) => {
156
204
  const files = (fileList || []).filter((v) => v.url !== file.url);
@@ -206,7 +254,7 @@ export function Uploader({ onChange, ...props }) {
206
254
  multiple: multiple,
207
255
  listType: listType,
208
256
  maxCount: maxCount,
209
- fileList: fileList,
257
+ fileList: displayFileList,
210
258
  onChange: handleChange,
211
259
  disabled,
212
260
  readOnly,
@@ -249,10 +297,14 @@ export function Uploader({ onChange, ...props }) {
249
297
  setChecking(false);
250
298
  },
251
299
  customRequest,
300
+ ...(props || {}),
301
+ // 保证展示列表与预览逻辑不被 props 覆盖丢失
302
+ fileList: displayFileList,
252
303
  onPreview(file) {
253
304
  // TODO: 逻辑合并到数据处理中
254
305
  const isShowPreview = hasPreviewRender(file) || hasPreviewMedium(props.accept);
255
- const fileUrl = file.previewUrl;
306
+ // 预览出口统一走 mergePreviewConfig;提交仍使用 file.url 相对路径
307
+ const fileUrl = mergePreviewConfig(file.url || file, resolvedPreviewConfig);
256
308
  if (isShowPreview) {
257
309
  setPreviewFile({
258
310
  ...file,
@@ -316,7 +368,7 @@ export function Uploader({ onChange, ...props }) {
316
368
  accept={props?.accept}
317
369
  setPreviewFile={setPreviewFile}
318
370
  modalConf={props.modalConf}
319
- previewConfig={previewConfig}
371
+ previewConfig={resolvedPreviewConfig}
320
372
  />
321
373
  </>
322
374
  );