Files
vue-driven-cloud-storage/frontend/share.html
喻勇祥 70b03c3c6b fix: 修复分享页面文件名过长导致布局溢出的问题
修改内容:
1. 单文件显示 (.single-file-name):
   - 添加 max-width: 100% 限制最大宽度
   - 使用 -webkit-line-clamp: 3 限制最多显示3行
   - 超出部分显示省略号 (...)
   - 改用 word-break: break-word 更优雅的换行

2. 列表视图文件名:
   - 添加 .file-name-container 和 .file-name-text 类
   - 使用 overflow: hidden 和 text-overflow: ellipsis
   - 单行显示,超出显示省略号
   - 设置 min-width: 0 确保flex容器正确缩小

修复效果:
- 文件名再长也不会溢出到界面外
- 单文件视图最多显示3行
- 列表视图单行显示带省略号
- 鼠标悬停在网格视图文件名上会显示完整文件名(title属性)
2025-11-14 09:20:13 +08:00

834 lines
24 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-name-container {
flex: 1;
min-width: 0; /* 允许flex子项缩小到内容大小以下 */
max-width: 100%;
}
.file-name-text {
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}
.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-word;
margin-bottom: 15px;
color: #333;
/* 限制文件名显示,防止过长溢出 */
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3; /* 最多显示3行 */
-webkit-box-orient: vertical;
line-height: 1.4;
}
.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">
<!-- 返回按钮:仅在查看单个文件详情且不是单文件分享时显示 -->
<button v-if="viewingFile && shareInfo.share_type !== 'file'" class="btn btn-secondary" @click="backToList" style="margin-right: 10px;">
<i class="fas fa-arrow-left"></i> 返回列表
</button>
<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="viewingFile || files.length === 1" class="single-file-container">
<div class="single-file-card">
<i class="single-file-icon fas" :class="getFileIcon(viewingFile || files[0])" :style="getIconColor(viewingFile || files[0])"></i>
<div class="single-file-name">{{ (viewingFile || files[0]).name }}</div>
<div class="single-file-size">{{ (viewingFile || files[0]).sizeFormatted }}</div>
<button v-if="!(viewingFile || files[0]).isDirectory" class="btn single-file-download" @click="downloadFile(viewingFile || files[0])">
<i class="fas fa-download"></i> 下载文件
</button>
</div>
</div>
<!-- 大图标视图 - 多文件网格显示 -->
<div v-else-if="!viewingFile && 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-if="!viewingFile" 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 class="file-name-container">
<div class="file-name-text">{{ 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,
// 查看单个文件详情(用于多文件分享时点击查看)
viewingFile: 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;
}
},
// 处理文件点击 - 可预览的文件打开预览,其他文件查看详情
handleFileClick(file) {
// 如果是图片/视频/音频,打开媒体预览
const isImage = /\.(jpg|jpeg|png|gif|bmp|webp|svg)$/i.test(file.name);
const isVideo = /\.(mp4|webm|ogg|mov)$/i.test(file.name);
const isAudio = /\.(mp3|wav|ogg|m4a|flac)$/i.test(file.name);
if (isImage || isVideo || isAudio) {
this.previewMedia(file);
} else {
// 其他文件类型,显示详情页面
this.viewFileDetail(file);
}
},
// 查看文件详情(放大显示)
viewFileDetail(file) {
this.viewingFile = file;
},
// 返回文件列表
backToList() {
this.viewingFile = null;
},
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 '';
// SQLite 返回的是 UTC 时间字符串,需要显式处理
let dateStr = dateString;
if (!dateStr.includes('Z') && !dateStr.includes('+') && !dateStr.includes('T')) {
// SQLite 格式: "2025-11-13 16:37:19" -> ISO格式: "2025-11-13T16:37:19Z"
dateStr = dateStr.replace(' ', 'T') + 'Z';
}
const date = new Date(dateStr);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
}
},
mounted() {
this.init();
}
}).mount('#app');
</script>
</body>
</html>