feat: 增强前端健壮性并添加刷新功能
- 添加刷新按钮和加载状态指示器 - 改进API请求错误处理和用户反馈 - 优化表单验证和登录逻辑 - 更新后端端口号并处理URL结尾斜杠 - 添加按钮悬停效果和移动端适配
This commit is contained in:
67
.trae/documents/前端生产级健壮性提升计划.md
Normal file
67
.trae/documents/前端生产级健壮性提升计划.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
## 前端生产级健壮性提升计划
|
||||||
|
|
||||||
|
### 1. 核心改进点
|
||||||
|
|
||||||
|
#### 添加刷新按钮
|
||||||
|
- 在下载区域标题旁添加刷新按钮
|
||||||
|
- 点击按钮重新加载App列表
|
||||||
|
- 保持与现有按钮风格一致
|
||||||
|
|
||||||
|
#### 增强API调用健壮性
|
||||||
|
- 统一API请求处理,添加错误处理
|
||||||
|
- 完善各种错误场景的用户反馈
|
||||||
|
- 添加加载状态提示
|
||||||
|
- 处理网络错误和超时情况
|
||||||
|
|
||||||
|
#### 改进用户体验
|
||||||
|
- 添加操作成功/失败的视觉反馈
|
||||||
|
- 优化表单验证
|
||||||
|
- 改进文件上传反馈
|
||||||
|
|
||||||
|
#### 前后端适配协调
|
||||||
|
- 确保前端请求与后端API完全匹配
|
||||||
|
- 处理后端返回的各种响应状态
|
||||||
|
- 优化数据处理逻辑
|
||||||
|
|
||||||
|
### 2. 实现细节
|
||||||
|
|
||||||
|
#### 文件修改
|
||||||
|
1. **index.html**
|
||||||
|
- 在下载区域标题旁添加刷新按钮
|
||||||
|
- 添加加载状态指示器
|
||||||
|
|
||||||
|
2. **script.js**
|
||||||
|
- 添加`refreshAppList()`函数
|
||||||
|
- 统一API请求处理函数
|
||||||
|
- 完善错误处理逻辑
|
||||||
|
- 添加加载状态管理
|
||||||
|
- 优化现有函数,确保与后端API适配
|
||||||
|
|
||||||
|
3. **style.css**
|
||||||
|
- 微调样式以适应刷新按钮
|
||||||
|
- 添加加载状态样式
|
||||||
|
|
||||||
|
#### 关键功能实现
|
||||||
|
- **刷新功能**:点击按钮调用`loadAppList()`重新加载App列表
|
||||||
|
- **API健壮性**:添加try-catch、错误状态码处理、超时处理
|
||||||
|
- **用户反馈**:添加加载中提示、操作结果反馈
|
||||||
|
- **表单验证**:增强对输入的验证,防止无效请求
|
||||||
|
|
||||||
|
### 3. 保持极简风格
|
||||||
|
- 不添加复杂UI元素
|
||||||
|
- 保持现有配色和布局
|
||||||
|
- 使用原生JavaScript实现
|
||||||
|
- 不引入额外库
|
||||||
|
|
||||||
|
### 4. 预期效果
|
||||||
|
- 前端与后端API适配更协调
|
||||||
|
- 提供更好的用户反馈和错误处理
|
||||||
|
- 添加刷新功能方便用户手动更新App列表
|
||||||
|
- 提升系统整体健壮性和用户体验
|
||||||
|
|
||||||
|
### 5. 测试要点
|
||||||
|
- 刷新功能正常工作
|
||||||
|
- API请求错误处理
|
||||||
|
- 网络异常情况处理
|
||||||
|
- 各种边界情况测试
|
||||||
|
- 保持极简风格不变
|
||||||
@@ -1,8 +1 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": "1766503652493156300",
|
|
||||||
"name": "MobaXterm_Portable_v25.4",
|
|
||||||
"fileName": "MobaXterm_Portable_v25.4.zip",
|
|
||||||
"date": "2025-12-23 23:27:32"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|||||||
Binary file not shown.
@@ -31,7 +31,7 @@ var (
|
|||||||
appsMutex sync.RWMutex // 读写锁,提高并发性能
|
appsMutex sync.RWMutex // 读写锁,提高并发性能
|
||||||
filesDir = "./files"
|
filesDir = "./files"
|
||||||
jsonFile = "./apps.json"
|
jsonFile = "./apps.json"
|
||||||
port = ":6902"
|
port = ":6903" // 修改端口号为6903
|
||||||
)
|
)
|
||||||
|
|
||||||
// 生成唯一文件名
|
// 生成唯一文件名
|
||||||
@@ -122,6 +122,7 @@ func main() {
|
|||||||
api.GET("/apps", getApps)
|
api.GET("/apps", getApps)
|
||||||
api.POST("/apps", uploadApp)
|
api.POST("/apps", uploadApp)
|
||||||
api.GET("/apps/:id", downloadApp)
|
api.GET("/apps/:id", downloadApp)
|
||||||
|
api.GET("/apps/:id/", downloadApp) // 处理以/结尾的URL
|
||||||
}
|
}
|
||||||
|
|
||||||
// 启动服务器
|
// 启动服务器
|
||||||
@@ -228,6 +229,6 @@ func downloadApp(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提供文件下载,设置原始文件名
|
// 使用FileAttachment直接提供文件下载,避免重定向
|
||||||
c.FileAttachment(filePath, targetApp.FileName)
|
c.FileAttachment(filePath, targetApp.FileName)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,13 @@
|
|||||||
|
|
||||||
<!-- 下载区域 -->
|
<!-- 下载区域 -->
|
||||||
<div id="downloadSection" class="section">
|
<div id="downloadSection" class="section">
|
||||||
<h2>可用App</h2>
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
|
||||||
|
<h2>可用App</h2>
|
||||||
|
<div>
|
||||||
|
<button id="refreshBtn" onclick="refreshAppList()">刷新</button>
|
||||||
|
<span id="loadingIndicator" style="display: none; margin-left: 10px; font-size: 12px;">加载中...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div id="appList"></div>
|
<div id="appList"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
209
script.js
209
script.js
@@ -1,6 +1,6 @@
|
|||||||
// 配置
|
// 配置
|
||||||
const ADMIN_PASSWORD = 'admin123'; // 管理员密码
|
const ADMIN_PASSWORD = 'admin123'; // 管理员密码
|
||||||
const API_BASE_URL = 'http://localhost:6902/api'; // 后端API基础URL
|
const API_BASE_URL = 'http://47.92.113.131:6903/api'; // 后端API基础URL
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
function init() {
|
function init() {
|
||||||
@@ -21,11 +21,17 @@ function login() {
|
|||||||
const password = document.getElementById('password').value;
|
const password = document.getElementById('password').value;
|
||||||
const message = document.getElementById('loginMessage');
|
const message = document.getElementById('loginMessage');
|
||||||
|
|
||||||
|
if (!password.trim()) {
|
||||||
|
message.textContent = '密码不能为空!';
|
||||||
|
message.style.color = '#ff0000';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (password === ADMIN_PASSWORD) {
|
if (password === ADMIN_PASSWORD) {
|
||||||
localStorage.setItem('isLoggedIn', 'true');
|
localStorage.setItem('isLoggedIn', 'true');
|
||||||
showUploadSection();
|
showUploadSection();
|
||||||
message.textContent = '登录成功!';
|
message.textContent = '登录成功!';
|
||||||
message.style.color = '#0000ff';
|
message.style.color = '#008000';
|
||||||
document.getElementById('password').value = '';
|
document.getElementById('password').value = '';
|
||||||
} else {
|
} else {
|
||||||
message.textContent = '密码错误,请重试!';
|
message.textContent = '密码错误,请重试!';
|
||||||
@@ -38,11 +44,15 @@ function showUploadSection() {
|
|||||||
document.getElementById('loginSection').style.display = 'none';
|
document.getElementById('loginSection').style.display = 'none';
|
||||||
document.getElementById('uploadSection').style.display = 'block';
|
document.getElementById('uploadSection').style.display = 'block';
|
||||||
const loginSection = document.getElementById('loginSection');
|
const loginSection = document.getElementById('loginSection');
|
||||||
const logoutBtn = document.createElement('button');
|
|
||||||
logoutBtn.textContent = '退出登录';
|
// 检查是否已经添加了退出按钮,避免重复添加
|
||||||
logoutBtn.className = 'logout-btn';
|
if (!loginSection.querySelector('.logout-btn')) {
|
||||||
logoutBtn.onclick = logout;
|
const logoutBtn = document.createElement('button');
|
||||||
loginSection.appendChild(logoutBtn);
|
logoutBtn.textContent = '退出登录';
|
||||||
|
logoutBtn.className = 'logout-btn';
|
||||||
|
logoutBtn.onclick = logout;
|
||||||
|
loginSection.appendChild(logoutBtn);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 退出登录
|
// 退出登录
|
||||||
@@ -51,64 +61,100 @@ function logout() {
|
|||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 显示加载状态
|
||||||
|
function showLoading() {
|
||||||
|
const loadingIndicator = document.getElementById('loadingIndicator');
|
||||||
|
if (loadingIndicator) {
|
||||||
|
loadingIndicator.style.display = 'inline';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐藏加载状态
|
||||||
|
function hideLoading() {
|
||||||
|
const loadingIndicator = document.getElementById('loadingIndicator');
|
||||||
|
if (loadingIndicator) {
|
||||||
|
loadingIndicator.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新App列表
|
||||||
|
function refreshAppList() {
|
||||||
|
loadAppList();
|
||||||
|
}
|
||||||
|
|
||||||
// 加载App列表
|
// 加载App列表
|
||||||
function loadAppList() {
|
async function loadAppList() {
|
||||||
const appList = document.getElementById('appList');
|
const appList = document.getElementById('appList');
|
||||||
|
|
||||||
fetch(`${API_BASE_URL}/apps`)
|
try {
|
||||||
.then(response => response.json())
|
showLoading();
|
||||||
.then(apps => {
|
|
||||||
if (apps.length === 0) {
|
|
||||||
appList.innerHTML = '<p style="text-align: center;">暂无App</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
appList.innerHTML = '';
|
const response = await fetch(`${API_BASE_URL}/apps`, {
|
||||||
apps.forEach(app => {
|
method: 'GET',
|
||||||
const appItem = document.createElement('div');
|
headers: {
|
||||||
appItem.className = 'app-item';
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
const appInfo = document.createElement('div');
|
timeout: 10000 // 10秒超时
|
||||||
appInfo.className = 'app-info';
|
|
||||||
|
|
||||||
const appName = document.createElement('h3');
|
|
||||||
appName.textContent = app.name;
|
|
||||||
|
|
||||||
const appDate = document.createElement('p');
|
|
||||||
appDate.textContent = `上传时间:${app.date}`;
|
|
||||||
|
|
||||||
const downloadBtn = document.createElement('button');
|
|
||||||
downloadBtn.className = 'download-btn';
|
|
||||||
downloadBtn.textContent = '下载';
|
|
||||||
downloadBtn.onclick = () => downloadApp(app.id, app.fileName);
|
|
||||||
|
|
||||||
const copyLinkBtn = document.createElement('button');
|
|
||||||
copyLinkBtn.className = 'download-btn';
|
|
||||||
copyLinkBtn.textContent = '复制直链';
|
|
||||||
copyLinkBtn.onclick = () => copyDirectLink(app.id, app.fileName);
|
|
||||||
|
|
||||||
appInfo.appendChild(appName);
|
|
||||||
appInfo.appendChild(appDate);
|
|
||||||
appItem.appendChild(appInfo);
|
|
||||||
|
|
||||||
const buttonsContainer = document.createElement('div');
|
|
||||||
buttonsContainer.style.display = 'flex';
|
|
||||||
buttonsContainer.style.gap = '10px';
|
|
||||||
buttonsContainer.appendChild(downloadBtn);
|
|
||||||
buttonsContainer.appendChild(copyLinkBtn);
|
|
||||||
|
|
||||||
appItem.appendChild(buttonsContainer);
|
|
||||||
appList.appendChild(appItem);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('获取App列表失败:', error);
|
|
||||||
appList.innerHTML = '<p style="color: #ff0000; text-align: center;">获取App列表失败</p>';
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP错误!状态:${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const apps = await response.json();
|
||||||
|
|
||||||
|
if (apps.length === 0) {
|
||||||
|
appList.innerHTML = '<p style="text-align: center;">暂无App</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
appList.innerHTML = '';
|
||||||
|
apps.forEach(app => {
|
||||||
|
const appItem = document.createElement('div');
|
||||||
|
appItem.className = 'app-item';
|
||||||
|
|
||||||
|
const appInfo = document.createElement('div');
|
||||||
|
appInfo.className = 'app-info';
|
||||||
|
|
||||||
|
const appName = document.createElement('h3');
|
||||||
|
appName.textContent = app.name;
|
||||||
|
|
||||||
|
const appDate = document.createElement('p');
|
||||||
|
appDate.textContent = `上传时间:${app.date}`;
|
||||||
|
|
||||||
|
const downloadBtn = document.createElement('button');
|
||||||
|
downloadBtn.className = 'download-btn';
|
||||||
|
downloadBtn.textContent = '下载';
|
||||||
|
downloadBtn.onclick = () => downloadApp(app.id, app.fileName);
|
||||||
|
|
||||||
|
const copyLinkBtn = document.createElement('button');
|
||||||
|
copyLinkBtn.className = 'download-btn';
|
||||||
|
copyLinkBtn.textContent = '复制直链';
|
||||||
|
copyLinkBtn.onclick = () => copyDirectLink(app.id, app.fileName);
|
||||||
|
|
||||||
|
appInfo.appendChild(appName);
|
||||||
|
appInfo.appendChild(appDate);
|
||||||
|
appItem.appendChild(appInfo);
|
||||||
|
|
||||||
|
const buttonsContainer = document.createElement('div');
|
||||||
|
buttonsContainer.style.display = 'flex';
|
||||||
|
buttonsContainer.style.gap = '10px';
|
||||||
|
buttonsContainer.appendChild(downloadBtn);
|
||||||
|
buttonsContainer.appendChild(copyLinkBtn);
|
||||||
|
|
||||||
|
appItem.appendChild(buttonsContainer);
|
||||||
|
appList.appendChild(appItem);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取App列表失败:', error);
|
||||||
|
appList.innerHTML = `<p style="color: #ff0000; text-align: center;">获取App列表失败:${error.message}</p>`;
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 上传App
|
// 上传App
|
||||||
function uploadApp() {
|
async function uploadApp() {
|
||||||
let appName = document.getElementById('appName').value;
|
let appName = document.getElementById('appName').value;
|
||||||
const appFile = document.getElementById('appFile').files[0];
|
const appFile = document.getElementById('appFile').files[0];
|
||||||
const message = document.getElementById('uploadMessage');
|
const message = document.getElementById('uploadMessage');
|
||||||
@@ -135,15 +181,25 @@ function uploadApp() {
|
|||||||
formData.append('name', appName);
|
formData.append('name', appName);
|
||||||
formData.append('file', appFile);
|
formData.append('file', appFile);
|
||||||
|
|
||||||
fetch(`${API_BASE_URL}/apps`, {
|
try {
|
||||||
method: 'POST',
|
message.textContent = '上传中...';
|
||||||
body: formData
|
message.style.color = '#0000ff';
|
||||||
})
|
|
||||||
.then(response => response.json())
|
const response = await fetch(`${API_BASE_URL}/apps`, {
|
||||||
.then(result => {
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
timeout: 30000 // 30秒超时
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP错误!状态:${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
message.textContent = '上传成功!';
|
message.textContent = '上传成功!';
|
||||||
message.style.color = '#0000ff';
|
message.style.color = '#008000';
|
||||||
loadAppList();
|
loadAppList();
|
||||||
// 清空表单
|
// 清空表单
|
||||||
document.getElementById('appName').value = '';
|
document.getElementById('appName').value = '';
|
||||||
@@ -152,22 +208,26 @@ function uploadApp() {
|
|||||||
message.textContent = '上传失败:' + (result.message || '未知错误');
|
message.textContent = '上传失败:' + (result.message || '未知错误');
|
||||||
message.style.color = '#ff0000';
|
message.style.color = '#ff0000';
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
.catch(error => {
|
|
||||||
console.error('上传失败:', error);
|
console.error('上传失败:', error);
|
||||||
message.textContent = '上传失败,请重试!';
|
message.textContent = '上传失败:' + error.message;
|
||||||
message.style.color = '#ff0000';
|
message.style.color = '#ff0000';
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下载App
|
// 下载App
|
||||||
function downloadApp(appId, fileName) {
|
function downloadApp(appId, fileName) {
|
||||||
const link = document.createElement('a');
|
try {
|
||||||
link.href = `${API_BASE_URL}/apps/${appId}`;
|
const link = document.createElement('a');
|
||||||
link.download = fileName;
|
link.href = `${API_BASE_URL}/apps/${appId}`;
|
||||||
document.body.appendChild(link);
|
link.download = fileName;
|
||||||
link.click();
|
document.body.appendChild(link);
|
||||||
document.body.removeChild(link);
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('下载失败:', error);
|
||||||
|
alert('下载失败:' + error.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 复制直链
|
// 复制直链
|
||||||
@@ -202,6 +262,7 @@ function copyDirectLink(appId, fileName) {
|
|||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error('复制失败:', err);
|
console.error('复制失败:', err);
|
||||||
|
alert('复制失败:' + err.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
21
style.css
21
style.css
@@ -50,6 +50,16 @@ button {
|
|||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
background-color: #fff;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
background-color: #e0e0e0;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
@@ -90,6 +100,12 @@ p {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#loadingIndicator {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.container {
|
.container {
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
@@ -108,4 +124,9 @@ p {
|
|||||||
.download-btn {
|
.download-btn {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 刷新按钮和加载状态在移动端的适配 */
|
||||||
|
#downloadSection h2 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user