- 修复右键菜单白色背景问题(改为暗色玻璃效果) - 修复分享成功链接不可见问题(添加绿色高亮) - 修复媒体预览器白色背景 - 完全重写share.html为暗色主题(与主应用风格统一) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1105 lines
32 KiB
HTML
1105 lines
32 KiB
HTML
<!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>
|
|
/* 防止 Vue 初始化前显示原始模板 */
|
|
[v-cloak] { display: none !important; }
|
|
|
|
/* ========== 暗色主题 CSS 变量 ========== */
|
|
:root {
|
|
--bg-primary: #0a0a0f;
|
|
--bg-secondary: #12121a;
|
|
--bg-card: rgba(255, 255, 255, 0.03);
|
|
--bg-card-hover: rgba(255, 255, 255, 0.06);
|
|
--glass-border: rgba(255, 255, 255, 0.08);
|
|
--glass-border-hover: rgba(102, 126, 234, 0.3);
|
|
--text-primary: #ffffff;
|
|
--text-secondary: rgba(255, 255, 255, 0.6);
|
|
--text-muted: rgba(255, 255, 255, 0.4);
|
|
--accent-1: #667eea;
|
|
--accent-2: #764ba2;
|
|
--accent-3: #f093fb;
|
|
--glow: rgba(102, 126, 234, 0.4);
|
|
--danger: #ef4444;
|
|
--success: #22c55e;
|
|
--warning: #f59e0b;
|
|
}
|
|
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
}
|
|
|
|
/* 动态背景 */
|
|
body::before {
|
|
content: '';
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background:
|
|
radial-gradient(ellipse at top right, rgba(102, 126, 234, 0.15) 0%, transparent 50%),
|
|
radial-gradient(ellipse at bottom left, rgba(118, 75, 162, 0.15) 0%, transparent 50%);
|
|
z-index: -1;
|
|
}
|
|
|
|
.container {
|
|
max-width: 1000px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.card {
|
|
background: var(--bg-card);
|
|
backdrop-filter: blur(20px);
|
|
border: 1px solid var(--glass-border);
|
|
border-radius: 16px;
|
|
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
|
padding: 30px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.title {
|
|
font-size: 24px;
|
|
font-weight: 700;
|
|
background: linear-gradient(135deg, var(--accent-1), var(--accent-3));
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
margin-bottom: 20px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.title i {
|
|
-webkit-text-fill-color: var(--accent-1);
|
|
}
|
|
|
|
.form-group { margin-bottom: 20px; }
|
|
|
|
.form-label {
|
|
display: block;
|
|
margin-bottom: 8px;
|
|
font-weight: 500;
|
|
color: var(--text-secondary);
|
|
font-size: 14px;
|
|
}
|
|
|
|
.form-input {
|
|
width: 100%;
|
|
padding: 14px 16px;
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border: 1px solid var(--glass-border);
|
|
border-radius: 12px;
|
|
font-size: 14px;
|
|
color: var(--text-primary);
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.form-input:focus {
|
|
outline: none;
|
|
border-color: var(--accent-1);
|
|
background: rgba(255, 255, 255, 0.08);
|
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
|
|
}
|
|
|
|
.form-input::placeholder {
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.btn {
|
|
padding: 12px 24px;
|
|
border: none;
|
|
border-radius: 10px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
transition: all 0.3s;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
|
|
color: white;
|
|
box-shadow: 0 4px 15px var(--glow);
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 6px 20px var(--glow);
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border: 1px solid var(--glass-border);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background: rgba(255, 255, 255, 0.15);
|
|
border-color: var(--glass-border-hover);
|
|
}
|
|
|
|
.alert {
|
|
padding: 14px 16px;
|
|
border-radius: 12px;
|
|
margin-bottom: 15px;
|
|
border: 1px solid transparent;
|
|
}
|
|
|
|
.alert-error {
|
|
background: rgba(239, 68, 68, 0.15);
|
|
border-color: rgba(239, 68, 68, 0.3);
|
|
color: #fca5a5;
|
|
}
|
|
|
|
.file-list {
|
|
list-style: none;
|
|
}
|
|
|
|
.file-item {
|
|
padding: 15px;
|
|
border-bottom: 1px solid var(--glass-border);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.file-item:hover {
|
|
background: var(--bg-card-hover);
|
|
}
|
|
|
|
.file-info {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 15px;
|
|
}
|
|
|
|
.file-name-container {
|
|
flex: 1;
|
|
min-width: 0;
|
|
max-width: 100%;
|
|
}
|
|
|
|
.file-name-text {
|
|
font-weight: 500;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
max-width: 100%;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.file-icon {
|
|
font-size: 24px;
|
|
}
|
|
|
|
.loading {
|
|
text-align: center;
|
|
padding: 40px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.spinner {
|
|
border: 3px solid rgba(255, 255, 255, 0.1);
|
|
border-top: 3px solid var(--accent-1);
|
|
border-radius: 50%;
|
|
width: 40px;
|
|
height: 40px;
|
|
animation: spin 1s linear infinite;
|
|
margin: 0 auto 16px;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
|
|
/* 视图切换按钮 */
|
|
.view-controls {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 10px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
/* 大图标视图 */
|
|
.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: 1px solid var(--glass-border);
|
|
border-radius: 12px;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
background: var(--bg-card);
|
|
}
|
|
|
|
.file-grid-item:hover {
|
|
background: var(--bg-card-hover);
|
|
border-color: var(--glass-border-hover);
|
|
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.15);
|
|
transform: translateY(-4px);
|
|
}
|
|
|
|
.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: var(--text-primary);
|
|
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: var(--text-muted);
|
|
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: 1px solid var(--glass-border);
|
|
border-radius: 20px;
|
|
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1), rgba(118, 75, 162, 0.1));
|
|
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
|
max-width: 500px;
|
|
width: 100%;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.single-file-card:hover {
|
|
transform: translateY(-5px);
|
|
box-shadow: 0 12px 40px rgba(102, 126, 234, 0.2);
|
|
}
|
|
|
|
.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: var(--text-primary);
|
|
max-width: 100%;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 3;
|
|
-webkit-box-orient: vertical;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.single-file-size {
|
|
font-size: 16px;
|
|
color: var(--text-secondary);
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.single-file-download {
|
|
padding: 15px 40px;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
|
|
color: white;
|
|
border: none;
|
|
border-radius: 30px;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
box-shadow: 0 4px 15px var(--glow);
|
|
}
|
|
|
|
.single-file-download:hover {
|
|
transform: scale(1.05);
|
|
box-shadow: 0 6px 25px var(--glow);
|
|
}
|
|
|
|
/* 分享不存在提示 */
|
|
.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: var(--text-muted);
|
|
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: var(--text-secondary);
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.share-not-found-message {
|
|
font-size: 16px;
|
|
color: var(--text-muted);
|
|
line-height: 1.6;
|
|
}
|
|
|
|
/* 移动端适配 */
|
|
@media (max-width: 768px) {
|
|
body {
|
|
padding: 10px;
|
|
}
|
|
.container {
|
|
max-width: 100%;
|
|
}
|
|
.card {
|
|
padding: 20px;
|
|
border-radius: 12px;
|
|
}
|
|
.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;
|
|
}
|
|
}
|
|
|
|
/* 滚动条样式 */
|
|
::-webkit-scrollbar {
|
|
width: 8px;
|
|
height: 8px;
|
|
}
|
|
::-webkit-scrollbar-track {
|
|
background: rgba(255, 255, 255, 0.02);
|
|
}
|
|
::-webkit-scrollbar-thumb {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 4px;
|
|
}
|
|
::-webkit-scrollbar-thumb:hover {
|
|
background: rgba(255, 255, 255, 0.15);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="app" v-cloak>
|
|
<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: #ef4444;"></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: var(--text-secondary); margin-bottom: 20px;">
|
|
分享者: <strong style="color: var(--text-primary);">{{ shareInfo.username }}</strong> |
|
|
创建时间: {{ formatDate(shareInfo.created_at) }}
|
|
<span v-if="shareInfo.expires_at"> | 到期时间: <strong :style="{color: isExpiringSoon(shareInfo.expires_at) ? '#f59e0b' : isExpired(shareInfo.expires_at) ? '#ef4444' : '#22c55e'}">{{ formatExpireTime(shareInfo.expires_at) }}</strong></span>
|
|
<span v-else> | 有效期: <strong style="color: #22c55e;">永久有效</strong></span>
|
|
</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: var(--text-muted);">{{ 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: var(--text-muted); 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,新窗口打开直接下载(避免Mixed Content问题)
|
|
console.log("[分享下载] 使用HTTP下载:", file.httpDownloadUrl);
|
|
window.open(file.httpDownloadUrl, '_blank');
|
|
return;
|
|
} 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)}`;
|
|
}
|
|
|
|
this.triggerDownload(downloadUrl, file.name);
|
|
}
|
|
},
|
|
|
|
// 触发下载(使用隐藏的a标签,避免页面闪动)
|
|
triggerDownload(url, filename) {
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = filename || '';
|
|
link.style.display = 'none';
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
|
|
// 延迟移除,确保下载已触发
|
|
setTimeout(() => {
|
|
document.body.removeChild(link);
|
|
}, 100);
|
|
},
|
|
|
|
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
|
|
});
|
|
},
|
|
|
|
// 格式化到期时间显示
|
|
formatExpireTime(expiresAt) {
|
|
if (!expiresAt) return '永久有效';
|
|
|
|
const expireDate = new Date(expiresAt);
|
|
const now = new Date();
|
|
const diffMs = expireDate - now;
|
|
const diffMinutes = Math.floor(diffMs / (1000 * 60));
|
|
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
|
|
// 格式化日期
|
|
const dateStr = expireDate.toLocaleString('zh-CN', {
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
|
|
if (diffMs < 0) {
|
|
return `已过期 (${dateStr})`;
|
|
} else if (diffMinutes < 60) {
|
|
return `${diffMinutes}分钟后过期 (${dateStr})`;
|
|
} else if (diffHours < 24) {
|
|
return `${diffHours}小时后过期 (${dateStr})`;
|
|
} else if (diffDays === 1) {
|
|
return `明天过期 (${dateStr})`;
|
|
} else if (diffDays <= 7) {
|
|
return `${diffDays}天后过期 (${dateStr})`;
|
|
} else {
|
|
return dateStr;
|
|
}
|
|
},
|
|
|
|
// 判断是否即将过期(3天内)
|
|
isExpiringSoon(expiresAt) {
|
|
if (!expiresAt) return false;
|
|
const expireDate = new Date(expiresAt);
|
|
const now = new Date();
|
|
const diffMs = expireDate - now;
|
|
const diffDays = diffMs / (1000 * 60 * 60 * 24);
|
|
return diffDays > 0 && diffDays <= 3;
|
|
},
|
|
|
|
// 判断是否已过期
|
|
isExpired(expiresAt) {
|
|
if (!expiresAt) return false;
|
|
const expireDate = new Date(expiresAt);
|
|
const now = new Date();
|
|
return expireDate <= now;
|
|
}
|
|
},
|
|
|
|
mounted() {
|
|
this.init();
|
|
}
|
|
}).mount('#app');
|
|
</script>
|
|
|
|
<script>
|
|
// 检查是否启用调试模式
|
|
const isDebugMode = localStorage.getItem('debugMode') === 'true';
|
|
|
|
// 禁用右键菜单(调试模式下不禁用)
|
|
if (!isDebugMode) {
|
|
document.addEventListener('contextmenu', function(e) {
|
|
e.preventDefault();
|
|
return false;
|
|
});
|
|
}
|
|
|
|
// 禁用F12和常见开发者工具快捷键(调试模式下不禁用)
|
|
if (!isDebugMode) {
|
|
document.addEventListener('keydown', function(e) {
|
|
// F12
|
|
if (e.key === 'F12' || e.keyCode === 123) {
|
|
e.preventDefault();
|
|
return false;
|
|
}
|
|
// Ctrl+Shift+I (开发者工具)
|
|
if (e.ctrlKey && e.shiftKey && (e.key === 'I' || e.keyCode === 73)) {
|
|
e.preventDefault();
|
|
return false;
|
|
}
|
|
// Ctrl+Shift+J (控制台)
|
|
if (e.ctrlKey && e.shiftKey && (e.key === 'J' || e.keyCode === 74)) {
|
|
e.preventDefault();
|
|
return false;
|
|
}
|
|
// Ctrl+U (查看源代码)
|
|
if (e.ctrlKey && (e.key === 'U' || e.keyCode === 85)) {
|
|
e.preventDefault();
|
|
return false;
|
|
}
|
|
// Ctrl+Shift+C (元素选择器)
|
|
if (e.ctrlKey && e.shiftKey && (e.key === 'C' || e.keyCode === 67)) {
|
|
e.preventDefault();
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
|
|
// 检测开发者工具是否打开(调试模式下不检测)
|
|
if (!isDebugMode) {
|
|
(function() {
|
|
const threshold = 160;
|
|
let isDevToolsOpen = false;
|
|
|
|
setInterval(function() {
|
|
const widthThreshold = window.outerWidth - window.innerWidth > threshold;
|
|
const heightThreshold = window.outerHeight - window.innerHeight > threshold;
|
|
|
|
if (!(heightThreshold && widthThreshold) &&
|
|
((window.Firebug && window.Firebug.chrome && window.Firebug.chrome.isInitialized) || widthThreshold || heightThreshold)) {
|
|
if (!isDevToolsOpen) {
|
|
isDevToolsOpen = true;
|
|
console.clear();
|
|
}
|
|
} else {
|
|
isDevToolsOpen = false;
|
|
}
|
|
}, 500);
|
|
})();
|
|
}
|
|
|
|
// 禁用console输出(调试模式下不禁用)
|
|
if (!isDebugMode && window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1') {
|
|
console.log = function() {};
|
|
console.info = function() {};
|
|
console.warn = function() {};
|
|
console.error = function() {};
|
|
console.debug = function() {};
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|