seeder-resources-view 1.0.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.
- package/README.md +31 -0
- package/dist/index.css +1 -0
- package/dist/index.esm.js +1276 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +1278 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -0
- package/src/style/resources.less +164 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1278 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var antd = require('antd');
|
|
5
|
+
var icons = require('@ant-design/icons');
|
|
6
|
+
var axios = require('axios');
|
|
7
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
8
|
+
|
|
9
|
+
function _defineProperty(e, r, t) {
|
|
10
|
+
return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
|
|
11
|
+
value: t,
|
|
12
|
+
enumerable: !0,
|
|
13
|
+
configurable: !0,
|
|
14
|
+
writable: !0
|
|
15
|
+
}) : e[r] = t, e;
|
|
16
|
+
}
|
|
17
|
+
function _extends() {
|
|
18
|
+
return _extends = Object.assign ? Object.assign.bind() : function (n) {
|
|
19
|
+
for (var e = 1; e < arguments.length; e++) {
|
|
20
|
+
var t = arguments[e];
|
|
21
|
+
for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]);
|
|
22
|
+
}
|
|
23
|
+
return n;
|
|
24
|
+
}, _extends.apply(null, arguments);
|
|
25
|
+
}
|
|
26
|
+
function _objectDestructuringEmpty(t) {
|
|
27
|
+
if (null == t) throw new TypeError("Cannot destructure " + t);
|
|
28
|
+
}
|
|
29
|
+
function ownKeys(e, r) {
|
|
30
|
+
var t = Object.keys(e);
|
|
31
|
+
if (Object.getOwnPropertySymbols) {
|
|
32
|
+
var o = Object.getOwnPropertySymbols(e);
|
|
33
|
+
r && (o = o.filter(function (r) {
|
|
34
|
+
return Object.getOwnPropertyDescriptor(e, r).enumerable;
|
|
35
|
+
})), t.push.apply(t, o);
|
|
36
|
+
}
|
|
37
|
+
return t;
|
|
38
|
+
}
|
|
39
|
+
function _objectSpread2(e) {
|
|
40
|
+
for (var r = 1; r < arguments.length; r++) {
|
|
41
|
+
var t = null != arguments[r] ? arguments[r] : {};
|
|
42
|
+
r % 2 ? ownKeys(Object(t), !0).forEach(function (r) {
|
|
43
|
+
_defineProperty(e, r, t[r]);
|
|
44
|
+
}) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) {
|
|
45
|
+
Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r));
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return e;
|
|
49
|
+
}
|
|
50
|
+
function _toPrimitive(t, r) {
|
|
51
|
+
if ("object" != typeof t || !t) return t;
|
|
52
|
+
var e = t[Symbol.toPrimitive];
|
|
53
|
+
if (void 0 !== e) {
|
|
54
|
+
var i = e.call(t, r || "default");
|
|
55
|
+
if ("object" != typeof i) return i;
|
|
56
|
+
throw new TypeError("@@toPrimitive must return a primitive value.");
|
|
57
|
+
}
|
|
58
|
+
return ("string" === r ? String : Number)(t);
|
|
59
|
+
}
|
|
60
|
+
function _toPropertyKey(t) {
|
|
61
|
+
var i = _toPrimitive(t, "string");
|
|
62
|
+
return "symbol" == typeof i ? i : i + "";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const BASE_MENU_ITEMS = [{
|
|
66
|
+
label: 'Download',
|
|
67
|
+
key: 'download',
|
|
68
|
+
featureKey: 'download'
|
|
69
|
+
}, {
|
|
70
|
+
label: 'Copy',
|
|
71
|
+
key: 'copy',
|
|
72
|
+
featureKey: 'copy'
|
|
73
|
+
}, {
|
|
74
|
+
label: 'Delete',
|
|
75
|
+
key: 'del',
|
|
76
|
+
featureKey: 'delete'
|
|
77
|
+
}];
|
|
78
|
+
const MediaGridItem = /*#__PURE__*/react.memo(_ref => {
|
|
79
|
+
let {
|
|
80
|
+
item,
|
|
81
|
+
onContextMenu,
|
|
82
|
+
menuItems = DEFAULT_MENU_ITEMS,
|
|
83
|
+
checked,
|
|
84
|
+
onSelectChange,
|
|
85
|
+
showCheckbox = true,
|
|
86
|
+
onClick,
|
|
87
|
+
getFullUrl
|
|
88
|
+
} = _ref;
|
|
89
|
+
const handleMenuClick = react.useCallback(_ref2 => {
|
|
90
|
+
let {
|
|
91
|
+
key
|
|
92
|
+
} = _ref2;
|
|
93
|
+
return onContextMenu(key, item);
|
|
94
|
+
}, [item, onContextMenu]);
|
|
95
|
+
const handleItemClick = react.useCallback(() => {
|
|
96
|
+
onClick === null || onClick === void 0 || onClick(item);
|
|
97
|
+
}, [item, onClick]);
|
|
98
|
+
const handleCheckboxChange = react.useCallback(e => {
|
|
99
|
+
e.stopPropagation();
|
|
100
|
+
onSelectChange === null || onSelectChange === void 0 || onSelectChange(item, e.target.checked);
|
|
101
|
+
}, [item, onSelectChange]);
|
|
102
|
+
return /*#__PURE__*/jsxRuntime.jsx(antd.Dropdown, {
|
|
103
|
+
menu: {
|
|
104
|
+
items: menuItems,
|
|
105
|
+
onClick: handleMenuClick
|
|
106
|
+
},
|
|
107
|
+
trigger: ['contextMenu'],
|
|
108
|
+
disabled: !onContextMenu,
|
|
109
|
+
children: /*#__PURE__*/jsxRuntime.jsxs(antd.List.Item, {
|
|
110
|
+
className: "media-grid-item ".concat(checked ? 'media-grid-item-selected' : ''),
|
|
111
|
+
title: item.name,
|
|
112
|
+
onClick: handleItemClick,
|
|
113
|
+
children: [showCheckbox && /*#__PURE__*/jsxRuntime.jsx("div", {
|
|
114
|
+
className: "media-grid-checkbox-wrapper",
|
|
115
|
+
onClick: e => e.stopPropagation(),
|
|
116
|
+
children: /*#__PURE__*/jsxRuntime.jsx(antd.Checkbox, {
|
|
117
|
+
checked: checked,
|
|
118
|
+
onChange: handleCheckboxChange
|
|
119
|
+
})
|
|
120
|
+
}), /*#__PURE__*/jsxRuntime.jsx("div", {
|
|
121
|
+
className: "media-grid-thumbnail",
|
|
122
|
+
children: /*#__PURE__*/jsxRuntime.jsx(antd.Image, {
|
|
123
|
+
width: "100%",
|
|
124
|
+
height: "100%",
|
|
125
|
+
src: getFullUrl(item.head_thumbnail),
|
|
126
|
+
preview: false
|
|
127
|
+
})
|
|
128
|
+
}), /*#__PURE__*/jsxRuntime.jsx("div", {
|
|
129
|
+
className: "media-grid-info",
|
|
130
|
+
children: /*#__PURE__*/jsxRuntime.jsx(antd.Typography.Text, {
|
|
131
|
+
ellipsis: true,
|
|
132
|
+
children: item.name
|
|
133
|
+
})
|
|
134
|
+
})]
|
|
135
|
+
})
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
const MediaGrid = /*#__PURE__*/react.memo(_ref3 => {
|
|
139
|
+
let {
|
|
140
|
+
items = [],
|
|
141
|
+
loading = false,
|
|
142
|
+
onContextMenu,
|
|
143
|
+
selectedKeys = [],
|
|
144
|
+
onSelectChange,
|
|
145
|
+
menuItems: customMenuItems,
|
|
146
|
+
gridConfig = {
|
|
147
|
+
gutter: 0,
|
|
148
|
+
column: 6
|
|
149
|
+
},
|
|
150
|
+
showCheckbox = true,
|
|
151
|
+
onClick,
|
|
152
|
+
features = {},
|
|
153
|
+
getFullUrl
|
|
154
|
+
} = _ref3;
|
|
155
|
+
// 合并默认 features 和传入的 features
|
|
156
|
+
const mergedFeatures = useMemo(() => _objectSpread2({
|
|
157
|
+
download: true,
|
|
158
|
+
copy: true,
|
|
159
|
+
delete: true
|
|
160
|
+
}, features), [features]);
|
|
161
|
+
|
|
162
|
+
// 根据 features 动态生成 menuItems
|
|
163
|
+
const menuItems = useMemo(() => {
|
|
164
|
+
// 如果传入了自定义 menuItems,则优先使用
|
|
165
|
+
if (customMenuItems) return customMenuItems;
|
|
166
|
+
|
|
167
|
+
// 否则根据 features 过滤出可用的菜单项
|
|
168
|
+
return BASE_MENU_ITEMS.filter(item => mergedFeatures[item.featureKey] !== false);
|
|
169
|
+
}, [customMenuItems, mergedFeatures]);
|
|
170
|
+
const renderItem = react.useCallback(item => /*#__PURE__*/jsxRuntime.jsx(MediaGridItem, {
|
|
171
|
+
item: item,
|
|
172
|
+
onContextMenu: menuItems.length > 0 ? onContextMenu : null,
|
|
173
|
+
menuItems: menuItems,
|
|
174
|
+
checked: selectedKeys.includes(item.url) // 使用url作为唯一标识
|
|
175
|
+
,
|
|
176
|
+
onSelectChange: onSelectChange,
|
|
177
|
+
showCheckbox: showCheckbox,
|
|
178
|
+
onClick: onClick,
|
|
179
|
+
getFullUrl: getFullUrl // 传递 getFullUrl 给子组件
|
|
180
|
+
}), [onContextMenu, menuItems, selectedKeys, onSelectChange, showCheckbox, onClick]);
|
|
181
|
+
return /*#__PURE__*/jsxRuntime.jsx(antd.Spin, {
|
|
182
|
+
spinning: loading,
|
|
183
|
+
size: "large",
|
|
184
|
+
children: /*#__PURE__*/jsxRuntime.jsx(antd.List, {
|
|
185
|
+
grid: gridConfig,
|
|
186
|
+
dataSource: items,
|
|
187
|
+
renderItem: renderItem,
|
|
188
|
+
pagination: false
|
|
189
|
+
// locale={{ emptyText: 'No media files found' }}
|
|
190
|
+
})
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
var MediaGrid$1 = MediaGrid;
|
|
194
|
+
|
|
195
|
+
const SelectFolderPathModal = _ref => {
|
|
196
|
+
let {
|
|
197
|
+
open,
|
|
198
|
+
onClose,
|
|
199
|
+
onOk,
|
|
200
|
+
directoryTree,
|
|
201
|
+
loading: externalLoading,
|
|
202
|
+
batchLoading,
|
|
203
|
+
title = "Select Folder Path",
|
|
204
|
+
okText = "Copy",
|
|
205
|
+
cancelText = "Cancel",
|
|
206
|
+
placeholder = "Please select (click arrow to expand)",
|
|
207
|
+
width = 520,
|
|
208
|
+
formItemLabel = "Folder Path",
|
|
209
|
+
showFullPath = false,
|
|
210
|
+
disabled = false,
|
|
211
|
+
allowClear = true,
|
|
212
|
+
expandTrigger = "hover"
|
|
213
|
+
} = _ref;
|
|
214
|
+
const [form] = antd.Form.useForm();
|
|
215
|
+
const [options, setOptions] = react.useState([]);
|
|
216
|
+
const [internalLoading, setInternalLoading] = react.useState(false);
|
|
217
|
+
const [selectedPath, setSelectedPath] = react.useState([]);
|
|
218
|
+
const loading = externalLoading || internalLoading;
|
|
219
|
+
|
|
220
|
+
// 将 directoryTree 转换为 Cascader 需要的 options 格式
|
|
221
|
+
const convertToCascaderOptions = react.useCallback(treeData => {
|
|
222
|
+
if (!treeData || !Array.isArray(treeData)) return [];
|
|
223
|
+
return treeData.map(item => {
|
|
224
|
+
if (!item) return null;
|
|
225
|
+
const option = {
|
|
226
|
+
value: item.id,
|
|
227
|
+
label: item.sd_index || item.name,
|
|
228
|
+
isLeaf: !item.contents || item.contents.length === 0,
|
|
229
|
+
path: item.path,
|
|
230
|
+
rawData: item // 保留原始数据
|
|
231
|
+
};
|
|
232
|
+
if (item.contents && Array.isArray(item.contents) && item.contents.length > 0) {
|
|
233
|
+
// 递归处理子目录
|
|
234
|
+
option.children = convertToCascaderOptions(item.contents.filter(child => child && child.type === 'directory'));
|
|
235
|
+
}
|
|
236
|
+
return option;
|
|
237
|
+
}).filter(Boolean);
|
|
238
|
+
}, []);
|
|
239
|
+
|
|
240
|
+
// 初始化 options
|
|
241
|
+
react.useEffect(() => {
|
|
242
|
+
if (open && directoryTree) {
|
|
243
|
+
setInternalLoading(true);
|
|
244
|
+
try {
|
|
245
|
+
const formattedOptions = convertToCascaderOptions(directoryTree);
|
|
246
|
+
setOptions(formattedOptions);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
console.error('Error converting directory tree:', error);
|
|
249
|
+
} finally {
|
|
250
|
+
setInternalLoading(false);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}, [open, directoryTree, convertToCascaderOptions]);
|
|
254
|
+
|
|
255
|
+
// 根据value路径查找对应的节点信息
|
|
256
|
+
const getSelectedNodes = react.useCallback(function (valuePath, currentOptions) {
|
|
257
|
+
let collectedNodes = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [];
|
|
258
|
+
if (!valuePath || valuePath.length === 0 || !currentOptions) return collectedNodes;
|
|
259
|
+
const currentValue = valuePath[0];
|
|
260
|
+
const currentNode = currentOptions.find(opt => opt && opt.value === currentValue);
|
|
261
|
+
if (!currentNode) return collectedNodes;
|
|
262
|
+
collectedNodes.push(currentNode);
|
|
263
|
+
if (valuePath.length > 1 && currentNode.children) {
|
|
264
|
+
return getSelectedNodes(valuePath.slice(1), currentNode.children, collectedNodes);
|
|
265
|
+
}
|
|
266
|
+
return collectedNodes;
|
|
267
|
+
}, []);
|
|
268
|
+
const handleOk = react.useCallback(() => {
|
|
269
|
+
form.validateFields().then(values => {
|
|
270
|
+
if (!values.path) {
|
|
271
|
+
message.warning('Please select a folder path');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const selectedNodes = getSelectedNodes(values.path, options);
|
|
275
|
+
if (selectedNodes.length === 0) {
|
|
276
|
+
message.warning('Invalid path selected');
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const lastNode = selectedNodes[selectedNodes.length - 1];
|
|
280
|
+
|
|
281
|
+
// 返回完整路径或最后节点路径
|
|
282
|
+
const resultPath = showFullPath ? selectedNodes.map(node => node.path).join('/') : lastNode.path;
|
|
283
|
+
onOk === null || onOk === void 0 || onOk(resultPath);
|
|
284
|
+
}).catch(error => {
|
|
285
|
+
console.error('Validation failed:', error);
|
|
286
|
+
});
|
|
287
|
+
}, [form, options, getSelectedNodes, onOk, showFullPath]);
|
|
288
|
+
const handleAfterClose = react.useCallback(() => {
|
|
289
|
+
form.resetFields();
|
|
290
|
+
setSelectedPath([]);
|
|
291
|
+
}, [form]);
|
|
292
|
+
return /*#__PURE__*/jsxRuntime.jsx(antd.Modal, {
|
|
293
|
+
title: title,
|
|
294
|
+
open: open,
|
|
295
|
+
onCancel: onClose,
|
|
296
|
+
afterClose: handleAfterClose,
|
|
297
|
+
width: width,
|
|
298
|
+
destroyOnHidden: true,
|
|
299
|
+
footer: [/*#__PURE__*/jsxRuntime.jsx(antd.Button, {
|
|
300
|
+
onClick: onClose,
|
|
301
|
+
disabled: batchLoading,
|
|
302
|
+
children: cancelText
|
|
303
|
+
}, "cancel"), /*#__PURE__*/jsxRuntime.jsx(antd.Button, {
|
|
304
|
+
type: "primary",
|
|
305
|
+
onClick: handleOk,
|
|
306
|
+
loading: batchLoading,
|
|
307
|
+
disabled: disabled || selectedPath.length === 0,
|
|
308
|
+
children: okText
|
|
309
|
+
}, "submit")],
|
|
310
|
+
children: /*#__PURE__*/jsxRuntime.jsx(antd.Spin, {
|
|
311
|
+
spinning: loading,
|
|
312
|
+
children: /*#__PURE__*/jsxRuntime.jsx(antd.Form, {
|
|
313
|
+
form: form,
|
|
314
|
+
children: /*#__PURE__*/jsxRuntime.jsx(antd.Form.Item, {
|
|
315
|
+
name: "path",
|
|
316
|
+
label: formItemLabel,
|
|
317
|
+
children: /*#__PURE__*/jsxRuntime.jsx(antd.Cascader, {
|
|
318
|
+
options: options,
|
|
319
|
+
changeOnSelect: true,
|
|
320
|
+
placeholder: placeholder,
|
|
321
|
+
style: {
|
|
322
|
+
width: '100%'
|
|
323
|
+
},
|
|
324
|
+
disabled: disabled || loading,
|
|
325
|
+
expandTrigger: expandTrigger,
|
|
326
|
+
allowClear: allowClear
|
|
327
|
+
})
|
|
328
|
+
})
|
|
329
|
+
})
|
|
330
|
+
})
|
|
331
|
+
});
|
|
332
|
+
};
|
|
333
|
+
var SelectFolderPathModal$1 = /*#__PURE__*/react.memo(SelectFolderPathModal);
|
|
334
|
+
|
|
335
|
+
const UploadProgress = _ref => {
|
|
336
|
+
let {
|
|
337
|
+
percent
|
|
338
|
+
} = _ref;
|
|
339
|
+
return /*#__PURE__*/jsxRuntime.jsx("div", {
|
|
340
|
+
className: "upload-progress",
|
|
341
|
+
children: /*#__PURE__*/jsxRuntime.jsx(antd.Progress, {
|
|
342
|
+
type: "circle",
|
|
343
|
+
percent: percent
|
|
344
|
+
})
|
|
345
|
+
});
|
|
346
|
+
};
|
|
347
|
+
var UploadProgress$1 = /*#__PURE__*/react.memo(UploadProgress);
|
|
348
|
+
|
|
349
|
+
const PopoverContent = /*#__PURE__*/react.forwardRef((_ref, ref) => {
|
|
350
|
+
let {
|
|
351
|
+
onClose,
|
|
352
|
+
onConfirm
|
|
353
|
+
} = _ref;
|
|
354
|
+
const [value, setValue] = react.useState('');
|
|
355
|
+
const handleConfirm = react.useCallback(() => {
|
|
356
|
+
onConfirm(value, () => setValue(''));
|
|
357
|
+
}, [confirm, value]);
|
|
358
|
+
const handleKeyDown = react.useCallback(e => {
|
|
359
|
+
if (e.key === 'Enter') {
|
|
360
|
+
handleConfirm();
|
|
361
|
+
}
|
|
362
|
+
}, [handleConfirm]);
|
|
363
|
+
return /*#__PURE__*/jsxRuntime.jsxs(jsxRuntime.Fragment, {
|
|
364
|
+
children: [/*#__PURE__*/jsxRuntime.jsx(antd.Form.Item, {
|
|
365
|
+
label: "Folder Name",
|
|
366
|
+
style: {
|
|
367
|
+
marginBottom: 8
|
|
368
|
+
},
|
|
369
|
+
children: /*#__PURE__*/jsxRuntime.jsx(antd.Input, {
|
|
370
|
+
ref: ref,
|
|
371
|
+
value: value,
|
|
372
|
+
onChange: e => setValue(e.target.value),
|
|
373
|
+
onKeyDown: handleKeyDown,
|
|
374
|
+
size: "small",
|
|
375
|
+
style: {
|
|
376
|
+
width: 120
|
|
377
|
+
},
|
|
378
|
+
autoFocus: true
|
|
379
|
+
})
|
|
380
|
+
}), /*#__PURE__*/jsxRuntime.jsxs(antd.Flex, {
|
|
381
|
+
justify: "flex-end",
|
|
382
|
+
gap: "small",
|
|
383
|
+
children: [/*#__PURE__*/jsxRuntime.jsx(antd.Button, {
|
|
384
|
+
type: "default",
|
|
385
|
+
size: "small",
|
|
386
|
+
onClick: onClose,
|
|
387
|
+
children: "Close"
|
|
388
|
+
}), /*#__PURE__*/jsxRuntime.jsx(antd.Button, {
|
|
389
|
+
type: "primary",
|
|
390
|
+
size: "small",
|
|
391
|
+
onClick: handleConfirm,
|
|
392
|
+
children: "OK"
|
|
393
|
+
})]
|
|
394
|
+
})]
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
const EditablePopover = _ref2 => {
|
|
398
|
+
let {
|
|
399
|
+
children,
|
|
400
|
+
add
|
|
401
|
+
} = _ref2;
|
|
402
|
+
const inputRef = react.useRef(null);
|
|
403
|
+
const [open, setOpen] = react.useState(false);
|
|
404
|
+
react.useEffect(() => {
|
|
405
|
+
if (open && inputRef.current) {
|
|
406
|
+
const timer = setTimeout(() => {
|
|
407
|
+
var _inputRef$current;
|
|
408
|
+
(_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 || _inputRef$current.focus();
|
|
409
|
+
}, 100);
|
|
410
|
+
return () => clearTimeout(timer);
|
|
411
|
+
}
|
|
412
|
+
}, [open]);
|
|
413
|
+
|
|
414
|
+
// 使用 open 属性控制浮层显示
|
|
415
|
+
const hide = react.useCallback(() => {
|
|
416
|
+
setOpen(false);
|
|
417
|
+
}, []);
|
|
418
|
+
const handleConfirm = react.useCallback((newTitle, callback) => {
|
|
419
|
+
add(newTitle);
|
|
420
|
+
callback === null || callback === void 0 || callback();
|
|
421
|
+
hide();
|
|
422
|
+
}, [add, hide]);
|
|
423
|
+
const handleOpenChange = react.useCallback(newOpen => {
|
|
424
|
+
setOpen(newOpen);
|
|
425
|
+
}, []);
|
|
426
|
+
return /*#__PURE__*/jsxRuntime.jsx(antd.Popover, {
|
|
427
|
+
content: /*#__PURE__*/jsxRuntime.jsx(PopoverContent, {
|
|
428
|
+
ref: inputRef,
|
|
429
|
+
onClose: hide,
|
|
430
|
+
onConfirm: handleConfirm
|
|
431
|
+
}),
|
|
432
|
+
trigger: "click",
|
|
433
|
+
open: open,
|
|
434
|
+
onOpenChange: handleOpenChange,
|
|
435
|
+
destroyOnHidden: true,
|
|
436
|
+
children: children
|
|
437
|
+
});
|
|
438
|
+
};
|
|
439
|
+
var EditablePopover$1 = /*#__PURE__*/react.memo(EditablePopover);
|
|
440
|
+
|
|
441
|
+
const EditableContext = /*#__PURE__*/react.createContext(null);
|
|
442
|
+
const TreeTitle = _ref => {
|
|
443
|
+
let props = _extends({}, (_objectDestructuringEmpty(_ref), _ref));
|
|
444
|
+
const [form] = antd.Form.useForm();
|
|
445
|
+
return /*#__PURE__*/jsxRuntime.jsx(antd.Form, {
|
|
446
|
+
form: form,
|
|
447
|
+
component: false,
|
|
448
|
+
children: /*#__PURE__*/jsxRuntime.jsx(EditableContext.Provider, {
|
|
449
|
+
value: form,
|
|
450
|
+
children: /*#__PURE__*/jsxRuntime.jsx(TreeTitleNode, _objectSpread2({}, props))
|
|
451
|
+
})
|
|
452
|
+
});
|
|
453
|
+
};
|
|
454
|
+
const TreeTitleNode = _ref2 => {
|
|
455
|
+
let {
|
|
456
|
+
title,
|
|
457
|
+
nodeData,
|
|
458
|
+
handleSave,
|
|
459
|
+
handleDel,
|
|
460
|
+
handleAdd
|
|
461
|
+
} = _ref2;
|
|
462
|
+
const [editing, setEditing] = react.useState(false);
|
|
463
|
+
const inputRef = react.useRef(null);
|
|
464
|
+
const form = react.useContext(EditableContext);
|
|
465
|
+
react.useEffect(() => {
|
|
466
|
+
if (editing) {
|
|
467
|
+
var _inputRef$current;
|
|
468
|
+
(_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 || _inputRef$current.focus();
|
|
469
|
+
}
|
|
470
|
+
}, [editing]);
|
|
471
|
+
const toggleEdit = react.useCallback(() => {
|
|
472
|
+
setEditing(prev => !prev);
|
|
473
|
+
form.setFieldsValue({
|
|
474
|
+
[nodeData.title]: nodeData.title
|
|
475
|
+
});
|
|
476
|
+
}, [form, nodeData.title]);
|
|
477
|
+
const save = react.useCallback(async () => {
|
|
478
|
+
try {
|
|
479
|
+
const values = await form.validateFields();
|
|
480
|
+
toggleEdit();
|
|
481
|
+
handleSave(_objectSpread2(_objectSpread2({}, nodeData), {}, {
|
|
482
|
+
title: values[nodeData.title]
|
|
483
|
+
}));
|
|
484
|
+
} catch (errInfo) {
|
|
485
|
+
console.error('Save failed:', errInfo);
|
|
486
|
+
}
|
|
487
|
+
}, [form, toggleEdit, handleSave, nodeData]);
|
|
488
|
+
|
|
489
|
+
// 新建文件夹
|
|
490
|
+
// 修改文件/文件夹
|
|
491
|
+
// 删除文件/文件夹
|
|
492
|
+
const renderIconNode = react.useCallback(() => {
|
|
493
|
+
return /*#__PURE__*/jsxRuntime.jsxs("span", {
|
|
494
|
+
className: "tree-node-icons",
|
|
495
|
+
onClick: e => e.stopPropagation(),
|
|
496
|
+
children: [/*#__PURE__*/jsxRuntime.jsx(EditablePopover$1, {
|
|
497
|
+
add: newTitle => handleAdd(nodeData, newTitle),
|
|
498
|
+
children: /*#__PURE__*/jsxRuntime.jsx("div", {
|
|
499
|
+
className: "px-1",
|
|
500
|
+
title: "create",
|
|
501
|
+
children: /*#__PURE__*/jsxRuntime.jsx("i", {
|
|
502
|
+
className: "iconfont icon-jia"
|
|
503
|
+
})
|
|
504
|
+
})
|
|
505
|
+
}), !nodeData.isRoot && /*#__PURE__*/jsxRuntime.jsxs(jsxRuntime.Fragment, {
|
|
506
|
+
children: [/*#__PURE__*/jsxRuntime.jsx("div", {
|
|
507
|
+
className: "px-1",
|
|
508
|
+
onClick: toggleEdit,
|
|
509
|
+
title: "edit",
|
|
510
|
+
children: /*#__PURE__*/jsxRuntime.jsx("i", {
|
|
511
|
+
className: "iconfont icon-bianji"
|
|
512
|
+
})
|
|
513
|
+
}), /*#__PURE__*/jsxRuntime.jsx(antd.Popconfirm, {
|
|
514
|
+
title: "Confirm deletion?",
|
|
515
|
+
onConfirm: () => handleDel(nodeData),
|
|
516
|
+
okText: "Yes",
|
|
517
|
+
cancelText: "No",
|
|
518
|
+
children: /*#__PURE__*/jsxRuntime.jsx("div", {
|
|
519
|
+
className: "px-1",
|
|
520
|
+
title: "delete",
|
|
521
|
+
children: /*#__PURE__*/jsxRuntime.jsx("i", {
|
|
522
|
+
className: "iconfont icon-jian"
|
|
523
|
+
})
|
|
524
|
+
})
|
|
525
|
+
})]
|
|
526
|
+
})]
|
|
527
|
+
});
|
|
528
|
+
}, [handleAdd, nodeData, title, toggleEdit, handleDel]);
|
|
529
|
+
const renderChildNode = react.useCallback(() => {
|
|
530
|
+
return editing ? /*#__PURE__*/jsxRuntime.jsx(antd.Form.Item, {
|
|
531
|
+
style: {
|
|
532
|
+
margin: 0,
|
|
533
|
+
width: "100%"
|
|
534
|
+
},
|
|
535
|
+
name: nodeData.title,
|
|
536
|
+
children: /*#__PURE__*/jsxRuntime.jsx(antd.Input, {
|
|
537
|
+
ref: inputRef,
|
|
538
|
+
onPressEnter: save,
|
|
539
|
+
onBlur: save,
|
|
540
|
+
autoComplete: "off",
|
|
541
|
+
size: "small"
|
|
542
|
+
})
|
|
543
|
+
}) : /*#__PURE__*/jsxRuntime.jsxs(jsxRuntime.Fragment, {
|
|
544
|
+
children: [/*#__PURE__*/jsxRuntime.jsx(antd.Typography.Text, {
|
|
545
|
+
ellipsis: true,
|
|
546
|
+
style: {
|
|
547
|
+
width: "75%"
|
|
548
|
+
},
|
|
549
|
+
children: title
|
|
550
|
+
}), renderIconNode()]
|
|
551
|
+
});
|
|
552
|
+
}, [editing, nodeData.title, save, title, renderIconNode]);
|
|
553
|
+
return /*#__PURE__*/jsxRuntime.jsx("div", {
|
|
554
|
+
className: "tree-title",
|
|
555
|
+
children: renderChildNode()
|
|
556
|
+
});
|
|
557
|
+
};
|
|
558
|
+
var TreeTitle$1 = /*#__PURE__*/react.memo(TreeTitle);
|
|
559
|
+
|
|
560
|
+
const buildDirectoryTree = function (data) {
|
|
561
|
+
let basePath = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '0';
|
|
562
|
+
let parentRoute = arguments.length > 2 ? arguments[2] : undefined;
|
|
563
|
+
if (!Array.isArray(data)) return [];
|
|
564
|
+
return data.reduce((acc, item, index) => {
|
|
565
|
+
var _item$sd_index;
|
|
566
|
+
// 跳过文件类型,只处理目录
|
|
567
|
+
if (item.type === 'file') return acc;
|
|
568
|
+
const title = parentRoute ? item.name : (_item$sd_index = item.sd_index) !== null && _item$sd_index !== void 0 ? _item$sd_index : item.name;
|
|
569
|
+
const key = "".concat(basePath, "-").concat(index);
|
|
570
|
+
const currentPath = parentRoute ? "".concat(parentRoute, "/").concat(title) : item.path;
|
|
571
|
+
const treeNode = {
|
|
572
|
+
title,
|
|
573
|
+
// 文件/文件夹名称
|
|
574
|
+
key,
|
|
575
|
+
icon: _ref => {
|
|
576
|
+
let {
|
|
577
|
+
expanded
|
|
578
|
+
} = _ref;
|
|
579
|
+
return expanded ? /*#__PURE__*/jsxRuntime.jsx(icons.FolderOpenOutlined, {}) : /*#__PURE__*/jsxRuntime.jsx(icons.FolderOutlined, {});
|
|
580
|
+
},
|
|
581
|
+
children: [],
|
|
582
|
+
isLeaf: false,
|
|
583
|
+
isRoot: Boolean(item.sd_index),
|
|
584
|
+
path: currentPath,
|
|
585
|
+
contents: (item.contents || []).filter(content => content.type === 'file'),
|
|
586
|
+
rawData: item // 保留原始数据
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
// 递归处理子目录
|
|
590
|
+
if (Array.isArray(item.contents) && item.contents.length > 0) {
|
|
591
|
+
treeNode.children = buildDirectoryTree(item.contents.filter(content => content.type !== 'file'), key, currentPath);
|
|
592
|
+
}
|
|
593
|
+
acc.push(treeNode);
|
|
594
|
+
return acc;
|
|
595
|
+
}, []);
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* 查找树中指定key的节点
|
|
600
|
+
* @param {Array} treeData - 树数据
|
|
601
|
+
* @param {string} targetKey - 要查找的key
|
|
602
|
+
* @returns {Array} 包含匹配节点的数组
|
|
603
|
+
*/
|
|
604
|
+
const findTreeNode = (treeData, targetKey) => {
|
|
605
|
+
if (!Array.isArray(treeData) || typeof targetKey !== 'string') return [];
|
|
606
|
+
const result = [];
|
|
607
|
+
const stack = [...treeData];
|
|
608
|
+
while (stack.length) {
|
|
609
|
+
const node = stack.pop();
|
|
610
|
+
if (node.key === targetKey) {
|
|
611
|
+
result.push(node);
|
|
612
|
+
// 如果只需要第一个匹配项,可以在这里break
|
|
613
|
+
}
|
|
614
|
+
if (Array.isArray(node.children)) {
|
|
615
|
+
stack.push(...node.children);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return result;
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* 获取树中所有节点的keys
|
|
623
|
+
* @param {Array} treeData - 树数据
|
|
624
|
+
* @returns {Array} 包含所有节点keys的数组
|
|
625
|
+
*/
|
|
626
|
+
const getAllNodeKeys = treeData => {
|
|
627
|
+
if (!Array.isArray(treeData)) return [];
|
|
628
|
+
const keys = [];
|
|
629
|
+
const stack = [...treeData];
|
|
630
|
+
while (stack.length) {
|
|
631
|
+
const node = stack.pop();
|
|
632
|
+
if (node.key != null) {
|
|
633
|
+
keys.push(node.key);
|
|
634
|
+
}
|
|
635
|
+
if (Array.isArray(node.children)) {
|
|
636
|
+
stack.push(...node.children);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return keys;
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
const useDirectoryTree = _ref => {
|
|
643
|
+
let {
|
|
644
|
+
getFolderData,
|
|
645
|
+
createFolder,
|
|
646
|
+
removeFolderFile,
|
|
647
|
+
renameFolderFile,
|
|
648
|
+
height = 760,
|
|
649
|
+
theme = {
|
|
650
|
+
components: {
|
|
651
|
+
Tree: {
|
|
652
|
+
titleHeight: 30
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
} = _ref;
|
|
657
|
+
const [treeState, setTreeState] = react.useState({
|
|
658
|
+
data: [],
|
|
659
|
+
selectedKeys: [],
|
|
660
|
+
expandedKeys: [],
|
|
661
|
+
currentPath: "",
|
|
662
|
+
contents: [],
|
|
663
|
+
loading: false
|
|
664
|
+
});
|
|
665
|
+
const [originTreeData, setOriginTreeData] = react.useState([]);
|
|
666
|
+
const updateTreeState = partialState => {
|
|
667
|
+
setTreeState(prev => _objectSpread2(_objectSpread2({}, prev), partialState));
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
// 错误处理
|
|
671
|
+
const handleError = (error, operation) => {
|
|
672
|
+
console.error("".concat(operation, " ERROR"), error);
|
|
673
|
+
// 可以添加通知等统一错误处理
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
// 路径处理工具
|
|
677
|
+
const pathUtils = {
|
|
678
|
+
getNewPath: (node, newTitle) => {
|
|
679
|
+
const arr = node.path.split('/');
|
|
680
|
+
arr[arr.length - 1] = newTitle;
|
|
681
|
+
return arr.join('/');
|
|
682
|
+
},
|
|
683
|
+
isSamePath: (path1, path2) => {
|
|
684
|
+
return path1 === path2;
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
// 节点查找工具
|
|
689
|
+
const nodeUtils = {
|
|
690
|
+
findNode: (treeData, key) => {
|
|
691
|
+
return findTreeNode(treeData, key);
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
// 获取文件夹数据 文件夹名称为空时返回根目录数据
|
|
696
|
+
const fetchFolderData = react.useCallback(async function () {
|
|
697
|
+
let initialization = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
|
|
698
|
+
try {
|
|
699
|
+
const data = await getFolderData({
|
|
700
|
+
folder: ""
|
|
701
|
+
});
|
|
702
|
+
if (!(data !== null && data !== void 0 && data.directoryTree)) {
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
705
|
+
setOriginTreeData(data.directoryTree);
|
|
706
|
+
|
|
707
|
+
// 递归生成treenodes
|
|
708
|
+
const treeNodes = buildDirectoryTree(data.directoryTree);
|
|
709
|
+
if (!treeNodes.length) {
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
const firstChild = treeNodes[0];
|
|
713
|
+
const newState = _objectSpread2({
|
|
714
|
+
data: treeNodes
|
|
715
|
+
}, initialization && {
|
|
716
|
+
selectedKeys: [firstChild === null || firstChild === void 0 ? void 0 : firstChild.key],
|
|
717
|
+
expandedKeys: getAllNodeKeys(treeNodes),
|
|
718
|
+
currentPath: firstChild === null || firstChild === void 0 ? void 0 : firstChild.path,
|
|
719
|
+
contents: (firstChild === null || firstChild === void 0 ? void 0 : firstChild.contents) || []
|
|
720
|
+
});
|
|
721
|
+
updateTreeState(newState);
|
|
722
|
+
return treeNodes;
|
|
723
|
+
} catch (error) {
|
|
724
|
+
handleError(error, 'GET FOLDER DATA');
|
|
725
|
+
}
|
|
726
|
+
}, []);
|
|
727
|
+
|
|
728
|
+
// 初始化数据
|
|
729
|
+
react.useEffect(() => {
|
|
730
|
+
fetchFolderData(true);
|
|
731
|
+
}, [fetchFolderData]);
|
|
732
|
+
|
|
733
|
+
// 创建文件夹
|
|
734
|
+
const handleCreate = react.useCallback(async (node, newTitle) => {
|
|
735
|
+
if (!(newTitle !== null && newTitle !== void 0 && newTitle.trim())) return false;
|
|
736
|
+
try {
|
|
737
|
+
var _parentNode$;
|
|
738
|
+
const path = "".concat(node.path, "/").concat(newTitle);
|
|
739
|
+
await createFolder({
|
|
740
|
+
path
|
|
741
|
+
});
|
|
742
|
+
const newTreeData = await fetchFolderData();
|
|
743
|
+
|
|
744
|
+
// 找到新增节点的父节点
|
|
745
|
+
const parentNode = nodeUtils.findNode(newTreeData, node.key);
|
|
746
|
+
if (parentNode !== null && parentNode !== void 0 && (_parentNode$ = parentNode[0]) !== null && _parentNode$ !== void 0 && _parentNode$.children) {
|
|
747
|
+
const expectedPath = "".concat(node.path, "/").concat(newTitle);
|
|
748
|
+
// 通过 path 找到新增节点,得到新增节点的key
|
|
749
|
+
const addedNode = parentNode[0].children.find(ch => pathUtils.isSamePath(ch.path, expectedPath));
|
|
750
|
+
if (addedNode) {
|
|
751
|
+
updateTreeState({
|
|
752
|
+
expandedKeys: [...treeState.expandedKeys, addedNode.key, parentNode[0].key]
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
} catch (error) {
|
|
757
|
+
handleError(error, 'CREATE FOLDER');
|
|
758
|
+
}
|
|
759
|
+
}, [fetchFolderData, treeState.expandedKeys]);
|
|
760
|
+
|
|
761
|
+
// 删除文件/文件夹
|
|
762
|
+
const handleRemove = react.useCallback(async node => {
|
|
763
|
+
if (!node.path) return;
|
|
764
|
+
try {
|
|
765
|
+
await removeFolderFile({
|
|
766
|
+
paths: [{
|
|
767
|
+
path: node.path
|
|
768
|
+
}]
|
|
769
|
+
});
|
|
770
|
+
// 如果删除的是当前选中节点,则重新初始化
|
|
771
|
+
const shouldReinitialize = treeState.selectedKeys[0] === node.key;
|
|
772
|
+
await fetchFolderData(shouldReinitialize);
|
|
773
|
+
} catch (error) {
|
|
774
|
+
handleError(error, 'REMOVE FOLDER/FILE');
|
|
775
|
+
}
|
|
776
|
+
}, [fetchFolderData, treeState.selectedKeys]);
|
|
777
|
+
|
|
778
|
+
// 修改文件/文件夹
|
|
779
|
+
const handleRename = react.useCallback(async node => {
|
|
780
|
+
const newPath = pathUtils.getNewPath(node, node.title);
|
|
781
|
+
if (pathUtils.isSamePath(node.path, newPath)) return false;
|
|
782
|
+
try {
|
|
783
|
+
await renameFolderFile({
|
|
784
|
+
old_path: node.path,
|
|
785
|
+
new_path: newPath
|
|
786
|
+
});
|
|
787
|
+
await fetchFolderData();
|
|
788
|
+
} catch (error) {
|
|
789
|
+
handleError(error, 'RENAME FOLDER/FILE');
|
|
790
|
+
}
|
|
791
|
+
}, [fetchFolderData]);
|
|
792
|
+
|
|
793
|
+
// 选择节点
|
|
794
|
+
const onSelect = react.useCallback(async (keys, info) => {
|
|
795
|
+
if (!keys.length || keys[0] === treeState.selectedKeys[0] && pathUtils.isSamePath(info.node.path, treeState.currentPath)) return;
|
|
796
|
+
updateTreeState({
|
|
797
|
+
selectedKeys: keys,
|
|
798
|
+
currentPath: info.node.path,
|
|
799
|
+
loading: true
|
|
800
|
+
});
|
|
801
|
+
try {
|
|
802
|
+
// 模拟延迟加载
|
|
803
|
+
// eslint-disable-next-line no-promise-executor-return
|
|
804
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
805
|
+
updateTreeState({
|
|
806
|
+
contents: info.node.contents,
|
|
807
|
+
loading: false
|
|
808
|
+
});
|
|
809
|
+
} catch (error) {
|
|
810
|
+
handleError(error, 'SELECT NODE');
|
|
811
|
+
updateTreeState({
|
|
812
|
+
loading: false
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
}, [treeState.selectedKeys, treeState.currentPath]);
|
|
816
|
+
|
|
817
|
+
// 展开节点
|
|
818
|
+
const onExpand = react.useCallback(keys => {
|
|
819
|
+
updateTreeState({
|
|
820
|
+
expandedKeys: keys
|
|
821
|
+
});
|
|
822
|
+
}, []);
|
|
823
|
+
|
|
824
|
+
// 上传右侧文件或删除右侧文件 成功后的回调函数
|
|
825
|
+
const updateFileContents = react.useCallback(async () => {
|
|
826
|
+
// 找到当前选中的节点,更新当前节点的 contents
|
|
827
|
+
const newTreeData = await fetchFolderData();
|
|
828
|
+
const selectedNode = nodeUtils.findNode(newTreeData, treeState.selectedKeys[0]);
|
|
829
|
+
if (selectedNode !== null && selectedNode !== void 0 && selectedNode[0]) {
|
|
830
|
+
updateTreeState({
|
|
831
|
+
contents: selectedNode[0].contents
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
}, [fetchFolderData, treeState.selectedKeys]);
|
|
835
|
+
|
|
836
|
+
// 删除文件
|
|
837
|
+
const removeFile = react.useCallback(async url => {
|
|
838
|
+
try {
|
|
839
|
+
await removeFolderFile({
|
|
840
|
+
path: url
|
|
841
|
+
});
|
|
842
|
+
await updateFileContents();
|
|
843
|
+
} catch (error) {
|
|
844
|
+
handleError(error, 'REMOVE FILE');
|
|
845
|
+
}
|
|
846
|
+
}, [updateFileContents]);
|
|
847
|
+
const MemoizedTree = react.useMemo(() => {
|
|
848
|
+
var _treeState$data;
|
|
849
|
+
if (!((_treeState$data = treeState.data) !== null && _treeState$data !== void 0 && _treeState$data.length)) {
|
|
850
|
+
return /*#__PURE__*/jsxRuntime.jsx(antd.Empty, {
|
|
851
|
+
image: antd.Empty.PRESENTED_IMAGE_SIMPLE
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
return /*#__PURE__*/jsxRuntime.jsx("div", {
|
|
855
|
+
style: {
|
|
856
|
+
paddingTop: 16
|
|
857
|
+
},
|
|
858
|
+
children: /*#__PURE__*/jsxRuntime.jsx(antd.ConfigProvider, {
|
|
859
|
+
theme: theme,
|
|
860
|
+
children: /*#__PURE__*/jsxRuntime.jsx(antd.Tree, {
|
|
861
|
+
blockNode: true,
|
|
862
|
+
showIcon: true,
|
|
863
|
+
selectedKeys: treeState.selectedKeys,
|
|
864
|
+
expandedKeys: treeState.expandedKeys,
|
|
865
|
+
onSelect: onSelect,
|
|
866
|
+
onExpand: onExpand,
|
|
867
|
+
treeData: treeState.data,
|
|
868
|
+
titleRender: nodeData => /*#__PURE__*/jsxRuntime.jsx(TreeTitle$1, {
|
|
869
|
+
title: nodeData.title,
|
|
870
|
+
nodeData: nodeData,
|
|
871
|
+
handleSave: handleRename,
|
|
872
|
+
handleDel: handleRemove,
|
|
873
|
+
handleAdd: handleCreate
|
|
874
|
+
}),
|
|
875
|
+
height: height
|
|
876
|
+
})
|
|
877
|
+
})
|
|
878
|
+
}, "folder-directory");
|
|
879
|
+
}, [treeState.data, treeState.selectedKeys, treeState.expandedKeys]);
|
|
880
|
+
return {
|
|
881
|
+
directoryTree: MemoizedTree,
|
|
882
|
+
contents: treeState.contents,
|
|
883
|
+
currentPath: treeState.currentPath,
|
|
884
|
+
loading: treeState.loading,
|
|
885
|
+
originTreeData,
|
|
886
|
+
updateFileContents,
|
|
887
|
+
removeFile
|
|
888
|
+
};
|
|
889
|
+
};
|
|
890
|
+
|
|
891
|
+
const createUrlBuilder = function () {
|
|
892
|
+
let config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
893
|
+
const {
|
|
894
|
+
baseUrl = '',
|
|
895
|
+
isDev = false,
|
|
896
|
+
devBaseUrl = '',
|
|
897
|
+
isBrowser = false
|
|
898
|
+
} = config;
|
|
899
|
+
return path => {
|
|
900
|
+
if (!path) return '';
|
|
901
|
+
|
|
902
|
+
// 如果传入的是完整 URL,直接返回
|
|
903
|
+
if (/^https?:\/\//.test(path)) {
|
|
904
|
+
return path;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// 去除 path 开头可能的多余斜杠
|
|
908
|
+
const cleanPath = path.replace(/^\/+/, '');
|
|
909
|
+
|
|
910
|
+
// 开发环境使用 devBaseUrl,否则使用 baseUrl
|
|
911
|
+
const host = isDev ? devBaseUrl : baseUrl;
|
|
912
|
+
|
|
913
|
+
// 如果没有配置 baseUrl,则根据环境返回相对路径或完整路径
|
|
914
|
+
if (!host) {
|
|
915
|
+
return isBrowser ? "/".concat(cleanPath) : cleanPath;
|
|
916
|
+
}
|
|
917
|
+
return host ? "".concat(host, "/").concat(cleanPath) : "/".concat(cleanPath);
|
|
918
|
+
};
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
const ResourcesView = _ref => {
|
|
922
|
+
let {
|
|
923
|
+
apiConfig = {},
|
|
924
|
+
features = {},
|
|
925
|
+
className = '',
|
|
926
|
+
style = {},
|
|
927
|
+
renderTreeHeader,
|
|
928
|
+
renderGridHeader,
|
|
929
|
+
acceptFileTypes = "video/*,.mxf,application/mxf,video/mxf",
|
|
930
|
+
uploadTimeout = 60 * 60 * 1000,
|
|
931
|
+
searchPlaceholder = "Search resources..."
|
|
932
|
+
} = _ref;
|
|
933
|
+
const {
|
|
934
|
+
message,
|
|
935
|
+
modal
|
|
936
|
+
} = antd.App.useApp();
|
|
937
|
+
|
|
938
|
+
// 合并默认配置和传入配置
|
|
939
|
+
const mergedApiConfig = react.useMemo(() => _objectSpread2({
|
|
940
|
+
upload: '/api/dvr/media/source/upload',
|
|
941
|
+
download: '/download'
|
|
942
|
+
}, apiConfig), [apiConfig]);
|
|
943
|
+
const mergedFeatures = react.useMemo(() => _objectSpread2({
|
|
944
|
+
upload: true,
|
|
945
|
+
download: true,
|
|
946
|
+
copy: true,
|
|
947
|
+
delete: true,
|
|
948
|
+
search: true,
|
|
949
|
+
batchOperations: true
|
|
950
|
+
}, features), [features]);
|
|
951
|
+
const {
|
|
952
|
+
directoryTree,
|
|
953
|
+
contents,
|
|
954
|
+
currentPath,
|
|
955
|
+
loading,
|
|
956
|
+
originTreeData,
|
|
957
|
+
updateFileContents,
|
|
958
|
+
removeFile
|
|
959
|
+
} = useDirectoryTree({
|
|
960
|
+
getFolderData: mergedApiConfig.getFolderData,
|
|
961
|
+
createFolder: mergedApiConfig.createFolder,
|
|
962
|
+
removeFolderFile: mergedApiConfig.removeFolderFile,
|
|
963
|
+
renameFolderFile: mergedApiConfig.renameFolderFile
|
|
964
|
+
});
|
|
965
|
+
const [showProgress, setShowProgress] = react.useState(false);
|
|
966
|
+
const [percent, setPercent] = react.useState(0);
|
|
967
|
+
const inputRef = react.useRef(null);
|
|
968
|
+
const uploadController = react.useRef(new AbortController());
|
|
969
|
+
const [modalVisible, setModalVisible] = react.useState(false);
|
|
970
|
+
// 处理批量操作
|
|
971
|
+
const [selectedItems, setSelectedItems] = react.useState([]);
|
|
972
|
+
const [indeterminate, setIndeterminate] = react.useState(false);
|
|
973
|
+
const [checkAll, setCheckAll] = react.useState(false);
|
|
974
|
+
const [batchLoading, setBatchLoading] = react.useState(false);
|
|
975
|
+
const [keyword, setKeyword] = react.useState('');
|
|
976
|
+
|
|
977
|
+
// 过滤内容
|
|
978
|
+
const filteredContents = react.useMemo(() => {
|
|
979
|
+
if (!keyword) return contents;
|
|
980
|
+
return contents.filter(item => item.name.toLowerCase().includes(keyword.toLowerCase()));
|
|
981
|
+
}, [contents, keyword]);
|
|
982
|
+
|
|
983
|
+
// 在组件卸载时取消上传
|
|
984
|
+
react.useEffect(() => {
|
|
985
|
+
return () => {
|
|
986
|
+
var _uploadController$cur;
|
|
987
|
+
return (_uploadController$cur = uploadController.current) === null || _uploadController$cur === void 0 ? void 0 : _uploadController$cur.abort();
|
|
988
|
+
};
|
|
989
|
+
}, []);
|
|
990
|
+
|
|
991
|
+
// 重置批量状态的效果
|
|
992
|
+
react.useEffect(() => {
|
|
993
|
+
setSelectedItems([]);
|
|
994
|
+
setIndeterminate(false);
|
|
995
|
+
setCheckAll(false);
|
|
996
|
+
}, [currentPath]);
|
|
997
|
+
|
|
998
|
+
// 上传相关函数
|
|
999
|
+
const onTriggerUpload = react.useCallback(() => {
|
|
1000
|
+
var _inputRef$current;
|
|
1001
|
+
(_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 || _inputRef$current.click();
|
|
1002
|
+
}, []);
|
|
1003
|
+
const onUploadFile = async event => {
|
|
1004
|
+
const files = event.target.files;
|
|
1005
|
+
if (!(files !== null && files !== void 0 && files.length)) return;
|
|
1006
|
+
|
|
1007
|
+
// 初始化上传状态
|
|
1008
|
+
setShowProgress(true);
|
|
1009
|
+
setPercent(0);
|
|
1010
|
+
try {
|
|
1011
|
+
await uploadFiles(files, currentPath);
|
|
1012
|
+
// 延迟2s刷新数据
|
|
1013
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
1014
|
+
await updateFileContents();
|
|
1015
|
+
} catch (error) {
|
|
1016
|
+
if (axios.isCancel(error)) {
|
|
1017
|
+
console.log('Upload canceled');
|
|
1018
|
+
} else if (error.response) {
|
|
1019
|
+
var _error$response$data;
|
|
1020
|
+
console.error("Upload failed: ".concat(((_error$response$data = error.response.data) === null || _error$response$data === void 0 ? void 0 : _error$response$data.message) || error.response.status));
|
|
1021
|
+
} else {
|
|
1022
|
+
console.error("Upload error: ".concat(error.message));
|
|
1023
|
+
}
|
|
1024
|
+
} finally {
|
|
1025
|
+
resetUploadState();
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
const uploadFiles = async (files, folder) => {
|
|
1029
|
+
// 计算所有文件的总大小
|
|
1030
|
+
const totalSize = calculateTotalSize(files);
|
|
1031
|
+
// 存储每个文件的上一次 loaded 值(避免重复计算)
|
|
1032
|
+
const previousLoadedMap = new Map();
|
|
1033
|
+
let uploadedSize = 0; // 记录已上传的字节数
|
|
1034
|
+
|
|
1035
|
+
const uploadTasks = Array.from(files).map(file => uploadSingleFile(file, folder, previousLoadedMap, newLoaded => {
|
|
1036
|
+
uploadedSize += newLoaded;
|
|
1037
|
+
updateProgress(uploadedSize, totalSize);
|
|
1038
|
+
}));
|
|
1039
|
+
await Promise.all(uploadTasks);
|
|
1040
|
+
};
|
|
1041
|
+
|
|
1042
|
+
// 计算总文件大小
|
|
1043
|
+
const calculateTotalSize = files => Array.from(files).reduce((sum, file) => sum + file.size, 0);
|
|
1044
|
+
|
|
1045
|
+
// 上传单个文件
|
|
1046
|
+
const uploadSingleFile = (file, folder, previousLoadedMap, onProgress) => {
|
|
1047
|
+
const formData = new FormData();
|
|
1048
|
+
formData.append('file', file);
|
|
1049
|
+
formData.append('folder', folder);
|
|
1050
|
+
return axios.post(mergedApiConfig.upload, formData, {
|
|
1051
|
+
timeout: uploadTimeout,
|
|
1052
|
+
headers: {
|
|
1053
|
+
'Content-Type': 'multipart/form-data'
|
|
1054
|
+
},
|
|
1055
|
+
// signal: uploadController.current.signal,
|
|
1056
|
+
onUploadProgress: progressEvent => {
|
|
1057
|
+
if (progressEvent.lengthComputable) {
|
|
1058
|
+
// 获取当前文件的上一次 loaded 值(默认 0)
|
|
1059
|
+
const previousLoaded = previousLoadedMap.get(file.name) || 0;
|
|
1060
|
+
// 计算新增的字节数(避免重复累加)
|
|
1061
|
+
const newLoaded = progressEvent.loaded - previousLoaded;
|
|
1062
|
+
// 更新当前文件的 loaded 值
|
|
1063
|
+
previousLoadedMap.set(file.name, progressEvent.loaded);
|
|
1064
|
+
onProgress(newLoaded);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
};
|
|
1069
|
+
const updateProgress = (uploadedSize, totalSize) => {
|
|
1070
|
+
const totalPercent = Math.round(uploadedSize / totalSize * 100);
|
|
1071
|
+
setPercent(totalPercent);
|
|
1072
|
+
};
|
|
1073
|
+
|
|
1074
|
+
// 重置上传状态
|
|
1075
|
+
const resetUploadState = () => {
|
|
1076
|
+
setShowProgress(false);
|
|
1077
|
+
setPercent(0);
|
|
1078
|
+
if (inputRef.current) inputRef.current.value = "";
|
|
1079
|
+
};
|
|
1080
|
+
|
|
1081
|
+
// 文件操作函数
|
|
1082
|
+
const downloadFile = async url => {
|
|
1083
|
+
try {
|
|
1084
|
+
const isDev = process.env.NODE_ENV === 'development';
|
|
1085
|
+
const baseUrl = isDev ? process.env.API_HOST // 开发环境默认值
|
|
1086
|
+
: window.location.origin; // 生产环境用当前域名
|
|
1087
|
+
|
|
1088
|
+
const link = document.createElement('a');
|
|
1089
|
+
link.href = "".concat(baseUrl).concat(mergedApiConfig.download, "/").concat(url);
|
|
1090
|
+
// link.download = filename;
|
|
1091
|
+
link.style.display = 'none';
|
|
1092
|
+
document.body.appendChild(link);
|
|
1093
|
+
link.click();
|
|
1094
|
+
document.body.removeChild(link);
|
|
1095
|
+
} catch (error) {
|
|
1096
|
+
console.error("Download failed:", error);
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
const copyFile = () => {
|
|
1100
|
+
setModalVisible(true);
|
|
1101
|
+
};
|
|
1102
|
+
const onContextMenu = (key, item) => {
|
|
1103
|
+
if (key === 'del') removeFile(item.url);
|
|
1104
|
+
if (key === 'download') downloadFile(item.url);
|
|
1105
|
+
if (key === 'copy') copyFile(item.url);
|
|
1106
|
+
};
|
|
1107
|
+
const handleBatchRemove = react.useCallback(async () => {
|
|
1108
|
+
if (!selectedItems.length) return;
|
|
1109
|
+
modal.confirm({
|
|
1110
|
+
icon: /*#__PURE__*/jsxRuntime.jsx(icons.ExclamationCircleFilled, {}),
|
|
1111
|
+
title: "Are you sure you want to delete ".concat(selectedItems.length, " files?"),
|
|
1112
|
+
cancelText: "No",
|
|
1113
|
+
okText: "Yes",
|
|
1114
|
+
onOk: async () => {
|
|
1115
|
+
try {
|
|
1116
|
+
// 批量处理所有删除请求
|
|
1117
|
+
await mergedApiConfig.removeFolderFile({
|
|
1118
|
+
paths: selectedItems.map(itemUrl => ({
|
|
1119
|
+
path: itemUrl
|
|
1120
|
+
}))
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
// 更新状态
|
|
1124
|
+
setSelectedItems([]);
|
|
1125
|
+
setIndeterminate(false);
|
|
1126
|
+
setCheckAll(false);
|
|
1127
|
+
await updateFileContents(); // 刷新文件列表
|
|
1128
|
+
} catch (error) {
|
|
1129
|
+
console.error('Batch deletion error:', error);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
});
|
|
1133
|
+
}, [selectedItems, mergedApiConfig, updateFileContents]);
|
|
1134
|
+
const handleCopyConfirm = react.useCallback(async newPath => {
|
|
1135
|
+
setBatchLoading(true);
|
|
1136
|
+
try {
|
|
1137
|
+
// 批量处理所有拷贝请求
|
|
1138
|
+
await mergedApiConfig.copyFile({
|
|
1139
|
+
old_paths: selectedItems.map(itemUrl => ({
|
|
1140
|
+
old_path: itemUrl
|
|
1141
|
+
})),
|
|
1142
|
+
new_path: newPath
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
// 更新状态
|
|
1146
|
+
setSelectedItems([]);
|
|
1147
|
+
setIndeterminate(false);
|
|
1148
|
+
setCheckAll(false);
|
|
1149
|
+
message.success('Success');
|
|
1150
|
+
setModalVisible(false);
|
|
1151
|
+
|
|
1152
|
+
// 延迟2s刷新数据
|
|
1153
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
1154
|
+
await updateFileContents();
|
|
1155
|
+
} catch (error) {
|
|
1156
|
+
console.error("Batch copy error:", error);
|
|
1157
|
+
} finally {
|
|
1158
|
+
setBatchLoading(false);
|
|
1159
|
+
}
|
|
1160
|
+
}, [selectedItems, mergedApiConfig, updateFileContents]);
|
|
1161
|
+
|
|
1162
|
+
// 处理单个项目的选择变化
|
|
1163
|
+
const handleSelectChange = (item, checked) => {
|
|
1164
|
+
const newSelectedItems = checked ? [...selectedItems, item.url] : selectedItems.filter(url => url !== item.url);
|
|
1165
|
+
setSelectedItems(newSelectedItems);
|
|
1166
|
+
setIndeterminate(!!newSelectedItems.length && newSelectedItems.length !== filteredContents.length);
|
|
1167
|
+
setCheckAll(newSelectedItems.length === filteredContents.length);
|
|
1168
|
+
};
|
|
1169
|
+
|
|
1170
|
+
// 处理全选/取消全选
|
|
1171
|
+
const handleSelectAll = e => {
|
|
1172
|
+
const checked = e.target.checked;
|
|
1173
|
+
setSelectedItems(checked ? filteredContents.map(item => item.url) : []);
|
|
1174
|
+
setIndeterminate(false);
|
|
1175
|
+
setCheckAll(checked);
|
|
1176
|
+
};
|
|
1177
|
+
|
|
1178
|
+
// 搜索处理
|
|
1179
|
+
const handleSearchChange = e => {
|
|
1180
|
+
setKeyword(e.target.value);
|
|
1181
|
+
};
|
|
1182
|
+
|
|
1183
|
+
// 创建 URL 构建器
|
|
1184
|
+
const getFullUrl = react.useMemo(() => {
|
|
1185
|
+
return createUrlBuilder({
|
|
1186
|
+
baseUrl: apiConfig.baseUrl,
|
|
1187
|
+
devBaseUrl: apiConfig.devBaseUrl,
|
|
1188
|
+
isDev: process.env.NODE_ENV === 'development',
|
|
1189
|
+
isBrowser: typeof window !== 'undefined'
|
|
1190
|
+
});
|
|
1191
|
+
}, [apiConfig.baseUrl, apiConfig.devBaseUrl]);
|
|
1192
|
+
return /*#__PURE__*/jsxRuntime.jsxs("div", {
|
|
1193
|
+
className: "resources-view ".concat(className),
|
|
1194
|
+
style: style,
|
|
1195
|
+
children: [/*#__PURE__*/jsxRuntime.jsxs("div", {
|
|
1196
|
+
className: "content-container",
|
|
1197
|
+
children: [/*#__PURE__*/jsxRuntime.jsxs("div", {
|
|
1198
|
+
className: "directory-tree",
|
|
1199
|
+
children: [renderTreeHeader ? renderTreeHeader() : /*#__PURE__*/jsxRuntime.jsx("div", {
|
|
1200
|
+
className: "search-bar"
|
|
1201
|
+
}), directoryTree]
|
|
1202
|
+
}), /*#__PURE__*/jsxRuntime.jsxs("div", {
|
|
1203
|
+
className: "folder-contents",
|
|
1204
|
+
children: [/*#__PURE__*/jsxRuntime.jsxs(antd.Flex, {
|
|
1205
|
+
justify: "center",
|
|
1206
|
+
align: "center",
|
|
1207
|
+
className: "search-bar",
|
|
1208
|
+
children: [mergedFeatures.search && /*#__PURE__*/jsxRuntime.jsx("input", {
|
|
1209
|
+
type: "text",
|
|
1210
|
+
placeholder: searchPlaceholder,
|
|
1211
|
+
value: keyword,
|
|
1212
|
+
onChange: handleSearchChange,
|
|
1213
|
+
className: "search-input"
|
|
1214
|
+
}), mergedFeatures.upload && /*#__PURE__*/jsxRuntime.jsxs(jsxRuntime.Fragment, {
|
|
1215
|
+
children: [/*#__PURE__*/jsxRuntime.jsx(icons.CloudUploadOutlined, {
|
|
1216
|
+
className: "upload-icon",
|
|
1217
|
+
title: "upload",
|
|
1218
|
+
onClick: onTriggerUpload
|
|
1219
|
+
}), /*#__PURE__*/jsxRuntime.jsx("input", {
|
|
1220
|
+
ref: inputRef,
|
|
1221
|
+
type: "file",
|
|
1222
|
+
onChange: onUploadFile,
|
|
1223
|
+
className: "hidden",
|
|
1224
|
+
accept: acceptFileTypes,
|
|
1225
|
+
multiple: true
|
|
1226
|
+
})]
|
|
1227
|
+
})]
|
|
1228
|
+
}), /*#__PURE__*/jsxRuntime.jsxs("div", {
|
|
1229
|
+
className: "media-grid-container",
|
|
1230
|
+
onContextMenu: e => e.preventDefault(),
|
|
1231
|
+
children: [renderGridHeader ? renderGridHeader() : filteredContents.length > 0 && mergedFeatures.batchOperations && /*#__PURE__*/jsxRuntime.jsx("div", {
|
|
1232
|
+
className: "batch-operations",
|
|
1233
|
+
children: /*#__PURE__*/jsxRuntime.jsxs(antd.Space, {
|
|
1234
|
+
size: "middle",
|
|
1235
|
+
children: [/*#__PURE__*/jsxRuntime.jsx(antd.Checkbox, {
|
|
1236
|
+
indeterminate: indeterminate,
|
|
1237
|
+
checked: checkAll,
|
|
1238
|
+
onChange: handleSelectAll,
|
|
1239
|
+
children: "Select All"
|
|
1240
|
+
}), /*#__PURE__*/jsxRuntime.jsx(antd.Button, {
|
|
1241
|
+
type: "primary",
|
|
1242
|
+
disabled: !selectedItems.length,
|
|
1243
|
+
onClick: () => setModalVisible(true),
|
|
1244
|
+
children: "Batch Copy"
|
|
1245
|
+
}), /*#__PURE__*/jsxRuntime.jsx(antd.Button, {
|
|
1246
|
+
className: "btn-gray",
|
|
1247
|
+
disabled: !selectedItems.length,
|
|
1248
|
+
onClick: handleBatchRemove,
|
|
1249
|
+
children: "Batch Delete"
|
|
1250
|
+
})]
|
|
1251
|
+
})
|
|
1252
|
+
}), /*#__PURE__*/jsxRuntime.jsx(MediaGrid$1, {
|
|
1253
|
+
items: filteredContents,
|
|
1254
|
+
loading: loading,
|
|
1255
|
+
onContextMenu: mergedFeatures.delete || mergedFeatures.download || mergedFeatures.copy ? onContextMenu : null,
|
|
1256
|
+
selectedKeys: selectedItems,
|
|
1257
|
+
onSelectChange: mergedFeatures.batchOperations ? handleSelectChange : null,
|
|
1258
|
+
onClick: () => {},
|
|
1259
|
+
features: mergedFeatures,
|
|
1260
|
+
getFullUrl: getFullUrl
|
|
1261
|
+
})]
|
|
1262
|
+
})]
|
|
1263
|
+
})]
|
|
1264
|
+
}), showProgress && /*#__PURE__*/jsxRuntime.jsx(UploadProgress$1, {
|
|
1265
|
+
percent: percent
|
|
1266
|
+
}), /*#__PURE__*/jsxRuntime.jsx(SelectFolderPathModal$1, {
|
|
1267
|
+
open: modalVisible,
|
|
1268
|
+
directoryTree: originTreeData,
|
|
1269
|
+
batchLoading: batchLoading,
|
|
1270
|
+
onClose: () => setModalVisible(false),
|
|
1271
|
+
onOk: handleCopyConfirm
|
|
1272
|
+
})]
|
|
1273
|
+
});
|
|
1274
|
+
};
|
|
1275
|
+
var ResourcesView$1 = ResourcesView;
|
|
1276
|
+
|
|
1277
|
+
exports.ResourcesView = ResourcesView$1;
|
|
1278
|
+
//# sourceMappingURL=index.js.map
|