Files
vue-driven-cloud-storage/frontend/share.html
T

1559 lines
44 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;
}
/* ========== 亮色玻璃主题 ========== */
.light-theme {
--bg-primary: #f0f4f8;
--bg-secondary: #ffffff;
--bg-card: rgba(255, 255, 255, 0.7);
--bg-card-hover: rgba(255, 255, 255, 0.9);
--glass-border: rgba(102, 126, 234, 0.2);
--glass-border-hover: rgba(102, 126, 234, 0.4);
--text-primary: #1a1a2e;
--text-secondary: rgba(26, 26, 46, 0.7);
--text-muted: rgba(26, 26, 46, 0.5);
--accent-1: #5a67d8;
--accent-2: #6b46c1;
--accent-3: #d53f8c;
--glow: rgba(90, 103, 216, 0.3);
}
/* 亮色主题背景渐变 */
body.light-theme::before {
background:
radial-gradient(ellipse at top right, rgba(102, 126, 234, 0.12) 0%, transparent 50%),
radial-gradient(ellipse at bottom left, rgba(118, 75, 162, 0.12) 0%, transparent 50%),
linear-gradient(135deg, #e0e7ff 0%, #f0f4f8 50%, #fdf2f8 100%);
}
* { 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>
<style>
/* ===== Enterprise Netdisk Share UI Rebuild (Classic Cloud Disk) ===== */
body.enterprise-netdisk-share,
body.enterprise-netdisk-share.light-theme,
body.enterprise-netdisk-share:not(.light-theme) {
--bg-primary: #edf2fb;
--bg-secondary: #fbfdff;
--bg-card: #f8fbff;
--bg-card-hover: #eef3fb;
--glass-border: #d2ddec;
--glass-border-hover: #bccce2;
--text-primary: #1f2937;
--text-secondary: #5b6472;
--text-muted: #8b95a7;
--accent-1: #2563eb;
--accent-2: #1d4ed8;
--accent-3: #1e40af;
--glow: rgba(37, 99, 235, 0.14);
--danger: #dc2626;
--success: #16a34a;
--warning: #d97706;
background: linear-gradient(180deg, #eef3fb 0%, #f8fafe 42%, #edf2fb 100%) !important;
color: var(--text-primary) !important;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', Roboto, sans-serif;
line-height: 1.5;
padding: 14px 8px;
background-attachment: fixed;
}
body.enterprise-netdisk-share::before {
display: none !important;
}
body.enterprise-netdisk-share .container {
max-width: 1140px;
margin: 0 auto;
}
body.enterprise-netdisk-share .card {
border: 1px solid var(--glass-border);
border-radius: 10px;
background: var(--bg-card);
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.06);
backdrop-filter: none;
padding: 18px;
margin-bottom: 0;
}
body.enterprise-netdisk-share .title {
margin-bottom: 14px;
padding-bottom: 12px;
border-bottom: 1px solid var(--glass-border);
color: var(--text-primary);
font-size: 21px;
background: none;
-webkit-text-fill-color: currentColor;
}
body.enterprise-netdisk-share .title i {
color: var(--accent-1);
-webkit-text-fill-color: currentColor;
}
body.enterprise-netdisk-share .share-back-btn {
margin-right: 6px;
}
body.enterprise-netdisk-share .share-meta-bar {
margin-bottom: 12px;
border: 1px solid var(--glass-border);
border-radius: 8px;
background: var(--bg-card-hover);
color: var(--text-secondary);
font-size: 13px;
line-height: 1.7;
padding: 10px 12px;
}
body.enterprise-netdisk-share .share-expire-time {
color: var(--success);
}
body.enterprise-netdisk-share .share-expire-time.expiring {
color: var(--warning);
}
body.enterprise-netdisk-share .share-expire-time.expired {
color: var(--danger);
}
body.enterprise-netdisk-share .share-expire-time.valid {
color: var(--success);
}
body.enterprise-netdisk-share .btn {
border-radius: 8px;
border: 1px solid transparent;
font-size: 13px;
font-weight: 600;
line-height: 1.2;
padding: 9px 14px;
box-shadow: none;
transition: all .2s ease;
}
body.enterprise-netdisk-share .btn-primary {
background: var(--accent-1);
border-color: var(--accent-1);
color: #fff;
}
body.enterprise-netdisk-share .btn-primary:hover {
background: var(--accent-2);
border-color: var(--accent-2);
transform: none;
box-shadow: none;
}
body.enterprise-netdisk-share .btn-secondary {
background: var(--bg-secondary);
border-color: var(--glass-border);
color: var(--text-primary);
}
body.enterprise-netdisk-share .btn-secondary:hover {
background: var(--bg-card-hover);
border-color: var(--glass-border-hover);
}
body.enterprise-netdisk-share .form-label {
color: var(--text-secondary);
font-size: 13px;
margin-bottom: 6px;
font-weight: 600;
}
body.enterprise-netdisk-share .form-input {
border: 1px solid var(--glass-border);
border-radius: 8px;
background: var(--bg-secondary);
color: var(--text-primary);
padding: 10px 12px;
font-size: 14px;
}
body.enterprise-netdisk-share .form-input:focus {
border-color: var(--accent-1);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.14);
outline: none;
background: var(--bg-secondary);
}
body.enterprise-netdisk-share .alert {
border-radius: 8px;
font-size: 13px;
padding: 10px 12px;
}
body.enterprise-netdisk-share .alert-error {
background: #fef2f2;
border-color: #fca5a5;
color: #991b1b;
}
body.enterprise-netdisk-share .download-alert {
margin-bottom: 12px;
animation: fadeInOut 0.2s ease;
}
body.enterprise-netdisk-share .view-controls {
display: flex;
gap: 8px;
margin-bottom: 12px;
flex-wrap: wrap;
}
body.enterprise-netdisk-share .loading {
border: 1px solid var(--glass-border);
border-radius: 8px;
background: var(--bg-card-hover);
color: var(--text-secondary);
padding: 28px 14px;
}
body.enterprise-netdisk-share .spinner {
width: 30px;
height: 30px;
border: 3px solid rgba(148, 163, 184, 0.25);
border-top: 3px solid var(--accent-1);
margin-bottom: 10px;
}
body.enterprise-netdisk-share .file-grid {
gap: 10px;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
}
body.enterprise-netdisk-share .file-grid-item {
border: 1px solid var(--glass-border);
border-radius: 8px;
background: var(--bg-secondary);
min-height: 170px;
padding: 12px;
transition: all .2s ease;
}
body.enterprise-netdisk-share .file-grid-item:hover {
border-color: #93c5fd;
background: #f8fbff;
transform: none;
box-shadow: none;
}
body.enterprise-netdisk-share .file-grid-icon {
margin-bottom: 10px;
}
body.enterprise-netdisk-share .file-grid-name {
font-size: 13px;
line-height: 1.35;
color: var(--text-primary);
}
body.enterprise-netdisk-share .file-grid-size {
font-size: 12px;
color: var(--text-muted);
margin: 6px 0 10px;
}
body.enterprise-netdisk-share .file-list {
border: 1px solid var(--glass-border);
border-radius: 8px;
overflow: hidden;
background: var(--bg-secondary);
}
body.enterprise-netdisk-share .file-item {
padding: 10px 12px;
border-bottom: 1px solid var(--glass-border);
transition: all .2s ease;
}
body.enterprise-netdisk-share .file-item:hover {
background: var(--bg-card-hover);
}
body.enterprise-netdisk-share .file-icon {
width: 26px;
text-align: center;
flex-shrink: 0;
}
body.enterprise-netdisk-share .single-file-container {
border: 1px solid var(--glass-border);
border-radius: 8px;
background: var(--bg-card-hover);
padding: 14px;
min-height: auto;
}
body.enterprise-netdisk-share .single-file-card {
border: 1px solid var(--glass-border);
border-radius: 8px;
background: var(--bg-secondary);
box-shadow: none;
padding: 20px;
max-width: 460px;
}
body.enterprise-netdisk-share .single-file-name {
color: var(--text-primary);
font-size: 18px;
}
body.enterprise-netdisk-share .single-file-size {
color: var(--text-secondary);
font-size: 13px;
}
body.enterprise-netdisk-share .single-file-download {
padding: 10px 20px;
font-size: 14px;
}
body.enterprise-netdisk-share .share-not-found {
border: 1px solid var(--glass-border);
border-radius: 8px;
background: var(--bg-card-hover);
padding: 40px 14px;
text-align: center;
}
body.enterprise-netdisk-share .share-not-found-title {
margin-bottom: 10px;
font-size: 22px;
color: var(--text-primary);
}
body.enterprise-netdisk-share .share-not-found-message {
color: var(--text-secondary);
font-size: 14px;
line-height: 1.6;
}
body.enterprise-netdisk-share ::-webkit-scrollbar {
width: 8px;
height: 8px;
}
body.enterprise-netdisk-share ::-webkit-scrollbar-track {
background: transparent;
}
body.enterprise-netdisk-share ::-webkit-scrollbar-thumb {
background: rgba(148, 163, 184, 0.45);
border-radius: 999px;
}
@media (max-width: 900px) {
body.enterprise-netdisk-share .card {
padding: 14px;
}
body.enterprise-netdisk-share .title {
font-size: 20px;
}
body.enterprise-netdisk-share .view-controls {
flex-direction: column;
}
body.enterprise-netdisk-share .view-controls .btn {
width: 100%;
}
body.enterprise-netdisk-share .file-grid {
grid-template-columns: repeat(auto-fill, minmax(118px, 1fr));
gap: 8px;
}
body.enterprise-netdisk-share .file-grid-item {
min-height: 152px;
}
body.enterprise-netdisk-share .single-file-card {
padding: 16px;
}
}
@keyframes fadeInOut {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
</head>
<body class="enterprise-netdisk-share">
<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 share-back-btn" @click="backToList">
<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">
<div v-if="downloadAlertMessage" class="alert alert-error download-alert">{{ downloadAlertMessage }}</div>
<p class="share-meta-bar">
分享者: <strong style="color: var(--text-primary);">{{ shareInfo.username }}</strong> |
创建时间: {{ formatDate(shareInfo.created_at) }}
<span v-if="shareInfo.expires_at"> | 到期时间: <strong class="share-expire-time" :class="{ expiring: isExpiringSoon(shareInfo.expires_at), expired: isExpired(shareInfo.expires_at) }">{{ formatExpireTime(shareInfo.expires_at) }}</strong></span>
<span v-else> | 有效期: <strong class="share-expire-time valid">永久有效</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)">
<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: '',
downloadAlertMessage: '',
downloadAlertTimer: null,
viewMode: "grid", // 视图模式: grid 大图标, list 列表(默认大图标)
// 主题
currentTheme: 'dark',
// 查看单个文件详情(用于多文件分享时点击查看)
viewingFile: null
};
},
methods: {
async init() {
const urlParams = new URLSearchParams(window.location.search);
let shareCode = (urlParams.get('code') || '').trim();
// 兼容 /s/{code} 直链(无 query 参数)
if (!shareCode) {
const match = window.location.pathname.match(/^\/s\/([^/?#]+)/i);
if (match && match[1]) {
try {
shareCode = decodeURIComponent(match[1]).trim();
} catch (_) {
shareCode = match[1].trim();
}
}
}
this.shareCode = shareCode;
if (!this.shareCode) {
this.errorMessage = '无效的分享链接';
this.loading = false;
return;
}
// 加载分享页面主题(基于分享者的主题偏好)
await this.loadTheme();
// 尝试验证分享
await this.verifyShare();
},
// 加载主题
async loadTheme() {
try {
const response = await axios.get(`${this.apiBase}/api/share/${this.shareCode}/theme`);
if (response.data.success) {
this.currentTheme = response.data.theme;
this.applyTheme(this.currentTheme);
}
} catch (error) {
// 出错时使用默认暗色主题
console.error('加载主题失败:', error);
}
},
// 应用主题
applyTheme(theme) {
if (theme === 'light') {
document.body.classList.add('light-theme');
} else {
document.body.classList.remove('light-theme');
}
},
async verifyShare() {
this.errorMessage = '';
this.downloadAlertMessage = '';
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) {
// 所有文件类型都显示详情页面(分享页面不提供媒体预览)
this.viewFileDetail(file);
},
// 查看文件详情(放大显示)
viewFileDetail(file) {
this.viewingFile = file;
},
// 返回文件列表
backToList() {
this.viewingFile = null;
},
async downloadFile(file) {
console.log("[分享下载] 文件:", file);
// 构建文件路径
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}`;
}
try {
// 获取下载 URL(OSS 直连或后端代理)
const { data } = await axios.post(`${this.apiBase}/api/share/${this.shareCode}/download-url`, {
path: filePath,
password: this.password || undefined
});
if (data.success) {
if (data.direct) {
// OSS 直连下载:新窗口打开
console.log("[分享下载] OSS 直连下载");
// 仅直连下载需要单独记录下载次数(本地代理下载在后端接口内已计数)
axios.post(`${this.apiBase}/api/share/${this.shareCode}/download`)
.catch(err => console.error('记录下载次数失败:', err));
window.open(data.downloadUrl, '_blank');
} else {
// 本地存储:通过后端下载
console.log("[分享下载] 后端代理下载");
this.triggerDownload(data.downloadUrl, file.name);
}
}
} catch (error) {
console.error('[分享下载] 获取下载链接失败:', error);
const message = error.response?.data?.message || '当前网络繁忙,请稍后再试';
this.showDownloadAlert(message);
}
},
showDownloadAlert(message) {
const safeMessage = typeof message === 'string' && message.trim()
? message.trim()
: '当前网络繁忙,请稍后再试';
this.downloadAlertMessage = safeMessage;
if (this.downloadAlertTimer) {
clearTimeout(this.downloadAlertTimer);
this.downloadAlertTimer = null;
}
this.downloadAlertTimer = setTimeout(() => {
this.downloadAlertMessage = '';
this.downloadAlertTimer = null;
}, 3000);
},
// 触发下载(使用隐藏的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;
}
},
async mounted() {
axios.defaults.withCredentials = true;
axios.interceptors.request.use(config => {
const csrfToken = document.cookie
.split('; ')
.find(row => row.startsWith('csrf_token='))
?.split('=')[1];
if (csrfToken && ['POST', 'PUT', 'DELETE', 'PATCH'].includes(config.method?.toUpperCase())) {
config.headers['X-CSRF-Token'] = csrfToken;
}
return config;
});
try {
await axios.get(`${this.apiBase}/api/csrf-token`);
} catch (error) {
console.warn('初始化 CSRF Token 失败:', error?.message || error);
}
this.init();
},
beforeUnmount() {
if (this.downloadAlertTimer) {
clearTimeout(this.downloadAlertTimer);
this.downloadAlertTimer = null;
}
}
}).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>