功能: 添加管理员上传工具检测和上传功能

- 后端: 添加 GET /api/admin/check-upload-tool 检测工具是否存在
- 后端: 添加 POST /api/admin/upload-tool 允许管理员手动上传exe
- 前端: 管理员界面新增上传工具管理卡片
- 前端: 支持检测工具状态、显示文件信息、手动上传
- 验证: 文件必须是.exe格式且大小>20MB
This commit is contained in:
WanWanYun
2025-11-12 20:44:41 +08:00
parent 0fc378576f
commit e5ba17329c
3 changed files with 268 additions and 2 deletions

View File

@@ -2333,6 +2333,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;

View File

@@ -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>
<!-- 忘记密码模态框 -->

View File

@@ -171,7 +171,12 @@ createApp({
},
// 定期检查用户配置更新的定时器
profileCheckInterval: null
profileCheckInterval: null,
// 上传工具管理
uploadToolStatus: null, // 上传工具状态 { exists, fileInfo: { size, sizeMB, modifiedAt } }
checkingUploadTool: false, // 是否正在检测上传工具
uploadingTool: false // 是否正在上传工具
};
},
@@ -1762,8 +1767,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) => {