Merge branch 'master' of https://gitee.com/yu-yon/vue-driven-cloud-storage
This commit is contained in:
@@ -2329,6 +2329,115 @@ app.delete('/api/admin/shares/:id', authMiddleware, adminMiddleware, (req, res)
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// 管理员:上传工具管理
|
||||
// ============================================
|
||||
|
||||
// 检查上传工具是否存在
|
||||
app.get('/api/admin/check-upload-tool', authMiddleware, adminMiddleware, (req, res) => {
|
||||
try {
|
||||
const toolPath = path.join(__dirname, '..', 'upload-tool', 'dist', '玩玩云上传工具.exe');
|
||||
|
||||
if (fs.existsSync(toolPath)) {
|
||||
const stats = fs.statSync(toolPath);
|
||||
const sizeMB = (stats.size / (1024 * 1024)).toFixed(2);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
exists: true,
|
||||
fileInfo: {
|
||||
path: toolPath,
|
||||
size: stats.size,
|
||||
sizeMB: sizeMB,
|
||||
modifiedAt: stats.mtime
|
||||
}
|
||||
});
|
||||
} else {
|
||||
res.json({
|
||||
success: true,
|
||||
exists: false,
|
||||
message: '上传工具不存在'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查上传工具失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '检查失败: ' + error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 上传工具文件
|
||||
const uploadToolStorage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
const uploadDir = path.join(__dirname, '..', 'upload-tool', 'dist');
|
||||
// 确保目录存在
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
fs.mkdirSync(uploadDir, { recursive: true });
|
||||
}
|
||||
cb(null, uploadDir);
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
// 固定文件名
|
||||
cb(null, '玩玩云上传工具.exe');
|
||||
}
|
||||
});
|
||||
|
||||
const uploadTool = multer({
|
||||
storage: uploadToolStorage,
|
||||
limits: {
|
||||
fileSize: 100 * 1024 * 1024 // 限制100MB
|
||||
},
|
||||
fileFilter: (req, file, cb) => {
|
||||
// 只允许.exe文件
|
||||
if (!file.originalname.toLowerCase().endsWith('.exe')) {
|
||||
return cb(new Error('只允许上传.exe文件'));
|
||||
}
|
||||
cb(null, true);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/admin/upload-tool', authMiddleware, adminMiddleware, uploadTool.single('file'), (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请选择要上传的文件'
|
||||
});
|
||||
}
|
||||
|
||||
const fileSizeMB = (req.file.size / (1024 * 1024)).toFixed(2);
|
||||
|
||||
// 验证文件大小(至少20MB,上传工具通常很大)
|
||||
if (req.file.size < 20 * 1024 * 1024) {
|
||||
// 删除上传的文件
|
||||
fs.unlinkSync(req.file.path);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '文件大小异常,上传工具通常大于20MB'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`[上传工具] 管理员上传成功: ${fileSizeMB}MB`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '上传工具已上传',
|
||||
fileInfo: {
|
||||
size: req.file.size,
|
||||
sizeMB: fileSizeMB
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('上传工具失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '上传失败: ' + error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 分享页面访问路由
|
||||
app.get("/s/:code", (req, res) => {
|
||||
const shareCode = req.params.code;
|
||||
|
||||
@@ -1005,7 +1005,7 @@
|
||||
</div>
|
||||
|
||||
<!-- SFTP配置 - 仅普通用户且权限允许SFTP -->
|
||||
<div v-if="user && !user.is_admin && (storagePermission === 'sftp_only' || (storagePermission === 'user_choice' && storageType === 'sftp'))">
|
||||
<div v-if="user && !user.is_admin && (storagePermission === 'sftp_only' || (storagePermission === 'user_choice' && (storageType === 'sftp' || forceSftpConfigVisible)))" id="sftp-config-section">
|
||||
<h3 style="margin-bottom: 20px;">SFTP配置</h3>
|
||||
<div v-if="user && !user.has_ftp_config" class="alert alert-info">
|
||||
请配置SFTP服务器
|
||||
@@ -1381,6 +1381,73 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 上传工具管理区域 -->
|
||||
<div class="card" style="margin-top: 30px;">
|
||||
<h3 style="margin-bottom: 20px;">
|
||||
<i class="fas fa-cloud-upload-alt"></i> 上传工具管理
|
||||
</h3>
|
||||
|
||||
<!-- 工具状态显示 -->
|
||||
<div v-if="uploadToolStatus !== null">
|
||||
<div v-if="uploadToolStatus.exists" style="padding: 15px; background: #d4edda; border-left: 4px solid #28a745; border-radius: 6px; margin-bottom: 20px;">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between;">
|
||||
<div>
|
||||
<div style="color: #155724; font-weight: 600; margin-bottom: 5px;">
|
||||
<i class="fas fa-check-circle"></i> 上传工具已存在
|
||||
</div>
|
||||
<div style="color: #155724; font-size: 13px;">
|
||||
文件大小: {{ uploadToolStatus.fileInfo.sizeMB }} MB
|
||||
</div>
|
||||
<div style="color: #155724; font-size: 12px; margin-top: 3px;">
|
||||
最后修改: {{ formatDate(uploadToolStatus.fileInfo.modifiedAt) }}
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary" @click="checkUploadTool" style="background: #28a745;">
|
||||
<i class="fas fa-sync-alt"></i> 重新检测
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else style="padding: 15px; background: #fff3cd; border-left: 4px solid #ffc107; border-radius: 6px; margin-bottom: 20px;">
|
||||
<div style="color: #856404; font-weight: 600; margin-bottom: 5px;">
|
||||
<i class="fas fa-exclamation-triangle"></i> 上传工具不存在
|
||||
</div>
|
||||
<div style="color: #856404; font-size: 13px;">
|
||||
普通用户将无法下载上传工具,请上传工具文件
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮组 -->
|
||||
<div style="display: flex; gap: 10px; margin-top: 15px; flex-wrap: wrap;">
|
||||
<button class="btn btn-primary" @click="checkUploadTool" :disabled="checkingUploadTool" style="background: #17a2b8;">
|
||||
<i class="fas fa-search" v-if="!checkingUploadTool"></i>
|
||||
<i class="fas fa-spinner fa-spin" v-else></i>
|
||||
{{ checkingUploadTool ? '检测中...' : '检测上传工具' }}
|
||||
</button>
|
||||
|
||||
<button v-if="uploadToolStatus && !uploadToolStatus.exists" class="btn btn-primary" @click="$refs.uploadToolInput.click()" :disabled="uploadingTool" style="background: #28a745;">
|
||||
<i class="fas fa-upload" v-if="!uploadingTool"></i>
|
||||
<i class="fas fa-spinner fa-spin" v-else></i>
|
||||
{{ uploadingTool ? '上传中...' : '上传工具文件' }}
|
||||
</button>
|
||||
|
||||
<input ref="uploadToolInput" type="file" accept=".exe" style="display: none;" @change="handleUploadToolFile">
|
||||
</div>
|
||||
|
||||
<!-- 使用说明 -->
|
||||
<div style="margin-top: 20px; padding: 12px; background: #e7f3ff; border-left: 4px solid #2196F3; border-radius: 6px;">
|
||||
<div style="color: #0c5460; font-size: 13px; line-height: 1.6;">
|
||||
<strong><i class="fas fa-info-circle"></i> 说明:</strong>
|
||||
<ul style="margin: 8px 0 0 20px; padding-left: 0;">
|
||||
<li>上传工具文件应为 .exe 格式,大小通常在 20-50 MB</li>
|
||||
<li>上传后,普通用户可以在设置页面下载该工具</li>
|
||||
<li>如果安装脚本下载失败,可以在这里手动上传</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 忘记密码模态框 -->
|
||||
|
||||
157
frontend/app.js
157
frontend/app.js
@@ -171,7 +171,15 @@ createApp({
|
||||
},
|
||||
|
||||
// 定期检查用户配置更新的定时器
|
||||
profileCheckInterval: null
|
||||
profileCheckInterval: null,
|
||||
|
||||
// 上传工具管理
|
||||
uploadToolStatus: null, // 上传工具状态 { exists, fileInfo: { size, sizeMB, modifiedAt } }
|
||||
checkingUploadTool: false, // 是否正在检测上传工具
|
||||
uploadingTool: false, // 是否正在上传工具
|
||||
|
||||
// 强制显示SFTP配置(用于本地存储模式下临时显示SFTP配置)
|
||||
forceSftpConfigVisible: false
|
||||
};
|
||||
},
|
||||
|
||||
@@ -293,7 +301,21 @@ handleDragLeave(e) {
|
||||
this.localQuota = this.user.local_storage_quota || 0;
|
||||
this.localUsed = this.user.local_storage_used || 0;
|
||||
|
||||
console.log('[登录] 存储权限:', this.storagePermission, '存储类型:', this.storageType);
|
||||
console.log('[登录] 存储权限:', this.storagePermission, '存储类型:', this.storageType, 'SFTP配置:', this.user.has_ftp_config);
|
||||
|
||||
// 智能存储类型修正:如果当前是SFTP但未配置,且用户有本地存储权限,自动切换到本地
|
||||
if (this.storageType === 'sftp' && !this.user.has_ftp_config) {
|
||||
if (this.storagePermission === 'local_only' || this.storagePermission === 'user_choice') {
|
||||
console.log('[登录] SFTP未配置但用户有本地存储权限,自动切换到本地存储');
|
||||
this.storageType = 'local';
|
||||
// 异步更新到后端(不等待,避免阻塞登录流程)
|
||||
axios.post(
|
||||
`${this.apiBase}/api/user/switch-storage`,
|
||||
{ storage_type: 'local' },
|
||||
{ headers: { Authorization: `Bearer ${this.token}` } }
|
||||
).catch(err => console.error('[登录] 自动切换存储类型失败:', err));
|
||||
}
|
||||
}
|
||||
|
||||
// 启动定期检查用户配置
|
||||
this.startProfileSync();
|
||||
@@ -369,6 +391,28 @@ handleDragLeave(e) {
|
||||
alert('SFTP配置已保存!');
|
||||
// 更新用户信息
|
||||
this.user.has_ftp_config = 1;
|
||||
|
||||
// 如果用户有 user_choice 权限,自动切换到 SFTP 存储
|
||||
if (this.storagePermission === 'user_choice' || this.storagePermission === 'sftp_only') {
|
||||
try {
|
||||
const switchResponse = await axios.post(
|
||||
`${this.apiBase}/api/user/switch-storage`,
|
||||
{ storage_type: 'sftp' },
|
||||
{ headers: { Authorization: `Bearer ${this.token}` } }
|
||||
);
|
||||
|
||||
if (switchResponse.data.success) {
|
||||
this.storageType = 'sftp';
|
||||
console.log('[SFTP配置] 已自动切换到SFTP存储模式');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[SFTP配置] 自动切换存储模式失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// 重置强制显示标志
|
||||
this.forceSftpConfigVisible = false;
|
||||
|
||||
// 刷新到文件页面
|
||||
this.currentView = 'files';
|
||||
this.loadFiles('/');
|
||||
@@ -1511,7 +1555,6 @@ handleDragLeave(e) {
|
||||
console.error('加载用户资料失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// 启动定期检查用户配置
|
||||
startProfileSync() {
|
||||
// 清除已有的定时器
|
||||
@@ -1542,8 +1585,24 @@ handleDragLeave(e) {
|
||||
async switchStorage(type) {
|
||||
// 检查是否尝试切换到SFTP但未配置
|
||||
if (type === 'sftp' && !this.user.has_ftp_config) {
|
||||
this.showToast('warning', '提示', '请先在设置页面配置SFTP信息');
|
||||
const goToSettings = confirm('您还未配置SFTP服务器。\n\n是否现在前往设置页面进行配置?配置完成后将自动切换到SFTP存储。');
|
||||
if (goToSettings) {
|
||||
// 直接设置视图并加载配置,避免switchView的重复检查
|
||||
this.currentView = 'settings';
|
||||
// 强制显示SFTP配置区域
|
||||
this.forceSftpConfigVisible = true;
|
||||
// 如果是普通用户,手动加载SFTP配置
|
||||
if (this.user && !this.user.is_admin) {
|
||||
this.loadFtpConfig();
|
||||
}
|
||||
// 等待DOM更新后滚动到SFTP配置区域
|
||||
this.$nextTick(() => {
|
||||
const sftpSection = document.getElementById('sftp-config-section');
|
||||
if (sftpSection) {
|
||||
sftpSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1582,6 +1641,11 @@ handleDragLeave(e) {
|
||||
|
||||
this.currentView = view;
|
||||
|
||||
// 离开settings页面时,重置强制显示SFTP配置的标志
|
||||
if (this.currentView !== 'settings') {
|
||||
this.forceSftpConfigVisible = false;
|
||||
}
|
||||
|
||||
// 根据视图类型自动加载对应数据
|
||||
switch (view) {
|
||||
case 'files':
|
||||
@@ -1762,8 +1826,93 @@ handleDragLeave(e) {
|
||||
this.showToast('error', '错误', '更新系统设置失败');
|
||||
}
|
||||
}
|
||||
,
|
||||
|
||||
// ===== 上传工具管理 =====
|
||||
|
||||
// 检测上传工具是否存在
|
||||
async checkUploadTool() {
|
||||
this.checkingUploadTool = true;
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${this.apiBase}/api/admin/check-upload-tool`,
|
||||
{ headers: { Authorization: `Bearer ${this.token}` } }
|
||||
);
|
||||
|
||||
if (response.data.success) {
|
||||
this.uploadToolStatus = response.data;
|
||||
if (response.data.exists) {
|
||||
this.showToast('success', '检测完成', '上传工具文件存在');
|
||||
} else {
|
||||
this.showToast('warning', '提示', '上传工具文件不存在,请上传');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检测上传工具失败:', error);
|
||||
this.showToast('error', '错误', '检测失败: ' + (error.response?.data?.message || error.message));
|
||||
} finally {
|
||||
this.checkingUploadTool = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 处理上传工具文件
|
||||
async handleUploadToolFile(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
// 验证文件类型
|
||||
if (!file.name.toLowerCase().endsWith('.exe')) {
|
||||
this.showToast('error', '错误', '只能上传 .exe 文件');
|
||||
event.target.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证文件大小(至少20MB)
|
||||
const minSizeMB = 20;
|
||||
const fileSizeMB = file.size / (1024 * 1024);
|
||||
if (fileSizeMB < minSizeMB) {
|
||||
this.showToast('error', '错误', `文件大小过小(${fileSizeMB.toFixed(2)}MB),上传工具通常大于${minSizeMB}MB`);
|
||||
event.target.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 确认上传
|
||||
if (!confirm(`确定要上传 ${file.name} (${fileSizeMB.toFixed(2)}MB) 吗?`)) {
|
||||
event.target.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
this.uploadingTool = true;
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response = await axios.post(
|
||||
`${this.apiBase}/api/admin/upload-tool`,
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (response.data.success) {
|
||||
this.showToast('success', '成功', '上传工具已上传');
|
||||
// 重新检测
|
||||
await this.checkUploadTool();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上传工具失败:', error);
|
||||
this.showToast('error', '错误', error.response?.data?.message || '上传失败');
|
||||
} finally {
|
||||
this.uploadingTool = false;
|
||||
event.target.value = ''; // 清空input,允许重复上传
|
||||
}
|
||||
} },
|
||||
|
||||
mounted() {
|
||||
// 阻止全局拖拽默认行为(防止拖到区域外打开新页面)
|
||||
window.addEventListener("dragover", (e) => {
|
||||
|
||||
1892
install.sh
1892
install.sh
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user