mirror of
https://github.com/eyebluecn/tank-front
synced 2026-10-21 04:24:25 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c4a1431c4 | ||
|
|
81712dd62d | ||
|
|
8371133228 | ||
|
|
d7fe343455 | ||
|
|
0533dd2c56 | ||
|
|
447eb14200 | ||
|
|
b0b5a5ca35 | ||
|
|
19e6503da0 | ||
|
|
bc7737cd0e | ||
|
|
db31b4f204 | ||
|
|
fbfc9b7c83 | ||
|
|
dd7fbe762e | ||
|
|
2a78050f6b | ||
|
|
16e79ac4cf | ||
|
|
db2efc5ab7 | ||
|
|
dd29b890a8 | ||
|
|
1809bc4e09 | ||
|
|
59a4bfcbb3 | ||
|
|
e92b1f374b | ||
|
|
7c1c379bcb | ||
|
|
714c506b49 | ||
|
|
83d3db1000 | ||
|
|
648eb9429a | ||
|
|
35b0c8194b | ||
|
|
2787c95916 |
@@ -1,9 +0,0 @@
|
|||||||
# 3.2.0
|
|
||||||
1. PC端不展示左侧关于菜单
|
|
||||||
|
|
||||||
# 3.1.6
|
|
||||||
1. 增加云盘剩余容量显示
|
|
||||||
2. 优化拖拽文件夹进行上传的错误提示
|
|
||||||
3. 优化超出限制大小的错误提示
|
|
||||||
4. 文件列表页右键展示操作目录
|
|
||||||
5. 修复一些已知问题
|
|
||||||
@@ -2,12 +2,14 @@
|
|||||||
|
|
||||||
[English Version](https://tank-doc.eyeblue.cn/en)
|
[English Version](https://tank-doc.eyeblue.cn/en)
|
||||||
|
|
||||||
# 蓝眼云盘(3.1.6)
|
# 蓝眼云盘(4.2.0)
|
||||||
[在线Demo](https://tank.eyeblue.cn) (体验账号: demo 密码:123456)
|
[在线Demo](https://tank.eyeblue.cn) (体验账号:demo 密码:123456)
|
||||||
|
|
||||||
|
[更新文档 CHANGELOG](https://github.com/eyebluecn/tank/blob/master/CHANGELOG)
|
||||||
|
|
||||||
蓝眼云盘是蓝眼开源系列代表作品之一,致力于打造精致,优雅,简约的云盘。核心功能如下:
|
蓝眼云盘是蓝眼开源系列代表作品之一,致力于打造精致,优雅,简约的云盘。核心功能如下:
|
||||||
1. 文件列表
|
1. 文件列表
|
||||||
2. 文件/文件夹上传 + 拖拽上传
|
2. 文件/文件夹上传 + 拖拽上传 + 分片/断点续传
|
||||||
3. 文件分享
|
3. 文件分享
|
||||||
4. 文件监控统计
|
4. 文件监控统计
|
||||||
5. 回收站
|
5. 回收站
|
||||||
|
|||||||
+7
-6
@@ -9,25 +9,26 @@ module.exports = {
|
|||||||
target: 'http://localhost:6010',
|
target: 'http://localhost:6010',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
pathRewrite: {
|
pathRewrite: {
|
||||||
'^/api': '/api'
|
'^/api': '/api',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
{
|
{
|
||||||
|
|
||||||
// less loader.
|
// less loader.
|
||||||
plugin: CracoLessPlugin,
|
plugin: CracoLessPlugin,
|
||||||
options: {
|
options: {
|
||||||
// resolve-url-loader只对sass生效,craco-less默认使用sass配置,所以这里手动过滤掉resolve-url-loader
|
// resolve-url-loader只对sass生效,craco-less默认使用sass配置,所以这里手动过滤掉resolve-url-loader
|
||||||
modifyLessRule: (lessRule) => {
|
modifyLessRule: (lessRule) => {
|
||||||
lessRule.use = lessRule.use.filter(i => !i.loader.includes('resolve-url-loader'));
|
lessRule.use = lessRule.use.filter(
|
||||||
|
(i) => !i.loader.includes('resolve-url-loader')
|
||||||
|
);
|
||||||
return lessRule;
|
return lessRule;
|
||||||
},
|
},
|
||||||
lessLoaderOptions: {
|
lessLoaderOptions: {
|
||||||
lessOptions: {
|
lessOptions: {
|
||||||
modifyVars: {'@primary-color': '#215891'},
|
modifyVars: { '@primary-color': '#215891' },
|
||||||
javascriptEnabled: true,
|
javascriptEnabled: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 74 KiB |
+8
-6
@@ -1,13 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "tank-front",
|
"name": "tank-front",
|
||||||
"version": "3.1.6",
|
"version": "4.1.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "4.7.0",
|
"@ant-design/icons": "4.7.0",
|
||||||
"@craco/craco": "6.4.3",
|
"@craco/craco": "6.4.3",
|
||||||
"@types/echarts": "4.6.1",
|
"@types/echarts": "4.6.1",
|
||||||
"@types/node": "16.7.13",
|
"@types/node": "16.7.13",
|
||||||
"@types/photoswipe": "^4.1.2",
|
|
||||||
"@types/qs": "^6.9.7",
|
"@types/qs": "^6.9.7",
|
||||||
"@types/react": "17.0.2",
|
"@types/react": "17.0.2",
|
||||||
"@types/react-dom": "17.0.2",
|
"@types/react-dom": "17.0.2",
|
||||||
@@ -18,13 +17,14 @@
|
|||||||
"craco-less": "2.0.0",
|
"craco-less": "2.0.0",
|
||||||
"echarts": "4.8.0",
|
"echarts": "4.8.0",
|
||||||
"echarts-for-react": "2.0.16",
|
"echarts-for-react": "2.0.16",
|
||||||
"photoswipe": "^4.1.3",
|
|
||||||
"qs": "^6.10.3",
|
"qs": "^6.10.3",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
|
"react-photo-view": "^1.2.3",
|
||||||
"react-router": "^5.2.0",
|
"react-router": "^5.2.0",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scripts": "5.0.0",
|
"react-scripts": "5.0.0",
|
||||||
|
"spark-md5": "^3.0.2",
|
||||||
"typescript": "4.4.2",
|
"typescript": "4.4.2",
|
||||||
"velocity-react": "^1.4.3"
|
"velocity-react": "^1.4.3"
|
||||||
},
|
},
|
||||||
@@ -45,9 +45,10 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "^2.8.8",
|
"@types/spark-md5": "^3.0.5",
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"lint-staged": "^13.2.2"
|
"lint-staged": "^13.2.2",
|
||||||
|
"prettier": "^2.8.8"
|
||||||
},
|
},
|
||||||
"husky": {
|
"husky": {
|
||||||
"hooks": {
|
"hooks": {
|
||||||
@@ -59,5 +60,6 @@
|
|||||||
"prettier --write",
|
"prettier --write",
|
||||||
"git add"
|
"git add"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"packageManager": "yarn@1.22.19+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -175,20 +175,6 @@ export default class Pager<T> extends HttpBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//根据key来删除某个Filter
|
|
||||||
removeFilter(key: string) {
|
|
||||||
if (!this.filters || !this.filters.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (let i = 0; i < this.filters.length; i++) {
|
|
||||||
let filter = this.filters[i];
|
|
||||||
if (filter.key === key) {
|
|
||||||
this.filters.splice(i, 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//隐藏某个Filter,实际上我们可以根据这个filter来筛选,只不过不出现在NbFilter中而已。
|
//隐藏某个Filter,实际上我们可以根据这个filter来筛选,只不过不出现在NbFilter中而已。
|
||||||
showFilter(key: string, visible = true) {
|
showFilter(key: string, visible = true) {
|
||||||
if (!this.filters || !this.filters.length) {
|
if (!this.filters || !this.filters.length) {
|
||||||
|
|||||||
@@ -0,0 +1,733 @@
|
|||||||
|
import axios, { CancelTokenSource } from 'axios';
|
||||||
|
import HttpUtil from '../../util/HttpUtil';
|
||||||
|
import { calculateChunkMd5, isMd5Available } from '../../util/Md5Util';
|
||||||
|
|
||||||
|
// Default chunk size: 50MB
|
||||||
|
const DEFAULT_CHUNK_SIZE = 50 * 1024 * 1024;
|
||||||
|
// Minimum chunk size: 5MB (aligned with AWS S3 standard)
|
||||||
|
const MIN_CHUNK_SIZE = 5 * 1024 * 1024;
|
||||||
|
// Maximum chunk size: 100MB
|
||||||
|
const MAX_CHUNK_SIZE = 100 * 1024 * 1024;
|
||||||
|
// Minimum file size to use chunked upload: 100MB
|
||||||
|
const MIN_FILE_SIZE_FOR_CHUNK = 100 * 1024 * 1024;
|
||||||
|
// Maximum concurrent chunk uploads
|
||||||
|
const MAX_CONCURRENT_UPLOADS = 6;
|
||||||
|
// Maximum retry attempts for failed chunk uploads
|
||||||
|
const MAX_RETRY_ATTEMPTS = 3;
|
||||||
|
// Base delay for retry backoff (ms)
|
||||||
|
const RETRY_BASE_DELAY = 1000;
|
||||||
|
// Session expiration time for localStorage cleanup (24 hours in ms, matching backend)
|
||||||
|
const SESSION_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
||||||
|
// Upload timeout for large chunks (10 minutes in ms)
|
||||||
|
const UPLOAD_TIMEOUT_MS = 10 * 60 * 1000;
|
||||||
|
|
||||||
|
// Upload session status
|
||||||
|
export enum UploadSessionStatus {
|
||||||
|
UPLOADING = 'UPLOADING',
|
||||||
|
MERGING = 'MERGING',
|
||||||
|
COMPLETED = 'COMPLETED',
|
||||||
|
ERROR = 'ERROR',
|
||||||
|
CANCELLED = 'CANCELLED',
|
||||||
|
PAUSED = 'PAUSED',
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload session info from server
|
||||||
|
export interface UploadSession {
|
||||||
|
uuid: string;
|
||||||
|
userUuid: string;
|
||||||
|
spaceUuid: string;
|
||||||
|
matterUuid: string;
|
||||||
|
puuid: string;
|
||||||
|
filename: string;
|
||||||
|
totalSize: number;
|
||||||
|
chunkSize: number;
|
||||||
|
totalChunks: number;
|
||||||
|
privacy: boolean;
|
||||||
|
fileMd5: string;
|
||||||
|
status: string;
|
||||||
|
expireTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session info with uploaded chunks
|
||||||
|
export interface UploadSessionInfo {
|
||||||
|
session: UploadSession;
|
||||||
|
uploadedChunks: number[];
|
||||||
|
missingChunks: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chunk upload progress callback
|
||||||
|
export interface ChunkUploadProgress {
|
||||||
|
sessionUuid: string;
|
||||||
|
filename: string;
|
||||||
|
totalSize: number;
|
||||||
|
uploadedSize: number;
|
||||||
|
progress: number; // 0-1
|
||||||
|
speed: number; // bytes per second
|
||||||
|
uploadedChunks: number;
|
||||||
|
totalChunks: number;
|
||||||
|
status: UploadSessionStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local storage key for saving upload sessions
|
||||||
|
const UPLOAD_SESSION_STORAGE_KEY = 'tank_chunk_upload_sessions';
|
||||||
|
|
||||||
|
export default class ChunkUploader {
|
||||||
|
// API endpoints
|
||||||
|
static URL_CREATE_SESSION = '/api/chunk/create/session';
|
||||||
|
static URL_UPLOAD_CHUNK = '/api/chunk/upload';
|
||||||
|
static URL_MERGE_CHUNKS = '/api/chunk/merge';
|
||||||
|
static URL_SESSION_INFO = '/api/chunk/session/info';
|
||||||
|
static URL_CANCEL_SESSION = '/api/chunk/cancel';
|
||||||
|
|
||||||
|
// File to upload
|
||||||
|
file: File;
|
||||||
|
// Parent directory UUID
|
||||||
|
puuid: string;
|
||||||
|
// Space UUID
|
||||||
|
spaceUuid: string;
|
||||||
|
// Privacy flag
|
||||||
|
privacy: boolean;
|
||||||
|
// Chunk size in bytes
|
||||||
|
chunkSize: number;
|
||||||
|
// Upload session info
|
||||||
|
session: UploadSession | null = null;
|
||||||
|
// Current status
|
||||||
|
status: UploadSessionStatus = UploadSessionStatus.UPLOADING;
|
||||||
|
// Uploaded chunk indices
|
||||||
|
uploadedChunks: Set<number> = new Set();
|
||||||
|
// Uploading chunk indices (currently in progress)
|
||||||
|
uploadingChunks: Set<number> = new Set();
|
||||||
|
// Total chunks
|
||||||
|
totalChunks: number = 0;
|
||||||
|
// Cancel token sources for each chunk
|
||||||
|
cancelSources: Map<number, CancelTokenSource> = new Map();
|
||||||
|
// Progress callback
|
||||||
|
onProgress?: (progress: ChunkUploadProgress) => void;
|
||||||
|
// Complete callback
|
||||||
|
onComplete?: (matter: any) => void;
|
||||||
|
// Error callback
|
||||||
|
onError?: (error: string) => void;
|
||||||
|
// React component for UI updates
|
||||||
|
reactComponent?: React.Component;
|
||||||
|
|
||||||
|
// Speed calculation
|
||||||
|
private lastUpdateTime: number = 0;
|
||||||
|
private lastUploadedSize: number = 0;
|
||||||
|
private currentSpeed: number = 0;
|
||||||
|
|
||||||
|
// Track progress of each chunk (chunkIndex -> bytes uploaded)
|
||||||
|
private chunkProgress: Map<number, number> = new Map();
|
||||||
|
|
||||||
|
// Quick file fingerprint for session matching (not full MD5)
|
||||||
|
private fileFingerprint: string = '';
|
||||||
|
|
||||||
|
// Whether chunk-level MD5 verification is available (spark-md5 installed)
|
||||||
|
private chunkMd5Available: boolean = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
file: File,
|
||||||
|
puuid: string,
|
||||||
|
spaceUuid: string,
|
||||||
|
privacy: boolean = true,
|
||||||
|
chunkSize?: number,
|
||||||
|
reactComponent?: React.Component
|
||||||
|
) {
|
||||||
|
this.file = file;
|
||||||
|
this.puuid = puuid;
|
||||||
|
this.spaceUuid = spaceUuid;
|
||||||
|
this.privacy = privacy;
|
||||||
|
this.reactComponent = reactComponent;
|
||||||
|
|
||||||
|
// Validate and set chunk size
|
||||||
|
if (chunkSize) {
|
||||||
|
this.chunkSize = Math.max(MIN_CHUNK_SIZE, Math.min(MAX_CHUNK_SIZE, chunkSize));
|
||||||
|
} else {
|
||||||
|
this.chunkSize = DEFAULT_CHUNK_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate total chunks
|
||||||
|
this.totalChunks = Math.ceil(file.size / this.chunkSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if file should use chunked upload
|
||||||
|
static shouldUseChunkedUpload(fileSize: number): boolean {
|
||||||
|
return fileSize >= MIN_FILE_SIZE_FOR_CHUNK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the upload process
|
||||||
|
async start(): Promise<void> {
|
||||||
|
try {
|
||||||
|
this.status = UploadSessionStatus.UPLOADING;
|
||||||
|
this.lastUpdateTime = Date.now();
|
||||||
|
this.lastUploadedSize = 0;
|
||||||
|
|
||||||
|
// Calculate file fingerprint for better session matching
|
||||||
|
this.fileFingerprint = await this.calculateFileFingerprint();
|
||||||
|
|
||||||
|
// Try to resume from existing session
|
||||||
|
const existingSession = await this.loadSessionFromStorage();
|
||||||
|
if (existingSession) {
|
||||||
|
// Verify session with server
|
||||||
|
const sessionInfo = await this.getSessionInfo(existingSession.uuid);
|
||||||
|
if (sessionInfo && sessionInfo.session.status === 'UPLOADING') {
|
||||||
|
this.session = sessionInfo.session;
|
||||||
|
this.uploadedChunks = new Set(sessionInfo.uploadedChunks);
|
||||||
|
this.totalChunks = this.session.totalChunks;
|
||||||
|
this.updateProgress();
|
||||||
|
await this.uploadMissingChunks(sessionInfo.missingChunks);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// Session expired or completed, remove from storage
|
||||||
|
this.removeSessionFromStorage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if chunk-level MD5 verification is available
|
||||||
|
this.chunkMd5Available = await isMd5Available();
|
||||||
|
if (this.chunkMd5Available) {
|
||||||
|
console.info('Chunk-level MD5 verification enabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new session
|
||||||
|
await this.createSession();
|
||||||
|
|
||||||
|
// Save session to storage for resume capability
|
||||||
|
this.saveSessionToStorage();
|
||||||
|
|
||||||
|
// Upload all chunks
|
||||||
|
const missingChunks = Array.from({ length: this.totalChunks }, (_, i) => i);
|
||||||
|
await this.uploadMissingChunks(missingChunks);
|
||||||
|
} catch (error: any) {
|
||||||
|
// Don't change status if already PAUSED (pause() was called)
|
||||||
|
if (this.status === UploadSessionStatus.PAUSED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (axios.isCancel(error)) {
|
||||||
|
this.status = UploadSessionStatus.CANCELLED;
|
||||||
|
} else {
|
||||||
|
this.status = UploadSessionStatus.ERROR;
|
||||||
|
this.onError?.(error.message || 'Upload failed');
|
||||||
|
}
|
||||||
|
this.updateProgress();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create upload session
|
||||||
|
private async createSession(): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('puuid', this.puuid);
|
||||||
|
formData.append('filename', this.file.name);
|
||||||
|
formData.append('totalSize', this.file.size.toString());
|
||||||
|
formData.append('chunkSize', this.chunkSize.toString());
|
||||||
|
formData.append('privacy', this.privacy.toString());
|
||||||
|
formData.append('spaceUuid', this.spaceUuid);
|
||||||
|
|
||||||
|
HttpUtil.httpPost(
|
||||||
|
ChunkUploader.URL_CREATE_SESSION,
|
||||||
|
formData,
|
||||||
|
(response: any) => {
|
||||||
|
this.session = response.data.data;
|
||||||
|
this.totalChunks = this.session!.totalChunks;
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
(error: any) => {
|
||||||
|
const msg = error.response?.data?.msg || 'Failed to create upload session';
|
||||||
|
reject(new Error(msg));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get session info from server
|
||||||
|
private async getSessionInfo(sessionUuid: string): Promise<UploadSessionInfo | null> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
HttpUtil.httpGet(
|
||||||
|
ChunkUploader.URL_SESSION_INFO,
|
||||||
|
{ sessionUuid },
|
||||||
|
(response: any) => {
|
||||||
|
resolve(response.data.data);
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload missing chunks with concurrency control
|
||||||
|
private async uploadMissingChunks(missingChunks: number[]): Promise<void> {
|
||||||
|
const queue = [...missingChunks];
|
||||||
|
const uploading: Promise<void>[] = [];
|
||||||
|
|
||||||
|
while (queue.length > 0 || uploading.length > 0) {
|
||||||
|
// Check if cancelled or paused
|
||||||
|
if (this.status === UploadSessionStatus.CANCELLED || this.status === UploadSessionStatus.PAUSED) {
|
||||||
|
// Wait for ongoing uploads to finish
|
||||||
|
await Promise.all(uploading);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start new uploads up to max concurrent
|
||||||
|
while (queue.length > 0 && this.uploadingChunks.size < MAX_CONCURRENT_UPLOADS) {
|
||||||
|
const chunkIndex = queue.shift()!;
|
||||||
|
const promise = this.uploadChunk(chunkIndex).then(() => {
|
||||||
|
const index = uploading.indexOf(promise);
|
||||||
|
if (index > -1) {
|
||||||
|
uploading.splice(index, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
uploading.push(promise);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for at least one upload to complete
|
||||||
|
if (uploading.length > 0) {
|
||||||
|
await Promise.race(uploading);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All chunks uploaded, merge them
|
||||||
|
if (this.status === UploadSessionStatus.UPLOADING) {
|
||||||
|
await this.mergeChunks();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload a single chunk with retry support and chunk-level MD5 verification
|
||||||
|
private async uploadChunk(chunkIndex: number, retryCount: number = 0): Promise<void> {
|
||||||
|
if (this.uploadedChunks.has(chunkIndex)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.uploadingChunks.add(chunkIndex);
|
||||||
|
this.chunkProgress.set(chunkIndex, 0);
|
||||||
|
|
||||||
|
const start = chunkIndex * this.chunkSize;
|
||||||
|
const end = Math.min(start + this.chunkSize, this.file.size);
|
||||||
|
const chunk = this.file.slice(start, end);
|
||||||
|
const chunkActualSize = end - start;
|
||||||
|
|
||||||
|
// Calculate chunk MD5 if available (this is fast since chunk is small)
|
||||||
|
let chunkMd5 = '';
|
||||||
|
if (this.chunkMd5Available) {
|
||||||
|
try {
|
||||||
|
chunkMd5 = await calculateChunkMd5(chunk);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Failed to calculate MD5 for chunk ${chunkIndex}:`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('sessionUuid', this.session!.uuid);
|
||||||
|
formData.append('chunkIndex', chunkIndex.toString());
|
||||||
|
formData.append('file', chunk);
|
||||||
|
formData.append('spaceUuid', this.spaceUuid);
|
||||||
|
if (chunkMd5) {
|
||||||
|
formData.append('chunkMd5', chunkMd5);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelSource = axios.CancelToken.source();
|
||||||
|
this.cancelSources.set(chunkIndex, cancelSource);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
|
HttpUtil.httpPostFile(
|
||||||
|
ChunkUploader.URL_UPLOAD_CHUNK,
|
||||||
|
formData,
|
||||||
|
() => {
|
||||||
|
this.uploadingChunks.delete(chunkIndex);
|
||||||
|
this.uploadedChunks.add(chunkIndex);
|
||||||
|
this.chunkProgress.set(chunkIndex, chunkActualSize);
|
||||||
|
this.cancelSources.delete(chunkIndex);
|
||||||
|
this.updateProgress();
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
(error: any) => {
|
||||||
|
this.uploadingChunks.delete(chunkIndex);
|
||||||
|
this.cancelSources.delete(chunkIndex);
|
||||||
|
this.chunkProgress.delete(chunkIndex);
|
||||||
|
|
||||||
|
if (axios.isCancel(error)) {
|
||||||
|
reject(error);
|
||||||
|
} else if (retryCount < MAX_RETRY_ATTEMPTS) {
|
||||||
|
// Determine if error is retryable (network errors, timeouts)
|
||||||
|
const isRetryable =
|
||||||
|
error.code === 'ECONNABORTED' || // timeout
|
||||||
|
error.code === 'ERR_NETWORK' || // network error
|
||||||
|
!error.response || // no response (network issue)
|
||||||
|
(error.response?.status >= 500 && error.response?.status < 600); // server errors
|
||||||
|
|
||||||
|
if (isRetryable) {
|
||||||
|
// Retry with exponential backoff
|
||||||
|
const delay = RETRY_BASE_DELAY * Math.pow(2, retryCount);
|
||||||
|
console.warn(
|
||||||
|
`Chunk ${chunkIndex} upload failed (${error.code || error.response?.status || 'unknown'}), retrying in ${delay}ms (attempt ${retryCount + 1}/${MAX_RETRY_ATTEMPTS})`
|
||||||
|
);
|
||||||
|
setTimeout(() => {
|
||||||
|
this.uploadChunk(chunkIndex, retryCount + 1).then(resolve).catch(reject);
|
||||||
|
}, delay);
|
||||||
|
} else {
|
||||||
|
// Non-retryable error (4xx client errors)
|
||||||
|
const msg = error.response?.data?.msg || `Failed to upload chunk ${chunkIndex}: ${error.message}`;
|
||||||
|
reject(new Error(msg));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const msg = error.response?.data?.msg || `Failed to upload chunk ${chunkIndex} after ${MAX_RETRY_ATTEMPTS} attempts`;
|
||||||
|
reject(new Error(msg));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
(progressEvent: any) => {
|
||||||
|
// Track real-time progress for this chunk
|
||||||
|
if (progressEvent.loaded) {
|
||||||
|
this.chunkProgress.set(chunkIndex, progressEvent.loaded);
|
||||||
|
this.updateProgress();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ cancelToken: cancelSource.token, timeout: UPLOAD_TIMEOUT_MS }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge all chunks
|
||||||
|
private async mergeChunks(): Promise<void> {
|
||||||
|
this.status = UploadSessionStatus.MERGING;
|
||||||
|
this.updateProgress();
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('sessionUuid', this.session!.uuid);
|
||||||
|
formData.append('spaceUuid', this.spaceUuid);
|
||||||
|
|
||||||
|
HttpUtil.httpPost(
|
||||||
|
ChunkUploader.URL_MERGE_CHUNKS,
|
||||||
|
formData,
|
||||||
|
(response: any) => {
|
||||||
|
this.status = UploadSessionStatus.COMPLETED;
|
||||||
|
this.removeSessionFromStorage();
|
||||||
|
this.updateProgress();
|
||||||
|
this.onComplete?.(response.data.data);
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
(error: any) => {
|
||||||
|
this.status = UploadSessionStatus.ERROR;
|
||||||
|
const msg = error.response?.data?.msg || 'Failed to merge chunks';
|
||||||
|
this.onError?.(msg);
|
||||||
|
reject(new Error(msg));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update progress with precise chunk-level tracking
|
||||||
|
private updateProgress(): void {
|
||||||
|
// Calculate precise uploaded size including in-progress chunks
|
||||||
|
let uploadedSize = 0;
|
||||||
|
|
||||||
|
// Add completed chunks (use actual size for last chunk)
|
||||||
|
Array.from(this.uploadedChunks).forEach((chunkIndex) => {
|
||||||
|
const start = chunkIndex * this.chunkSize;
|
||||||
|
const end = Math.min(start + this.chunkSize, this.file.size);
|
||||||
|
uploadedSize += end - start;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add in-progress chunk bytes
|
||||||
|
this.chunkProgress.forEach((bytes, chunkIndex) => {
|
||||||
|
if (!this.uploadedChunks.has(chunkIndex)) {
|
||||||
|
uploadedSize += bytes;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const actualUploadedSize = Math.min(uploadedSize, this.file.size);
|
||||||
|
|
||||||
|
// Calculate speed
|
||||||
|
const now = Date.now();
|
||||||
|
const timeDelta = now - this.lastUpdateTime;
|
||||||
|
if (timeDelta > 500) {
|
||||||
|
const sizeDelta = actualUploadedSize - this.lastUploadedSize;
|
||||||
|
this.currentSpeed = Math.round(sizeDelta / (timeDelta / 1000));
|
||||||
|
this.lastUpdateTime = now;
|
||||||
|
this.lastUploadedSize = actualUploadedSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
const progress: ChunkUploadProgress = {
|
||||||
|
sessionUuid: this.session?.uuid || '',
|
||||||
|
filename: this.file.name,
|
||||||
|
totalSize: this.file.size,
|
||||||
|
uploadedSize: actualUploadedSize,
|
||||||
|
progress: actualUploadedSize / this.file.size,
|
||||||
|
speed: this.currentSpeed,
|
||||||
|
uploadedChunks: this.uploadedChunks.size,
|
||||||
|
totalChunks: this.totalChunks,
|
||||||
|
status: this.status,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.onProgress?.(progress);
|
||||||
|
|
||||||
|
// Update React component if provided
|
||||||
|
if (this.reactComponent) {
|
||||||
|
this.reactComponent.setState({});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pause upload
|
||||||
|
pause(): void {
|
||||||
|
this.status = UploadSessionStatus.PAUSED;
|
||||||
|
// Cancel ongoing chunk uploads
|
||||||
|
this.cancelSources.forEach((source) => {
|
||||||
|
source.cancel('Paused');
|
||||||
|
});
|
||||||
|
this.uploadingChunks.clear();
|
||||||
|
this.cancelSources.clear();
|
||||||
|
// Don't clear chunkProgress or call updateProgress() here
|
||||||
|
// to avoid progress bar jumping back. The progress will be
|
||||||
|
// recalculated when upload resumes.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resume upload
|
||||||
|
async resume(): Promise<void> {
|
||||||
|
if (this.status !== UploadSessionStatus.PAUSED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.status = UploadSessionStatus.UPLOADING;
|
||||||
|
// Clear stale progress data from cancelled chunks
|
||||||
|
this.chunkProgress.clear();
|
||||||
|
|
||||||
|
// Get missing chunks
|
||||||
|
const missingChunks: number[] = [];
|
||||||
|
for (let i = 0; i < this.totalChunks; i++) {
|
||||||
|
if (!this.uploadedChunks.has(i)) {
|
||||||
|
missingChunks.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.uploadMissingChunks(missingChunks);
|
||||||
|
} catch (error: any) {
|
||||||
|
// Don't change status if paused during resume (status may change during async operation)
|
||||||
|
const currentStatus = this.status as UploadSessionStatus;
|
||||||
|
if (currentStatus === UploadSessionStatus.PAUSED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!axios.isCancel(error)) {
|
||||||
|
this.status = UploadSessionStatus.ERROR;
|
||||||
|
this.onError?.(error.message || 'Upload failed');
|
||||||
|
}
|
||||||
|
this.updateProgress();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel upload
|
||||||
|
cancel(): void {
|
||||||
|
this.status = UploadSessionStatus.CANCELLED;
|
||||||
|
this.cancelSources.forEach((source) => {
|
||||||
|
source.cancel('Cancelled');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cancel session on server
|
||||||
|
if (this.session) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('sessionUuid', this.session.uuid);
|
||||||
|
formData.append('spaceUuid', this.spaceUuid);
|
||||||
|
HttpUtil.httpPost(ChunkUploader.URL_CANCEL_SESSION, formData);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.removeSessionFromStorage();
|
||||||
|
this.updateProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save session to local storage for resume capability
|
||||||
|
private saveSessionToStorage(): void {
|
||||||
|
if (!this.session) return;
|
||||||
|
|
||||||
|
const storageKey = this.getStorageKey();
|
||||||
|
const sessions = this.getAllSessionsFromStorage();
|
||||||
|
|
||||||
|
sessions[storageKey] = {
|
||||||
|
sessionUuid: this.session.uuid,
|
||||||
|
filename: this.file.name,
|
||||||
|
fileSize: this.file.size,
|
||||||
|
puuid: this.puuid,
|
||||||
|
spaceUuid: this.spaceUuid,
|
||||||
|
privacy: this.privacy,
|
||||||
|
lastModified: this.file.lastModified,
|
||||||
|
fingerprint: this.fileFingerprint, // Store fingerprint for better matching
|
||||||
|
savedAt: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem(UPLOAD_SESSION_STORAGE_KEY, JSON.stringify(sessions));
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to save upload session to localStorage', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load session from local storage with improved file matching
|
||||||
|
private async loadSessionFromStorage(): Promise<{ uuid: string } | null> {
|
||||||
|
const storageKey = this.getStorageKey();
|
||||||
|
const sessions = this.getAllSessionsFromStorage();
|
||||||
|
const saved = sessions[storageKey];
|
||||||
|
|
||||||
|
if (!saved) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic checks: size and lastModified must match
|
||||||
|
if (saved.fileSize !== this.file.size || saved.lastModified !== this.file.lastModified) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced check: if fingerprint exists in saved session, verify it matches
|
||||||
|
if (saved.fingerprint && this.fileFingerprint && saved.fingerprint !== this.fileFingerprint) {
|
||||||
|
console.warn('File fingerprint mismatch, file content may have changed');
|
||||||
|
// Remove stale session
|
||||||
|
delete sessions[storageKey];
|
||||||
|
localStorage.setItem(UPLOAD_SESSION_STORAGE_KEY, JSON.stringify(sessions));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { uuid: saved.sessionUuid };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove session from local storage
|
||||||
|
private removeSessionFromStorage(): void {
|
||||||
|
const storageKey = this.getStorageKey();
|
||||||
|
const sessions = this.getAllSessionsFromStorage();
|
||||||
|
delete sessions[storageKey];
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem(UPLOAD_SESSION_STORAGE_KEY, JSON.stringify(sessions));
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to remove upload session from localStorage', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all sessions from storage with automatic cleanup of expired sessions
|
||||||
|
private getAllSessionsFromStorage(): Record<string, any> {
|
||||||
|
try {
|
||||||
|
const data = localStorage.getItem(UPLOAD_SESSION_STORAGE_KEY);
|
||||||
|
if (!data) return {};
|
||||||
|
|
||||||
|
const sessions = JSON.parse(data);
|
||||||
|
const now = Date.now();
|
||||||
|
let hasExpired = false;
|
||||||
|
|
||||||
|
// Clean up expired sessions
|
||||||
|
for (const key of Object.keys(sessions)) {
|
||||||
|
const session = sessions[key];
|
||||||
|
if (session.savedAt && now - session.savedAt > SESSION_MAX_AGE_MS) {
|
||||||
|
delete sessions[key];
|
||||||
|
hasExpired = true;
|
||||||
|
console.info(`Cleaned up expired upload session: ${key}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save back if any sessions were removed
|
||||||
|
if (hasExpired) {
|
||||||
|
localStorage.setItem(UPLOAD_SESSION_STORAGE_KEY, JSON.stringify(sessions));
|
||||||
|
}
|
||||||
|
|
||||||
|
return sessions;
|
||||||
|
} catch (e) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate storage key based on file properties
|
||||||
|
private getStorageKey(): string {
|
||||||
|
return `${this.spaceUuid}_${this.puuid}_${this.file.name}_${this.file.size}_${this.file.lastModified}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current progress value (0-1)
|
||||||
|
getProgress(): number {
|
||||||
|
return this.uploadedChunks.size / this.totalChunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current speed in bytes per second
|
||||||
|
getSpeed(): number {
|
||||||
|
return this.currentSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get uploaded size with precise calculation
|
||||||
|
getUploadedSize(): number {
|
||||||
|
let uploadedSize = 0;
|
||||||
|
|
||||||
|
// Add completed chunks
|
||||||
|
Array.from(this.uploadedChunks).forEach((chunkIndex) => {
|
||||||
|
const start = chunkIndex * this.chunkSize;
|
||||||
|
const end = Math.min(start + this.chunkSize, this.file.size);
|
||||||
|
uploadedSize += end - start;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add in-progress chunk bytes
|
||||||
|
this.chunkProgress.forEach((bytes, chunkIndex) => {
|
||||||
|
if (!this.uploadedChunks.has(chunkIndex)) {
|
||||||
|
uploadedSize += bytes;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Math.min(uploadedSize, this.file.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if upload is complete
|
||||||
|
isComplete(): boolean {
|
||||||
|
return this.status === UploadSessionStatus.COMPLETED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if upload is in progress
|
||||||
|
isUploading(): boolean {
|
||||||
|
return this.status === UploadSessionStatus.UPLOADING || this.status === UploadSessionStatus.MERGING;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if upload is paused
|
||||||
|
isPaused(): boolean {
|
||||||
|
return this.status === UploadSessionStatus.PAUSED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if upload failed
|
||||||
|
isFailed(): boolean {
|
||||||
|
return this.status === UploadSessionStatus.ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if upload is cancelled
|
||||||
|
isCancelled(): boolean {
|
||||||
|
return this.status === UploadSessionStatus.CANCELLED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate a quick fingerprint from file's first and last bytes
|
||||||
|
// This provides better uniqueness than just size+lastModified without reading the whole file
|
||||||
|
private async calculateFileFingerprint(): Promise<string> {
|
||||||
|
const SAMPLE_SIZE = 64 * 1024; // 64KB sample from start and end
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Read first 64KB
|
||||||
|
const firstChunk = this.file.slice(0, Math.min(SAMPLE_SIZE, this.file.size));
|
||||||
|
const firstBuffer = await firstChunk.arrayBuffer();
|
||||||
|
const firstView = new Uint8Array(firstBuffer);
|
||||||
|
|
||||||
|
// Read last 64KB (if file is large enough)
|
||||||
|
let lastView = new Uint8Array(0);
|
||||||
|
if (this.file.size > SAMPLE_SIZE * 2) {
|
||||||
|
const lastChunk = this.file.slice(this.file.size - SAMPLE_SIZE);
|
||||||
|
const lastBuffer = await lastChunk.arrayBuffer();
|
||||||
|
lastView = new Uint8Array(lastBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple hash: combine samples into a fingerprint string
|
||||||
|
// Using a simple checksum approach for speed
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < firstView.length; i++) {
|
||||||
|
hash = ((hash << 5) - hash + firstView[i]) | 0;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < lastView.length; i++) {
|
||||||
|
hash = ((hash << 5) - hash + lastView[i]) | 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to hex string
|
||||||
|
return (hash >>> 0).toString(16).padStart(8, '0');
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to calculate file fingerprint:', e);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -98,6 +98,7 @@ let LangEn = {
|
|||||||
delete: 'Delete',
|
delete: 'Delete',
|
||||||
hardDelete: 'Hard delete',
|
hardDelete: 'Hard delete',
|
||||||
recovery: 'Recovery',
|
recovery: 'Recovery',
|
||||||
|
recoverySuccess: 'Recovery Succeed',
|
||||||
more: 'More',
|
more: 'More',
|
||||||
share: 'Share',
|
share: 'Share',
|
||||||
close: 'Close',
|
close: 'Close',
|
||||||
@@ -170,6 +171,20 @@ let LangEn = {
|
|||||||
crawlSuccessTip: 'Crawl successfully',
|
crawlSuccessTip: 'Crawl successfully',
|
||||||
crawlDescription:
|
crawlDescription:
|
||||||
'Crawl files refers to the backend downloading directly from the specified resource link to the current directory of EyeblueTank',
|
'Crawl files refers to the backend downloading directly from the specified resource link to the current directory of EyeblueTank',
|
||||||
|
// Chunked upload related
|
||||||
|
chunks: 'chunks',
|
||||||
|
chunkedUpload: 'Chunked Upload',
|
||||||
|
mergingChunks: 'Merging chunks...',
|
||||||
|
paused: 'Paused',
|
||||||
|
completed: 'Completed',
|
||||||
|
uploadFailed: 'Upload Failed',
|
||||||
|
cancelled: 'Cancelled',
|
||||||
|
pause: 'Pause',
|
||||||
|
resume: 'Resume',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
// MD5 verification related
|
||||||
|
calculatingMd5: 'Calculating MD5...',
|
||||||
|
md5VerificationEnabled: 'MD5 verification enabled',
|
||||||
},
|
},
|
||||||
router: {
|
router: {
|
||||||
allFiles: 'All Files',
|
allFiles: 'All Files',
|
||||||
@@ -208,6 +223,9 @@ let LangEn = {
|
|||||||
docLink: 'Document Link',
|
docLink: 'Document Link',
|
||||||
tankDocLink: 'https://tank-doc.eyeblue.cn',
|
tankDocLink: 'https://tank-doc.eyeblue.cn',
|
||||||
allowRegister: 'Allow register',
|
allowRegister: 'Allow register',
|
||||||
|
allowChunkUpload: 'Allow chunk upload',
|
||||||
|
allowChunkUploadTip:
|
||||||
|
'Chunked upload is for large files (≥100MB) with resume support. Recommended when server supports HTTP/2, otherwise concurrent connection limits may slow down uploads.',
|
||||||
systemCleanup: 'System Cleanup',
|
systemCleanup: 'System Cleanup',
|
||||||
systemCleanupDescription:
|
systemCleanupDescription:
|
||||||
"This operation will cleanup everything except administrators' data",
|
"This operation will cleanup everything except administrators' data",
|
||||||
@@ -242,19 +260,22 @@ let LangEn = {
|
|||||||
enableScan: 'Enable Scan disk',
|
enableScan: 'Enable Scan disk',
|
||||||
disabledScan: 'Disabled Scan disk',
|
disabledScan: 'Disabled Scan disk',
|
||||||
scanCron: 'Regularly Scan disk',
|
scanCron: 'Regularly Scan disk',
|
||||||
|
scanCronValidate: 'Regularly scan disk can not be null',
|
||||||
cron: 'Cron expression',
|
cron: 'Cron expression',
|
||||||
cronValidate: 'Cron expression can not be null',
|
cronValidate: 'Cron expression can not be null',
|
||||||
scanScope: 'Scan scope',
|
scanScope: 'Scan scope',
|
||||||
scanUsers: 'Scan users',
|
scanScopeValidate: 'Scan scope can not be null',
|
||||||
|
scanSpaces: 'Scan spaces',
|
||||||
scanPerTenSeconds: 'Scan every ten seconds',
|
scanPerTenSeconds: 'Scan every ten seconds',
|
||||||
scanPerThirtySeconds: 'Scan every thirty seconds',
|
scanPerThirtySeconds: 'Scan every thirty seconds',
|
||||||
scanPerMinute: 'Scan every minute',
|
scanPerMinute: 'Scan every minute',
|
||||||
scanPerHour: 'Scan every hour',
|
scanPerHour: 'Scan every hour',
|
||||||
scanCustom: 'Scan custom',
|
scanCustom: 'Scan custom',
|
||||||
chooseUsers: 'Fuzzy search users',
|
chooseSpaces: 'Fuzzy search spaces',
|
||||||
chooseUsersValidate: 'choose one more user',
|
chooseSpacesValidate: 'choose one more space',
|
||||||
scanLoading: 'waiting...',
|
scanLoading: 'waiting...',
|
||||||
matterBinDefaultTip: 'set zero means to turn off the recycle bin',
|
matterBinDefaultTip: 'set zero means to turn off the recycle bin',
|
||||||
|
scanImmediately: 'Scan immediately',
|
||||||
},
|
},
|
||||||
share: {
|
share: {
|
||||||
shareDetail: 'Share Detail',
|
shareDetail: 'Share Detail',
|
||||||
@@ -347,8 +368,6 @@ let LangEn = {
|
|||||||
transfigurationPromptText: 'Transfiguration Prompt',
|
transfigurationPromptText: 'Transfiguration Prompt',
|
||||||
transfigurationPrompt:
|
transfigurationPrompt:
|
||||||
'You will login as this user.Please visit this link in other browser, if in current browser, you will logout.',
|
'You will login as this user.Please visit this link in other browser, if in current browser, you will logout.',
|
||||||
allUsers: 'All users',
|
|
||||||
partialUsers: 'Partial users',
|
|
||||||
searchUser: 'Search user',
|
searchUser: 'Search user',
|
||||||
},
|
},
|
||||||
space: {
|
space: {
|
||||||
@@ -375,6 +394,11 @@ let LangEn = {
|
|||||||
memberRoleReadWrite: 'Read and write',
|
memberRoleReadWrite: 'Read and write',
|
||||||
memberRoleAdmin: 'Admin',
|
memberRoleAdmin: 'Admin',
|
||||||
emptyHint: 'No Space Yet',
|
emptyHint: 'No Space Yet',
|
||||||
|
allSpaces: 'All Spaces',
|
||||||
|
partialSpaces: 'Partial Spaces',
|
||||||
|
bin: 'Space recycle bin',
|
||||||
|
edit: 'Edit Space',
|
||||||
|
delete: 'Delete Space',
|
||||||
},
|
},
|
||||||
model: {
|
model: {
|
||||||
usernameRule: 'only lowercase letter and number and _ is permitted.',
|
usernameRule: 'only lowercase letter and number and _ is permitted.',
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ let LangZh = {
|
|||||||
delete: '删除',
|
delete: '删除',
|
||||||
hardDelete: '彻底删除',
|
hardDelete: '彻底删除',
|
||||||
recovery: '恢复',
|
recovery: '恢复',
|
||||||
|
recoverySuccess: '恢复成功',
|
||||||
more: '更多',
|
more: '更多',
|
||||||
share: '分享',
|
share: '分享',
|
||||||
close: '关闭',
|
close: '关闭',
|
||||||
@@ -164,6 +165,20 @@ let LangZh = {
|
|||||||
crawlSuccessTip: '抓取成功',
|
crawlSuccessTip: '抓取成功',
|
||||||
crawlDescription:
|
crawlDescription:
|
||||||
'抓取文件指后台将从指定资源链接直接下载到蓝眼云盘当前目录下',
|
'抓取文件指后台将从指定资源链接直接下载到蓝眼云盘当前目录下',
|
||||||
|
// Chunked upload related
|
||||||
|
chunks: '分片',
|
||||||
|
chunkedUpload: '分片上传',
|
||||||
|
mergingChunks: '合并分片中...',
|
||||||
|
paused: '已暂停',
|
||||||
|
completed: '已完成',
|
||||||
|
uploadFailed: '上传失败',
|
||||||
|
cancelled: '已取消',
|
||||||
|
pause: '暂停',
|
||||||
|
resume: '继续',
|
||||||
|
cancel: '取消',
|
||||||
|
// MD5 verification related
|
||||||
|
calculatingMd5: '计算MD5校验中...',
|
||||||
|
md5VerificationEnabled: 'MD5校验已启用',
|
||||||
},
|
},
|
||||||
router: {
|
router: {
|
||||||
allFiles: '全部文件',
|
allFiles: '全部文件',
|
||||||
@@ -202,6 +217,9 @@ let LangZh = {
|
|||||||
docLink: '文档链接',
|
docLink: '文档链接',
|
||||||
tankDocLink: 'https://tank-doc.eyeblue.cn/zh',
|
tankDocLink: 'https://tank-doc.eyeblue.cn/zh',
|
||||||
allowRegister: '允许自主注册',
|
allowRegister: '允许自主注册',
|
||||||
|
allowChunkUpload: '允许分片上传',
|
||||||
|
allowChunkUploadTip:
|
||||||
|
'分片上传适用于大文件(≥100MB),支持断点续传。建议在服务端支持HTTP/2协议时开启,否则可能因并发连接数限制导致上传变慢。',
|
||||||
systemCleanup: '重置系统',
|
systemCleanup: '重置系统',
|
||||||
systemCleanupDescription: '重置系统将清空除管理员账号外所有数据',
|
systemCleanupDescription: '重置系统将清空除管理员账号外所有数据',
|
||||||
systemCleanupPrompt:
|
systemCleanupPrompt:
|
||||||
@@ -235,19 +253,22 @@ let LangZh = {
|
|||||||
enableScan: '开启磁盘扫描',
|
enableScan: '开启磁盘扫描',
|
||||||
disabledScan: '暂未开启磁盘扫描',
|
disabledScan: '暂未开启磁盘扫描',
|
||||||
scanCron: '定时扫描',
|
scanCron: '定时扫描',
|
||||||
|
scanCronValidate: '定时扫描不能为空',
|
||||||
cron: 'cron表达式',
|
cron: 'cron表达式',
|
||||||
cronValidate: 'cron表达式不能为空',
|
cronValidate: 'cron表达式不能为空',
|
||||||
scanScope: '扫描范围',
|
scanScope: '扫描范围',
|
||||||
scanUsers: '扫描用户',
|
scanScopeValidate: '扫描范围不能为空',
|
||||||
|
scanSpaces: '扫描空间',
|
||||||
scanPerTenSeconds: '每十秒',
|
scanPerTenSeconds: '每十秒',
|
||||||
scanPerThirtySeconds: '每三十秒',
|
scanPerThirtySeconds: '每三十秒',
|
||||||
scanPerMinute: '每分钟',
|
scanPerMinute: '每分钟',
|
||||||
scanPerHour: '每小时',
|
scanPerHour: '每小时',
|
||||||
scanCustom: '自定义',
|
scanCustom: '自定义',
|
||||||
chooseUsers: '模糊搜索用户',
|
chooseSpaces: '模糊搜索空间',
|
||||||
chooseUsersValidate: '请选择至少一个用户',
|
chooseSpacesValidate: '请选择至少一个空间',
|
||||||
scanLoading: '扫描中,请稍后...',
|
scanLoading: '扫描中,请稍后...',
|
||||||
matterBinDefaultTip: '设置0表示关闭回收站功能',
|
matterBinDefaultTip: '设置0表示关闭回收站功能',
|
||||||
|
scanImmediately: '立即扫描',
|
||||||
},
|
},
|
||||||
share: {
|
share: {
|
||||||
shareDetail: '分享详情',
|
shareDetail: '分享详情',
|
||||||
@@ -340,8 +361,6 @@ let LangZh = {
|
|||||||
transfigurationPromptText: '变身提示',
|
transfigurationPromptText: '变身提示',
|
||||||
transfigurationPrompt:
|
transfigurationPrompt:
|
||||||
'您将使用该用户的身份登录。请复制以下链接到其他浏览器访问,在当前浏览器访问会导致当前用户登录信息失效。',
|
'您将使用该用户的身份登录。请复制以下链接到其他浏览器访问,在当前浏览器访问会导致当前用户登录信息失效。',
|
||||||
allUsers: '全部用户',
|
|
||||||
partialUsers: '局部用户',
|
|
||||||
searchUser: '搜索用户',
|
searchUser: '搜索用户',
|
||||||
},
|
},
|
||||||
space: {
|
space: {
|
||||||
@@ -367,6 +386,11 @@ let LangZh = {
|
|||||||
memberRoleReadWrite: '可读写',
|
memberRoleReadWrite: '可读写',
|
||||||
memberRoleAdmin: '管理员',
|
memberRoleAdmin: '管理员',
|
||||||
emptyHint: '暂无任何空间',
|
emptyHint: '暂无任何空间',
|
||||||
|
allSpaces: '全部空间',
|
||||||
|
partialSpaces: '局部空间',
|
||||||
|
bin: '空间回收站',
|
||||||
|
edit: '编辑空间',
|
||||||
|
delete: '删除空间',
|
||||||
},
|
},
|
||||||
model: {
|
model: {
|
||||||
usernameRule: '用户名只能包含字母,数字和"_"',
|
usernameRule: '用户名只能包含字母,数字和"_"',
|
||||||
|
|||||||
@@ -17,7 +17,12 @@ import ImagePreviewer from '../../../pages/widget/previewer/ImagePreviewer';
|
|||||||
import MessageBoxUtil from '../../util/MessageBoxUtil';
|
import MessageBoxUtil from '../../util/MessageBoxUtil';
|
||||||
import PreviewerHelper from '../../../pages/widget/previewer/PreviewerHelper';
|
import PreviewerHelper from '../../../pages/widget/previewer/PreviewerHelper';
|
||||||
import HttpBase from '../base/HttpBase';
|
import HttpBase from '../base/HttpBase';
|
||||||
import User from "../user/User";
|
import User from '../user/User';
|
||||||
|
import ChunkUploader, {
|
||||||
|
UploadSessionStatus,
|
||||||
|
ChunkUploadProgress,
|
||||||
|
} from '../chunk/ChunkUploader';
|
||||||
|
import Moon from '../global/Moon';
|
||||||
|
|
||||||
export default class Matter extends BaseEntity {
|
export default class Matter extends BaseEntity {
|
||||||
puuid: string = '';
|
puuid: string = '';
|
||||||
@@ -60,6 +65,17 @@ export default class Matter extends BaseEntity {
|
|||||||
//取消上传source
|
//取消上传source
|
||||||
cancelSource: CancelTokenSource = axios.CancelToken.source();
|
cancelSource: CancelTokenSource = axios.CancelToken.source();
|
||||||
|
|
||||||
|
// Chunked upload related
|
||||||
|
chunkUploader: ChunkUploader | null = null;
|
||||||
|
// Uploaded chunks count (for UI display)
|
||||||
|
uploadedChunks: number = 0;
|
||||||
|
// Total chunks count (for UI display)
|
||||||
|
totalChunks: number = 0;
|
||||||
|
// Whether using chunked upload
|
||||||
|
useChunkedUpload: boolean = false;
|
||||||
|
// Upload status for chunked upload
|
||||||
|
uploadStatus: UploadSessionStatus = UploadSessionStatus.UPLOADING;
|
||||||
|
|
||||||
static URL_MATTER_CREATE_DIRECTORY = '/api/matter/create/directory';
|
static URL_MATTER_CREATE_DIRECTORY = '/api/matter/create/directory';
|
||||||
static URL_MATTER_SOFT_DELETE = '/api/matter/soft/delete';
|
static URL_MATTER_SOFT_DELETE = '/api/matter/soft/delete';
|
||||||
static URL_MATTER_SOFT_DELETE_BATCH = '/api/matter/soft/delete/batch';
|
static URL_MATTER_SOFT_DELETE_BATCH = '/api/matter/soft/delete/batch';
|
||||||
@@ -73,6 +89,7 @@ export default class Matter extends BaseEntity {
|
|||||||
static URL_MATTER_UPLOAD = '/api/matter/upload';
|
static URL_MATTER_UPLOAD = '/api/matter/upload';
|
||||||
static URL_MATTER_ZIP = '/api/matter/zip';
|
static URL_MATTER_ZIP = '/api/matter/zip';
|
||||||
static URL_MATTER_CRAWL = '/api/matter/crawl';
|
static URL_MATTER_CRAWL = '/api/matter/crawl';
|
||||||
|
static URL_MATTER_SEARCH = '/api/matter/search';
|
||||||
|
|
||||||
static MATTER_ROOT = 'root';
|
static MATTER_ROOT = 'root';
|
||||||
|
|
||||||
@@ -247,6 +264,28 @@ export default class Matter extends BaseEntity {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static httpSearch(
|
||||||
|
params: {
|
||||||
|
puuid: string;
|
||||||
|
spaceUuid: string;
|
||||||
|
keyword?: string;
|
||||||
|
},
|
||||||
|
successCallback?: any,
|
||||||
|
errorCallback?: any,
|
||||||
|
finallyCallback?: any
|
||||||
|
) {
|
||||||
|
new HttpBase().httpGet(
|
||||||
|
Matter.URL_MATTER_SEARCH,
|
||||||
|
params,
|
||||||
|
(res: any) => {
|
||||||
|
const list = BaseEntity.renderList(res.data.data, Matter);
|
||||||
|
successCallback?.(list);
|
||||||
|
},
|
||||||
|
errorCallback,
|
||||||
|
finallyCallback
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
static httpCrawl(
|
static httpCrawl(
|
||||||
body: {
|
body: {
|
||||||
url: string;
|
url: string;
|
||||||
@@ -288,7 +327,7 @@ export default class Matter extends BaseEntity {
|
|||||||
httpRecovery(successCallback?: any, errorCallback?: any) {
|
httpRecovery(successCallback?: any, errorCallback?: any) {
|
||||||
this.httpPost(
|
this.httpPost(
|
||||||
Matter.URL_MATTER_RECOVERY,
|
Matter.URL_MATTER_RECOVERY,
|
||||||
{ uuid: this.uuid },
|
{ uuid: this.uuid, spaceUuid: this.spaceUuid },
|
||||||
function (response: any) {
|
function (response: any) {
|
||||||
typeof successCallback === 'function' && successCallback(response);
|
typeof successCallback === 'function' && successCallback(response);
|
||||||
},
|
},
|
||||||
@@ -298,12 +337,13 @@ export default class Matter extends BaseEntity {
|
|||||||
|
|
||||||
static httpRecoveryBatch(
|
static httpRecoveryBatch(
|
||||||
uuids: string,
|
uuids: string,
|
||||||
|
spaceUuid: string,
|
||||||
successCallback?: any,
|
successCallback?: any,
|
||||||
errorCallback?: any
|
errorCallback?: any
|
||||||
) {
|
) {
|
||||||
new HttpBase().httpPost(
|
new HttpBase().httpPost(
|
||||||
Matter.URL_MATTER_RECOVERY_BATCH,
|
Matter.URL_MATTER_RECOVERY_BATCH,
|
||||||
{ uuids: uuids },
|
{ uuids, spaceUuid },
|
||||||
function (response: any) {
|
function (response: any) {
|
||||||
typeof successCallback === 'function' && successCallback(response);
|
typeof successCallback === 'function' && successCallback(response);
|
||||||
},
|
},
|
||||||
@@ -604,7 +644,129 @@ export default class Matter extends BaseEntity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cancelUpload() {
|
cancelUpload() {
|
||||||
this.cancelSource.cancel();
|
if (this.chunkUploader) {
|
||||||
|
this.chunkUploader.cancel();
|
||||||
|
this.loading = false;
|
||||||
|
this.uploadStatus = UploadSessionStatus.CANCELLED;
|
||||||
|
this.updateUI();
|
||||||
|
} else {
|
||||||
|
this.cancelSource.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pause chunked upload
|
||||||
|
pauseUpload() {
|
||||||
|
if (this.chunkUploader && this.uploadStatus === UploadSessionStatus.UPLOADING) {
|
||||||
|
this.chunkUploader.pause();
|
||||||
|
this.uploadStatus = UploadSessionStatus.PAUSED;
|
||||||
|
// Force UI update to show resume button
|
||||||
|
if (this.reactComponent) {
|
||||||
|
this.reactComponent.setState({ _forceUpdate: Date.now() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resume chunked upload
|
||||||
|
resumeUpload() {
|
||||||
|
if (this.chunkUploader && this.uploadStatus === UploadSessionStatus.PAUSED) {
|
||||||
|
this.uploadStatus = UploadSessionStatus.UPLOADING;
|
||||||
|
this.chunkUploader.resume();
|
||||||
|
// Force UI update to show pause button
|
||||||
|
if (this.reactComponent) {
|
||||||
|
this.reactComponent.setState({ _forceUpdate: Date.now() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if using chunked upload and is paused
|
||||||
|
canResume(): boolean {
|
||||||
|
return this.useChunkedUpload && this.uploadStatus === UploadSessionStatus.PAUSED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if using chunked upload and is uploading (not merging, as merging cannot be paused)
|
||||||
|
canPause(): boolean {
|
||||||
|
return this.useChunkedUpload && this.uploadStatus === UploadSessionStatus.UPLOADING;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Smart upload: use chunked upload for large files, regular upload for small files
|
||||||
|
httpSmartUpload(successCallback?: any, failureCallback?: any) {
|
||||||
|
if (!this.file) {
|
||||||
|
this.errorMessage = '请选择上传文件';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if chunk upload is enabled in preference and file is large enough
|
||||||
|
const allowChunkUpload = Moon.getSingleton().preference.allowChunkUpload;
|
||||||
|
if (allowChunkUpload && ChunkUploader.shouldUseChunkedUpload(this.file.size)) {
|
||||||
|
this.httpChunkedUpload(successCallback, failureCallback);
|
||||||
|
} else {
|
||||||
|
this.httpUpload(successCallback, failureCallback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chunked upload for large files
|
||||||
|
// MD5 verification is always enabled when spark-md5 is available
|
||||||
|
httpChunkedUpload(successCallback?: any, failureCallback?: any) {
|
||||||
|
let that = this;
|
||||||
|
|
||||||
|
// Validate
|
||||||
|
if (!this.validate()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.validateFilter()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.validateFileType()) {
|
||||||
|
MessageBoxUtil.error('文件类型不满足,请重试');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.useChunkedUpload = true;
|
||||||
|
this.loading = true;
|
||||||
|
this.uploadStatus = UploadSessionStatus.UPLOADING;
|
||||||
|
|
||||||
|
// Create chunk uploader
|
||||||
|
this.chunkUploader = new ChunkUploader(
|
||||||
|
this.file!,
|
||||||
|
this.puuid,
|
||||||
|
this.spaceUuid!,
|
||||||
|
this.privacy,
|
||||||
|
undefined,
|
||||||
|
this.reactComponent!
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set callbacks
|
||||||
|
this.chunkUploader.onProgress = (progress: ChunkUploadProgress) => {
|
||||||
|
that.progress = progress.progress;
|
||||||
|
that.speed = progress.speed;
|
||||||
|
that.uploadedChunks = progress.uploadedChunks;
|
||||||
|
that.totalChunks = progress.totalChunks;
|
||||||
|
// Only update status if not manually paused (avoid race condition with progress events)
|
||||||
|
if (that.uploadStatus !== UploadSessionStatus.PAUSED) {
|
||||||
|
that.uploadStatus = progress.status;
|
||||||
|
}
|
||||||
|
that.updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.chunkUploader.onComplete = (matter: any) => {
|
||||||
|
that.uuid = matter.uuid;
|
||||||
|
that.loading = false;
|
||||||
|
that.uploadStatus = UploadSessionStatus.COMPLETED;
|
||||||
|
SafeUtil.safeCallback(successCallback)({ data: { data: matter } });
|
||||||
|
};
|
||||||
|
|
||||||
|
this.chunkUploader.onError = (error: string) => {
|
||||||
|
that.errorMessage = error;
|
||||||
|
that.loading = false;
|
||||||
|
that.uploadStatus = UploadSessionStatus.ERROR;
|
||||||
|
that.defaultErrorHandler(error);
|
||||||
|
SafeUtil.safeCallback(failureCallback)(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start upload
|
||||||
|
this.chunkUploader.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
//清除文件
|
//清除文件
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ export default class Preference extends BaseEntity {
|
|||||||
deletedKeepDays: number = 7;
|
deletedKeepDays: number = 7;
|
||||||
//是否允许自主注册
|
//是否允许自主注册
|
||||||
allowRegister: boolean = false;
|
allowRegister: boolean = false;
|
||||||
|
//是否允许分片上传
|
||||||
|
allowChunkUpload: boolean = false;
|
||||||
//预览配置
|
//预览配置
|
||||||
previewConfig: PreviewConfig = new PreviewConfig();
|
previewConfig: PreviewConfig = new PreviewConfig();
|
||||||
//扫描磁盘配置
|
//扫描磁盘配置
|
||||||
@@ -76,6 +78,7 @@ export default class Preference extends BaseEntity {
|
|||||||
defaultTotalSizeLimit: this.defaultTotalSizeLimit,
|
defaultTotalSizeLimit: this.defaultTotalSizeLimit,
|
||||||
deletedKeepDays: this.deletedKeepDays,
|
deletedKeepDays: this.deletedKeepDays,
|
||||||
allowRegister: this.allowRegister,
|
allowRegister: this.allowRegister,
|
||||||
|
allowChunkUpload: this.allowChunkUpload,
|
||||||
previewConfig: JsonUtil.toJson(this.previewConfig.getForm()),
|
previewConfig: JsonUtil.toJson(this.previewConfig.getForm()),
|
||||||
scanConfig: JsonUtil.toJson(this.scanConfig.getForm()),
|
scanConfig: JsonUtil.toJson(this.scanConfig.getForm()),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,12 +13,11 @@ export default class ScanConfig extends Base {
|
|||||||
cron: string | null = null;
|
cron: string | null = null;
|
||||||
// 扫描磁盘开启范围(全部用户 or 局部用户)
|
// 扫描磁盘开启范围(全部用户 or 局部用户)
|
||||||
scope: ScanScopeType = ScanScopeType.ALL;
|
scope: ScanScopeType = ScanScopeType.ALL;
|
||||||
// 开启扫描磁盘功能的用户们
|
// 开启扫描磁盘功能的空间
|
||||||
usernames: string[] = [];
|
spaceNames: string[] = [];
|
||||||
|
|
||||||
// 前端辅助字段
|
// 前端辅助字段
|
||||||
cronType: string | null = null;
|
cronType: string | null = null;
|
||||||
users: LabeledValue[] = [];
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -33,7 +32,7 @@ export default class ScanConfig extends Base {
|
|||||||
enable: this.enable,
|
enable: this.enable,
|
||||||
scope: this.scope,
|
scope: this.scope,
|
||||||
cron: this.cronType === ScanCronType.CUSTOM ? this.cron : this.cronType,
|
cron: this.cronType === ScanCronType.CUSTOM ? this.cron : this.cronType,
|
||||||
usernames: this.usernames,
|
spaceNames: this.spaceNames,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ const ScanScopeTypeMap: {
|
|||||||
[key in keyof typeof ScanScopeType]: SelectionOption;
|
[key in keyof typeof ScanScopeType]: SelectionOption;
|
||||||
} = {
|
} = {
|
||||||
ALL: {
|
ALL: {
|
||||||
name: Lang.t('user.allUsers'),
|
name: Lang.t('space.allSpaces'),
|
||||||
value: 'ALL',
|
value: 'ALL',
|
||||||
},
|
},
|
||||||
CUSTOM: {
|
CUSTOM: {
|
||||||
name: Lang.t('user.partialUsers'),
|
name: Lang.t('space.partialSpaces'),
|
||||||
value: 'CUSTOM',
|
value: 'CUSTOM',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import BaseEntity from '../base/BaseEntity';
|
import BaseEntity from '../base/BaseEntity';
|
||||||
import User from '../user/User';
|
import User from '../user/User';
|
||||||
import FileUtil from '../../util/FileUtil';
|
import FileUtil from '../../util/FileUtil';
|
||||||
|
import Filter from '../base/filter/Filter';
|
||||||
|
import InputFilter from '../base/filter/InputFilter';
|
||||||
|
|
||||||
export interface SpaceFormValues {
|
export interface SpaceFormValues {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -16,6 +18,7 @@ export default class Space extends BaseEntity {
|
|||||||
totalSizeLimit: number = 0;
|
totalSizeLimit: number = 0;
|
||||||
userUuid: string | null = null;
|
userUuid: string | null = null;
|
||||||
user: User | null = null;
|
user: User | null = null;
|
||||||
|
type: 'PRIVATE' | 'SHARED' | null = null;
|
||||||
|
|
||||||
constructor(reactComponent?: React.Component) {
|
constructor(reactComponent?: React.Component) {
|
||||||
super(reactComponent);
|
super(reactComponent);
|
||||||
@@ -25,6 +28,13 @@ export default class Space extends BaseEntity {
|
|||||||
return 'space';
|
return 'space';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getFilters(): Filter[] {
|
||||||
|
return [
|
||||||
|
...super.getFilters(),
|
||||||
|
new InputFilter('空间名称', 'name'),
|
||||||
|
new InputFilter('空间类型', 'type'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
assign(obj: any) {
|
assign(obj: any) {
|
||||||
super.assign(obj);
|
super.assign(obj);
|
||||||
|
|||||||
@@ -2,16 +2,9 @@ import BaseEntity from '../../base/BaseEntity';
|
|||||||
import User from '../../user/User';
|
import User from '../../user/User';
|
||||||
import { SpaceMemberRole } from './SpaceMemberRole';
|
import { SpaceMemberRole } from './SpaceMemberRole';
|
||||||
import Filter from '../../base/filter/Filter';
|
import Filter from '../../base/filter/Filter';
|
||||||
import SortFilter from '../../base/filter/SortFilter';
|
|
||||||
import InputFilter from '../../base/filter/InputFilter';
|
import InputFilter from '../../base/filter/InputFilter';
|
||||||
import SafeUtil from '../../../util/SafeUtil';
|
import SafeUtil from '../../../util/SafeUtil';
|
||||||
|
|
||||||
export interface SpaceMemberFormValues {
|
|
||||||
spaceUuid: string;
|
|
||||||
userUuid: string;
|
|
||||||
role: SpaceMemberRole;
|
|
||||||
uuid: string | null;
|
|
||||||
}
|
|
||||||
export default class SpaceMember extends BaseEntity {
|
export default class SpaceMember extends BaseEntity {
|
||||||
spaceUuid: string | null = null;
|
spaceUuid: string | null = null;
|
||||||
userUuid: string | null = null;
|
userUuid: string | null = null;
|
||||||
@@ -40,7 +33,7 @@ export default class SpaceMember extends BaseEntity {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
getForm(): SpaceMemberFormValues {
|
getForm() {
|
||||||
return {
|
return {
|
||||||
spaceUuid: this.spaceUuid!,
|
spaceUuid: this.spaceUuid!,
|
||||||
userUuid: this.userUuid!,
|
userUuid: this.userUuid!,
|
||||||
@@ -49,6 +42,29 @@ export default class SpaceMember extends BaseEntity {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
httpCreate(
|
||||||
|
body: {
|
||||||
|
spaceUuid: string;
|
||||||
|
userUuids: string;
|
||||||
|
role: SpaceMemberRole;
|
||||||
|
},
|
||||||
|
successCallback?: any
|
||||||
|
) {
|
||||||
|
this.httpPost(this.getUrlCreate(), body, successCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
httpEdit(
|
||||||
|
body: {
|
||||||
|
uuid: string;
|
||||||
|
spaceUuid: string;
|
||||||
|
userUuid: string;
|
||||||
|
role: SpaceMemberRole;
|
||||||
|
},
|
||||||
|
successCallback?: any
|
||||||
|
) {
|
||||||
|
this.httpPost(this.getUrlEdit(), body, successCallback);
|
||||||
|
}
|
||||||
|
|
||||||
httpMine(spaceUuid: string, successCallback?: any, errorCallback?: any) {
|
httpMine(spaceUuid: string, successCallback?: any, errorCallback?: any) {
|
||||||
this.httpGet(
|
this.httpGet(
|
||||||
SpaceMember.URL_SPACE_MEMBER_MINE,
|
SpaceMember.URL_SPACE_MEMBER_MINE,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import SortFilter from '../base/filter/SortFilter';
|
|||||||
import InputFilter from '../base/filter/InputFilter';
|
import InputFilter from '../base/filter/InputFilter';
|
||||||
import SelectionFilter from '../base/filter/SelectionFilter';
|
import SelectionFilter from '../base/filter/SelectionFilter';
|
||||||
import Space from '../space/Space';
|
import Space from '../space/Space';
|
||||||
|
import HttpBase from '../base/HttpBase';
|
||||||
|
|
||||||
export default class User extends BaseEntity {
|
export default class User extends BaseEntity {
|
||||||
//获取当前登录者的信息
|
//获取当前登录者的信息
|
||||||
@@ -22,6 +23,7 @@ export default class User extends BaseEntity {
|
|||||||
static URL_USER_TOGGLE_STATUS = '/api/user/toggle/status';
|
static URL_USER_TOGGLE_STATUS = '/api/user/toggle/status';
|
||||||
static URL_USER_TRANSFIGURATION = '/api/user/transfiguration';
|
static URL_USER_TRANSFIGURATION = '/api/user/transfiguration';
|
||||||
static URL_USER_SCAN = '/api/user/scan';
|
static URL_USER_SCAN = '/api/user/scan';
|
||||||
|
static URL_USER_SEARCH = '/api/user/search';
|
||||||
|
|
||||||
role: UserRole = UserRole.GUEST;
|
role: UserRole = UserRole.GUEST;
|
||||||
username: string | null = null;
|
username: string | null = null;
|
||||||
@@ -226,8 +228,7 @@ export default class User extends BaseEntity {
|
|||||||
oldPassword: string,
|
oldPassword: string,
|
||||||
newPassword: string,
|
newPassword: string,
|
||||||
successCallback?: any,
|
successCallback?: any,
|
||||||
errorCallback?: any,
|
errorCallback?: any
|
||||||
finalCallback?: any
|
|
||||||
) {
|
) {
|
||||||
let that = this;
|
let that = this;
|
||||||
this.httpPost(
|
this.httpPost(
|
||||||
@@ -279,11 +280,25 @@ export default class User extends BaseEntity {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static httpSearch(
|
||||||
|
keyword?: string,
|
||||||
|
successCallback?: any,
|
||||||
|
errorCallback?: any
|
||||||
|
) {
|
||||||
|
new HttpBase().httpGet(
|
||||||
|
User.URL_USER_SEARCH,
|
||||||
|
{
|
||||||
|
keyword,
|
||||||
|
},
|
||||||
|
successCallback,
|
||||||
|
errorCallback
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
httpAuthenticationLogin(
|
httpAuthenticationLogin(
|
||||||
authentication: string,
|
authentication: string,
|
||||||
successCallback?: any,
|
successCallback?: any,
|
||||||
errorCallback?: any,
|
errorCallback?: any
|
||||||
finalCallback?: any
|
|
||||||
) {
|
) {
|
||||||
let that = this;
|
let that = this;
|
||||||
let form = { authentication };
|
let form = { authentication };
|
||||||
|
|||||||
@@ -49,6 +49,13 @@ export default class HttpUtil {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 上传文件的请求
|
* 上传文件的请求
|
||||||
|
* @param url 请求地址
|
||||||
|
* @param formData 表单数据
|
||||||
|
* @param successCallback 成功回调
|
||||||
|
* @param errorCallback 失败回调
|
||||||
|
* @param finallyCallback 最终回调
|
||||||
|
* @param processCallback 进度回调
|
||||||
|
* @param opts 可选配置,支持 cancelToken 和 timeout
|
||||||
*/
|
*/
|
||||||
static httpPostFile(
|
static httpPostFile(
|
||||||
url: string,
|
url: string,
|
||||||
@@ -63,8 +70,10 @@ export default class HttpUtil {
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data',
|
'Content-Type': 'multipart/form-data',
|
||||||
},
|
},
|
||||||
cancelToken: opts ? opts.cancelToken : '',
|
cancelToken: opts?.cancelToken || undefined,
|
||||||
onUploadProgress: processCallback,
|
onUploadProgress: processCallback,
|
||||||
|
// Support custom timeout, default to 10 minutes for large file uploads
|
||||||
|
timeout: opts?.timeout || 10 * 60 * 1000,
|
||||||
};
|
};
|
||||||
|
|
||||||
axios
|
axios
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* MD5 utility for calculating chunk checksums.
|
||||||
|
*
|
||||||
|
* This module provides optional MD5 calculation for chunk integrity verification.
|
||||||
|
* For full MD5 support, install spark-md5: npm install spark-md5 @types/spark-md5
|
||||||
|
*
|
||||||
|
* Without spark-md5 installed, this will return an empty string and skip MD5 verification.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if MD5 calculation is available (spark-md5 is installed)
|
||||||
|
*/
|
||||||
|
export async function isMd5Available(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await import('spark-md5');
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate MD5 hash of a Blob (chunk).
|
||||||
|
* This is fast since the chunk is already in memory.
|
||||||
|
*
|
||||||
|
* @param blob The blob/chunk to calculate MD5 for
|
||||||
|
* @returns Promise<string> MD5 hash as hex string, or empty string if spark-md5 is not available
|
||||||
|
*/
|
||||||
|
export async function calculateChunkMd5(blob: Blob): Promise<string> {
|
||||||
|
try {
|
||||||
|
// @ts-ignore - dynamic import may not have types
|
||||||
|
const SparkMD5 = (await import('spark-md5')).default;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = (e: ProgressEvent<FileReader>) => {
|
||||||
|
if (e.target?.result) {
|
||||||
|
const spark = new SparkMD5.ArrayBuffer();
|
||||||
|
spark.append(e.target.result as ArrayBuffer);
|
||||||
|
resolve(spark.end());
|
||||||
|
} else {
|
||||||
|
resolve('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.onerror = () => {
|
||||||
|
reject(new Error('Failed to read chunk for MD5 calculation'));
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsArrayBuffer(blob);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// spark-md5 is not installed, return empty string
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -193,6 +193,11 @@ class RawFrame extends TankComponent<IProps, IState> {
|
|||||||
exact
|
exact
|
||||||
component={MatterDetail}
|
component={MatterDetail}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/space/:spaceUuid/bin/list"
|
||||||
|
exact
|
||||||
|
component={BinList}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="pages-content">
|
<div className="pages-content">
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
.page-bin {
|
.page-bin-list {
|
||||||
|
.buttons {
|
||||||
|
.ant-btn {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+140
-49
@@ -6,7 +6,16 @@ import Pager from '../../common/model/base/Pager';
|
|||||||
import Matter from '../../common/model/matter/Matter';
|
import Matter from '../../common/model/matter/Matter';
|
||||||
import Moon from '../../common/model/global/Moon';
|
import Moon from '../../common/model/global/Moon';
|
||||||
import SortDirection from '../../common/model/base/SortDirection';
|
import SortDirection from '../../common/model/base/SortDirection';
|
||||||
import { Button, Col, Empty, Input, Modal, Pagination, Row, Space } from 'antd';
|
import {
|
||||||
|
Button,
|
||||||
|
Col,
|
||||||
|
Empty,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
Pagination,
|
||||||
|
Row,
|
||||||
|
Space as AntdSpace,
|
||||||
|
} from 'antd';
|
||||||
import MessageBoxUtil from '../../common/util/MessageBoxUtil';
|
import MessageBoxUtil from '../../common/util/MessageBoxUtil';
|
||||||
import {
|
import {
|
||||||
CloseCircleOutlined,
|
CloseCircleOutlined,
|
||||||
@@ -19,15 +28,26 @@ import {
|
|||||||
import ImagePreviewer from '../widget/previewer/ImagePreviewer';
|
import ImagePreviewer from '../widget/previewer/ImagePreviewer';
|
||||||
import { UserRole } from '../../common/model/user/UserRole';
|
import { UserRole } from '../../common/model/user/UserRole';
|
||||||
import Lang from '../../common/model/global/Lang';
|
import Lang from '../../common/model/global/Lang';
|
||||||
import TankTitle from '../widget/TankTitle';
|
|
||||||
import BinMatterPanel from './widget/BinMatterPanel';
|
import BinMatterPanel from './widget/BinMatterPanel';
|
||||||
import Sun from '../../common/model/global/Sun';
|
import Sun from '../../common/model/global/Sun';
|
||||||
|
import Space from '../../common/model/space/Space';
|
||||||
|
import BreadcrumbPanel from '../widget/BreadcrumbPanel';
|
||||||
|
import BreadcrumbModel from '../../common/model/base/option/BreadcrumbModel';
|
||||||
|
import SpaceMember from '../../common/model/space/member/SpaceMember';
|
||||||
|
import { SpaceMemberRole } from '../../common/model/space/member/SpaceMemberRole';
|
||||||
|
|
||||||
interface IProps extends RouteComponentProps {}
|
interface IProps
|
||||||
|
extends RouteComponentProps<{
|
||||||
|
spaceUuid?: string;
|
||||||
|
}> {}
|
||||||
|
|
||||||
interface IState {}
|
interface IState {}
|
||||||
|
|
||||||
export default class List extends TankComponent<IProps, IState> {
|
export default class List extends TankComponent<IProps, IState> {
|
||||||
|
//当前空间信息
|
||||||
|
space = new Space();
|
||||||
|
// 当前用户在当前空间下的成员信息
|
||||||
|
spaceMember = new SpaceMember();
|
||||||
//当前文件夹信息。
|
//当前文件夹信息。
|
||||||
matter = new Matter();
|
matter = new Matter();
|
||||||
//当前选中的文件
|
//当前选中的文件
|
||||||
@@ -41,7 +61,26 @@ export default class List extends TankComponent<IProps, IState> {
|
|||||||
super(props);
|
super(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initSpace() {
|
||||||
|
if (this.props.match.params.spaceUuid) {
|
||||||
|
this.space.uuid = this.props.match.params.spaceUuid;
|
||||||
|
this.space.httpDetail(() => this.updateUI());
|
||||||
|
this.spaceMember.httpMine(this.props.match.params.spaceUuid, () =>
|
||||||
|
this.updateUI()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果在空间下,有些操作权限只对管理员和读写成员开放
|
||||||
|
checkHandlePermission() {
|
||||||
|
if (!this.props.match.params.spaceUuid) return true;
|
||||||
|
return [SpaceMemberRole.ADMIN, SpaceMemberRole.READ_WRITE].includes(
|
||||||
|
this.spaceMember.role!
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
this.initSpace();
|
||||||
//刷新一下列表
|
//刷新一下列表
|
||||||
if (this.user.role === UserRole.ADMINISTRATOR) {
|
if (this.user.role === UserRole.ADMINISTRATOR) {
|
||||||
this.pager.getFilter('userUuid')!.visible = true;
|
this.pager.getFilter('userUuid')!.visible = true;
|
||||||
@@ -73,6 +112,11 @@ export default class List extends TankComponent<IProps, IState> {
|
|||||||
this.pager.setFilterValue('orderDir', SortDirection.DESC);
|
this.pager.setFilterValue('orderDir', SortDirection.DESC);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果在共享空间中
|
||||||
|
if (this.props.match.params.spaceUuid) {
|
||||||
|
this.pager.setFilterValue('spaceUuid', this.props.match.params.spaceUuid);
|
||||||
|
}
|
||||||
|
|
||||||
// 过滤掉被软删除的文件
|
// 过滤掉被软删除的文件
|
||||||
this.pager.setFilterValue('deleted', true);
|
this.pager.setFilterValue('deleted', true);
|
||||||
|
|
||||||
@@ -115,13 +159,20 @@ export default class List extends TankComponent<IProps, IState> {
|
|||||||
this.checkMatter();
|
this.checkMatter();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSpaceUuid() {
|
||||||
|
if (this.props.match.params.spaceUuid) {
|
||||||
|
return this.props.match.params.spaceUuid;
|
||||||
|
}
|
||||||
|
return this.user.spaceUuid!;
|
||||||
|
}
|
||||||
|
|
||||||
deleteBatch() {
|
deleteBatch() {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: Lang.t('actionCanNotRevertConfirm'),
|
title: Lang.t('actionCanNotRevertConfirm'),
|
||||||
icon: <ExclamationCircleFilled twoToneColor="#FFDC00" />,
|
icon: <ExclamationCircleFilled twoToneColor="#FFDC00" />,
|
||||||
onOk: () => {
|
onOk: () => {
|
||||||
const uuids = this.selectedMatters.map((i) => i.uuid).toString();
|
const uuids = this.selectedMatters.map((i) => i.uuid).toString();
|
||||||
Matter.httpDeleteBatch(uuids, this.user.spaceUuid!, () => {
|
Matter.httpDeleteBatch(uuids, this.getSpaceUuid(), () => {
|
||||||
MessageBoxUtil.success(Lang.t('operationSuccess'));
|
MessageBoxUtil.success(Lang.t('operationSuccess'));
|
||||||
this.refresh();
|
this.refresh();
|
||||||
});
|
});
|
||||||
@@ -135,7 +186,7 @@ export default class List extends TankComponent<IProps, IState> {
|
|||||||
icon: <ExclamationCircleFilled twoToneColor="#FFDC00" />,
|
icon: <ExclamationCircleFilled twoToneColor="#FFDC00" />,
|
||||||
onOk: () => {
|
onOk: () => {
|
||||||
const uuids = this.selectedMatters.map((i) => i.uuid).toString();
|
const uuids = this.selectedMatters.map((i) => i.uuid).toString();
|
||||||
Matter.httpRecoveryBatch(uuids, () => {
|
Matter.httpRecoveryBatch(uuids, this.getSpaceUuid(), () => {
|
||||||
MessageBoxUtil.success(Lang.t('operationSuccess'));
|
MessageBoxUtil.success(Lang.t('operationSuccess'));
|
||||||
this.refresh();
|
this.refresh();
|
||||||
});
|
});
|
||||||
@@ -150,6 +201,12 @@ export default class List extends TankComponent<IProps, IState> {
|
|||||||
this.pager.setFilterValue('orderDir', SortDirection.DESC);
|
this.pager.setFilterValue('orderDir', SortDirection.DESC);
|
||||||
this.pager.setFilterValue('name', value);
|
this.pager.setFilterValue('name', value);
|
||||||
this.pager.setFilterValue('deleted', true);
|
this.pager.setFilterValue('deleted', true);
|
||||||
|
if (this.props.match.params.spaceUuid) {
|
||||||
|
this.pager.setFilterValue(
|
||||||
|
'spaceUuid',
|
||||||
|
this.props.match.params.spaceUuid
|
||||||
|
);
|
||||||
|
}
|
||||||
this.pager.httpList();
|
this.pager.httpList();
|
||||||
} else {
|
} else {
|
||||||
this.refresh();
|
this.refresh();
|
||||||
@@ -182,59 +239,87 @@ export default class List extends TankComponent<IProps, IState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
goDetail(matter: Matter) {
|
goDetail(matter: Matter) {
|
||||||
Sun.navigateTo(`/matter/detail/${matter.uuid}`);
|
const prefix = this.props.match.params.spaceUuid
|
||||||
|
? `/space/${this.props.match.params.spaceUuid}`
|
||||||
|
: '';
|
||||||
|
Sun.navigateTo(`${prefix}/matter/detail/${matter.uuid}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getBreadcrumbModels(): BreadcrumbModel[] {
|
||||||
|
const breadcrumbModels: BreadcrumbModel[] = [
|
||||||
|
{
|
||||||
|
name: Lang.t('layout.bin'),
|
||||||
|
path: '',
|
||||||
|
query: {},
|
||||||
|
displayDirect: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
if (this.space.name) {
|
||||||
|
breadcrumbModels.unshift({
|
||||||
|
name: this.space.name,
|
||||||
|
path: '/space',
|
||||||
|
query: {},
|
||||||
|
displayDirect: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return breadcrumbModels;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { pager, selectedMatters } = this;
|
const { pager, selectedMatters } = this;
|
||||||
return (
|
return (
|
||||||
<div className="matter-list">
|
<div className="page-bin-list">
|
||||||
<TankTitle name={Lang.t('layout.bin')} />
|
<BreadcrumbPanel breadcrumbModels={this.getBreadcrumbModels()} />
|
||||||
|
|
||||||
<Row className="mt10">
|
<Row className="mt10">
|
||||||
<Col xs={24} sm={24} md={14} lg={16}>
|
<Col xs={24} sm={24} md={14} lg={16}>
|
||||||
<Space className="buttons">
|
<AntdSpace className="buttons">
|
||||||
{selectedMatters.length !== pager.data.length ? (
|
{this.checkHandlePermission() && (
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
className="mb10"
|
|
||||||
onClick={() => this.checkAll()}
|
|
||||||
>
|
|
||||||
<PlusSquareOutlined />
|
|
||||||
{Lang.t('selectAll')}
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
{pager.data.length &&
|
|
||||||
selectedMatters.length === pager.data.length ? (
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
className="mb10"
|
|
||||||
onClick={() => this.checkNone()}
|
|
||||||
>
|
|
||||||
<MinusSquareOutlined />
|
|
||||||
{Lang.t('cancel')}
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
{selectedMatters.length ? (
|
|
||||||
<>
|
<>
|
||||||
<Button
|
{selectedMatters.length !== pager.data.length ? (
|
||||||
type="primary"
|
<Button
|
||||||
className="mb10"
|
type="primary"
|
||||||
onClick={() => this.recoverBatch()}
|
className="mb10"
|
||||||
>
|
onClick={() => this.checkAll()}
|
||||||
<RedoOutlined />
|
>
|
||||||
{Lang.t('matter.recovery')}
|
<PlusSquareOutlined />
|
||||||
</Button>
|
{Lang.t('selectAll')}
|
||||||
<Button
|
</Button>
|
||||||
type="primary"
|
) : null}
|
||||||
className="mb10"
|
{pager.data.length &&
|
||||||
onClick={() => this.deleteBatch()}
|
selectedMatters.length === pager.data.length ? (
|
||||||
>
|
<Button
|
||||||
<CloseCircleOutlined />
|
type="primary"
|
||||||
{Lang.t('matter.hardDelete')}
|
className="mb10"
|
||||||
</Button>
|
onClick={() => this.checkNone()}
|
||||||
|
>
|
||||||
|
<MinusSquareOutlined />
|
||||||
|
{Lang.t('cancel')}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
{selectedMatters.length ? (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
className="mb10"
|
||||||
|
onClick={() => this.recoverBatch()}
|
||||||
|
>
|
||||||
|
<RedoOutlined />
|
||||||
|
{Lang.t('matter.recovery')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
className="mb10"
|
||||||
|
onClick={() => this.deleteBatch()}
|
||||||
|
>
|
||||||
|
<CloseCircleOutlined />
|
||||||
|
{Lang.t('matter.hardDelete')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</>
|
</>
|
||||||
) : null}
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -244,7 +329,7 @@ export default class List extends TankComponent<IProps, IState> {
|
|||||||
<SyncOutlined />
|
<SyncOutlined />
|
||||||
{Lang.t('refresh')}
|
{Lang.t('refresh')}
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</AntdSpace>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={24} md={10} lg={8}>
|
<Col xs={24} sm={24} md={10} lg={8}>
|
||||||
<Input.Search
|
<Input.Search
|
||||||
@@ -262,7 +347,13 @@ export default class List extends TankComponent<IProps, IState> {
|
|||||||
pager.data.map((matter) => (
|
pager.data.map((matter) => (
|
||||||
<BinMatterPanel
|
<BinMatterPanel
|
||||||
key={matter.uuid}
|
key={matter.uuid}
|
||||||
|
mode={this.props.match.params.spaceUuid ? 'space' : 'normal'}
|
||||||
matter={matter}
|
matter={matter}
|
||||||
|
spaceMemberRole={
|
||||||
|
this.props.match.params.spaceUuid
|
||||||
|
? this.spaceMember.role!
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
onDeleteSuccess={() => this.refresh()}
|
onDeleteSuccess={() => this.refresh()}
|
||||||
onRecoverySuccess={() => this.refresh()}
|
onRecoverySuccess={() => this.refresh()}
|
||||||
onCheckMatter={(m) => this.checkMatter(m)}
|
onCheckMatter={(m) => this.checkMatter(m)}
|
||||||
|
|||||||
@@ -18,9 +18,12 @@ import { Checkbox, Dropdown, Menu, Modal, Tooltip } from 'antd';
|
|||||||
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||||
import SafeUtil from '../../../common/util/SafeUtil';
|
import SafeUtil from '../../../common/util/SafeUtil';
|
||||||
import Lang from '../../../common/model/global/Lang';
|
import Lang from '../../../common/model/global/Lang';
|
||||||
|
import { SpaceMemberRole } from '../../../common/model/space/member/SpaceMemberRole';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
matter: Matter;
|
matter: Matter;
|
||||||
|
mode: 'normal' | 'space'; // normal:正常模式,space:空间模式
|
||||||
|
spaceMemberRole?: SpaceMemberRole; // 用户在当前空间下的角色,只有在space模式下才有值
|
||||||
onDeleteSuccess: () => any;
|
onDeleteSuccess: () => any;
|
||||||
onRecoverySuccess: () => any;
|
onRecoverySuccess: () => any;
|
||||||
onCheckMatter: (matter?: Matter) => any;
|
onCheckMatter: (matter?: Matter) => any;
|
||||||
@@ -83,20 +86,20 @@ export default class BinMatterPanel extends TankComponent<IProps, IState> {
|
|||||||
this.updateUI();
|
this.updateUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果在空间下,有些操作权限只对管理员和读写成员开放
|
||||||
|
checkHandlePermission() {
|
||||||
|
if (this.props.mode === 'normal') return true;
|
||||||
|
return [SpaceMemberRole.ADMIN, SpaceMemberRole.READ_WRITE].includes(
|
||||||
|
this.props.spaceMemberRole!
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
renderPcOperation() {
|
renderPcOperation() {
|
||||||
const { matter } = this.props;
|
const { matter } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="right-part">
|
<div className="right-part">
|
||||||
<span className="matter-operation text-theme">
|
<span className="matter-operation text-theme">
|
||||||
<Tooltip title={Lang.t('matter.recovery')}>
|
|
||||||
<RedoOutlined
|
|
||||||
className="btn-action"
|
|
||||||
onClick={(e) =>
|
|
||||||
SafeUtil.stopPropagationWrap(e)(this.recoveryMatter())
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title={Lang.t('matter.fileDetail')}>
|
<Tooltip title={Lang.t('matter.fileDetail')}>
|
||||||
<InfoCircleOutlined
|
<InfoCircleOutlined
|
||||||
className="btn-action"
|
className="btn-action"
|
||||||
@@ -107,14 +110,26 @@ export default class BinMatterPanel extends TankComponent<IProps, IState> {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={Lang.t('matter.hardDelete')}>
|
{this.checkHandlePermission() && (
|
||||||
<CloseCircleOutlined
|
<>
|
||||||
className="btn-action text-danger"
|
<Tooltip title={Lang.t('matter.recovery')}>
|
||||||
onClick={(e) =>
|
<RedoOutlined
|
||||||
SafeUtil.stopPropagationWrap(e)(this.hardDeleteMatter())
|
className="btn-action"
|
||||||
}
|
onClick={(e) =>
|
||||||
/>
|
SafeUtil.stopPropagationWrap(e)(this.recoveryMatter())
|
||||||
</Tooltip>
|
}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title={Lang.t('matter.hardDelete')}>
|
||||||
|
<CloseCircleOutlined
|
||||||
|
className="btn-action text-danger"
|
||||||
|
onClick={(e) =>
|
||||||
|
SafeUtil.stopPropagationWrap(e)(this.hardDeleteMatter())
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
<Tooltip title={Lang.t('matter.size')}>
|
<Tooltip title={Lang.t('matter.size')}>
|
||||||
<span className="matter-size">
|
<span className="matter-size">
|
||||||
@@ -131,7 +146,7 @@ export default class BinMatterPanel extends TankComponent<IProps, IState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getHandles() {
|
getHandles() {
|
||||||
return [
|
const handles = [
|
||||||
<div
|
<div
|
||||||
className="cell-btn navy"
|
className="cell-btn navy"
|
||||||
onClick={(e) =>
|
onClick={(e) =>
|
||||||
@@ -143,23 +158,31 @@ export default class BinMatterPanel extends TankComponent<IProps, IState> {
|
|||||||
<InfoCircleOutlined className="btn-action mr5" />
|
<InfoCircleOutlined className="btn-action mr5" />
|
||||||
{Lang.t('matter.fileDetail')}
|
{Lang.t('matter.fileDetail')}
|
||||||
</div>,
|
</div>,
|
||||||
<div
|
|
||||||
className="cell-btn navy"
|
|
||||||
onClick={(e) => SafeUtil.stopPropagationWrap(e)(this.recoveryMatter())}
|
|
||||||
>
|
|
||||||
<RedoOutlined className="btn-action mr5" />
|
|
||||||
{Lang.t('matter.recovery')}
|
|
||||||
</div>,
|
|
||||||
<div
|
|
||||||
className="cell-btn text-danger"
|
|
||||||
onClick={(e) =>
|
|
||||||
SafeUtil.stopPropagationWrap(e)(this.hardDeleteMatter())
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<CloseCircleOutlined className="btn-action mr5" />
|
|
||||||
{Lang.t('matter.hardDelete')}
|
|
||||||
</div>,
|
|
||||||
];
|
];
|
||||||
|
if (this.checkHandlePermission()) {
|
||||||
|
handles.push(
|
||||||
|
<div
|
||||||
|
className="cell-btn navy"
|
||||||
|
onClick={(e) =>
|
||||||
|
SafeUtil.stopPropagationWrap(e)(this.recoveryMatter())
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<RedoOutlined className="btn-action mr5" />
|
||||||
|
{Lang.t('matter.recovery')}
|
||||||
|
</div>,
|
||||||
|
<div
|
||||||
|
className="cell-btn text-danger"
|
||||||
|
onClick={(e) =>
|
||||||
|
SafeUtil.stopPropagationWrap(e)(this.hardDeleteMatter())
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CloseCircleOutlined className="btn-action mr5" />
|
||||||
|
{Lang.t('matter.hardDelete')}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return handles;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderMobileOperation() {
|
renderMobileOperation() {
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import Lang from '../../common/model/global/Lang';
|
|||||||
import InfoCell from '../widget/InfoCell';
|
import InfoCell from '../widget/InfoCell';
|
||||||
import TankContentCard from '../widget/TankContentCard';
|
import TankContentCard from '../widget/TankContentCard';
|
||||||
import Color from '../../common/model/base/option/Color';
|
import Color from '../../common/model/base/option/Color';
|
||||||
|
import SpaceMember from '../../common/model/space/member/SpaceMember';
|
||||||
|
import { SpaceMemberRole } from '../../common/model/space/member/SpaceMemberRole';
|
||||||
|
|
||||||
interface RouteParam {
|
interface RouteParam {
|
||||||
spaceUuid?: string;
|
spaceUuid?: string;
|
||||||
@@ -27,6 +29,8 @@ interface IState {}
|
|||||||
|
|
||||||
export default class Detail extends TankComponent<IProps, IState> {
|
export default class Detail extends TankComponent<IProps, IState> {
|
||||||
matter = new Matter();
|
matter = new Matter();
|
||||||
|
// 当前用户在当前空间下的成员信息
|
||||||
|
spaceMember = new SpaceMember();
|
||||||
downloadToken = new DownloadToken();
|
downloadToken = new DownloadToken();
|
||||||
|
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
@@ -44,6 +48,9 @@ export default class Detail extends TankComponent<IProps, IState> {
|
|||||||
this.downloadToken.httpFetchDownloadToken(this.matter.uuid!);
|
this.downloadToken.httpFetchDownloadToken(this.matter.uuid!);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (spaceUuid) {
|
||||||
|
this.spaceMember.httpMine(spaceUuid, () => this.updateUI());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
copyLink() {
|
copyLink() {
|
||||||
@@ -61,10 +68,18 @@ export default class Detail extends TankComponent<IProps, IState> {
|
|||||||
this.matter.httpDetail(() => {
|
this.matter.httpDetail(() => {
|
||||||
this.updateUI();
|
this.updateUI();
|
||||||
});
|
});
|
||||||
MessageBoxUtil.success(Lang.t('copySuccess'));
|
MessageBoxUtil.success(Lang.t('matter.recoverySuccess'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果在空间下,有些操作权限只对管理员和读写成员开放
|
||||||
|
checkHandlePermission() {
|
||||||
|
if (!this.props.match.params.spaceUuid) return true;
|
||||||
|
return [SpaceMemberRole.ADMIN, SpaceMemberRole.READ_WRITE].includes(
|
||||||
|
this.spaceMember.role!
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { matter } = this;
|
const { matter } = this;
|
||||||
return (
|
return (
|
||||||
@@ -123,7 +138,7 @@ export default class Detail extends TankComponent<IProps, IState> {
|
|||||||
? Lang.t('matter.oneTimeLink')
|
? Lang.t('matter.oneTimeLink')
|
||||||
: Lang.t('matter.copyPath')}
|
: Lang.t('matter.copyPath')}
|
||||||
</a>
|
</a>
|
||||||
{matter.deleted ? (
|
{matter.deleted && this.checkHandlePermission() ? (
|
||||||
<a onClick={() => this.recovery()}>
|
<a onClick={() => this.recovery()}>
|
||||||
{Lang.t('matter.recovery')}
|
{Lang.t('matter.recovery')}
|
||||||
</a>
|
</a>
|
||||||
@@ -132,13 +147,13 @@ export default class Detail extends TankComponent<IProps, IState> {
|
|||||||
</InfoCell>
|
</InfoCell>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
{matter.dir && matter.deleted && (
|
{matter.dir && matter.deleted && this.checkHandlePermission() ? (
|
||||||
<InfoCell name={Lang.t('matter.operations')}>
|
<InfoCell name={Lang.t('matter.operations')}>
|
||||||
<a onClick={() => this.recovery()}>
|
<a onClick={() => this.recovery()}>
|
||||||
{Lang.t('matter.recovery')}
|
{Lang.t('matter.recovery')}
|
||||||
</a>
|
</a>
|
||||||
</InfoCell>
|
</InfoCell>
|
||||||
)}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</TankContentCard>
|
</TankContentCard>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,7 +24,10 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.matter-pagination {
|
|
||||||
margin-top: 10px;
|
.matter-list-content {
|
||||||
|
.ant-list-pagination {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+150
-67
@@ -20,6 +20,7 @@ import {
|
|||||||
Row,
|
Row,
|
||||||
Space as AntdSpace,
|
Space as AntdSpace,
|
||||||
Upload,
|
Upload,
|
||||||
|
List as AntdList,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import MessageBoxUtil from '../../common/util/MessageBoxUtil';
|
import MessageBoxUtil from '../../common/util/MessageBoxUtil';
|
||||||
import {
|
import {
|
||||||
@@ -33,6 +34,7 @@ import {
|
|||||||
MenuOutlined,
|
MenuOutlined,
|
||||||
MinusSquareOutlined,
|
MinusSquareOutlined,
|
||||||
PlusSquareOutlined,
|
PlusSquareOutlined,
|
||||||
|
SearchOutlined,
|
||||||
ShareAltOutlined,
|
ShareAltOutlined,
|
||||||
SyncOutlined,
|
SyncOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
@@ -56,6 +58,7 @@ import Space from '../../common/model/space/Space';
|
|||||||
import MatterCrawlModal from './widget/MatterCrawlModal';
|
import MatterCrawlModal from './widget/MatterCrawlModal';
|
||||||
import SpaceMember from '../../common/model/space/member/SpaceMember';
|
import SpaceMember from '../../common/model/space/member/SpaceMember';
|
||||||
import { SpaceMemberRole } from '../../common/model/space/member/SpaceMemberRole';
|
import { SpaceMemberRole } from '../../common/model/space/member/SpaceMemberRole';
|
||||||
|
import { debounce } from '../../common/util/OptimizeUtil';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
spaceUuid?: string;
|
spaceUuid?: string;
|
||||||
@@ -76,8 +79,6 @@ export default class List extends TankComponent<IProps, IState> {
|
|||||||
|
|
||||||
//当前选中的文件
|
//当前选中的文件
|
||||||
selectedMatters: Matter[] = [];
|
selectedMatters: Matter[] = [];
|
||||||
//搜索的文字
|
|
||||||
searchText: string | null = null;
|
|
||||||
//获取分页的一个帮助器
|
//获取分页的一个帮助器
|
||||||
pager = new Pager<Matter>(this, Matter, Pager.MAX_PAGE_SIZE);
|
pager = new Pager<Matter>(this, Matter, Pager.MAX_PAGE_SIZE);
|
||||||
user = Moon.getSingleton().user;
|
user = Moon.getSingleton().user;
|
||||||
@@ -111,6 +112,21 @@ export default class List extends TankComponent<IProps, IState> {
|
|||||||
// 抓取模态窗状态
|
// 抓取模态窗状态
|
||||||
crawlModalVisible = false;
|
crawlModalVisible = false;
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
searchState: {
|
||||||
|
keyword?: string;
|
||||||
|
loading: boolean;
|
||||||
|
data: Matter[];
|
||||||
|
} = {
|
||||||
|
loading: false,
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// 当前是否有关键词是搜索状态,逻辑上有不一样
|
||||||
|
isSearch() {
|
||||||
|
return !!this.searchState.keyword;
|
||||||
|
}
|
||||||
|
|
||||||
//持有全局唯一的实例。
|
//持有全局唯一的实例。
|
||||||
static instance: List | null = null;
|
static instance: List | null = null;
|
||||||
|
|
||||||
@@ -203,6 +219,7 @@ export default class List extends TankComponent<IProps, IState> {
|
|||||||
|
|
||||||
componentWillReceiveProps() {
|
componentWillReceiveProps() {
|
||||||
this.pager.enableHistory();
|
this.pager.enableHistory();
|
||||||
|
this.resetSearch();
|
||||||
this.refresh();
|
this.refresh();
|
||||||
this.refreshBreadcrumbs();
|
this.refreshBreadcrumbs();
|
||||||
}
|
}
|
||||||
@@ -218,8 +235,15 @@ export default class List extends TankComponent<IProps, IState> {
|
|||||||
refresh() {
|
refresh() {
|
||||||
// 清空暂存区
|
// 清空暂存区
|
||||||
this.selectedMatters = [];
|
this.selectedMatters = [];
|
||||||
// 刷新文件列表
|
|
||||||
this.refreshPager();
|
if (this.isSearch()) {
|
||||||
|
// 刷新搜索列表
|
||||||
|
console.log('search this2', this.searchState.keyword);
|
||||||
|
this.refreshSearch(this.searchState.keyword);
|
||||||
|
} else {
|
||||||
|
// 刷新分页列表
|
||||||
|
this.refreshPager();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshPager() {
|
refreshPager() {
|
||||||
@@ -244,6 +268,15 @@ export default class List extends TankComponent<IProps, IState> {
|
|||||||
this.pager.httpList();
|
this.pager.httpList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getList() {
|
||||||
|
if (this.isSearch()) return this.searchState;
|
||||||
|
return this.pager;
|
||||||
|
}
|
||||||
|
|
||||||
|
getListData() {
|
||||||
|
return this.getList().data;
|
||||||
|
}
|
||||||
|
|
||||||
checkMatter(matter?: Matter) {
|
checkMatter(matter?: Matter) {
|
||||||
if (matter) {
|
if (matter) {
|
||||||
if (matter.check) {
|
if (matter.check) {
|
||||||
@@ -257,7 +290,7 @@ export default class List extends TankComponent<IProps, IState> {
|
|||||||
} else {
|
} else {
|
||||||
//统计所有的勾选
|
//统计所有的勾选
|
||||||
this.selectedMatters = [];
|
this.selectedMatters = [];
|
||||||
this.pager.data.forEach((matter) => {
|
this.getListData().forEach((matter) => {
|
||||||
if (matter.check) {
|
if (matter.check) {
|
||||||
this.selectedMatters.push(matter);
|
this.selectedMatters.push(matter);
|
||||||
}
|
}
|
||||||
@@ -267,14 +300,14 @@ export default class List extends TankComponent<IProps, IState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
checkAll() {
|
checkAll() {
|
||||||
this.pager.data.forEach((i) => {
|
this.getListData().forEach((i) => {
|
||||||
i.check = true;
|
i.check = true;
|
||||||
});
|
});
|
||||||
this.checkMatter();
|
this.checkMatter();
|
||||||
}
|
}
|
||||||
|
|
||||||
checkNone() {
|
checkNone() {
|
||||||
this.pager.data.forEach((i) => {
|
this.getListData().forEach((i) => {
|
||||||
i.check = false;
|
i.check = false;
|
||||||
});
|
});
|
||||||
this.checkMatter();
|
this.checkMatter();
|
||||||
@@ -283,7 +316,6 @@ export default class List extends TankComponent<IProps, IState> {
|
|||||||
deleteBatch() {
|
deleteBatch() {
|
||||||
const uuids = this.selectedMatters.map((i) => i.uuid).toString();
|
const uuids = this.selectedMatters.map((i) => i.uuid).toString();
|
||||||
MatterDeleteModal.open(
|
MatterDeleteModal.open(
|
||||||
!!this.props.spaceUuid,
|
|
||||||
() => {
|
() => {
|
||||||
Matter.httpSoftDeleteBatch(uuids, this.getSpaceUuid(), () => {
|
Matter.httpSoftDeleteBatch(uuids, this.getSpaceUuid(), () => {
|
||||||
MessageBoxUtil.success(Lang.t('operationSuccess'));
|
MessageBoxUtil.success(Lang.t('operationSuccess'));
|
||||||
@@ -440,7 +472,8 @@ export default class List extends TankComponent<IProps, IState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
m.file = file;
|
m.file = file;
|
||||||
m.httpUpload(
|
// Use smart upload: automatically choose chunked upload for large files
|
||||||
|
m.httpSmartUpload(
|
||||||
() => {
|
() => {
|
||||||
const index = this.uploadMattersMap[this.getSpaceUuid()].findIndex(
|
const index = this.uploadMattersMap[this.getSpaceUuid()].findIndex(
|
||||||
(matter) => matter === m
|
(matter) => matter === m
|
||||||
@@ -474,16 +507,6 @@ export default class List extends TankComponent<IProps, IState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
searchFile(value?: string) {
|
|
||||||
this.pager.setFilterValue('name', value);
|
|
||||||
this.pager.setFilterValue('page', 0);
|
|
||||||
this.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
changeSearch(e: any) {
|
|
||||||
if (!e.currentTarget.value) this.searchFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
changePage(page: number) {
|
changePage(page: number) {
|
||||||
this.pager.page = page - 1; // page的页数0基
|
this.pager.page = page - 1; // page的页数0基
|
||||||
this.pager.httpList();
|
this.pager.httpList();
|
||||||
@@ -510,7 +533,7 @@ export default class List extends TankComponent<IProps, IState> {
|
|||||||
previewImage(matter: Matter) {
|
previewImage(matter: Matter) {
|
||||||
let imageArray: string[] = [];
|
let imageArray: string[] = [];
|
||||||
let startIndex = -1;
|
let startIndex = -1;
|
||||||
this.pager.data.forEach((item) => {
|
this.getListData().forEach((item) => {
|
||||||
if (item.isImage()) {
|
if (item.isImage()) {
|
||||||
imageArray.push(item.getPreviewUrl());
|
imageArray.push(item.getPreviewUrl());
|
||||||
if (item.uuid === matter.uuid) {
|
if (item.uuid === matter.uuid) {
|
||||||
@@ -546,9 +569,7 @@ export default class List extends TankComponent<IProps, IState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
goToDirectory(id: string) {
|
goToDirectory(id: string) {
|
||||||
this.searchText = null;
|
|
||||||
this.pager.setFilterValue('puuid', id);
|
this.pager.setFilterValue('puuid', id);
|
||||||
this.pager.removeFilter('name');
|
|
||||||
this.pager.page = 0;
|
this.pager.page = 0;
|
||||||
const query = this.pager.getParams();
|
const query = this.pager.getParams();
|
||||||
Sun.navigateQueryTo({ path: this.getMatterListPath(), query });
|
Sun.navigateQueryTo({ path: this.getMatterListPath(), query });
|
||||||
@@ -643,6 +664,52 @@ export default class List extends TankComponent<IProps, IState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleChangeSearch = (value?: string) => {
|
||||||
|
// 清空暂存区
|
||||||
|
this.selectedMatters = [];
|
||||||
|
this.searchState.keyword = value;
|
||||||
|
this.updateUI();
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
// search
|
||||||
|
console.log('search this', value);
|
||||||
|
this.refreshSearch(value);
|
||||||
|
} else {
|
||||||
|
// reset
|
||||||
|
this.resetSearch();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
refreshSearch = debounce((value: string) => {
|
||||||
|
this.searchState.loading = true;
|
||||||
|
this.updateUI();
|
||||||
|
console.log('this.searchState.keyword', this.searchState.keyword);
|
||||||
|
Matter.httpSearch(
|
||||||
|
{
|
||||||
|
keyword: value,
|
||||||
|
puuid: this.currentDirectory.uuid || Matter.MATTER_ROOT,
|
||||||
|
spaceUuid: this.getSpaceUuid(),
|
||||||
|
},
|
||||||
|
(data: Matter[]) => {
|
||||||
|
this.searchState.data = data;
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
() => {
|
||||||
|
this.searchState.loading = false;
|
||||||
|
this.updateUI();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
resetSearch() {
|
||||||
|
this.searchState = {
|
||||||
|
keyword: undefined,
|
||||||
|
loading: false,
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
this.updateUI();
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { pager, director, selectedMatters, dragEnterCount } = this;
|
const { pager, director, selectedMatters, dragEnterCount } = this;
|
||||||
return (
|
return (
|
||||||
@@ -657,7 +724,7 @@ export default class List extends TankComponent<IProps, IState> {
|
|||||||
<Row>
|
<Row>
|
||||||
<Col xs={24} sm={24} md={14} lg={16} className="mt10">
|
<Col xs={24} sm={24} md={14} lg={16} className="mt10">
|
||||||
<AntdSpace className="buttons">
|
<AntdSpace className="buttons">
|
||||||
{selectedMatters.length !== pager.data.length ? (
|
{selectedMatters.length !== this.getListData().length ? (
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
className="mb10"
|
className="mb10"
|
||||||
@@ -667,8 +734,8 @@ export default class List extends TankComponent<IProps, IState> {
|
|||||||
{Lang.t('selectAll')}
|
{Lang.t('selectAll')}
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{pager.data.length &&
|
{this.getListData().length &&
|
||||||
selectedMatters.length === pager.data.length ? (
|
selectedMatters.length === this.getListData().length ? (
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
className="mb10"
|
className="mb10"
|
||||||
@@ -680,14 +747,16 @@ export default class List extends TankComponent<IProps, IState> {
|
|||||||
) : null}
|
) : null}
|
||||||
{selectedMatters.length ? (
|
{selectedMatters.length ? (
|
||||||
<>
|
<>
|
||||||
<Button
|
{!this.isSearch() && (
|
||||||
type="primary"
|
<Button
|
||||||
className="mb10"
|
type="primary"
|
||||||
onClick={() => this.downloadZip()}
|
className="mb10"
|
||||||
>
|
onClick={() => this.downloadZip()}
|
||||||
<DownloadOutlined />
|
>
|
||||||
{Lang.t('download')}
|
<DownloadOutlined />
|
||||||
</Button>
|
{Lang.t('download')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{this.checkHandlePermission() && (
|
{this.checkHandlePermission() && (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
@@ -710,7 +779,7 @@ export default class List extends TankComponent<IProps, IState> {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/*共享空间下暂不支持分享功能*/}
|
{/*共享空间下暂不支持分享功能*/}
|
||||||
{!this.isInSpace && (
|
{!this.isInSpace && !this.isSearch() && (
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
className="mb10"
|
className="mb10"
|
||||||
@@ -723,7 +792,7 @@ export default class List extends TankComponent<IProps, IState> {
|
|||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{this.checkHandlePermission() && (
|
{this.checkHandlePermission() && !this.isSearch() && (
|
||||||
<>
|
<>
|
||||||
<Upload
|
<Upload
|
||||||
className="ant-upload"
|
className="ant-upload"
|
||||||
@@ -779,24 +848,29 @@ export default class List extends TankComponent<IProps, IState> {
|
|||||||
</AntdSpace>
|
</AntdSpace>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={24} md={10} lg={8} className="mt10">
|
<Col xs={24} sm={24} md={10} lg={8} className="mt10">
|
||||||
<Input.Search
|
<Input
|
||||||
className="mb10"
|
className="mb10"
|
||||||
placeholder={Lang.t('matter.searchFile')}
|
placeholder={Lang.t('matter.searchFile')}
|
||||||
onSearch={(value) => this.searchFile(value)}
|
prefix={<SearchOutlined />}
|
||||||
onChange={(e) => this.changeSearch(e)}
|
value={this.searchState.keyword}
|
||||||
enterButton
|
onChange={(e) => this.handleChangeSearch(e.target.value)}
|
||||||
|
allowClear
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
{Children.toArray(
|
{Children.toArray(
|
||||||
this.uploadMattersMap[this.getSpaceUuid()]?.map((m) => (
|
this.uploadMattersMap[this.getSpaceUuid()]?.map((m) => (
|
||||||
<UploadMatterPanel matter={m} />
|
<UploadMatterPanel key={m.autoId} matter={m} />
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{pager.data.length ? (
|
{this.getListData().length ? (
|
||||||
<MatterSortPanel pager={pager} refresh={() => this.refresh()} />
|
<MatterSortPanel
|
||||||
|
pager={pager}
|
||||||
|
refresh={() => this.refresh()}
|
||||||
|
disabled={this.isSearch()}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{director.createMode ? (
|
{director.createMode ? (
|
||||||
@@ -809,35 +883,44 @@ export default class List extends TankComponent<IProps, IState> {
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<div>
|
<div>
|
||||||
{pager.loading || pager.data.length ? (
|
{this.getList().loading || this.getList().data.length > 0 ? (
|
||||||
pager.data.map((matter) => (
|
<AntdList
|
||||||
<MatterPanel
|
rowKey="uuid"
|
||||||
mode={this.isInSpace ? 'space' : 'normal'}
|
className="matter-list-content"
|
||||||
spaceMemberRole={
|
loading={this.getList().loading}
|
||||||
this.isInSpace ? this.spaceMember.role! : undefined
|
dataSource={this.getList().data}
|
||||||
}
|
renderItem={(matter) => (
|
||||||
key={matter.uuid!}
|
<MatterPanel
|
||||||
director={director}
|
mode={this.isInSpace ? 'space' : 'normal'}
|
||||||
matter={matter}
|
spaceMemberRole={
|
||||||
onGoToDirectory={(id) => this.goToDirectory(id)}
|
this.isInSpace ? this.spaceMember.role! : undefined
|
||||||
onDeleteSuccess={() => this.pagerAndCapacityRefresh()}
|
}
|
||||||
onCheckMatter={(m) => this.checkMatter(m)}
|
key={matter.uuid}
|
||||||
onPreviewImage={(m) => this.previewImage(m)}
|
director={director}
|
||||||
onGoDetail={(m) => this.goDetail(m)}
|
matter={matter}
|
||||||
/>
|
onGoToDirectory={(id) => this.goToDirectory(id)}
|
||||||
))
|
onDeleteSuccess={() => this.pagerAndCapacityRefresh()}
|
||||||
|
onCheckMatter={(m) => this.checkMatter(m)}
|
||||||
|
onPreviewImage={(m) => this.previewImage(m)}
|
||||||
|
onGoDetail={(m) => this.goDetail(m)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
pagination={
|
||||||
|
this.isSearch()
|
||||||
|
? false
|
||||||
|
: {
|
||||||
|
onChange: (page) => this.changePage(page),
|
||||||
|
current: pager.page + 1,
|
||||||
|
total: pager.totalItems,
|
||||||
|
pageSize: pager.pageSize,
|
||||||
|
hideOnSinglePage: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Empty description={Lang.t('matter.noContentYet')} />
|
<Empty description={Lang.t('matter.noContentYet')} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Pagination
|
|
||||||
className="matter-pagination pull-right"
|
|
||||||
onChange={(page) => this.changePage(page)}
|
|
||||||
current={pager.page + 1}
|
|
||||||
total={pager.totalItems}
|
|
||||||
pageSize={pager.pageSize}
|
|
||||||
hideOnSinglePage
|
|
||||||
/>
|
|
||||||
{this.crawlModalVisible && (
|
{this.crawlModalVisible && (
|
||||||
<MatterCrawlModal
|
<MatterCrawlModal
|
||||||
spaceUuid={this.getSpaceUuid()}
|
spaceUuid={this.getSpaceUuid()}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import './MatterDeleteModal.less';
|
|||||||
import Preference from '../../../common/model/preference/Preference';
|
import Preference from '../../../common/model/preference/Preference';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
isSpace: boolean; // 是否是空间,空间下暂不展示放入回收站逻辑
|
|
||||||
onSoftDel: () => void;
|
onSoftDel: () => void;
|
||||||
onHardDel: () => void;
|
onHardDel: () => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -23,7 +22,7 @@ export default class MatterDeleteModal extends TankComponent<IProps, IState> {
|
|||||||
|
|
||||||
static preference: Preference = Moon.getSingleton().preference;
|
static preference: Preference = Moon.getSingleton().preference;
|
||||||
|
|
||||||
static open(isSpace: boolean, onSoftDel: () => void, onHardDel: () => void) {
|
static open(onSoftDel: () => void, onHardDel: () => void) {
|
||||||
let modal = Modal.warning({
|
let modal = Modal.warning({
|
||||||
className: 'delete-modal custom-handle-modal',
|
className: 'delete-modal custom-handle-modal',
|
||||||
title: Lang.t('delete'),
|
title: Lang.t('delete'),
|
||||||
@@ -31,7 +30,6 @@ export default class MatterDeleteModal extends TankComponent<IProps, IState> {
|
|||||||
okCancel: false,
|
okCancel: false,
|
||||||
content: (
|
content: (
|
||||||
<MatterDeleteModal
|
<MatterDeleteModal
|
||||||
isSpace={isSpace}
|
|
||||||
onSoftDel={() => {
|
onSoftDel={() => {
|
||||||
onSoftDel();
|
onSoftDel();
|
||||||
modal.destroy();
|
modal.destroy();
|
||||||
@@ -59,16 +57,15 @@ export default class MatterDeleteModal extends TankComponent<IProps, IState> {
|
|||||||
<Button type="primary" danger onClick={this.props.onHardDel}>
|
<Button type="primary" danger onClick={this.props.onHardDel}>
|
||||||
{Lang.t('deleteDirectly')}
|
{Lang.t('deleteDirectly')}
|
||||||
</Button>
|
</Button>
|
||||||
{!this.props.isSpace &&
|
{MatterDeleteModal.preference.getRecycleBinStatus() && (
|
||||||
MatterDeleteModal.preference.getRecycleBinStatus() && (
|
<Button
|
||||||
<Button
|
type="primary"
|
||||||
type="primary"
|
className="ml10"
|
||||||
className="ml10"
|
onClick={this.props.onSoftDel}
|
||||||
onClick={this.props.onSoftDel}
|
>
|
||||||
>
|
{Lang.t('matter.intoRecycleBin')}
|
||||||
{Lang.t('matter.intoRecycleBin')}
|
</Button>
|
||||||
</Button>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -101,7 +101,6 @@ export default class MatterPanel extends TankComponent<IProps, IState> {
|
|||||||
|
|
||||||
deleteMatter() {
|
deleteMatter() {
|
||||||
MatterDeleteModal.open(
|
MatterDeleteModal.open(
|
||||||
this.props.mode === 'space',
|
|
||||||
() => {
|
() => {
|
||||||
this.props.matter.httpSoftDelete(() => {
|
this.props.matter.httpSoftDelete(() => {
|
||||||
MessageBoxUtil.success(Lang.t('operationSuccess'));
|
MessageBoxUtil.success(Lang.t('operationSuccess'));
|
||||||
@@ -274,12 +273,17 @@ export default class MatterPanel extends TankComponent<IProps, IState> {
|
|||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
<Tooltip title={Lang.t('matter.copyPath')}>
|
{/*文件夹不支持复制路径 安全问题*/}
|
||||||
<LinkOutlined
|
{!matter.dir && (
|
||||||
className="btn-action"
|
<Tooltip title={Lang.t('matter.copyPath')}>
|
||||||
onClick={(e) => SafeUtil.stopPropagationWrap(e)(this.clipboard())}
|
<LinkOutlined
|
||||||
/>
|
className="btn-action"
|
||||||
</Tooltip>
|
onClick={(e) =>
|
||||||
|
SafeUtil.stopPropagationWrap(e)(this.clipboard())
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
<Tooltip title={Lang.t('matter.download')}>
|
<Tooltip title={Lang.t('matter.download')}>
|
||||||
<DownloadOutlined
|
<DownloadOutlined
|
||||||
className="btn-action"
|
className="btn-action"
|
||||||
@@ -340,13 +344,17 @@ export default class MatterPanel extends TankComponent<IProps, IState> {
|
|||||||
</div>,
|
</div>,
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
<div
|
...(matter.dir
|
||||||
className="cell-btn navy"
|
? []
|
||||||
onClick={(e) => SafeUtil.stopPropagationWrap(e)(this.clipboard())}
|
: [
|
||||||
>
|
<div
|
||||||
<LinkOutlined className="btn-action mr5" />
|
className="cell-btn navy"
|
||||||
{Lang.t('matter.copyLink')}
|
onClick={(e) => SafeUtil.stopPropagationWrap(e)(this.clipboard())}
|
||||||
</div>,
|
>
|
||||||
|
<LinkOutlined className="btn-action mr5" />
|
||||||
|
{Lang.t('matter.copyLink')}
|
||||||
|
</div>,
|
||||||
|
]),
|
||||||
<div
|
<div
|
||||||
className="cell-btn navy"
|
className="cell-btn navy"
|
||||||
onClick={(e) => SafeUtil.stopPropagationWrap(e)(matter.download())}
|
onClick={(e) => SafeUtil.stopPropagationWrap(e)(matter.download())}
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
line-height: 48px;
|
line-height: 48px;
|
||||||
color: #999;
|
color: #999;
|
||||||
|
&-disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.sort-part {
|
.sort-part {
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import Pager from '../../../common/model/base/Pager';
|
|||||||
interface IProps {
|
interface IProps {
|
||||||
pager: Pager<Matter>;
|
pager: Pager<Matter>;
|
||||||
refresh: () => any;
|
refresh: () => any;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {}
|
interface IState {}
|
||||||
@@ -38,10 +39,14 @@ export default class MatterSortPanel extends TankComponent<IProps, IState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { pager } = this.props;
|
const { pager, disabled } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="matter-sort-panel">
|
<div
|
||||||
|
className={`matter-sort-panel ${
|
||||||
|
disabled ? 'matter-sort-panel-disabled' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className="sort-part sort-part-name"
|
className="sort-part sort-part-name"
|
||||||
onClick={() => this.changeFilterOrder('orderName')}
|
onClick={() => this.changeFilterOrder('orderName')}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
.upload-matter-panel {
|
.upload-matter-panel {
|
||||||
|
|
||||||
.huge-block {
|
.huge-block {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
@@ -15,6 +14,8 @@
|
|||||||
.media {
|
.media {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
|
||||||
.media-body {
|
.media-body {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -23,9 +24,69 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.filename {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merging-icon {
|
||||||
|
color: #52c41a;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-actions {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 10px;
|
||||||
|
|
||||||
|
.action-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #666;
|
||||||
|
transition: color 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.resume-icon {
|
||||||
|
color: #52c41a;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #73d13d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-icon {
|
||||||
|
&:hover {
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
.upload-info {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
|
||||||
|
.uploaded-size {
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-speed {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,17 @@ import React from 'react';
|
|||||||
import TankComponent from '../../../common/component/TankComponent';
|
import TankComponent from '../../../common/component/TankComponent';
|
||||||
import Matter from '../../../common/model/matter/Matter';
|
import Matter from '../../../common/model/matter/Matter';
|
||||||
import StringUtil from '../../../common/util/StringUtil';
|
import StringUtil from '../../../common/util/StringUtil';
|
||||||
import { Progress } from 'antd';
|
import { Progress, Button, Space, Tooltip } from 'antd';
|
||||||
import { CloseOutlined } from '@ant-design/icons';
|
import {
|
||||||
|
CloseOutlined,
|
||||||
|
PauseCircleOutlined,
|
||||||
|
PlayCircleOutlined,
|
||||||
|
CloudUploadOutlined,
|
||||||
|
SyncOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
import './UploadMatterPanel.less';
|
import './UploadMatterPanel.less';
|
||||||
import Lang from '../../../common/model/global/Lang';
|
import Lang from '../../../common/model/global/Lang';
|
||||||
|
import { UploadSessionStatus } from '../../../common/model/chunk/ChunkUploader';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
matter: Matter;
|
matter: Matter;
|
||||||
@@ -33,38 +40,157 @@ export default class UploadMatterPanel extends TankComponent<IProps, IState> {
|
|||||||
this.props.matter.reactComponent = null;
|
this.props.matter.reactComponent = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get status text based on upload status
|
||||||
|
getStatusText(matter: Matter): string {
|
||||||
|
if (!matter.useChunkedUpload) {
|
||||||
|
if (matter.progress < 1) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return Lang.t('matter.finishingTip');
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (matter.uploadStatus) {
|
||||||
|
case UploadSessionStatus.UPLOADING:
|
||||||
|
return `${matter.uploadedChunks}/${matter.totalChunks} ${Lang.t(
|
||||||
|
'matter.chunks'
|
||||||
|
)}`;
|
||||||
|
case UploadSessionStatus.MERGING:
|
||||||
|
return Lang.t('matter.mergingChunks');
|
||||||
|
case UploadSessionStatus.PAUSED:
|
||||||
|
return Lang.t('matter.paused');
|
||||||
|
case UploadSessionStatus.COMPLETED:
|
||||||
|
return Lang.t('matter.completed');
|
||||||
|
case UploadSessionStatus.ERROR:
|
||||||
|
return Lang.t('matter.uploadFailed');
|
||||||
|
case UploadSessionStatus.CANCELLED:
|
||||||
|
return Lang.t('matter.cancelled');
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get progress bar status
|
||||||
|
getProgressStatus(
|
||||||
|
matter: Matter
|
||||||
|
): 'active' | 'success' | 'exception' | 'normal' {
|
||||||
|
if (!matter.useChunkedUpload) {
|
||||||
|
return 'active';
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (matter.uploadStatus) {
|
||||||
|
case UploadSessionStatus.UPLOADING:
|
||||||
|
return 'active';
|
||||||
|
case UploadSessionStatus.MERGING:
|
||||||
|
return 'active';
|
||||||
|
case UploadSessionStatus.PAUSED:
|
||||||
|
return 'normal';
|
||||||
|
case UploadSessionStatus.COMPLETED:
|
||||||
|
return 'success';
|
||||||
|
case UploadSessionStatus.ERROR:
|
||||||
|
return 'exception';
|
||||||
|
case UploadSessionStatus.CANCELLED:
|
||||||
|
return 'exception';
|
||||||
|
default:
|
||||||
|
return 'active';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get progress bar color
|
||||||
|
getStrokeColor(matter: Matter): any {
|
||||||
|
if (!matter.useChunkedUpload) {
|
||||||
|
return {
|
||||||
|
from: '#108ee9',
|
||||||
|
to: '#87d068',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (matter.uploadStatus) {
|
||||||
|
case UploadSessionStatus.PAUSED:
|
||||||
|
return {
|
||||||
|
from: '#faad14',
|
||||||
|
to: '#faad14',
|
||||||
|
};
|
||||||
|
case UploadSessionStatus.ERROR:
|
||||||
|
case UploadSessionStatus.CANCELLED:
|
||||||
|
return {
|
||||||
|
from: '#ff4d4f',
|
||||||
|
to: '#ff4d4f',
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
from: '#108ee9',
|
||||||
|
to: '#87d068',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { matter } = this.props;
|
const { matter } = this.props;
|
||||||
|
const statusText = this.getStatusText(matter);
|
||||||
|
const progressStatus = this.getProgressStatus(matter);
|
||||||
|
const strokeColor = this.getStrokeColor(matter);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="upload-matter-panel">
|
<div className="upload-matter-panel">
|
||||||
{matter.loading ? (
|
{matter.loading ? (
|
||||||
<div className="huge-block clearfix">
|
<div className="huge-block clearfix">
|
||||||
<div className="media">
|
<div className="media">
|
||||||
<div className="media-body">{matter.file!.name}</div>
|
<div className="media-body">
|
||||||
<div className="media-cancel">
|
<span className="filename">{matter.file!.name}</span>
|
||||||
<CloseOutlined onClick={() => matter.cancelUpload()} />
|
</div>
|
||||||
|
<div className="media-actions">
|
||||||
|
<Space>
|
||||||
|
{/* Pause/Resume button for chunked upload */}
|
||||||
|
{matter.canPause() && (
|
||||||
|
<Tooltip title={Lang.t('matter.pause')}>
|
||||||
|
<PauseCircleOutlined
|
||||||
|
className="action-icon"
|
||||||
|
onClick={() => matter.pauseUpload()}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{matter.canResume() && (
|
||||||
|
<Tooltip title={Lang.t('matter.resume')}>
|
||||||
|
<PlayCircleOutlined
|
||||||
|
className="action-icon resume-icon"
|
||||||
|
onClick={() => matter.resumeUpload()}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{/* Cancel button */}
|
||||||
|
<Tooltip title={Lang.t('matter.cancel')}>
|
||||||
|
<CloseOutlined
|
||||||
|
className="action-icon cancel-icon"
|
||||||
|
onClick={() => matter.cancelUpload()}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Progress
|
<Progress
|
||||||
className="progress"
|
className="progress"
|
||||||
strokeColor={{
|
strokeColor={strokeColor}
|
||||||
from: '#108ee9',
|
|
||||||
to: '#87d068',
|
|
||||||
}}
|
|
||||||
format={() => this.format(matter.progress * 100)}
|
format={() => this.format(matter.progress * 100)}
|
||||||
percent={matter.progress * 100}
|
percent={matter.progress * 100}
|
||||||
status="active"
|
status={progressStatus}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div className="upload-info">
|
||||||
{`${Lang.t('matter.uploaded')}: ${StringUtil.humanFileSize(
|
<span className="uploaded-size">
|
||||||
matter.file!.size * matter.progress
|
{`${Lang.t('matter.uploaded')}: ${StringUtil.humanFileSize(
|
||||||
)}/${StringUtil.humanFileSize(matter.file!.size)} ${
|
matter.file!.size * matter.progress
|
||||||
matter.progress < 1
|
)}/${StringUtil.humanFileSize(matter.file!.size)}`}
|
||||||
? `${Lang.t('matter.speed')}:${StringUtil.humanFileSize(
|
</span>
|
||||||
|
{matter.progress < 1 &&
|
||||||
|
matter.uploadStatus === UploadSessionStatus.UPLOADING && (
|
||||||
|
<span className="upload-speed">
|
||||||
|
{` ${Lang.t('matter.speed')}: ${StringUtil.humanFileSize(
|
||||||
matter.speed
|
matter.speed
|
||||||
)}s`
|
)}/s`}
|
||||||
: Lang.t('matter.finishingTip')
|
</span>
|
||||||
}`}
|
)}
|
||||||
|
{statusText && (
|
||||||
|
<span className="status-text">{` - ${statusText}`}</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import TankComponent from '../../common/component/TankComponent';
|
|||||||
import User from '../../common/model/user/User';
|
import User from '../../common/model/user/User';
|
||||||
import Moon from '../../common/model/global/Moon';
|
import Moon from '../../common/model/global/Moon';
|
||||||
import TankTitle from '../widget/TankTitle';
|
import TankTitle from '../widget/TankTitle';
|
||||||
import { Button, Form, Input, InputNumber, Switch } from 'antd';
|
import { Button, Form, Input, InputNumber, Switch, Tooltip } from 'antd';
|
||||||
import { SaveOutlined } from '@ant-design/icons';
|
import { QuestionCircleOutlined, SaveOutlined } from '@ant-design/icons';
|
||||||
import TankContentCard from '../widget/TankContentCard';
|
import TankContentCard from '../widget/TankContentCard';
|
||||||
import Preference from '../../common/model/preference/Preference';
|
import Preference from '../../common/model/preference/Preference';
|
||||||
import { FormInstance } from 'antd/lib/form';
|
import { FormInstance } from 'antd/lib/form';
|
||||||
@@ -191,7 +191,23 @@ export default class Edit extends TankComponent<IProps, IState> {
|
|||||||
<Form.Item
|
<Form.Item
|
||||||
label={Lang.t('preference.allowRegister')}
|
label={Lang.t('preference.allowRegister')}
|
||||||
name="allowRegister"
|
name="allowRegister"
|
||||||
required={true}
|
required
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={
|
||||||
|
<span>
|
||||||
|
{Lang.t('preference.allowChunkUpload')}
|
||||||
|
<Tooltip title={Lang.t('preference.allowChunkUploadTip')}>
|
||||||
|
<QuestionCircleOutlined className="ml5 text-gray" />
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
name="allowChunkUpload"
|
||||||
|
required
|
||||||
valuePropName="checked"
|
valuePropName="checked"
|
||||||
>
|
>
|
||||||
<Switch />
|
<Switch />
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import TankComponent from '../../common/component/TankComponent';
|
|||||||
import User from '../../common/model/user/User';
|
import User from '../../common/model/user/User';
|
||||||
import Moon from '../../common/model/global/Moon';
|
import Moon from '../../common/model/global/Moon';
|
||||||
import TankTitle from '../widget/TankTitle';
|
import TankTitle from '../widget/TankTitle';
|
||||||
import { Button, Divider, Empty, message } from 'antd';
|
import { Button, Divider, Empty, message, Tooltip } from 'antd';
|
||||||
import {
|
import {
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
|
QuestionCircleOutlined,
|
||||||
ScanOutlined,
|
ScanOutlined,
|
||||||
ThunderboltOutlined,
|
ThunderboltOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
@@ -174,6 +175,19 @@ export default class Index extends TankComponent<IProps, IState> {
|
|||||||
{this.preference.allowRegister ? Lang.t('yes') : Lang.t('no')}
|
{this.preference.allowRegister ? Lang.t('yes') : Lang.t('no')}
|
||||||
</InfoCell>
|
</InfoCell>
|
||||||
|
|
||||||
|
<InfoCell
|
||||||
|
name={
|
||||||
|
<span>
|
||||||
|
{Lang.t('preference.allowChunkUpload')}
|
||||||
|
<Tooltip title={Lang.t('preference.allowChunkUploadTip')}>
|
||||||
|
<QuestionCircleOutlined className="ml5 text-gray" />
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{this.preference.allowChunkUpload ? Lang.t('yes') : Lang.t('no')}
|
||||||
|
</InfoCell>
|
||||||
|
|
||||||
<InfoCell name={Lang.t('preference.docLink')}>
|
<InfoCell name={Lang.t('preference.docLink')}>
|
||||||
<a href={'https://tank-doc.eyeblue.cn/zh'} target="_blank">
|
<a href={'https://tank-doc.eyeblue.cn/zh'} target="_blank">
|
||||||
https://tank-doc.eyeblue.cn/zh
|
https://tank-doc.eyeblue.cn/zh
|
||||||
@@ -221,7 +235,7 @@ export default class Index extends TankComponent<IProps, IState> {
|
|||||||
icon={<ScanOutlined />}
|
icon={<ScanOutlined />}
|
||||||
onClick={() => this.immediateScan()}
|
onClick={() => this.immediateScan()}
|
||||||
>
|
>
|
||||||
立即扫描
|
{Lang.t('preference.scanImmediately')}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
''
|
''
|
||||||
@@ -244,8 +258,8 @@ export default class Index extends TankComponent<IProps, IState> {
|
|||||||
{ScanScopeTypeMap[scanConfig.scope].name}
|
{ScanScopeTypeMap[scanConfig.scope].name}
|
||||||
</InfoCell>
|
</InfoCell>
|
||||||
{scanConfig.scope === ScanScopeType.CUSTOM && (
|
{scanConfig.scope === ScanScopeType.CUSTOM && (
|
||||||
<InfoCell name={Lang.t('preference.scanUsers')}>
|
<InfoCell name={Lang.t('preference.scanSpaces')}>
|
||||||
{scanConfig.usernames.join(', ')}
|
{scanConfig.spaceNames.join(', ')}
|
||||||
</InfoCell>
|
</InfoCell>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
+186
-193
@@ -1,215 +1,208 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { RouteComponentProps } from 'react-router-dom';
|
|
||||||
import './ScanEdit.less';
|
|
||||||
import TankComponent from '../../common/component/TankComponent';
|
|
||||||
import Preference from '../../common/model/preference/Preference';
|
|
||||||
import Moon from '../../common/model/global/Moon';
|
|
||||||
import Lang from '../../common/model/global/Lang';
|
import Lang from '../../common/model/global/Lang';
|
||||||
import TankTitle from '../widget/TankTitle';
|
|
||||||
import TankContentCard from '../widget/TankContentCard';
|
import TankContentCard from '../widget/TankContentCard';
|
||||||
|
import TankTitle from '../widget/TankTitle';
|
||||||
|
import Preference from '../../common/model/preference/Preference';
|
||||||
import { Button, Form, Input, Select, Spin, Switch } from 'antd';
|
import { Button, Form, Input, Select, Spin, Switch } from 'antd';
|
||||||
import { FormInstance } from 'antd/lib/form';
|
|
||||||
import { SaveOutlined } from '@ant-design/icons';
|
import { SaveOutlined } from '@ant-design/icons';
|
||||||
import MessageBoxUtil from '../../common/util/MessageBoxUtil';
|
|
||||||
import Sun from '../../common/model/global/Sun';
|
|
||||||
import {
|
|
||||||
ScanScopeType,
|
|
||||||
ScanScopeTypeList,
|
|
||||||
} from '../../common/model/preference/model/ScanScopeType';
|
|
||||||
import {
|
import {
|
||||||
ScanCronType,
|
ScanCronType,
|
||||||
ScanCronTypeList,
|
ScanCronTypeList,
|
||||||
ScanCronTypeValueList,
|
ScanCronTypeValueList,
|
||||||
} from '../../common/model/preference/model/ScanCronType';
|
} from '../../common/model/preference/model/ScanCronType';
|
||||||
|
import {
|
||||||
|
ScanScopeType,
|
||||||
|
ScanScopeTypeList,
|
||||||
|
} from '../../common/model/preference/model/ScanScopeType';
|
||||||
|
import Space from '../../common/model/space/Space';
|
||||||
import Pager from '../../common/model/base/Pager';
|
import Pager from '../../common/model/base/Pager';
|
||||||
import User from '../../common/model/user/User';
|
|
||||||
import SortDirection from '../../common/model/base/SortDirection';
|
import SortDirection from '../../common/model/base/SortDirection';
|
||||||
|
import MessageBoxUtil from '../../common/util/MessageBoxUtil';
|
||||||
|
import Sun from '../../common/model/global/Sun';
|
||||||
|
import { debounce } from '../../common/util/OptimizeUtil';
|
||||||
|
|
||||||
interface IProps extends RouteComponentProps {}
|
const ScanEdit = () => {
|
||||||
|
const layout = {
|
||||||
|
labelCol: { span: 6 },
|
||||||
|
wrapperCol: { span: 18 },
|
||||||
|
};
|
||||||
|
const pager = useRef(new Pager<Space>(null, Space, Pager.MAX_PAGE_SIZE));
|
||||||
|
const preference = useRef(new Preference());
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [spaceList, setSpaceList] = useState<Space[]>([]);
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
interface IState {}
|
const handleFinish = async () => {
|
||||||
|
const values = await form.validateFields();
|
||||||
export default class ScanEdit extends TankComponent<IProps, IState> {
|
preference.current.scanConfig.assign(values);
|
||||||
formRef = React.createRef<FormInstance>();
|
preference.current.httpSaveScan(function () {
|
||||||
|
|
||||||
preference: Preference = Moon.getSingleton().preference;
|
|
||||||
pager: Pager<User> = new Pager<User>(this, User, 10);
|
|
||||||
|
|
||||||
constructor(props: IProps) {
|
|
||||||
super(props);
|
|
||||||
this.preference.detailLoading = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.preference.httpFetch(() => {
|
|
||||||
this.preference.detailLoading = false;
|
|
||||||
|
|
||||||
// 给前端辅助字段赋值
|
|
||||||
this.preference.scanConfig.cronType = this.preference.scanConfig.cron;
|
|
||||||
if (
|
|
||||||
!ScanCronTypeValueList.includes(this.preference.scanConfig.cronType!)
|
|
||||||
) {
|
|
||||||
this.preference.scanConfig.cronType = ScanCronType.CUSTOM;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.preference.scanConfig.scope !== ScanScopeType.ALL) {
|
|
||||||
this.preference.scanConfig.users =
|
|
||||||
this.preference.scanConfig.usernames.map((name: string) => ({
|
|
||||||
label: name,
|
|
||||||
value: name,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateUI();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
finish(values: any) {
|
|
||||||
if (values.scope === ScanScopeType.CUSTOM) {
|
|
||||||
this.preference.scanConfig.assign({
|
|
||||||
...values,
|
|
||||||
usernames: values.users.map((item: any) => item.value),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.preference.scanConfig.assign(values);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.preference.httpSaveScan(function () {
|
|
||||||
MessageBoxUtil.success(Lang.t('operationSuccess'));
|
MessageBoxUtil.success(Lang.t('operationSuccess'));
|
||||||
Sun.updateFrame();
|
Sun.updateFrame();
|
||||||
Sun.navigateTo('/preference/index');
|
Sun.navigateTo('/preference/index');
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
changeEnableScan(value: boolean) {
|
const handleRefreshSpace = debounce((keyword?: string) => {
|
||||||
this.preference.scanConfig.enable = value;
|
pager.current.setFilterValue('orderCreateTime', SortDirection.DESC);
|
||||||
this.updateUI();
|
pager.current.setFilterValue('name', keyword);
|
||||||
}
|
pager.current.httpList(() => setSpaceList(pager.current.data));
|
||||||
|
}, 200);
|
||||||
|
|
||||||
changeCronType(value: ScanCronType) {
|
useEffect(() => {
|
||||||
this.preference.scanConfig.cronType = value;
|
preference.current.detailLoading = true;
|
||||||
this.updateUI();
|
preference.current.httpFetch(() => {
|
||||||
}
|
preference.current.scanConfig.cronType = ScanCronTypeValueList.includes(
|
||||||
|
preference.current.scanConfig.cron!
|
||||||
|
)
|
||||||
|
? preference.current.scanConfig.cron
|
||||||
|
: ScanCronType.CUSTOM;
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
handleRefreshSpace();
|
||||||
|
}, []);
|
||||||
|
|
||||||
changeScopeType(value: ScanScopeType) {
|
return (
|
||||||
this.preference.scanConfig.scope = value;
|
<div className="preference-scan-edit">
|
||||||
this.updateUI();
|
<TankTitle name={Lang.t('preference.editScan')} />
|
||||||
}
|
<TankContentCard loading={loading}>
|
||||||
|
<Form
|
||||||
fetchUser(value: any) {
|
{...layout}
|
||||||
this.pager.resetFilter();
|
form={form}
|
||||||
this.pager.setFilterValue('orderCreateTime', SortDirection.DESC);
|
initialValues={preference.current.scanConfig}
|
||||||
this.pager.setFilterValue('username', value);
|
onFinish={handleFinish}
|
||||||
this.pager.httpList();
|
>
|
||||||
}
|
<Form.Item
|
||||||
|
label={Lang.t('preference.enableScan')}
|
||||||
render() {
|
valuePropName="checked"
|
||||||
const { preference, pager } = this;
|
name="enable"
|
||||||
|
required
|
||||||
const layout = {
|
|
||||||
labelCol: { span: 6 },
|
|
||||||
wrapperCol: { span: 18 },
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="preference-scan-edit">
|
|
||||||
<TankTitle name={Lang.t('preference.editScan')} />
|
|
||||||
<TankContentCard loading={preference.detailLoading}>
|
|
||||||
<Form
|
|
||||||
{...layout}
|
|
||||||
name="preview-engine"
|
|
||||||
ref={this.formRef}
|
|
||||||
initialValues={preference.scanConfig}
|
|
||||||
onFinish={this.finish.bind(this)}
|
|
||||||
onValuesChange={() => this.updateUI}
|
|
||||||
>
|
>
|
||||||
<Form.Item
|
<Switch />
|
||||||
label={Lang.t('preference.enableScan')}
|
</Form.Item>
|
||||||
valuePropName="checked"
|
|
||||||
name="enable"
|
<Form.Item dependencies={['enable']} noStyle>
|
||||||
>
|
{({ getFieldValue }) => {
|
||||||
<Switch onChange={this.changeEnableScan.bind(this)} />
|
return (
|
||||||
</Form.Item>
|
getFieldValue('enable') && (
|
||||||
{preference.scanConfig.enable && (
|
<>
|
||||||
<>
|
<Form.Item
|
||||||
<Form.Item
|
label={Lang.t('preference.scanCron')}
|
||||||
label={Lang.t('preference.scanCron')}
|
name="cronType"
|
||||||
name="cronType"
|
required
|
||||||
>
|
rules={[
|
||||||
<Select onChange={this.changeCronType.bind(this)}>
|
{
|
||||||
{ScanCronTypeList.map((option) => (
|
required: true,
|
||||||
<Select.Option key={option.value} value={option.value}>
|
message: Lang.t('preference.scanCronValidate'),
|
||||||
{option.name}
|
},
|
||||||
</Select.Option>
|
]}
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
{preference.scanConfig.cronType === ScanCronType.CUSTOM && (
|
|
||||||
<Form.Item
|
|
||||||
label={Lang.t('preference.cron')}
|
|
||||||
name="cron"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: Lang.t('preference.cronValidate'),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
<Form.Item label={Lang.t('preference.scanScope')} name="scope">
|
|
||||||
<Select onChange={this.changeScopeType.bind(this)}>
|
|
||||||
{ScanScopeTypeList.map((option) => (
|
|
||||||
<Select.Option key={option.value} value={option.value}>
|
|
||||||
{option.name}
|
|
||||||
</Select.Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
{preference.scanConfig.scope === ScanScopeType.CUSTOM && (
|
|
||||||
<Form.Item
|
|
||||||
label={Lang.t('preference.scanUsers')}
|
|
||||||
name="users"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: Lang.t('preference.chooseUsersValidate'),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
mode="multiple"
|
|
||||||
labelInValue
|
|
||||||
placeholder={Lang.t('preference.chooseUsers')}
|
|
||||||
notFoundContent={
|
|
||||||
pager.loading ? <Spin size="small" /> : null
|
|
||||||
}
|
|
||||||
filterOption={false}
|
|
||||||
onSearch={this.fetchUser.bind(this)}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
>
|
>
|
||||||
{pager.data.map((user: User) => (
|
<Select>
|
||||||
<Select.Option
|
{ScanCronTypeList.map((option) => (
|
||||||
key={user.username!}
|
<Select.Option
|
||||||
value={user.username!}
|
key={option.value}
|
||||||
>
|
value={option.value}
|
||||||
{user.username}
|
>
|
||||||
</Select.Option>
|
{option.name}
|
||||||
))}
|
</Select.Option>
|
||||||
</Select>
|
))}
|
||||||
</Form.Item>
|
</Select>
|
||||||
)}
|
</Form.Item>
|
||||||
</>
|
|
||||||
)}
|
<Form.Item dependencies={['cronType']} noStyle>
|
||||||
<div className="text-right">
|
{({ getFieldValue }) => {
|
||||||
<Button type="primary" htmlType="submit" icon={<SaveOutlined />}>
|
return (
|
||||||
{Lang.t('save')}
|
getFieldValue('cronType') === ScanCronType.CUSTOM && (
|
||||||
</Button>
|
<Form.Item
|
||||||
</div>
|
label={Lang.t('preference.cron')}
|
||||||
</Form>
|
name="cron"
|
||||||
</TankContentCard>
|
rules={[
|
||||||
</div>
|
{
|
||||||
);
|
required: true,
|
||||||
}
|
message: Lang.t('preference.cronValidate'),
|
||||||
}
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={Lang.t('preference.scanScope')}
|
||||||
|
name="scope"
|
||||||
|
required
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: Lang.t('preference.scanScopeValidate'),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Select>
|
||||||
|
{ScanScopeTypeList.map((option) => (
|
||||||
|
<Select.Option
|
||||||
|
key={option.value}
|
||||||
|
value={option.value}
|
||||||
|
>
|
||||||
|
{option.name}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item dependencies={['scope']} noStyle>
|
||||||
|
{({ getFieldValue }) => {
|
||||||
|
return (
|
||||||
|
getFieldValue('scope') === ScanScopeType.CUSTOM && (
|
||||||
|
<Form.Item
|
||||||
|
label={Lang.t('preference.scanSpaces')}
|
||||||
|
name="spaceNames"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: Lang.t(
|
||||||
|
'preference.chooseSpacesValidate'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
mode="multiple"
|
||||||
|
placeholder={Lang.t('preference.chooseSpaces')}
|
||||||
|
className="wp100"
|
||||||
|
filterOption={false}
|
||||||
|
onSearch={handleRefreshSpace}
|
||||||
|
>
|
||||||
|
{spaceList.map((space: Space) => (
|
||||||
|
<Select.Option
|
||||||
|
key={space.name}
|
||||||
|
value={space.name}
|
||||||
|
>
|
||||||
|
{space.name}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
<div className="text-right">
|
||||||
|
<Button type="primary" htmlType="submit" icon={<SaveOutlined />}>
|
||||||
|
{Lang.t('save')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</TankContentCard>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ScanEdit;
|
||||||
|
|||||||
@@ -59,7 +59,11 @@ export default class Detail extends TankComponent<IProps, IState> {
|
|||||||
this.share.uuid = this.props.match.params.uuid;
|
this.share.uuid = this.props.match.params.uuid;
|
||||||
this.pager.urlPage = Share.URL_MATTER_PAGE;
|
this.pager.urlPage = Share.URL_MATTER_PAGE;
|
||||||
|
|
||||||
if (this.user.hasLogin()) {
|
if (!!BrowserUtil.getQueryByName('code')) {
|
||||||
|
this.getFiles(BrowserUtil.getQueryByName('code')!);
|
||||||
|
// 去除url中的code query
|
||||||
|
window.history.replaceState({}, '', window.location.pathname);
|
||||||
|
} else if (this.user.hasLogin()) {
|
||||||
this.pager.enableHistory();
|
this.pager.enableHistory();
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import ShareBar from './widget/ShareBar';
|
|||||||
|
|
||||||
import './List.less';
|
import './List.less';
|
||||||
import Lang from '../../common/model/global/Lang';
|
import Lang from '../../common/model/global/Lang';
|
||||||
|
import SortDirection from '../../common/model/base/SortDirection';
|
||||||
|
|
||||||
interface IProps extends RouteComponentProps {}
|
interface IProps extends RouteComponentProps {}
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ export default class List extends TankComponent<IProps, IState> {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.pager.enableHistory();
|
this.pager.enableHistory();
|
||||||
|
this.pager.setFilterValue('orderCreateTime', SortDirection.DESC);
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,10 +30,8 @@ export default class ShareDialogModal extends TankComponent<IProps, IState> {
|
|||||||
|
|
||||||
copyAll() {
|
copyAll() {
|
||||||
const { share } = this.props;
|
const { share } = this.props;
|
||||||
const text = `${Lang.t('share.link')}:${share.getLink()} ${Lang.t(
|
const url = `${share.getLink()}?code=${share.code}`;
|
||||||
'share.code'
|
this.clipboard(url);
|
||||||
)}:${share.code}`;
|
|
||||||
this.clipboard(text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static open = (share: Share) => {
|
static open = (share: Share) => {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.btn {
|
.btn {
|
||||||
&-member, &-edit {
|
&-member, &-edit, &-bin {
|
||||||
color: @primary-color;
|
color: @primary-color;
|
||||||
}
|
}
|
||||||
&-del {
|
&-del {
|
||||||
|
|||||||
+50
-24
@@ -13,6 +13,7 @@ import {
|
|||||||
Space as AntdSpace,
|
Space as AntdSpace,
|
||||||
Modal,
|
Modal,
|
||||||
Empty,
|
Empty,
|
||||||
|
Tooltip,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import './List.less';
|
import './List.less';
|
||||||
import Lang from '../../common/model/global/Lang';
|
import Lang from '../../common/model/global/Lang';
|
||||||
@@ -22,6 +23,7 @@ import MessageBoxUtil from '../../common/util/MessageBoxUtil';
|
|||||||
import {
|
import {
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
ExclamationCircleFilled,
|
ExclamationCircleFilled,
|
||||||
|
RestOutlined,
|
||||||
TeamOutlined,
|
TeamOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { DeleteOutlined } from '@ant-design/icons/lib';
|
import { DeleteOutlined } from '@ant-design/icons/lib';
|
||||||
@@ -50,6 +52,7 @@ export default class List extends TankComponent<IProps, IState> {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.pager.enableHistory();
|
this.pager.enableHistory();
|
||||||
|
this.pager.setFilterValue('type', 'SHARED'); // 列表只展示共享空间
|
||||||
this.pager.httpList();
|
this.pager.httpList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,6 +118,10 @@ export default class List extends TankComponent<IProps, IState> {
|
|||||||
this.props.history.push(`/space/${space.uuid}/member`);
|
this.props.history.push(`/space/${space.uuid}/member`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleSpaceRecycleBin(space: Space) {
|
||||||
|
this.props.history.push(`/space/${space.uuid}/bin/list`);
|
||||||
|
}
|
||||||
|
|
||||||
handleSpaceMatterList(space: Space) {
|
handleSpaceMatterList(space: Space) {
|
||||||
this.props.history.push(`/space/${space.uuid}/matter/list`);
|
this.props.history.push(`/space/${space.uuid}/matter/list`);
|
||||||
}
|
}
|
||||||
@@ -149,32 +156,51 @@ export default class List extends TankComponent<IProps, IState> {
|
|||||||
{space.name}
|
{space.name}
|
||||||
</div>
|
</div>
|
||||||
<AntdSpace className="space-item-icons">
|
<AntdSpace className="space-item-icons">
|
||||||
<TeamOutlined
|
<Tooltip title={Lang.t('space.memberManage')}>
|
||||||
className="btn-action btn-member"
|
<TeamOutlined
|
||||||
onClick={(e) =>
|
className="btn-action btn-member"
|
||||||
SafeUtil.stopPropagationWrap(e)(
|
onClick={(e) =>
|
||||||
this.handleSpaceMember(space)
|
SafeUtil.stopPropagationWrap(e)(
|
||||||
)
|
this.handleSpaceMember(space)
|
||||||
}
|
)
|
||||||
/>
|
}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip title={Lang.t('space.bin')}>
|
||||||
|
<RestOutlined
|
||||||
|
className="btn-action btn-bin"
|
||||||
|
onClick={(e) =>
|
||||||
|
SafeUtil.stopPropagationWrap(e)(
|
||||||
|
this.handleSpaceRecycleBin(space)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
{this.user.isAdmin() && (
|
{this.user.isAdmin() && (
|
||||||
<>
|
<>
|
||||||
<EditOutlined
|
<Tooltip title={Lang.t('space.edit')}>
|
||||||
className="btn-action btn-edit"
|
<EditOutlined
|
||||||
onClick={(e) =>
|
className="btn-action btn-edit"
|
||||||
SafeUtil.stopPropagationWrap(e)(
|
onClick={(e) =>
|
||||||
this.handleEdit(space)
|
SafeUtil.stopPropagationWrap(e)(
|
||||||
)
|
this.handleEdit(space)
|
||||||
}
|
)
|
||||||
/>
|
}
|
||||||
<DeleteOutlined
|
/>
|
||||||
className="btn-action btn-del"
|
</Tooltip>
|
||||||
onClick={(e) =>
|
|
||||||
SafeUtil.stopPropagationWrap(e)(
|
<Tooltip title={Lang.t('space.delete')}>
|
||||||
this.handleDelete(space)
|
<DeleteOutlined
|
||||||
)
|
className="btn-action btn-del"
|
||||||
}
|
onClick={(e) =>
|
||||||
/>
|
SafeUtil.stopPropagationWrap(e)(
|
||||||
|
this.handleDelete(space)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</AntdSpace>
|
</AntdSpace>
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import TankComponent from '../../../common/component/TankComponent';
|
import TankComponent from '../../../common/component/TankComponent';
|
||||||
import { Link, RouteComponentProps } from 'react-router-dom';
|
import { Link, RouteComponentProps } from 'react-router-dom';
|
||||||
import Pager from '../../../common/model/base/Pager';
|
import Pager from '../../../common/model/base/Pager';
|
||||||
import SpaceMember, {
|
import SpaceMember from '../../../common/model/space/member/SpaceMember';
|
||||||
SpaceMemberFormValues,
|
|
||||||
} from '../../../common/model/space/member/SpaceMember';
|
|
||||||
import Lang from '../../../common/model/global/Lang';
|
import Lang from '../../../common/model/global/Lang';
|
||||||
import { Button, Modal, Table, Tag, Tooltip, Typography } from 'antd';
|
import { Button, Modal, Table, Tag, Tooltip, Typography } from 'antd';
|
||||||
import TankTitle from '../../widget/TankTitle';
|
import TankTitle from '../../widget/TankTitle';
|
||||||
@@ -25,7 +23,7 @@ import {
|
|||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import Color from '../../../common/model/base/option/Color';
|
import Color from '../../../common/model/base/option/Color';
|
||||||
import MessageBoxUtil from '../../../common/util/MessageBoxUtil';
|
import MessageBoxUtil from '../../../common/util/MessageBoxUtil';
|
||||||
import ModalForm from './widget/ModalForm';
|
import ModalForm, { SpaceMemberModalFormValues } from './widget/ModalForm';
|
||||||
|
|
||||||
interface IProps
|
interface IProps
|
||||||
extends RouteComponentProps<{
|
extends RouteComponentProps<{
|
||||||
@@ -41,7 +39,7 @@ export default class SpaceMemberList extends TankComponent<IProps, IState> {
|
|||||||
modalState: {
|
modalState: {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
mode: 'create' | 'edit';
|
mode: 'create' | 'edit';
|
||||||
initialValues?: SpaceMemberFormValues;
|
targetSpaceMember?: SpaceMember;
|
||||||
} = {
|
} = {
|
||||||
visible: false,
|
visible: false,
|
||||||
mode: 'create',
|
mode: 'create',
|
||||||
@@ -74,7 +72,7 @@ export default class SpaceMemberList extends TankComponent<IProps, IState> {
|
|||||||
this.modalState = {
|
this.modalState = {
|
||||||
visible: true,
|
visible: true,
|
||||||
mode: 'edit',
|
mode: 'edit',
|
||||||
initialValues: spaceMember.getForm(),
|
targetSpaceMember: spaceMember,
|
||||||
};
|
};
|
||||||
this.updateUI();
|
this.updateUI();
|
||||||
}
|
}
|
||||||
@@ -94,18 +92,38 @@ export default class SpaceMemberList extends TankComponent<IProps, IState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleConfirmModalForm(values: SpaceMemberFormValues) {
|
handleConfirmModalForm(values: SpaceMemberModalFormValues) {
|
||||||
const spaceMember = new SpaceMember();
|
const spaceMember = new SpaceMember();
|
||||||
spaceMember.spaceUuid = this.spaceUuid;
|
const body = {
|
||||||
spaceMember.assign(values);
|
spaceUuid: this.spaceUuid,
|
||||||
spaceMember.httpSave(
|
role: values.role,
|
||||||
() => {
|
};
|
||||||
MessageBoxUtil.success(Lang.t('operationSuccess'));
|
if (this.modalState.mode === 'edit') {
|
||||||
this.refresh();
|
spaceMember.httpEdit(
|
||||||
},
|
{
|
||||||
null,
|
...body,
|
||||||
() => this.handleHideModalForm()
|
uuid: this.modalState.targetSpaceMember?.uuid!,
|
||||||
);
|
userUuid: values.userUuids.join(','),
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
MessageBoxUtil.success(Lang.t('operationSuccess'));
|
||||||
|
this.handleHideModalForm();
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
spaceMember.httpCreate(
|
||||||
|
{
|
||||||
|
...body,
|
||||||
|
userUuids: values.userUuids.join(','),
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
MessageBoxUtil.success(Lang.t('operationSuccess'));
|
||||||
|
this.handleHideModalForm();
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleHideModalForm() {
|
handleHideModalForm() {
|
||||||
@@ -218,7 +236,7 @@ export default class SpaceMemberList extends TankComponent<IProps, IState> {
|
|||||||
{modalState.visible && (
|
{modalState.visible && (
|
||||||
<ModalForm
|
<ModalForm
|
||||||
mode={modalState.mode}
|
mode={modalState.mode}
|
||||||
initialValues={modalState.initialValues}
|
targetSpaceMember={modalState.targetSpaceMember}
|
||||||
onOk={this.handleConfirmModalForm.bind(this)}
|
onOk={this.handleConfirmModalForm.bind(this)}
|
||||||
onCancel={() => this.handleHideModalForm()}
|
onCancel={() => this.handleHideModalForm()}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,37 +2,45 @@ import Lang from '../../../../common/model/global/Lang';
|
|||||||
import { Form, Input, Modal, Select } from 'antd';
|
import { Form, Input, Modal, Select } from 'antd';
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { FORM_LAYOUT } from '../../../../common/const/Form';
|
import { FORM_LAYOUT } from '../../../../common/const/Form';
|
||||||
import { SpaceMemberFormValues } from '../../../../common/model/space/member/SpaceMember';
|
import SpaceMember from '../../../../common/model/space/member/SpaceMember';
|
||||||
import { SpaceMemberRoleList } from '../../../../common/model/space/member/SpaceMemberRole';
|
import {
|
||||||
import Pager from '../../../../common/model/base/Pager';
|
SpaceMemberRole,
|
||||||
|
SpaceMemberRoleList,
|
||||||
|
} from '../../../../common/model/space/member/SpaceMemberRole';
|
||||||
import User from '../../../../common/model/user/User';
|
import User from '../../../../common/model/user/User';
|
||||||
|
|
||||||
|
export interface SpaceMemberModalFormValues {
|
||||||
|
userUuids: string[];
|
||||||
|
role: SpaceMemberRole;
|
||||||
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mode: 'create' | 'edit';
|
mode: 'create' | 'edit';
|
||||||
initialValues?: SpaceMemberFormValues;
|
targetSpaceMember?: SpaceMember;
|
||||||
onOk: (values: SpaceMemberFormValues) => void;
|
onOk: (values: SpaceMemberModalFormValues) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
const ModalForm = ({ mode, initialValues, onOk, onCancel }: Props) => {
|
const ModalForm = ({ mode, targetSpaceMember, onOk, onCancel }: Props) => {
|
||||||
const isCreate = mode === 'create';
|
const isCreate = mode === 'create';
|
||||||
const userPager = useRef(new Pager<User>(null, User, 100));
|
const [form] = Form.useForm<SpaceMemberModalFormValues>();
|
||||||
const [form] = Form.useForm<SpaceMemberFormValues>();
|
|
||||||
const [userList, setUserList] = useState<User[]>([]); // 用户列表
|
const [userList, setUserList] = useState<User[]>([]); // 用户列表
|
||||||
|
const initialValues: SpaceMemberModalFormValues | undefined =
|
||||||
|
targetSpaceMember
|
||||||
|
? {
|
||||||
|
userUuids: [targetSpaceMember.userUuid!],
|
||||||
|
role: targetSpaceMember.role!,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
useEffect(() => {
|
const refreshUserList = (keyword?: string) => {
|
||||||
refresh();
|
User.httpSearch(keyword, (res: any) => {
|
||||||
}, []);
|
setUserList(res.data.data);
|
||||||
|
|
||||||
const refresh = () => {
|
|
||||||
userPager.current.httpList(() => {
|
|
||||||
setUserList(userPager.current.data);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearchUser = (value?: string) => {
|
useEffect(() => {
|
||||||
userPager.current.setFilterValue('username', value);
|
refreshUserList();
|
||||||
refresh();
|
}, []);
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -49,12 +57,8 @@ const ModalForm = ({ mode, initialValues, onOk, onCancel }: Props) => {
|
|||||||
onFinish={onOk}
|
onFinish={onOk}
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
>
|
>
|
||||||
{/*编辑时暂存uuid*/}
|
|
||||||
<Form.Item name="uuid" hidden>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="userUuid"
|
name="userUuids"
|
||||||
label={Lang.t('space.member')}
|
label={Lang.t('space.member')}
|
||||||
rules={[{ required: true, message: Lang.t('selectRequired') }]}
|
rules={[{ required: true, message: Lang.t('selectRequired') }]}
|
||||||
>
|
>
|
||||||
@@ -63,12 +67,13 @@ const ModalForm = ({ mode, initialValues, onOk, onCancel }: Props) => {
|
|||||||
label: item.username,
|
label: item.username,
|
||||||
value: item.uuid,
|
value: item.uuid,
|
||||||
}))}
|
}))}
|
||||||
|
mode="multiple"
|
||||||
placeholder={Lang.t('space.memberTip')}
|
placeholder={Lang.t('space.memberTip')}
|
||||||
disabled={!isCreate}
|
disabled={!isCreate}
|
||||||
showSearch
|
showSearch
|
||||||
allowClear
|
allowClear
|
||||||
filterOption={false}
|
filterOption={false}
|
||||||
onSearch={handleSearchUser}
|
onSearch={refreshUserList}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import './InfoCell.less';
|
|||||||
import { Col, Row } from 'antd';
|
import { Col, Row } from 'antd';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
name: string;
|
name: React.ReactNode;
|
||||||
firstSpan?: number;
|
firstSpan?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ export default class InfoCell extends React.Component<IProps, IState> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
let that = this;
|
let that = this;
|
||||||
let name: string = this.props.name;
|
let name = this.props.name;
|
||||||
|
|
||||||
let firstSpan: number = 8;
|
let firstSpan: number = 8;
|
||||||
if (this.props.firstSpan !== undefined) {
|
if (this.props.firstSpan !== undefined) {
|
||||||
|
|||||||
@@ -20,7 +20,11 @@ const InputSize = ({
|
|||||||
|
|
||||||
const renderAfter = () => {
|
const renderAfter = () => {
|
||||||
return (
|
return (
|
||||||
<Select value={unit} onChange={handleChangeUnit}>
|
<Select
|
||||||
|
value={unit}
|
||||||
|
disabled={otherProps.disabled}
|
||||||
|
onChange={handleChangeUnit}
|
||||||
|
>
|
||||||
{FileUtil.sizeUnits.map((u) => (
|
{FileUtil.sizeUnits.map((u) => (
|
||||||
<Select.Option key={u} value={u}>
|
<Select.Option key={u} value={u}>
|
||||||
{u}
|
{u}
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
.browser-previewer {
|
.browser-previewer-wrapper {
|
||||||
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
padding: 24px;
|
||||||
|
background: rgba(0,0,0,0.6);
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
background: white;
|
|
||||||
|
|
||||||
display: flex;
|
.browser-previewer {
|
||||||
flex-direction: column;
|
height: 100%;
|
||||||
|
background: white;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.title-bar {
|
.title-bar {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
|
|||||||
@@ -51,28 +51,33 @@ export default class BrowserPreviewer extends React.Component<IProps, IState> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="browser-previewer">
|
<div
|
||||||
<div className="title-bar">
|
className="browser-previewer-wrapper"
|
||||||
<div className="left-part"></div>
|
onClick={() => this.props.onClose()}
|
||||||
|
>
|
||||||
|
<div className="browser-previewer" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<div className="title-bar">
|
||||||
|
<div className="left-part"></div>
|
||||||
|
|
||||||
<div className="middle-part">
|
<div className="middle-part">
|
||||||
{this.props.name}({FileUtil.humanFileSize(this.props.size)})
|
{this.props.name}({FileUtil.humanFileSize(this.props.size)})
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="right-part">
|
<div className="right-part">
|
||||||
<span className="close btn-action">
|
<span className="close btn-action">
|
||||||
<CloseOutlined
|
<CloseOutlined
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
this.props.onClose();
|
this.props.onClose();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="frame-area">
|
||||||
|
<iframe width="100%" height="100%" src={this.props.url}>
|
||||||
|
iframe may not supported.
|
||||||
|
</iframe>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="frame-area">
|
|
||||||
<iframe width="100%" height="100%" src={this.props.url}>
|
|
||||||
iframe may not supported.
|
|
||||||
</iframe>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,17 @@
|
|||||||
.widget-image-previewer {
|
.widget-image-previewer{
|
||||||
|
.PhotoView-Slider__toolbarIcon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
.toolbar-icon {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0.7;
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,90 +1,45 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { PhotoSlider } from 'react-photo-view';
|
||||||
|
import 'react-photo-view/dist/react-photo-view.css';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import { OverlayRenderProps } from 'react-photo-view/dist/types';
|
||||||
|
import {
|
||||||
|
CloseOutlined,
|
||||||
|
DownloadOutlined,
|
||||||
|
RotateRightOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import TankComponent from '../../../common/component/TankComponent';
|
||||||
|
import { Space } from 'antd';
|
||||||
import './ImagePreviewer.less';
|
import './ImagePreviewer.less';
|
||||||
import PhotoSwipe from 'photoswipe';
|
|
||||||
import PhotoSwipeUIDefault from 'photoswipe/dist/photoswipe-ui-default';
|
|
||||||
//PhotoSwipe的样式
|
|
||||||
import 'photoswipe/dist/photoswipe.css';
|
|
||||||
import 'photoswipe/dist/default-skin/default-skin.css';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 图片预览器
|
* 图片预览器
|
||||||
* 支持预览一张图片或者多张图片
|
* 支持预览一张图片或者多张图片
|
||||||
|
* 参考文档:https://zhuanlan.zhihu.com/p/473554342
|
||||||
|
* https://gitee.com/MinJieLiu/react-photo-view
|
||||||
*/
|
*/
|
||||||
export default class ImagePreviewer {
|
export default class ImagePreviewer extends TankComponent<any, any> {
|
||||||
private static pswpDom(): HTMLElement {
|
static initialized: boolean = false;
|
||||||
let innerHTML = `
|
|
||||||
<!-- Background of PhotoSwipe.
|
|
||||||
It's a separate element as animating opacity is faster than rgba(). -->
|
|
||||||
<div class="pswp__bg"></div>
|
|
||||||
|
|
||||||
<!-- Slides wrapper with overflow:hidden. -->
|
static singletonRef = React.createRef<ImagePreviewer>();
|
||||||
<div class="pswp__scroll-wrap">
|
|
||||||
|
|
||||||
<!-- Container that holds slides.
|
//组件的私有变量
|
||||||
PhotoSwipe keeps only 3 of them in the DOM to save memory.
|
visible: boolean = true;
|
||||||
Don't modify these 3 pswp__item elements, data is added later on. -->
|
urls: string[] = [];
|
||||||
<div class="pswp__container">
|
index: number = 0;
|
||||||
<div class="pswp__item"></div>
|
|
||||||
<div class="pswp__item"></div>
|
|
||||||
<div class="pswp__item"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Default (PhotoSwipeUI_Default) interface on top of sliding area. Can be changed. -->
|
|
||||||
<div class="pswp__ui pswp__ui--hidden">
|
|
||||||
|
|
||||||
<div class="pswp__top-bar">
|
|
||||||
|
|
||||||
<!-- Controls are self-explanatory. Order can be changed. -->
|
|
||||||
|
|
||||||
<div class="pswp__counter"></div>
|
|
||||||
|
|
||||||
<button class="pswp__button pswp__button--close" title="Close (Esc)"></button>
|
|
||||||
|
|
||||||
<button class="pswp__button pswp__button--share" title="Share"></button>
|
|
||||||
|
|
||||||
<button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button>
|
|
||||||
|
|
||||||
<button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button>
|
|
||||||
|
|
||||||
<!-- Preloader demo https://codepen.io/dimsemenov/pen/yyBWoR -->
|
|
||||||
<!-- element will get class pswp__preloader--active when preloader is running -->
|
|
||||||
<div class="pswp__preloader">
|
|
||||||
<div class="pswp__preloader__icn">
|
|
||||||
<div class="pswp__preloader__cut">
|
|
||||||
<div class="pswp__preloader__donut"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
|
|
||||||
<div class="pswp__share-tooltip"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)">
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)">
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="pswp__caption">
|
|
||||||
<div class="pswp__caption__center"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
|
//初始化
|
||||||
|
private static init() {
|
||||||
let div: HTMLElement = document.createElement('div');
|
let div: HTMLElement = document.createElement('div');
|
||||||
div.className = 'pswp';
|
div.id = 'photo-container';
|
||||||
div.setAttribute('tabIndex', '-1');
|
|
||||||
div.setAttribute('role', 'dialog');
|
|
||||||
div.setAttribute('aria-hidden', 'true');
|
|
||||||
|
|
||||||
div.innerHTML = innerHTML;
|
//添加到body
|
||||||
|
document.body.appendChild(div);
|
||||||
|
|
||||||
return div;
|
ReactDOM.render(
|
||||||
|
<ImagePreviewer ref={ImagePreviewer.singletonRef} />,
|
||||||
|
document.getElementById('photo-container')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
//展示一张图片
|
//展示一张图片
|
||||||
@@ -94,59 +49,70 @@ export default class ImagePreviewer {
|
|||||||
|
|
||||||
//展示一系列图片
|
//展示一系列图片
|
||||||
static showMultiPhoto(urls: string[], index: number = 0) {
|
static showMultiPhoto(urls: string[], index: number = 0) {
|
||||||
let that = this;
|
if (!ImagePreviewer.initialized) {
|
||||||
let items: PhotoSwipe.Item[] = [];
|
ImagePreviewer.initialized = true;
|
||||||
urls.forEach((url) => {
|
ImagePreviewer.init();
|
||||||
items.push({
|
}
|
||||||
src: url,
|
|
||||||
w: 0,
|
|
||||||
h: 0,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
let options = {
|
ImagePreviewer.singletonRef.current?.show(urls, index);
|
||||||
closeEl: false,
|
}
|
||||||
//不需要历史纪录
|
|
||||||
history: false,
|
show(urls: string[], index: number = 0) {
|
||||||
//不需要全屏按钮
|
this.urls = urls;
|
||||||
fullscreenEl: false,
|
this.index = index;
|
||||||
//不需要分享按钮
|
this.visible = true;
|
||||||
shareEl: false,
|
this.updateUI();
|
||||||
//当前从第0张展示。
|
}
|
||||||
index: index,
|
|
||||||
tapToClose: true,
|
render() {
|
||||||
|
let toolBar: (overlayProps: OverlayRenderProps) => React.ReactNode = (
|
||||||
|
overlayProps: OverlayRenderProps
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<Space>
|
||||||
|
<DownloadOutlined
|
||||||
|
className="f18 toolbar-icon btn-action"
|
||||||
|
onClick={() => {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.setAttribute('download', '');
|
||||||
|
a.setAttribute('target', '_blank');
|
||||||
|
a.setAttribute('rel', 'noopener');
|
||||||
|
a.href = overlayProps.images[overlayProps.index].src!;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<RotateRightOutlined
|
||||||
|
className="f18 toolbar-icon btn-action"
|
||||||
|
onClick={() => {
|
||||||
|
overlayProps.onRotate(overlayProps.rotate + 90);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<CloseOutlined
|
||||||
|
className="f18 toolbar-icon btn-action"
|
||||||
|
onClick={() => overlayProps.onClose()}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
//获取dom节点
|
return (
|
||||||
let div = ImagePreviewer.pswpDom();
|
<PhotoSlider
|
||||||
//添加到body
|
className="widget-image-previewer"
|
||||||
document.body.appendChild(div);
|
images={this.urls.map((item: string) => ({ src: item, key: item }))}
|
||||||
|
visible={this.visible}
|
||||||
let photoSwipe = new PhotoSwipe(div, PhotoSwipeUIDefault, items, options);
|
onClose={() => {
|
||||||
|
this.visible = false;
|
||||||
photoSwipe.listen(
|
this.updateUI();
|
||||||
'gettingData',
|
}}
|
||||||
function (index: number, item: PhotoSwipe.Item) {
|
index={this.index}
|
||||||
if (!item.w || !item.h || item.w < 1 || item.h < 1) {
|
onIndexChange={(index: number) => {
|
||||||
//这里为了获取图片真实大小
|
this.index = index;
|
||||||
const img: HTMLImageElement = new Image();
|
this.updateUI();
|
||||||
img.onload = function () {
|
}}
|
||||||
item.w = img.width;
|
toolbarRender={toolBar}
|
||||||
item.h = img.height;
|
/>
|
||||||
photoSwipe.updateSize(true); // reinit Items
|
|
||||||
};
|
|
||||||
img.src = item.src!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
photoSwipe.init();
|
|
||||||
|
|
||||||
photoSwipe.listen('close', () => {
|
|
||||||
if (div.parentNode) {
|
|
||||||
//移除节点
|
|
||||||
div.parentNode.removeChild(div);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
photoSwipe.listen('afterChange', () => {});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import './ImagePreviewer.less';
|
|
||||||
import MimeUtil from '../../../common/util/MimeUtil';
|
import MimeUtil from '../../../common/util/MimeUtil';
|
||||||
import MessageBoxUtil from '../../../common/util/MessageBoxUtil';
|
import MessageBoxUtil from '../../../common/util/MessageBoxUtil';
|
||||||
import Moon from '../../../common/model/global/Moon';
|
import Moon from '../../../common/model/global/Moon';
|
||||||
@@ -106,8 +105,8 @@ export default class PreviewerHelper {
|
|||||||
|
|
||||||
if (previewUrl) {
|
if (previewUrl) {
|
||||||
targetUrl = targetUrl.replace('{originUrl}', previewUrl);
|
targetUrl = targetUrl.replace('{originUrl}', previewUrl);
|
||||||
targetUrl = targetUrl.replace('{b64Url}', previewUrl);
|
targetUrl = targetUrl.replace('{b64Url}', encodeURIComponent(PreviewerHelper.b64EncodeUnicode(previewUrl)));
|
||||||
targetUrl = targetUrl.replace('{url}', previewUrl);
|
targetUrl = targetUrl.replace('{url}', encodeURIComponent(previewUrl));
|
||||||
if (engine.previewInSite) {
|
if (engine.previewInSite) {
|
||||||
BrowserPreviewer.show(fileName, targetUrl, matter.size);
|
BrowserPreviewer.show(fileName, targetUrl, matter.size);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1859,11 +1859,6 @@
|
|||||||
resolved "https://registry.npmmirror.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
resolved "https://registry.npmmirror.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
||||||
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
|
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
|
||||||
|
|
||||||
"@types/photoswipe@^4.1.2":
|
|
||||||
version "4.1.2"
|
|
||||||
resolved "https://registry.npmmirror.com/@types/photoswipe/-/photoswipe-4.1.2.tgz#3ddafb668bf6800f8dfab5bea2ba82dda879cd84"
|
|
||||||
integrity sha512-HA9TtCAQKToldgxRiyJ1DbsElg/cQV/SQ8COVjqIqghjy60Zxfh78E1WiFotthquqkS86nz13Za9wEbToe0svQ==
|
|
||||||
|
|
||||||
"@types/prettier@^2.1.5":
|
"@types/prettier@^2.1.5":
|
||||||
version "2.4.4"
|
version "2.4.4"
|
||||||
resolved "https://registry.npmmirror.com/@types/prettier/-/prettier-2.4.4.tgz#5d9b63132df54d8909fce1c3f8ca260fdd693e17"
|
resolved "https://registry.npmmirror.com/@types/prettier/-/prettier-2.4.4.tgz#5d9b63132df54d8909fce1c3f8ca260fdd693e17"
|
||||||
@@ -1969,6 +1964,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/spark-md5@^3.0.5":
|
||||||
|
version "3.0.5"
|
||||||
|
resolved "https://registry.npmmirror.com/@types/spark-md5/-/spark-md5-3.0.5.tgz#eddec8639217e518c26e9e221ff56bf5f5f5c900"
|
||||||
|
integrity sha512-lWf05dnD42DLVKQJZrDHtWFidcLrHuip01CtnC2/S6AMhX4t9ZlEUj4iuRlAnts0PQk7KESOqKxeGE/b6sIPGg==
|
||||||
|
|
||||||
"@types/stack-utils@^2.0.0":
|
"@types/stack-utils@^2.0.0":
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.npmmirror.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
|
resolved "https://registry.npmmirror.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
|
||||||
@@ -6766,11 +6766,6 @@ performance-now@^2.1.0:
|
|||||||
resolved "https://registry.npmmirror.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
|
resolved "https://registry.npmmirror.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
|
||||||
integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==
|
integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==
|
||||||
|
|
||||||
photoswipe@^4.1.3:
|
|
||||||
version "4.1.3"
|
|
||||||
resolved "https://registry.npmmirror.com/photoswipe/-/photoswipe-4.1.3.tgz#59f49494eeb9ddab5888d03392926a19bc197550"
|
|
||||||
integrity sha512-89Z43IRUyw7ycTolo+AaiDn3W1EEIfox54hERmm9bI12IB9cvRfHSHez3XhAyU8XW2EAFrC+2sKMhh7SJwn0bA==
|
|
||||||
|
|
||||||
picocolors@^0.2.1:
|
picocolors@^0.2.1:
|
||||||
version "0.2.1"
|
version "0.2.1"
|
||||||
resolved "https://registry.npmmirror.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f"
|
resolved "https://registry.npmmirror.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f"
|
||||||
@@ -7933,6 +7928,11 @@ react-lifecycles-compat@^3.0.4:
|
|||||||
resolved "https://registry.npmmirror.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
|
resolved "https://registry.npmmirror.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
|
||||||
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
|
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
|
||||||
|
|
||||||
|
react-photo-view@^1.2.3:
|
||||||
|
version "1.2.3"
|
||||||
|
resolved "https://registry.npmmirror.com/react-photo-view/-/react-photo-view-1.2.3.tgz#436f81ad0a611dfaa43bbc72a4fef26a057b5aef"
|
||||||
|
integrity sha512-dtaFjU8nAB/rD8mxBaaJcvESlDOweAd1tmvXujS/d3oh1QyGew3xD4C02l46E6Dhn3Tshzl2+IM1h0MWV9nG7g==
|
||||||
|
|
||||||
react-refresh@^0.11.0:
|
react-refresh@^0.11.0:
|
||||||
version "0.11.0"
|
version "0.11.0"
|
||||||
resolved "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046"
|
resolved "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046"
|
||||||
@@ -8631,6 +8631,11 @@ sourcemap-codec@^1.4.8:
|
|||||||
resolved "https://registry.npmmirror.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
|
resolved "https://registry.npmmirror.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
|
||||||
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
|
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
|
||||||
|
|
||||||
|
spark-md5@^3.0.2:
|
||||||
|
version "3.0.2"
|
||||||
|
resolved "https://registry.npmmirror.com/spark-md5/-/spark-md5-3.0.2.tgz#7952c4a30784347abcee73268e473b9c0167e3fc"
|
||||||
|
integrity sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==
|
||||||
|
|
||||||
spdy-transport@^3.0.0:
|
spdy-transport@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.npmmirror.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31"
|
resolved "https://registry.npmmirror.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31"
|
||||||
|
|||||||
Reference in New Issue
Block a user