Files
vue-driven-cloud-storage/frontend/share.html
WanWanYun 2dc6323554 修复: 移除API地址硬编码,统一使用nginx代理
- 修复frontend/app.js中的localhost:40001硬编码
- 修复frontend/share.html中的localhost:40001硬编码
- 所有API请求现在统一通过nginx代理访问
- 支持任意端口号部署,无需修改前端代码
2025-11-11 23:54:40 +08:00

766 lines
22 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文件分享 - 玩玩云</title>
<script src="libs/vue.global.prod.js"></script>
<script src="libs/axios.min.js"></script>
<link rel="stylesheet" href="libs/fontawesome/css/all.min.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1000px;
margin: 0 auto;
}
.card {
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
padding: 30px;
margin-bottom: 20px;
}
.title {
font-size: 24px;
font-weight: 700;
color: #667eea;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
}
.form-group { margin-bottom: 20px; }
.form-label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #555;
}
.form-input {
width: 100%;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover { background: #5568d3; }
.alert { padding: 12px; border-radius: 8px; margin-bottom: 15px; }
.alert-error { background: #f8d7da; color: #721c24; }
.file-list {
list-style: none;
}
.file-item {
padding: 15px;
border-bottom: 1px solid #eee;
display: flex;
align-items: center;
justify-content: space-between;
}
.file-item:hover {
background: #f5f5f5;
}
.file-info {
display: flex;
align-items: center;
gap: 15px;
}
.file-icon {
font-size: 24px;
}
.loading {
text-align: center;
padding: 40px;
color: #999;
}
/* 视图切换按钮 */
.view-controls {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-bottom: 20px;
}
.btn-secondary {
background: #e0e0e0;
color: #333;
}
.btn-secondary:hover {
background: #d0d0d0;
}
/* 大图标视图 */
.file-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 25px;
padding: 10px 0;
}
.file-grid-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px 15px;
border: 2px solid #e8e8e8;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s;
background: #fff;
}
.file-grid-item:hover {
background: #f8f9fa;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
border-color: #667eea;
transform: translateY(-3px);
}
.file-grid-icon {
font-size: 56px;
margin-bottom: 12px;
}
.file-grid-name {
font-size: 14px;
font-weight: 500;
text-align: center;
word-break: break-all;
margin-bottom: 8px;
max-width: 100%;
color: #333;
/* 固定显示2行超出显示省略号 */
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.4;
min-height: 39px;
max-height: 39px;
}
.file-grid-size {
font-size: 12px;
color: #999;
margin-bottom: 12px;
}
/* 单文件居中显示 */
.single-file-container {
display: flex;
justify-content: center;
align-items: center;
padding: 40px 20px;
min-height: 300px;
}
.single-file-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 50px 40px;
border: 2px solid #e0e0e0;
border-radius: 16px;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
max-width: 500px;
width: 100%;
transition: all 0.3s;
}
.single-file-card:hover {
transform: translateY(-5px);
box-shadow: 0 12px 32px rgba(0,0,0,0.18);
}
.single-file-icon {
font-size: 120px;
margin-bottom: 25px;
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
}
.single-file-name {
font-size: 20px;
font-weight: 600;
text-align: center;
word-break: break-all;
margin-bottom: 15px;
color: #333;
}
.single-file-size {
font-size: 16px;
color: #666;
margin-bottom: 30px;
}
.single-file-download {
padding: 15px 40px;
font-size: 16px;
font-weight: 600;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 30px;
cursor: pointer;
transition: all 0.3s;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
}
.single-file-download:hover {
transform: scale(1.05);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
}
/* 响应式设计 */
@media (max-width: 768px) {
.single-file-card {
padding: 30px 20px;
}
.single-file-icon {
font-size: 80px;
}
.single-file-name {
font-size: 16px;
}
.single-file-size {
font-size: 14px;
}
.single-file-download {
padding: 12px 30px;
font-size: 14px;
}
}
/* 分享不存在提示 */
.share-not-found {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
}
.share-not-found-icon {
font-size: 100px;
color: #ccc;
margin-bottom: 30px;
animation: fadeIn 0.5s;
}
@keyframes fadeIn {
from { opacity: 0; transform: scale(0.8); }
to { opacity: 1; transform: scale(1); }
}
.share-not-found-title {
font-size: 24px;
font-weight: 600;
color: #666;
margin-bottom: 15px;
}
.share-not-found-message {
font-size: 16px;
color: #999;
line-height: 1.6;
}
/* 移动端适配 */
@media (max-width: 768px) {
body {
padding: 10px;
}
.container {
max-width: 100%;
}
.card {
padding: 20px;
border-radius: 8px;
}
.title {
font-size: 20px;
margin-bottom: 15px;
}
/* 视图切换按钮移动端优化 */
.view-controls {
margin-bottom: 15px;
}
.view-controls .btn {
padding: 8px 14px;
font-size: 13px;
}
/* 表单移动端优化 */
.form-group {
margin-bottom: 15px;
}
.form-label {
font-size: 14px;
margin-bottom: 6px;
}
.form-input {
padding: 10px 12px;
font-size: 14px;
}
.btn {
padding: 8px 16px;
font-size: 14px;
}
/* 文件网格视图移动端优化 */
.file-grid {
grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
gap: 15px;
}
.file-grid-item {
padding: 15px 10px;
}
.file-grid-icon {
font-size: 44px !important;
}
.file-grid-name {
font-size: 12px;
min-height: 34px;
max-height: 34px;
margin-bottom: 6px;
}
.file-grid-size {
font-size: 11px;
margin-bottom: 10px;
}
.file-grid-item .btn {
padding: 6px 10px;
font-size: 12px;
}
/* 列表视图移动端优化 */
.file-list {
padding: 0;
}
.file-item {
padding: 12px 8px;
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.file-info {
gap: 10px;
width: 100%;
}
.file-icon {
font-size: 20px !important;
}
.file-item .btn {
width: 100%;
padding: 8px 12px;
}
/* 单文件显示移动端优化 */
.single-file-container {
padding: 20px 10px;
min-height: 250px;
}
.single-file-card {
padding: 30px 20px;
max-width: 100%;
}
.single-file-icon {
font-size: 80px !important;
margin-bottom: 20px;
}
.single-file-name {
font-size: 16px;
margin-bottom: 12px;
}
.single-file-size {
font-size: 14px;
margin-bottom: 20px;
}
.single-file-download {
padding: 12px 30px;
font-size: 14px;
}
/* 分享不存在提示移动端优化 */
.share-not-found {
padding: 40px 15px;
}
.share-not-found-icon {
font-size: 70px;
margin-bottom: 20px;
}
.share-not-found-title {
font-size: 20px;
margin-bottom: 12px;
}
.share-not-found-message {
font-size: 14px;
}
/* 加载状态移动端优化 */
.loading {
padding: 30px 15px;
}
}
/* 超小屏幕优化 (手机竖屏) */
@media (max-width: 480px) {
.card {
padding: 15px;
}
.title {
font-size: 18px;
}
/* 文件网格更紧凑 */
.file-grid {
grid-template-columns: repeat(auto-fill, minmax(75px, 1fr));
gap: 10px;
}
.file-grid-icon {
font-size: 36px !important;
}
.file-grid-name {
font-size: 11px;
min-height: 31px;
max-height: 31px;
}
.file-grid-size {
font-size: 10px;
}
/* 单文件显示更紧凑 */
.single-file-icon {
font-size: 60px !important;
}
.single-file-name {
font-size: 14px;
}
.single-file-size {
font-size: 13px;
}
.single-file-download {
padding: 10px 24px;
font-size: 13px;
}
/* 视图切换按钮 */
.view-controls .btn {
padding: 6px 10px;
font-size: 12px;
}
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<div class="card">
<div class="title">
<i class="fas fa-cloud"></i>
文件分享
</div>
<!-- 通用错误显示 -->
<div v-if="errorMessage && !needPassword && !verified && !shareNotFound" class="share-not-found">
<i class="fas fa-exclamation-circle share-not-found-icon" style="color: #dc3545;"></i>
<div class="share-not-found-title">访问失败</div>
<div class="share-not-found-message">{{ errorMessage }}</div>
</div>
<!-- 密码验证 -->
<div v-if="needPassword && !verified && !shareNotFound">
<div v-if="errorMessage" class="alert alert-error">{{ errorMessage }}</div>
<div class="form-group">
<label class="form-label">请输入分享密码</label>
<input type="password" class="form-input" v-model="password" @keyup.enter="verifyShare" placeholder="输入密码">
</div>
<button class="btn btn-primary" @click="verifyShare">
<i class="fas fa-unlock"></i> 验证
</button>
</div>
<!-- 分享不存在 -->
<div v-else-if="shareNotFound" class="share-not-found">
<i class="fas fa-inbox share-not-found-icon"></i>
<div class="share-not-found-title">来晚了~</div>
<div class="share-not-found-message">
分享的内容已经取消了<br>
或者该分享链接已过期
</div>
</div>
<!-- 文件列表 -->
<div v-else-if="verified">
<p style="color: #666; margin-bottom: 20px;">
分享者: <strong>{{ shareInfo.username }}</strong> |
创建时间: {{ formatDate(shareInfo.created_at) }}
</p>
<!-- 视图切换按钮 (多文件时才显示) -->
<div v-if="files.length > 1" class="view-controls">
<button class="btn" :class="viewMode === 'grid' ? 'btn-primary' : 'btn-secondary'" @click="viewMode = 'grid'">
<i class="fas fa-th-large"></i> 大图标
</button>
<button class="btn" :class="viewMode === 'list' ? 'btn-primary' : 'btn-secondary'" @click="viewMode = 'list'">
<i class="fas fa-list"></i> 列表
</button>
</div>
<div v-if="loading" class="loading">
<div class="spinner"></div>
<p>加载中...</p>
</div>
<!-- 大图标视图 - 单文件居中显示 -->
<!-- 单文件居中显示 -->
<div v-else-if="files.length === 1" class="single-file-container">
<div class="single-file-card">
<i class="single-file-icon fas" :class="getFileIcon(files[0])" :style="getIconColor(files[0])"></i>
<div class="single-file-name">{{ files[0].name }}</div>
<div class="single-file-size">{{ files[0].sizeFormatted }}</div>
<button v-if="!files[0].isDirectory" class="btn single-file-download" @click="downloadFile(files[0])">
<i class="fas fa-download"></i> 下载文件
</button>
</div>
</div>
<!-- 大图标视图 - 多文件网格显示 -->
<div v-else-if="viewMode === 'grid'" class="file-grid">
<div v-for="file in files" :key="file.name" class="file-grid-item"
@click="handleFileClick(file)"
@contextmenu="showFileContextMenu($event, file)"
@touchstart="startLongPress($event, file)"
@touchend="cancelLongPress"
@touchmove="cancelLongPress">
<i class="file-grid-icon fas" :class="getFileIcon(file)" :style="getIconColor(file)"></i>
<div class="file-grid-name" :title="file.name">{{ file.name }}</div>
<div class="file-grid-size">{{ file.sizeFormatted }}</div>
<button v-if="!file.isDirectory" class="btn btn-primary" @click.stop="downloadFile(file)" style="width: 100%;">
<i class="fas fa-download"></i> 下载
</button>
</div>
</div>
<!-- 列表视图 -->
<ul v-else class="file-list">
<li v-for="file in files" :key="file.name" class="file-item">
<div class="file-info">
<i class="file-icon fas" :class="getFileIcon(file)" :style="getIconColor(file)"></i>
<div>
<div style="font-weight: 500;">{{ file.name }}</div>
<div style="font-size: 12px; color: #999;">{{ file.sizeFormatted }}</div>
</div>
</div>
<button v-if="!file.isDirectory" class="btn btn-primary" @click.stop="downloadFile(file)">
<i class="fas fa-download"></i> 下载
</button>
</li>
</ul>
<p v-if="files.length === 0" style="text-align: center; color: #999; padding: 40px;">
暂无文件
</p>
</div>
<!-- 加载中 -->
<div v-else-if="loading" class="loading">
<div class="spinner"></div>
<p>加载中...</p>
</div>
</div>
</div>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
// API配置 - 通过nginx代理访问
apiBase: window.location.protocol + '//' + window.location.host,
shareCode: '',
password: '',
needPassword: false,
verified: false,
shareNotFound: false,
shareInfo: null,
files: [],
loading: true,
errorMessage: '',
viewMode: "grid", // 视图模式: grid 大图标, list 列表(默认大图标)
// 媒体预览
showImageViewer: false,
showVideoPlayer: false,
showAudioPlayer: false,
currentMediaUrl: '',
currentMediaName: '',
currentMediaType: '', // 'image', 'video', 'audio'
// 右键菜单
showContextMenu: false,
contextMenuX: 0,
contextMenuY: 0,
contextMenuFile: null,
// 长按支持(移动端)
longPressTimer: null,
longPressFile: null
};
},
methods: {
async init() {
const urlParams = new URLSearchParams(window.location.search);
this.shareCode = urlParams.get('code');
if (!this.shareCode) {
this.errorMessage = '无效的分享链接';
this.loading = false;
return;
}
// 尝试验证分享
await this.verifyShare();
},
async verifyShare() {
this.errorMessage = '';
this.loading = true;
try {
const response = await axios.post(`${this.apiBase}/api/share/${this.shareCode}/verify`, {
password: this.password
});
if (response.data.success) {
this.verified = true;
this.shareInfo = response.data.share;
// 如果是单文件分享且后端已返回文件信息,直接使用,无需再次请求
if (response.data.file) {
this.files = [response.data.file];
this.loading = false;
} else {
// 目录分享,需要加载文件列表
await this.loadFiles();
}
}
} catch (error) {
// 404错误 - 分享不存在
if (error.response?.status === 404) {
this.shareNotFound = true;
this.loading = false;
}
// 需要密码
else if (error.response?.data?.needPassword) {
this.needPassword = true;
this.loading = false;
}
// 其他错误
else {
this.errorMessage = error.response?.data?.message || '验证失败';
this.loading = false;
}
}
},
async loadFiles() {
try {
const response = await axios.post(`${this.apiBase}/api/share/${this.shareCode}/list`, {
password: this.password,
path: ''
});
if (response.data.success) {
this.files = response.data.items;
}
} catch (error) {
console.error('加载文件失败:', error);
this.errorMessage = '加载文件失败';
} finally {
this.loading = false;
}
},
downloadFile(file) {
console.log("[分享下载] 文件:", file);
// 记录下载次数(异步,不等待)
axios.post(`${this.apiBase}/api/share/${this.shareCode}/download`)
.catch(err => console.error('记录下载次数失败:', err));
if (file.httpDownloadUrl) {
// 如果配置了HTTP下载URL使用HTTP直接下载
console.log("[分享下载] 使用HTTP下载:", file.httpDownloadUrl);
window.open(file.httpDownloadUrl, '_blank');
} else {
// 如果没有配置HTTP URL通过后端SFTP下载
console.log("[分享下载] 使用SFTP下载");
// 构建文件路径
let filePath;
if (this.shareInfo.share_type === 'file') {
// 单文件分享,使用 share_path
filePath = this.shareInfo.share_path;
} else {
// 目录分享,组合路径
const basePath = this.shareInfo.share_path;
filePath = basePath === '/' ? `/${file.name}` : `${basePath}/${file.name}`;
}
// 使用分享下载API公开API不需要认证
let downloadUrl = `${this.apiBase}/api/share/${this.shareCode}/download-file?path=${encodeURIComponent(filePath)}`;
// 如果有密码,附加密码参数
if (this.password) {
downloadUrl += `&password=${encodeURIComponent(this.password)}`;
}
window.open(downloadUrl, '_blank');
}
},
getFileIcon(file) {
if (file.isDirectory) return 'fa-folder';
if (file.name.match(/\.(jpg|jpeg|png|gif|bmp|svg)$/i)) return 'fa-file-image';
if (file.name.match(/\.(mp4|avi|mov|wmv|flv|mkv)$/i)) return 'fa-file-video';
if (file.name.match(/\.(mp3|wav|flac|aac|ogg)$/i)) return 'fa-file-audio';
if (file.name.match(/\.(pdf)$/i)) return 'fa-file-pdf';
if (file.name.match(/\.(doc|docx)$/i)) return 'fa-file-word';
if (file.name.match(/\.(xls|xlsx)$/i)) return 'fa-file-excel';
if (file.name.match(/\.(zip|rar|7z|tar|gz)$/i)) return 'fa-file-archive';
return 'fa-file';
},
getIconColor(file) {
if (file.isDirectory) return 'color: #FFC107;';
if (file.name.match(/\.(jpg|jpeg|png|gif|bmp|svg)$/i)) return 'color: #4CAF50;';
if (file.name.match(/\.(mp4|avi|mov|wmv|flv|mkv)$/i)) return 'color: #9C27B0;';
if (file.name.match(/\.(mp3|wav|flac|aac|ogg)$/i)) return 'color: #FF5722;';
if (file.name.match(/\.(pdf)$/i)) return 'color: #F44336;';
if (file.name.match(/\.(doc|docx)$/i)) return 'color: #2196F3;';
if (file.name.match(/\.(xls|xlsx)$/i)) return 'color: #4CAF50;';
if (file.name.match(/\.(zip|rar|7z|tar|gz)$/i)) return 'color: #795548;';
return 'color: #9E9E9E;';
},
formatDate(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
return date.toLocaleString('zh-CN');
}
},
mounted() {
this.init();
}
}).mount('#app');
</script>
</body>
</html>