好的,我们将一步步来构建一个漂亮、功能齐全且兼容手机端的阿里云 OSS 目录和文件管理器。
核心思路:
1. 后端 API (这里模拟): 实际应用中,你需要一个后端服务来与阿里云 OSS 进行交互。这个后端服务会提供接口供前端调用,例如:
* listObjects(prefix): 列出指定前缀(目录)下的文件和目录。
* uploadFile(file, key): 上传文件到 OSS。
* deleteObject(key): 删除 OSS 中的文件或目录。
* createFolder(prefix): 在 OSS 中创建一个空目录(通过上传一个以 / 结尾的空文件实现)。
* renameObject(oldKey, newKey): 重命名 OSS 中的文件或目录(实际操作是复制然后删除)。
出于演示目的,我们将直接在前端模拟这些 API 调用,不涉及真实的 OSS SDK。 如果你需要集成真实的 OSS SDK,请参考阿里云官方文档。
2. 前端 HTML 结构: 使用 HTML 构建页面的骨架,包括:
* 导航栏/面包屑导航:显示当前路径。
* 文件/目录列表区域:展示当前目录下的内容。
* 操作按钮:用于创建目录、上传文件等。
* 模态框:用于重命名、确认删除等操作。
3. 前端 JavaScript 逻辑: 实现页面的动态交互和与后端(模拟)的通信。
* 数据获取与渲染: 从后端获取文件列表,动态生成 HTML 元素展示。
* 文件上传: 使用 <input type=“file”> 和 FormData。
* 目录/文件操作: 绑定点击事件,触发相应的 API 调用。
* 状态管理: 跟踪当前路径、选中项等。
* 响应式设计: 确保在不同屏幕尺寸下都有良好的用户体验。
4. 前端 CSS 样式: 使用 CSS 美化界面,使其“漂亮”且具有良好的可读性。
* 布局: 使用 Flexbox 或 Grid 实现响应式布局。
* 颜色、字体、间距: 提升视觉效果。
* 图标: 使用 Font Awesome 或 SVG 图标增强辨识度。
* 模态框样式: 弹出层的样式。
—
index.html)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>阿里云 OSS 文件管理器</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="style.css">
<style>
/* 更多样式将在 style.css 中定义 */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
margin: 0;
padding: 0;
background-color: #f8f9fa;
color: #333;
}
.container {
max-width: 1200px;
margin: 20px auto;
padding: 15px;
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
/* Header and Navigation */
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 15px;
border-bottom: 1px solid #eee;
margin-bottom: 20px;
flex-wrap: wrap; /* Allow wrapping on smaller screens */
}
.header h1 {
margin: 0;
font-size: 24px;
color: #0070f3;
}
.actions {
display: flex;
gap: 10px;
}
.btn {
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s ease, transform 0.2s ease;
display: flex;
align-items: center;
gap: 5px;
}
.btn-primary {
background-color: #0070f3;
color: white;
}
.btn-primary:hover {
background-color: #005bb5;
transform: translateY(-1px);
}
.btn-secondary {
background-color: #e0e0e0;
color: #333;
}
.btn-secondary:hover {
background-color: #d5d5d5;
transform: translateY(-1px);
}
.btn-danger {
background-color: #dc3545;
color: white;
}
.btn-danger:hover {
background-color: #c82333;
transform: translateY(-1px);
}
.btn-icon i {
font-size: 16px;
}
/* Breadcrumbs */
.breadcrumbs {
display: flex;
align-items: center;
flex-wrap: wrap;
margin-bottom: 20px;
font-size: 16px;
color: #555;
}
.breadcrumbs a {
text-decoration: none;
color: #0070f3;
margin-right: 5px;
}
.breadcrumbs a:hover {
text-decoration: underline;
}
.breadcrumbs .current {
color: #333;
font-weight: bold;
}
/* File/Directory List */
.file-list {
list-style: none;
padding: 0;
margin: 0;
}
.file-item {
display: flex;
align-items: center;
padding: 12px 15px;
border-bottom: 1px solid #eee;
cursor: pointer;
transition: background-color 0.2s ease;
position: relative; /* For hover actions */
}
.file-item:last-child {
border-bottom: none;
}
.file-item:hover {
background-color: #f0f0f0;
}
.file-item .icon {
margin-right: 15px;
font-size: 20px;
color: #666;
width: 30px; /* Fixed width for alignment */
text-align: center;
}
.file-item.folder .icon {
color: #ffc107; /* Yellow for folders */
}
.file-item.file .icon {
color: #007bff; /* Blue for files */
}
.file-item .name {
flex-grow: 1;
font-size: 16px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-item .actions {
display: none; /* Initially hidden */
gap: 8px;
margin-left: 15px;
font-size: 16px;
}
.file-item:hover .actions {
display: flex; /* Show on hover */
}
.file-item .actions .action-icon {
color: #555;
transition: color 0.2s ease;
}
.file-item .actions .action-icon:hover {
color: #0070f3;
}
.file-item .actions .action-icon.delete-icon:hover {
color: #dc3545;
}
/* Upload Area */
.upload-area {
margin-top: 20px;
padding: 20px;
border: 2px dashed #ccc;
border-radius: 8px;
text-align: center;
cursor: pointer;
transition: border-color 0.2s ease, background-color 0.2s ease;
}
.upload-area:hover {
border-color: #0070f3;
background-color: #f0f8ff;
}
.upload-area input[type="file"] {
display: none; /* Hide the default input */
}
.upload-area i {
font-size: 48px;
color: #aaa;
margin-bottom: 10px;
}
.upload-area p {
margin: 0;
font-size: 18px;
color: #666;
}
/* Modal */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
}
.modal-content {
background-color: #fff;
padding: 30px;
border-radius: 8px;
width: 90%;
max-width: 500px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
animation: fadeIn 0.3s ease;
position: relative;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-20px); }
to { opacity: 1; transform: translateY(0); }
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #eee;
padding-bottom: 15px;
margin-bottom: 20px;
}
.modal-header h2 {
margin: 0;
font-size: 22px;
color: #0070f3;
}
.close-button {
font-size: 24px;
font-weight: bold;
cursor: pointer;
color: #aaa;
transition: color 0.2s ease;
}
.close-button:hover,
.close-button:focus {
color: #333;
text-decoration: none;
}
.modal-body .input-group {
margin-bottom: 20px;
}
.modal-body label {
display: block;
margin-bottom: 8px;
font-weight: bold;
color: #555;
}
.modal-body input[type="text"] {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 16px;
box-sizing: border-box; /* Include padding and border in the element's total width and height */
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
padding-top: 20px;
border-top: 1px solid #eee;
margin-top: 20px;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.container {
margin: 10px;
padding: 10px;
}
.header {
flex-direction: column;
align-items: flex-start;
}
.header h1 {
font-size: 20px;
margin-bottom: 10px;
}
.actions {
flex-wrap: wrap;
justify-content: center;
width: 100%;
}
.btn {
font-size: 12px;
padding: 8px 10px;
}
.breadcrumbs {
font-size: 14px;
}
.file-item {
padding: 10px 12px;
}
.file-item .icon {
margin-right: 10px;
font-size: 18px;
width: 25px;
}
.file-item .name {
font-size: 14px;
}
.file-item .actions {
display: flex !important; /* Always show actions on mobile for easier access */
margin-left: 10px;
}
.upload-area {
padding: 15px;
}
.upload-area i {
font-size: 36px;
}
.upload-area p {
font-size: 16px;
}
.modal-content {
width: 95%;
padding: 20px;
}
.modal-header h2 {
font-size: 18px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>OSS 文件管理器</h1>
<div class="actions">
<button class="btn btn-primary" id="create-folder-btn"><i class="fa-solid fa-folder-plus"></i>创建目录</button>
<label for="file-upload" class="btn btn-primary btn-icon"><i class="fa-solid fa-upload"></i>上传文件</label>
<input type="file" id="file-upload" multiple>
</div>
</div>
<nav class="breadcrumbs" id="breadcrumbs"></nav>
<ul class="file-list" id="file-list">
<!-- 文件和目录列表将在这里渲染 -->
</ul>
<div class="upload-area" id="upload-area">
<input type="file" id="drag-drop-upload" multiple>
<i class="fa-solid fa-cloud-arrow-up"></i>
<p>拖拽文件到此处或点击选择文件</p>
</div>
</div>
<!-- 重命名模态框 -->
<div id="rename-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>重命名</h2>
<span class="close-button" id="close-rename-modal">×</span>
</div>
<div class="modal-body">
<div class="input-group">
<label for="rename-input">新名称:</label>
<input type="text" id="rename-input" placeholder="输入新名称">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="cancel-rename-btn">取消</button>
<button class="btn btn-primary" id="confirm-rename-btn">重命名</button>
</div>
</div>
</div>
<!-- 删除确认模态框 -->
<div id="delete-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>确认删除</h2>
<span class="close-button" id="close-delete-modal">×</span>
</div>
<div class="modal-body">
<p>确定要删除 <strong id="delete-target-name"></strong> 吗?此操作无法撤销。</p>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="cancel-delete-btn">取消</button>
<button class="btn btn-danger" id="confirm-delete-btn">删除</button>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
—
script.js)
javascript
// script.js
// --- 模拟 OSS API ---
// 在真实应用中,这些函数将与阿里云 OSS SDK 交互
const mockOssApi = {
// 模拟的 OSS 存储
// structure: { 'folder/': { files: [], folders: [] }, 'file.txt': 'content' }
storage: {
'': { folders: ['documents/', 'images/'], files: ['readme.md'] }
},
currentPath: '', // 当前模拟的 OSS 路径
// 模拟列出对象
listObjects: async function(prefix = '') {
console.log(`[API MOCK] Listing objects for prefix: "${prefix}"`);
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 150));
if (!prefix.endsWith('/')) {
prefix += '/';
}
const currentDir = this.storage[prefix] || { folders: [], files: [] };
const items = [];
// Add folders
currentDir.folders.forEach(folderName => {
items.push({ type: 'folder', name: folderName.replace(/\/$/, ''), prefix: folderName });
});
// Add files
currentDir.files.forEach(fileName => {
items.push({ type: 'file', name: fileName, key: prefix + fileName });
});
console.log(`[API MOCK] Found ${items.length} items.`);
return { objects: items };
},
// 模拟创建目录
createFolder: async function(prefix) {
console.log(`[API MOCK] Creating folder: "${prefix}"`);
await new Promise(resolve => setTimeout(resolve, 100));
if (!prefix.endsWith('/')) {
prefix += '/';
}
if (!this.storage[prefix]) {
this.storage[prefix] = { folders: [], files: [] };
// Find the parent directory and add this folder
const parentPrefix = prefix.substring(0, prefix.lastIndexOf('/', prefix.length - 2) + 1);
if (this.storage[parentPrefix]) {
this.storage[parentPrefix].folders.push(prefix);
} else {
// If parent doesn't exist, create root if it's the first folder
if (parentPrefix === '' && !this.storage['']) {
this.storage[''] = { folders: [], files: [] };
}
// If creating a nested folder and parent doesn't exist, it's an issue
if (parentPrefix !== '' && !this.storage[parentPrefix]) {
console.error(`[API MOCK] Parent directory "${parentPrefix}" not found.`);
throw new Error("Parent directory not found.");
}
}
console.log(`[API MOCK] Folder created successfully.`);
return { success: true };
} else {
console.warn(`[API MOCK] Folder "${prefix}" already exists.`);
throw new Error("Folder already exists.");
}
},
// 模拟删除对象 (文件或目录)
deleteObject: async function(key) {
console.log(`[API MOCK] Deleting object: "${key}"`);
await new Promise(resolve => setTimeout(resolve, 100));
const isFolder = key.endsWith('/');
if (isFolder) {
// Check if directory is empty before deleting
const dirContent = this.storage[key];
if (dirContent && (dirContent.folders.length > 0 || dirContent.files.length > 0)) {
console.error(`[API MOCK] Directory "${key}" is not empty.`);
throw new Error("Directory is not empty.");
}
const parentPrefix = key.substring(0, key.lastIndexOf('/', key.length - 2) + 1);
if (this.storage[parentPrefix]) {
this.storage[parentPrefix].folders = this.storage[parentPrefix].folders.filter(f => f !== key);
}
delete this.storage[key];
console.log(`[API MOCK] Folder "${key}" deleted.`);
} else {
const parentPrefix = key.substring(0, key.lastIndexOf('/') + 1);
if (this.storage[parentPrefix]) {
this.storage[parentPrefix].files = this.storage[parentPrefix].files.filter(f => f !== key);
}
delete this.storage[key];
console.log(`[API MOCK] File "${key}" deleted.`);
}
return { success: true };
},
// 模拟重命名对象
renameObject: async function(oldKey, newKey) {
console.log(`[API MOCK] Renaming "${oldKey}" to "${newKey}"`);
await new Promise(resolve => setTimeout(resolve, 150));
const isFolder = oldKey.endsWith('/');
const newIsFolder = newKey.endsWith('/');
if (isFolder !== newIsFolder) {
throw new Error("Cannot change type from file to folder or vice versa.");
}
const parentOld = oldKey.substring(0, oldKey.lastIndexOf('/') + 1);
const parentNew = newKey.substring(0, newKey.lastIndexOf('/') + 1);
if (parentOld !== parentNew) {
throw new Error("Renaming across different directories is not supported in this mock.");
}
if (isFolder) {
if (this.storage[oldKey]) {
// Rename folder in parent's list
if (this.storage[parentOld]) {
this.storage[parentOld].folders = this.storage[parentOld].folders.map(f => f === oldKey ? newKey : f);
}
// Rename the storage key itself
this.storage[newKey] = this.storage[oldKey];
delete this.storage[oldKey];
// IMPORTANT: Update all nested paths (simplified for mock)
// In a real scenario, you'd need to update all nested objects' prefixes.
// For this mock, we'll assume no complex nested operations.
console.log(`[API MOCK] Folder renamed to "${newKey}"`);
return { success: true };
}
} else {
if (this.storage[parentOld] && this.storage[parentOld].files.includes(oldKey.substring(parentOld.length))) {
// Rename file in parent's list
this.storage[parentOld].files = this.storage[parentOld].files.map(f => f === oldKey.substring(parentOld.length) ? newKey.substring(parentNew.length) : f);
// Rename the storage key itself
if (this.storage[oldKey]) { // Check if the file itself has a value (e.g., content)
this.storage[newKey] = this.storage[oldKey];
delete this.storage[oldKey];
} else {
// If it was just an entry in the parent's file list, add it
this.storage[newKey] = ''; // Assume empty content for simplicity
}
console.log(`[API MOCK] File renamed to "${newKey}"`);
return { success: true };
}
}
throw new Error("Object not found or rename failed.");
},
// 模拟上传文件
uploadFile: async function(file, key) {
console.log(`[API MOCK] Uploading file "${file.name}" to "${key}"`);
await new Promise(resolve => setTimeout(resolve, 300 + Math.random() * 200)); // Simulate upload time
const parentPrefix = key.substring(0, key.lastIndexOf('/') + 1);
if (!this.storage[parentPrefix]) {
// If parent doesn't exist, try to create it or handle error
// For simplicity, we assume parent exists or can be implicitly created
if (parentPrefix !== '') {
console.warn(`[API MOCK] Parent directory "${parentPrefix}" might not exist. Attempting to add file anyway.`);
// In a real scenario, you might need to create parent directories recursively.
if (!this.storage['']) { this.storage[''] = { folders: [], files: [] }; } // Ensure root exists
}
}
if (!this.storage[parentPrefix]) { // Ensure parent dir entry exists
this.storage[parentPrefix] = { folders: [], files: [] };
}
const fileName = key.substring(parentPrefix.length);
if (!this.storage[parentPrefix].files.includes(fileName)) {
this.storage[parentPrefix].files.push(fileName);
}
this.storage[key] = `content of ${file.name}`; // Store dummy content
console.log(`[API MOCK] File "${key}" uploaded.`);
return { success: true, url: `mock-oss-url/${key}` };
}
};
// --- DOM Elements ---
const fileListElement = document.getElementById('file-list');
const breadcrumbsElement = document.getElementById('breadcrumbs');
const fileUploadInput = document.getElementById('file-upload');
const dragDropUploadInput = document.getElementById('drag-drop-upload');
const uploadArea = document.getElementById('upload-area');
const createFolderBtn = document.getElementById('create-folder-btn');
const renameModal = document.getElementById('rename-modal');
const closeRenameModalBtn = document.getElementById('close-rename-modal');
const cancelRenameBtn = document.getElementById('cancel-rename-btn');
const confirmRenameBtn = document.getElementById('confirm-rename-btn');
const renameInput = document.getElementById('rename-input');
const deleteModal = document.getElementById('delete-modal');
const closeDeleteModalBtn = document.getElementById('close-delete-modal');
const cancelDeleteBtn = document.getElementById('cancel-delete-btn');
const confirmDeleteBtn = document.getElementById('confirm-delete-btn');
const deleteTargetNameElement = document.getElementById('delete-target-name');
// --- State Variables ---
let currentPrefix = '';
let itemToRename = null; // { type: 'folder' | 'file', originalKey: string }
let itemToDelete = null; // { type: 'folder' | 'file', originalKey: string }
// --- Helper Functions ---
// Function to update the breadcrumbs
function renderBreadcrumbs() {
breadcrumbsElement.innerHTML = '';
const parts = currentPrefix.split('/').filter(part => part !== '');
let path = '';
// Home link
const homeLink = document.createElement('a');
homeLink.href = '#';
homeLink.textContent = '根目录';
homeLink.setAttribute('data-prefix', '');
breadcrumbsElement.appendChild(homeLink);
parts.forEach((part, index) => {
path += part + '/';
const separator = document.createElement('span');
separator.textContent = ' / ';
breadcrumbsElement.appendChild(separator);
const link = document.createElement('a');
link.href = '#';
link.textContent = part;
link.setAttribute('data-prefix', path);
breadcrumbsElement.appendChild(link);
});
// Highlight the current part (or mark as current if it's the end)
if (parts.length > 0) {
const lastLink = breadcrumbsElement.lastChild;
if (lastLink && lastLink.tagName === 'A') {
const currentSpan = document.createElement('span');
currentSpan.classList.add('current');
currentSpan.textContent = parts[parts.length - 1];
breadcrumbsElement.replaceChild(currentSpan, lastLink);
}
} else {
// If currentPrefix is empty, the "根目录" link should be marked as current
const firstChild = breadcrumbsElement.firstChild;
if (firstChild && firstChild.tagName === 'A') {
const currentSpan = document.createElement('span');
currentSpan.classList.add('current');
currentSpan.textContent = '根目录';
breadcrumbsElement.replaceChild(currentSpan, firstChild);
}
}
}
// Function to navigate to a directory
async function navigateTo(prefix) {
currentPrefix = prefix;
await loadFileList();
renderBreadcrumbs();
}
// Function to render the file list
function renderFileList(items) {
fileListElement.innerHTML = ''; // Clear existing list
if (items.length === 0) {
fileListElement.innerHTML = '<li class="file-item"><i class="fa-solid fa-info-circle icon"></i><span class="name">此目录为空。</span></li>';
return;
}
items.sort((a, b) => {
// Folders first, then files
if (a.type === 'folder' && b.type !== 'folder') return -1;
if (a.type !== 'folder' && b.type === 'folder') return 1;
// Sort alphabetically
return a.name.localeCompare(b.name);
});
items.forEach(item => {
const listItem = document.createElement('li');
listItem.classList.add('file-item');
if (item.type === 'folder') {
listItem.classList.add('folder');
listItem.setAttribute('data-prefix', item.prefix); // Store prefix for navigation
} else {
listItem.classList.add('file');
listItem.setAttribute('data-key', item.key); // Store key for operations
}
const iconClass = item.type === 'folder' ? 'fa-solid fa-folder' : 'fa-solid fa-file-alt';
listItem.innerHTML = `
<div class="icon"><i class="${iconClass}"></i></div>
<span class="name">${item.name}</span>
<div class="actions">
${item.type === 'folder' ? '<i class="fa-solid fa-edit action-icon rename-icon" title="重命名"></i>' : '<i class="fa-solid fa-edit action-icon rename-icon" title="重命名"></i>'}
<i class="fa-solid fa-trash-alt action-icon delete-icon" title="删除"></i>
</div>
`;
fileListElement.appendChild(listItem);
});
}
// Function to load the file list from OSS
async function loadFileList() {
try {
// Show a loading indicator
fileListElement.innerHTML = '<li class="file-item"><div class="icon"></div><span class="name">正在加载...</span></li>';
const response = await mockOssApi.listObjects(currentPrefix);
renderFileList(response.objects);
} catch (error) {
console.error("Error loading file list:", error);
fileListElement.innerHTML = <li class="file-item"><i class="fa-solid fa-exclamation-triangle icon" style="color: #dc3545;"></i><span class="name">加载失败: ${error.message}</span></li>;
}
}
// Function to handle file/folder clicks
function handleItemClick(event) {
const target = event.target.closest('.file-item');
if (!target) return;
if (event.target.closest('.action-icon')) {
// Ignore clicks on action icons (handled separately)
return;
}
if (target.classList.contains('folder')) {
const prefix = target.getAttribute('data-prefix');
navigateTo(prefix);
} else {
// For files, you might want to open them or show details.
// For now, we'll just log.
const key = target.getAttribute('data-key');
console.log(`Clicked on file: ${key}`);
// Example: Open file in new tab (if you had direct URLs)
// window.open(`mock-oss-url/${key}`, '_blank');
}
}
// Function to handle breadcrumb clicks
function handleBreadcrumbClick(event) {
const target = event.target.closest('a[data-prefix]');
if (target) {
event.preventDefault();
const prefix = target.getAttribute('data-prefix');
navigateTo(prefix);
}
}
// Function to show the create folder modal
function showCreateFolderModal() {
const folderName = prompt("请输入新目录的名称:");
if (folderName && folderName.trim() !== '') {
const newFolderKey = (currentPrefix + folderName.trim() + '/');
createFolder(newFolderKey);
}
}
// Function to create a folder via API
async function createFolder(folderKey) {
try {
await mockOssApi.createFolder(folderKey);
await loadFileList(); // Refresh the list
} catch (error) {
alert(`创建目录失败: ${error.message}`);
}
}
// Function to handle file uploads
async function handleFileUpload(files) {
if (!files || files.length === 0) return;
for (const file of files) {
const key = currentPrefix + file.name;
try {
await mockOssApi.uploadFile(file, key);
// Optionally show progress
} catch (error) {
alert(`上传文件 "${file.name}" 失败: ${error.message}`);
}
}
await loadFileList(); // Refresh after all uploads
}
// Function to show rename modal
function showRenameModal(item) {
itemToRename = item;
renameInput.value = item.name;
renameModal.style.display = 'flex';
renameInput.focus();
}
function hideRenameModal() {
renameModal.style.display = 'none';
itemToRename = null;
renameInput.value = '';
}
async function confirmRename() {
const newName = renameInput.value.trim();
if (!newName || !itemToRename) {
hideRenameModal();
return;
}
const oldKey = itemToRename.originalKey;
const parentPrefix = oldKey.substring(0, oldKey.lastIndexOf('/') + 1);
const newKey = parentPrefix + newName + (itemToRename.type === 'folder' ? '/' : '');
try {
await mockOssApi.renameObject(oldKey, newKey);
await loadFileList(); // Refresh
hideRenameModal();
} catch (error) {
alert(`重命名失败: ${error.message}`);
hideRenameModal();
}
}
// Function to show delete confirmation modal
function showDeleteModal(item) {
itemToDelete = item;
deleteTargetNameElement.textContent = item.name;
deleteModal.style.display = 'flex';
}
function hideDeleteModal() {
deleteModal.style.display = 'none';
itemToDelete = null;
}
async function confirmDelete() {
if (!itemToDelete) {
hideDeleteModal();
return;
}
const key = itemToDelete.originalKey;
try {
await mockOssApi.deleteObject(key);
await loadFileList(); // Refresh
hideDeleteModal();
} catch (error) {
alert(`删除失败: ${error.message}`);
hideDeleteModal();
}
}
// Event Listeners
document.addEventListener('click', (event) => {
if (event.target.closest('.file-item')) {
handleItemClick(event);
} else if (event.target.closest('.action-icon.rename-icon')) {
const itemElement = event.target.closest('.file-item');
if (itemElement) {
const name = itemElement.querySelector('.name').textContent;
const type = itemElement.classList.contains('folder') ? 'folder' : 'file';
const originalKey = type === 'folder' ? itemElement.getAttribute('data-prefix') : itemElement.getAttribute('data-key');
showRenameModal({ name, type, originalKey });
}
} else if (event.target.closest('.action-icon.delete-icon')) {
const itemElement = event.target.closest('.file-item');
if (itemElement) {
const name = itemElement.querySelector('.name').textContent;
const type = itemElement.classList.contains('folder') ? 'folder' : 'file';
const originalKey = type === 'folder' ? itemElement.getAttribute('data-prefix') : itemElement.getAttribute('data-key');
showDeleteModal({ name, type, originalKey });
}
} else if (event.target.closest('#breadcrumbs a')) {
handleBreadcrumbClick(event);
}
});
createFolderBtn.addEventListener('click', showCreateFolderModal);
fileUploadInput.addEventListener('change', (event) => handleFileUpload(event.target.files));
// Drag and Drop for upload
uploadArea.addEventListener('dragover', (event) => {
event.preventDefault();
uploadArea.classList.add('drag-over');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('drag-over');
});
uploadArea.addEventListener('drop', (event) => {
event.preventDefault();
uploadArea.classList.remove('drag-over');
handleFileUpload(event.dataTransfer.files);
});
dragDropUploadInput.addEventListener('change', (event) => handleFileUpload(event.target.files));
// Modal event listeners
closeRenameModalBtn.addEventListener('click', hideRenameModal);
cancelRenameBtn.addEventListener('click', hideRenameModal);
confirmRenameBtn.addEventListener('click', confirmRename);
window.addEventListener('click', (event) => {
if (event.target === renameModal) hideRenameModal();
});
closeDeleteModalBtn.addEventListener('click', hideDeleteModal);
cancelDeleteBtn.addEventListener('click', hideDeleteModal);
confirmDeleteBtn.addEventListener('click', confirmDelete);
window.addEventListener('click', (event) => {
if (event.target === deleteModal) hideDeleteModal();
});
// --- Initialization ---
async function init() {
await loadFileList();
renderBreadcrumbs();
}
init();
—
style.css - 您可以复制上面的 <style> 块到这个文件中)
1. 创建一个名为 style.css 的文件。
2. 将 HTML 文件中 <style> 标签内的所有 CSS 代码复制并粘贴到 style.css 文件中。
—
1. 保存文件:
* 将上面的 HTML 代码保存为 index.html。
* 将上面的 JavaScript 代码保存为 script.js。
* 确保 style.css 文件(或者将 CSS 直接放在 HTML 的 <style> 标签内)也在同一个目录下。
2. 在浏览器中打开:
* 使用您的浏览器(Chrome, Firefox, Edge 等)直接打开 index.html 文件。
* 模拟 OSS: 代码中使用 mockOssApi 对象来模拟阿里云 OSS 的行为。它在内存中存储数据,不与真实的 OSS 服务交互。
* 文件/目录列表:
* 显示当前目录下的文件和文件夹。
* 文件夹可点击进入。
* 文件可以右侧出现重命名和删除图标(在桌面端鼠标悬停时显示,在手机端一直显示)。
* 面包屑导航: 显示当前路径,点击可以返回上一级或根目录。
* 创建目录: 点击“创建目录”按钮,会弹出一个输入框让您输入目录名,然后创建。
* 上传文件:
* 按钮上传: 点击“上传文件”按钮,选择文件。
* 拖拽上传: 将文件拖拽到页面指定的区域。
* 支持多文件上传。
* 重命名: 点击文件或目录右侧的编辑图标,弹出模态框,输入新名称后确认。
* 删除: 点击文件或目录右侧的删除图标,弹出确认模态框,确认后删除。
* 响应式设计:
* 使用了 meta name="viewport",确保在手机上正确缩放。
* CSS 中包含 @media 查询,调整了布局、字体大小、间距等,以适应小屏幕设备。
* 在手机端,操作按钮(重命名、删除)会一直显示,方便用户操作。
* 漂亮的外观:
* 使用了 Font Awesome 的图标。
* 简单的配色和间距,提供清晰的视觉层次。
* 模态框的动画效果。
1. 创建后端服务:
* 选择一种后端语言(Node.js, Python, Java, Go 等)。
* 使用阿里云 OSS SDK(例如 aliyun-sdk-oss for Node.js)来处理文件操作。
* 核心 API 接口:
* POST /api/oss/list: 接收 prefix,返回 objects 数组。
* POST /api/oss/upload: 接收文件和 key,上传到 OSS。
* POST /api/oss/delete: 接收 key,删除 OSS 对象。
* POST /api/oss/mkdir: 接收 prefix,在 OSS 创建目录(通过上传一个空文件实现)。
* POST /api/oss/rename: 接收 oldKey, newKey,进行重命名(OSS 中没有直接的 rename,通常是 copy + delete)。
* 注意: 为了安全,上传和删除等操作应进行签名认证,不应直接暴露 OSS 的 Access Key/Secret。
2. 修改前端 JavaScript:
* 移除 mockOssApi 对象。
* 在 loadFileList, createFolder, handleFileUpload, confirmRename, confirmDelete 等函数中,将调用 mockOssApi 的地方替换为 fetch 请求,发送到您创建的后端 API。
* 例如,loadFileList 会变成:
javascript
async function loadFileList() {
try {
fileListElement.innerHTML = '<li class="file-item"><div class="icon"></div><span class="name">正在加载...</span></li>';
const response = await fetch('/api/oss/list', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prefix: currentPrefix })
});
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
renderFileList(data.objects);
} catch (error) {
console.error("Error loading file list:", error);
fileListElement.innerHTML = <li class="file-item"><i class="fa-solid fa-exclamation-triangle icon" style="color: #dc3545;"></i><span class="name">加载失败: ${error.message}</span></li>;
}
}
* 文件上传可能需要使用 FormData 并发送到后端 /api/oss/upload 接口。
3. 安全考虑:
* OSS Bucket 配置: 确保您的 OSS Bucket 具有适当的访问权限。对于浏览器直接上传,可以考虑使用 STS (Security Token Service) 来生成临时访问凭证,这是比直接使用 Access Key/Secret 更安全的方式。
* 后端 API 鉴权: 保护您的后端 API,确保只有授权用户才能进行操作。
这个完整的解决方案提供了一个良好的起点。如果您需要特定后端语言的集成示例,或者对某个功能有疑问,请随时提出!