feat: apply UI/storage/share optimizations and quota improvements

This commit is contained in:
2026-02-12 18:02:57 +08:00
parent 1fcc60b9aa
commit 12859cbb20
13 changed files with 4476 additions and 828 deletions

View File

@@ -4,9 +4,9 @@
<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">
<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; }
@@ -626,14 +626,359 @@
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 .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;
}
}
</style>
</head>
<body>
<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" @click="backToList" style="margin-right: 10px;">
<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>
@@ -671,11 +1016,11 @@
<!-- 文件列表 -->
<div v-else-if="verified">
<p style="color: var(--text-secondary); margin-bottom: 20px;">
<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 :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>
<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>
<!-- 视图切换按钮 (多文件时才显示) -->
@@ -777,7 +1122,21 @@
methods: {
async init() {
const urlParams = new URLSearchParams(window.location.search);
this.shareCode = urlParams.get('code');
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 = '无效的分享链接';
@@ -906,21 +1265,20 @@
try {
// 获取下载 URLOSS 直连或后端代理)
const params = { path: filePath };
if (this.password) {
params.password = this.password;
}
const { data } = await axios.get(`${this.apiBase}/api/share/${this.shareCode}/download-url`, { params });
const { data } = await axios.post(`${this.apiBase}/api/share/${this.shareCode}/download-url`, {
path: filePath,
password: this.password || undefined
});
if (data.success) {
// 记录下载次数(异步,不等待)
axios.post(`${this.apiBase}/api/share/${this.shareCode}/download`)
.catch(err => console.error('记录下载次数失败:', err));
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 {
// 本地存储:通过后端下载
@@ -930,7 +1288,7 @@
}
} catch (error) {
console.error('[分享下载] 获取下载链接失败:', error);
alert('获取下载链接失败: ' + (error.response?.data?.message || error.message));
this.errorMessage = '获取下载链接失败: ' + (error.response?.data?.message || error.message);
}
},
@@ -1049,7 +1407,28 @@
}
},
mounted() {
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();
}
}).mount('#app');