7618 lines
290 KiB
HTML
7618 lines
290 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<!-- 邮件链接重定向到独立页面 -->
|
||
<script>
|
||
(function() {
|
||
const search = window.location.search;
|
||
if (search.includes('verifyToken')) {
|
||
window.location.replace('verify.html' + search);
|
||
} else if (search.includes('resetToken')) {
|
||
window.location.replace('reset-password.html' + search);
|
||
}
|
||
})();
|
||
</script>
|
||
<!-- 安全快捷键拦截(避免误判开发者工具) -->
|
||
<script>
|
||
(function() {
|
||
'use strict';
|
||
|
||
const isDebugMode = localStorage.getItem('debugMode') === 'true';
|
||
if (isDebugMode) return;
|
||
|
||
document.addEventListener('contextmenu', e => e.preventDefault());
|
||
document.addEventListener('keydown', function(e) {
|
||
const key = String(e.key || '').toLowerCase();
|
||
const blocked = e.keyCode === 123
|
||
|| (e.ctrlKey && e.shiftKey && ['i', 'j', 'c'].includes(key))
|
||
|| (e.ctrlKey && key === 'u');
|
||
if (blocked) {
|
||
e.preventDefault();
|
||
return false;
|
||
}
|
||
});
|
||
|
||
if (window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1') {
|
||
const noop = function() {};
|
||
['log', 'info', 'warn', 'error', 'debug'].forEach(method => {
|
||
try {
|
||
console[method] = noop;
|
||
} catch (_) {}
|
||
});
|
||
}
|
||
})();
|
||
</script>
|
||
<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>
|
||
/* ========== 暗色主题 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;
|
||
--info: #3b82f6;
|
||
}
|
||
|
||
/* ========== 亮色玻璃主题 ========== */
|
||
.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);
|
||
}
|
||
|
||
/* 亮色主题背景渐变 */
|
||
.light-theme body::before,
|
||
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%);
|
||
}
|
||
|
||
/* 亮色主题导航栏 */
|
||
body.light-theme .navbar {
|
||
background: rgba(255, 255, 255, 0.85);
|
||
border-bottom: 1px solid rgba(102, 126, 234, 0.15);
|
||
}
|
||
|
||
body.light-theme .nav-item {
|
||
color: #1a1a2e;
|
||
}
|
||
|
||
body.light-theme .nav-item:hover {
|
||
background: rgba(102, 126, 234, 0.1);
|
||
color: #5a67d8;
|
||
}
|
||
|
||
body.light-theme .nav-item.active {
|
||
color: white;
|
||
}
|
||
|
||
body.light-theme .user-info {
|
||
background: rgba(102, 126, 234, 0.08);
|
||
border-color: rgba(102, 126, 234, 0.2);
|
||
color: #1a1a2e;
|
||
}
|
||
|
||
body.light-theme .user-info .user-avatar {
|
||
color: #5a67d8;
|
||
}
|
||
|
||
/* 亮色主题卡片 */
|
||
body.light-theme .card {
|
||
background: rgba(255, 255, 255, 0.7);
|
||
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.1);
|
||
}
|
||
|
||
/* 亮色主题表格 */
|
||
body.light-theme table tr:hover {
|
||
background: rgba(102, 126, 234, 0.05);
|
||
}
|
||
|
||
/* 亮色主题模态框 */
|
||
body.light-theme .modal-content {
|
||
background: rgba(255, 255, 255, 0.95);
|
||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
/* 亮色主题输入框 */
|
||
body.light-theme .form-input {
|
||
background: rgba(255, 255, 255, 0.8);
|
||
border-color: rgba(102, 126, 234, 0.2);
|
||
}
|
||
|
||
body.light-theme .form-input:focus {
|
||
background: rgba(255, 255, 255, 0.95);
|
||
border-color: var(--accent-1);
|
||
}
|
||
|
||
/* 亮色主题按钮 */
|
||
body.light-theme .btn-secondary {
|
||
background: rgba(102, 126, 234, 0.1);
|
||
border-color: rgba(102, 126, 234, 0.3);
|
||
color: #1a1a2e;
|
||
}
|
||
|
||
body.light-theme .btn-secondary:hover {
|
||
background: rgba(102, 126, 234, 0.2);
|
||
border-color: rgba(102, 126, 234, 0.5);
|
||
}
|
||
|
||
body.light-theme .btn-icon {
|
||
color: #1a1a2e;
|
||
}
|
||
|
||
body.light-theme .btn-icon:hover {
|
||
background: rgba(102, 126, 234, 0.1);
|
||
}
|
||
|
||
/* 防止 Vue 初始化前显示原始模板 */
|
||
[v-cloak] { display: none !important; }
|
||
|
||
/* 应用加载占位符 */
|
||
.app-loading {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: var(--bg-primary);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 9999;
|
||
}
|
||
.app-loading .loading-spinner {
|
||
font-size: 48px;
|
||
color: var(--accent-primary);
|
||
animation: pulse 1.5s ease-in-out infinite;
|
||
}
|
||
@keyframes pulse {
|
||
0%, 100% { opacity: 0.4; transform: scale(0.95); }
|
||
50% { opacity: 1; transform: scale(1.05); }
|
||
}
|
||
|
||
* { 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;
|
||
}
|
||
|
||
/* 动态背景 */
|
||
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;
|
||
}
|
||
|
||
#app { min-height: 100vh; }
|
||
|
||
/* ========== 认证页面 ========== */
|
||
.auth-container {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-height: 100vh;
|
||
padding: 20px;
|
||
}
|
||
.auth-box {
|
||
background: var(--bg-card);
|
||
backdrop-filter: blur(20px);
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 20px;
|
||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||
padding: 40px;
|
||
width: 100%;
|
||
max-width: 450px;
|
||
}
|
||
.auth-title {
|
||
font-size: 28px;
|
||
font-weight: 700;
|
||
text-align: center;
|
||
margin-bottom: 30px;
|
||
background: linear-gradient(135deg, var(--accent-1), var(--accent-3));
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
}
|
||
.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: 15px;
|
||
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: 15px;
|
||
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;
|
||
width: 100%;
|
||
box-shadow: 0 4px 20px var(--glow);
|
||
}
|
||
.btn-primary:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 8px 30px var(--glow);
|
||
}
|
||
.btn:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
transform: none !important;
|
||
}
|
||
.alert {
|
||
padding: 14px 16px;
|
||
border-radius: 12px;
|
||
margin-bottom: 20px;
|
||
font-size: 14px;
|
||
border: 1px solid transparent;
|
||
}
|
||
.alert-error {
|
||
background: rgba(239, 68, 68, 0.15);
|
||
border-color: rgba(239, 68, 68, 0.3);
|
||
color: #fca5a5;
|
||
}
|
||
.alert-success {
|
||
background: rgba(34, 197, 94, 0.15);
|
||
border-color: rgba(34, 197, 94, 0.3);
|
||
color: #86efac;
|
||
}
|
||
.alert-info {
|
||
background: rgba(59, 130, 246, 0.15);
|
||
border-color: rgba(59, 130, 246, 0.3);
|
||
color: #93c5fd;
|
||
}
|
||
.auth-switch {
|
||
text-align: center;
|
||
margin-top: 24px;
|
||
color: var(--text-secondary);
|
||
font-size: 14px;
|
||
}
|
||
.auth-switch a {
|
||
color: var(--accent-3);
|
||
cursor: pointer;
|
||
text-decoration: none;
|
||
font-weight: 500;
|
||
}
|
||
.auth-switch a:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
/* ========== 导航栏 ========== */
|
||
.navbar {
|
||
background: rgba(10, 10, 15, 0.85);
|
||
backdrop-filter: blur(20px);
|
||
border-bottom: 1px solid var(--glass-border);
|
||
padding: 15px 30px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
.navbar-brand {
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
color: #667eea;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
.navbar-menu {
|
||
display: flex;
|
||
gap: 20px;
|
||
align-items: center;
|
||
}
|
||
.nav-item {
|
||
padding: 8px 16px;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
color: var(--text-secondary);
|
||
font-weight: 500;
|
||
}
|
||
.nav-item:hover {
|
||
background: rgba(255, 255, 255, 0.08);
|
||
color: var(--accent-1);
|
||
}
|
||
.nav-item.active {
|
||
background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
|
||
color: white;
|
||
box-shadow: 0 4px 15px var(--glow);
|
||
}
|
||
.user-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 8px 16px;
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 20px;
|
||
color: var(--text-primary);
|
||
}
|
||
.main-container {
|
||
max-width: 1200px;
|
||
margin: 30px auto;
|
||
padding: 0 20px;
|
||
}
|
||
.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;
|
||
}
|
||
.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); }
|
||
}
|
||
|
||
/* 文件网格视图 */
|
||
.file-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||
gap: 20px;
|
||
padding: 10px;
|
||
}
|
||
.file-grid-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 15px 10px 10px 10px;
|
||
border-radius: 12px;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
text-align: center;
|
||
min-height: 180px;
|
||
position: relative;
|
||
border: 1px solid transparent;
|
||
}
|
||
.file-grid-item:hover {
|
||
background: var(--bg-card-hover);
|
||
border-color: var(--glass-border-hover);
|
||
transform: translateY(-4px);
|
||
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.15);
|
||
}
|
||
|
||
.file-grid-item,
|
||
.file-grid-item *,
|
||
.file-list-row,
|
||
.file-list-row *,
|
||
.file-list-name-wrap,
|
||
.file-list-name-text {
|
||
-webkit-touch-callout: none;
|
||
-webkit-user-select: none;
|
||
user-select: none;
|
||
-webkit-tap-highlight-color: transparent;
|
||
}
|
||
.file-grid-item,
|
||
.file-list-row {
|
||
touch-action: pan-y;
|
||
}
|
||
.file-thumbnail,
|
||
.file-list-thumb {
|
||
-webkit-user-drag: none;
|
||
user-drag: none;
|
||
pointer-events: none;
|
||
}
|
||
.file-icon {
|
||
margin-bottom: 8px;
|
||
}
|
||
.file-thumbnail {
|
||
width: 64px;
|
||
height: 64px;
|
||
object-fit: cover;
|
||
border-radius: 8px;
|
||
border: 1px solid var(--glass-border);
|
||
}
|
||
.file-name {
|
||
font-size: 13px;
|
||
color: var(--text-primary);
|
||
word-break: break-all;
|
||
max-width: 100%;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 2;
|
||
-webkit-box-orient: vertical;
|
||
min-height: 34px;
|
||
}
|
||
.file-size {
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
margin-top: 5px;
|
||
margin-bottom: 8px;
|
||
}
|
||
.file-actions {
|
||
display: flex;
|
||
gap: 5px;
|
||
justify-content: center;
|
||
align-items: center;
|
||
position: absolute;
|
||
bottom: 10px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
}
|
||
.empty-hint {
|
||
text-align: center;
|
||
color: var(--text-muted);
|
||
padding: 40px;
|
||
font-size: 14px;
|
||
}
|
||
.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);
|
||
}
|
||
.download-traffic-range-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
.download-traffic-range-btn {
|
||
padding: 5px 10px;
|
||
font-size: 12px;
|
||
white-space: nowrap;
|
||
width: auto !important;
|
||
min-width: 0;
|
||
}
|
||
.download-traffic-range-btn-active {
|
||
background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
|
||
color: #fff;
|
||
border-color: transparent;
|
||
box-shadow: 0 4px 14px var(--glow);
|
||
}
|
||
.btn-icon {
|
||
background: none;
|
||
border: none;
|
||
padding: 8px;
|
||
cursor: pointer;
|
||
border-radius: 6px;
|
||
transition: all 0.3s;
|
||
color: var(--accent-1);
|
||
}
|
||
.btn-icon:hover {
|
||
background: rgba(102, 126, 234, 0.2);
|
||
color: var(--accent-3);
|
||
}
|
||
.file-actions {
|
||
display: flex;
|
||
gap: 5px;
|
||
margin-top: 8px;
|
||
justify-content: center;
|
||
}
|
||
.modal-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.7);
|
||
backdrop-filter: blur(4px);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
}
|
||
.modal-content {
|
||
background: var(--bg-secondary);
|
||
border: 1px solid var(--glass-border);
|
||
padding: 30px;
|
||
border-radius: 16px;
|
||
width: 500px;
|
||
max-width: 90%;
|
||
max-height: 90vh;
|
||
overflow-y: auto;
|
||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
||
}
|
||
.modal-content h3 {
|
||
color: var(--text-primary);
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
|
||
/* 管理端响应式增强 */
|
||
.admin-tabs-card {
|
||
overflow: hidden;
|
||
}
|
||
.admin-users-table-wrap,
|
||
.admin-log-list {
|
||
-webkit-overflow-scrolling: touch;
|
||
}
|
||
|
||
.admin-users-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.admin-users-toolbar {
|
||
display: grid;
|
||
grid-template-columns: minmax(220px, 1.2fr) repeat(4, minmax(130px, 1fr)) minmax(84px, 0.6fr) auto;
|
||
gap: 10px;
|
||
margin-bottom: 10px;
|
||
padding: 12px;
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 10px;
|
||
background: rgba(255, 255, 255, 0.03);
|
||
}
|
||
|
||
.admin-users-filter {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.admin-users-filter label {
|
||
font-size: 12px;
|
||
color: var(--text-secondary);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.admin-users-filter input,
|
||
.admin-users-filter select {
|
||
width: 100%;
|
||
min-width: 0;
|
||
padding: 8px 10px;
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 8px;
|
||
background: var(--bg-secondary);
|
||
color: var(--text-primary);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.admin-users-filter input::placeholder {
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.admin-users-filter-reset {
|
||
align-self: end;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.admin-users-filter-reset .btn {
|
||
height: 36px;
|
||
width: 100%;
|
||
padding: 0 12px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.admin-users-stats {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.admin-users-stat-chip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
padding: 5px 10px;
|
||
border-radius: 999px;
|
||
border: 1px solid var(--glass-border);
|
||
background: rgba(255, 255, 255, 0.06);
|
||
color: var(--text-secondary);
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.admin-users-empty-state {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 10px;
|
||
min-height: 130px;
|
||
border: 1px dashed var(--glass-border);
|
||
border-radius: 10px;
|
||
color: var(--text-muted);
|
||
font-size: 14px;
|
||
}
|
||
|
||
.admin-users-empty-state i {
|
||
font-size: 18px;
|
||
}
|
||
|
||
.admin-users-table thead th {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 2;
|
||
background: rgba(40, 50, 80, 0.92) !important;
|
||
backdrop-filter: blur(6px);
|
||
}
|
||
|
||
.admin-user-status-tag {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-width: 66px;
|
||
padding: 3px 8px;
|
||
border-radius: 999px;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
line-height: 1.2;
|
||
white-space: nowrap;
|
||
border: 1px solid transparent;
|
||
}
|
||
|
||
.admin-user-status-tag.status-active {
|
||
color: #22c55e;
|
||
background: rgba(34, 197, 94, 0.15);
|
||
border-color: rgba(34, 197, 94, 0.35);
|
||
}
|
||
|
||
.admin-user-status-tag.status-banned {
|
||
color: #ef4444;
|
||
background: rgba(239, 68, 68, 0.15);
|
||
border-color: rgba(239, 68, 68, 0.35);
|
||
}
|
||
|
||
.admin-user-status-tag.status-unverified {
|
||
color: #f59e0b;
|
||
background: rgba(245, 158, 11, 0.15);
|
||
border-color: rgba(245, 158, 11, 0.35);
|
||
}
|
||
|
||
.admin-user-status-tag.status-download_blocked {
|
||
color: #f97316;
|
||
background: rgba(249, 115, 22, 0.14);
|
||
border-color: rgba(249, 115, 22, 0.34);
|
||
}
|
||
|
||
.admin-users-pagination {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
margin-top: 10px;
|
||
padding: 10px 12px;
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 10px;
|
||
background: rgba(255, 255, 255, 0.03);
|
||
}
|
||
|
||
.admin-users-pagination-info {
|
||
color: var(--text-secondary);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.admin-users-pagination-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.admin-users-pagination-actions .btn {
|
||
min-width: 76px;
|
||
padding: 6px 10px;
|
||
}
|
||
|
||
.admin-users-page-indicator {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-width: 78px;
|
||
height: 32px;
|
||
padding: 0 8px;
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 8px;
|
||
font-size: 13px;
|
||
color: var(--text-secondary);
|
||
background: rgba(255, 255, 255, 0.04);
|
||
}
|
||
|
||
.admin-search-hit {
|
||
background: rgba(250, 204, 21, 0.28);
|
||
color: var(--text-primary);
|
||
padding: 0 2px;
|
||
border-radius: 3px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
|
||
/* 移动端适配 */
|
||
@media (max-width: 768px) {
|
||
/* 导航栏移动端优化 */
|
||
.navbar {
|
||
padding: 10px 15px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.navbar-brand {
|
||
font-size: 18px;
|
||
gap: 8px;
|
||
}
|
||
.navbar-menu {
|
||
width: 100%;
|
||
margin-top: 10px;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
justify-content: space-between;
|
||
}
|
||
.nav-item {
|
||
padding: 6px 10px;
|
||
font-size: 13px;
|
||
flex: 1;
|
||
text-align: center;
|
||
min-width: auto;
|
||
}
|
||
.user-info {
|
||
padding: 6px 12px;
|
||
font-size: 13px;
|
||
gap: 6px;
|
||
}
|
||
.btn-danger {
|
||
padding: 6px 12px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
/* 主容器移动端优化 */
|
||
.main-container {
|
||
margin: 15px auto;
|
||
padding: 0 10px;
|
||
}
|
||
.card {
|
||
padding: 15px;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
/* 文件网格视图移动端优化 */
|
||
.file-grid {
|
||
grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
|
||
gap: 12px;
|
||
padding: 5px;
|
||
}
|
||
.file-grid-item {
|
||
padding: 10px 5px;
|
||
}
|
||
.file-icon i {
|
||
font-size: 48px !important;
|
||
}
|
||
/* 图片缩略图移动端适配 */
|
||
.file-thumbnail {
|
||
width: 48px !important;
|
||
height: 48px !important;
|
||
}
|
||
/* 视频图标容器移动端适配 */
|
||
.file-icon div[style*="background: linear-gradient"] {
|
||
width: 48px !important;
|
||
height: 48px !important;
|
||
}
|
||
.file-icon div[style*="background: linear-gradient"] i {
|
||
font-size: 24px !important;
|
||
}
|
||
.file-name {
|
||
font-size: 12px;
|
||
}
|
||
.file-size {
|
||
font-size: 11px;
|
||
}
|
||
|
||
/* 文件操作按钮移动端优化 */
|
||
.file-actions {
|
||
gap: 3px;
|
||
margin-top: 5px;
|
||
}
|
||
.btn-icon {
|
||
padding: 5px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
/* 工具栏移动端优化 - 修正版 */
|
||
.card > div[style*="display: flex"][style*="justify-content: space-between"] {
|
||
flex-direction: column !important;
|
||
gap: 15px !important;
|
||
}
|
||
.card > div[style*="display: flex"][style*="justify-content: space-between"] > div {
|
||
width: 100% !important;
|
||
justify-content: center !important;
|
||
}
|
||
.card > div[style*="display: flex"][style*="justify-content: space-between"] > div button {
|
||
flex: 1;
|
||
max-width: calc(50% - 5px);
|
||
}
|
||
.download-traffic-range-actions {
|
||
width: 100%;
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
gap: 8px;
|
||
}
|
||
.download-traffic-range-btn {
|
||
width: 100% !important;
|
||
padding: 7px 6px;
|
||
}
|
||
.download-traffic-range-refresh {
|
||
grid-column: 1 / -1;
|
||
}
|
||
.file-list table {
|
||
font-size: 13px;
|
||
}
|
||
.file-list th,
|
||
.file-list td {
|
||
padding: 8px 5px !important;
|
||
}
|
||
.file-list th:nth-child(3),
|
||
.file-list td:nth-child(3) {
|
||
display: none; /* 隐藏修改时间列 */
|
||
}
|
||
.file-list .file-icon {
|
||
font-size: 16px !important;
|
||
}
|
||
.file-list-action-col {
|
||
display: table-cell !important;
|
||
}
|
||
.file-row-mobile-more {
|
||
display: inline-flex !important;
|
||
}
|
||
/* 列表视图视频图标容器移动端适配 */
|
||
.file-list div[style*="background: linear-gradient"] {
|
||
width: 28px !important;
|
||
height: 28px !important;
|
||
}
|
||
.file-list div[style*="background: linear-gradient"] i {
|
||
font-size: 14px !important;
|
||
}
|
||
|
||
/* 模态框移动端优化 */
|
||
.modal-content {
|
||
padding: 20px;
|
||
width: 95%;
|
||
}
|
||
.modal-content h3 {
|
||
font-size: 18px;
|
||
margin-bottom: 15px;
|
||
}
|
||
.form-group {
|
||
margin-bottom: 15px;
|
||
}
|
||
.form-input {
|
||
padding: 10px 12px;
|
||
font-size: 14px;
|
||
}
|
||
.btn {
|
||
padding: 8px 16px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
/* 认证页面移动端优化 */
|
||
.auth-box {
|
||
padding: 25px;
|
||
}
|
||
.auth-title {
|
||
font-size: 22px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
/* 管理员表格移动端优化 */
|
||
.card table {
|
||
font-size: 12px;
|
||
}
|
||
.card table th,
|
||
.card table td {
|
||
padding: 6px 4px !important;
|
||
}
|
||
.card table button {
|
||
padding: 4px 8px;
|
||
font-size: 11px;
|
||
margin: 1px;
|
||
}
|
||
.card table button i {
|
||
font-size: 10px;
|
||
}
|
||
|
||
/* 管理员页面移动端增强 */
|
||
.admin-tabs-nav {
|
||
display: grid !important;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 6px;
|
||
border-bottom: none !important;
|
||
padding: 8px;
|
||
}
|
||
.admin-tab-btn {
|
||
width: 100%;
|
||
padding: 10px 8px !important;
|
||
font-size: 13px !important;
|
||
border-radius: 8px !important;
|
||
}
|
||
.admin-debug-row,
|
||
.admin-health-header,
|
||
.admin-log-header {
|
||
flex-direction: column !important;
|
||
align-items: flex-start !important;
|
||
gap: 10px;
|
||
}
|
||
.admin-debug-toggle-btn {
|
||
width: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
padding: 10px 12px !important;
|
||
font-size: 14px !important;
|
||
}
|
||
.admin-health-summary {
|
||
width: 100%;
|
||
flex-wrap: wrap;
|
||
gap: 8px !important;
|
||
}
|
||
.admin-log-actions {
|
||
width: 100%;
|
||
display: grid !important;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 8px;
|
||
}
|
||
.admin-log-actions .btn {
|
||
width: 100%;
|
||
}
|
||
.admin-log-filters {
|
||
gap: 10px !important;
|
||
padding: 12px !important;
|
||
}
|
||
.admin-log-filter-item {
|
||
width: 100%;
|
||
flex-direction: column;
|
||
align-items: stretch !important;
|
||
gap: 6px !important;
|
||
}
|
||
.admin-log-filter-item select,
|
||
.admin-log-filter-item input {
|
||
width: 100%;
|
||
min-width: 0;
|
||
}
|
||
.admin-log-filter-search {
|
||
min-width: 0 !important;
|
||
}
|
||
.admin-log-row {
|
||
display: grid !important;
|
||
grid-template-columns: 1fr auto;
|
||
gap: 8px 10px;
|
||
}
|
||
.admin-log-time {
|
||
width: auto !important;
|
||
}
|
||
.admin-log-level {
|
||
width: auto !important;
|
||
justify-self: end;
|
||
}
|
||
.admin-log-category {
|
||
display: none !important;
|
||
}
|
||
.admin-log-content {
|
||
grid-column: 1 / -1;
|
||
}
|
||
.admin-log-pager {
|
||
flex-wrap: wrap;
|
||
}
|
||
.admin-log-pager .btn {
|
||
min-width: 110px;
|
||
}
|
||
.admin-users-header {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
.admin-users-toolbar {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
.admin-users-filter-search {
|
||
grid-column: 1 / -1;
|
||
}
|
||
.admin-users-filter-reset {
|
||
grid-column: 1 / -1;
|
||
}
|
||
.admin-users-filter-reset .btn {
|
||
width: 100%;
|
||
}
|
||
.admin-users-pagination {
|
||
padding: 10px;
|
||
}
|
||
.admin-users-pagination-info {
|
||
width: 100%;
|
||
}
|
||
.admin-users-pagination-actions {
|
||
width: 100%;
|
||
justify-content: space-between;
|
||
}
|
||
.admin-users-pagination-actions .btn {
|
||
min-width: 0;
|
||
flex: 1;
|
||
}
|
||
.admin-users-page-indicator {
|
||
min-width: 64px;
|
||
}
|
||
.admin-users-table {
|
||
min-width: 700px !important;
|
||
}
|
||
.admin-users-table th:nth-child(1),
|
||
.admin-users-table td:nth-child(1),
|
||
.admin-users-table th:nth-child(6),
|
||
.admin-users-table td:nth-child(6) {
|
||
display: none;
|
||
}
|
||
.admin-user-actions {
|
||
justify-content: flex-start !important;
|
||
gap: 4px !important;
|
||
}
|
||
.admin-user-action-btn {
|
||
padding: 4px 8px !important;
|
||
}
|
||
|
||
}
|
||
|
||
/* 超小屏幕优化 (手机竖屏) */
|
||
@media (max-width: 480px) {
|
||
.navbar-brand {
|
||
font-size: 16px;
|
||
}
|
||
.nav-item {
|
||
font-size: 11px;
|
||
padding: 5px 8px;
|
||
}
|
||
.file-grid {
|
||
grid-template-columns: repeat(auto-fill, minmax(75px, 1fr));
|
||
gap: 8px;
|
||
}
|
||
.file-icon i {
|
||
font-size: 40px !important;
|
||
}
|
||
/* 视频图标容器超小屏幕适配 */
|
||
.file-icon div[style*="background: linear-gradient"] {
|
||
width: 40px !important;
|
||
height: 40px !important;
|
||
}
|
||
.file-icon div[style*="background: linear-gradient"] i {
|
||
font-size: 20px !important;
|
||
}
|
||
.file-name {
|
||
font-size: 11px;
|
||
}
|
||
|
||
|
||
/* 管理员页面超小屏优化 */
|
||
.admin-tabs-nav {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
.admin-log-actions {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
.admin-users-toolbar {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
.admin-health-summary {
|
||
font-size: 12px !important;
|
||
}
|
||
.admin-log-row {
|
||
padding: 8px 4px !important;
|
||
}
|
||
.admin-log-time {
|
||
font-size: 11px !important;
|
||
}
|
||
.admin-users-table {
|
||
min-width: 620px !important;
|
||
}
|
||
.admin-user-action-btn {
|
||
font-size: 10px !important;
|
||
}
|
||
.admin-users-table th:nth-child(3),
|
||
.admin-users-table td:nth-child(3),
|
||
.admin-users-table th:nth-child(7),
|
||
.admin-users-table td:nth-child(7) {
|
||
display: none;
|
||
}
|
||
.admin-users-pagination-actions {
|
||
gap: 6px;
|
||
}
|
||
.admin-users-pagination-actions .btn {
|
||
padding: 6px 8px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
/* 列表视图在超小屏幕隐藏文件大小列 */
|
||
.file-list th:nth-child(2),
|
||
.file-list td:nth-child(2) {
|
||
display: none;
|
||
}
|
||
}
|
||
|
||
/* 拖拽上传样式 */
|
||
.files-container {
|
||
position: relative;
|
||
min-height: 200px;
|
||
}
|
||
|
||
.drag-drop-overlay {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(102, 126, 234, 0.1);
|
||
backdrop-filter: blur(2px);
|
||
border: 3px dashed #667eea;
|
||
border-radius: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 100;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.drag-drop-content {
|
||
text-align: center;
|
||
padding: 40px;
|
||
}
|
||
|
||
.files-container.drag-over {
|
||
background: rgba(102, 126, 234, 0.05);
|
||
border-radius: 12px;
|
||
}
|
||
|
||
/* 拖拽上传样式 */
|
||
.files-container {
|
||
position: relative;
|
||
min-height: 200px;
|
||
}
|
||
|
||
.drag-drop-overlay {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(102, 126, 234, 0.1);
|
||
backdrop-filter: blur(2px);
|
||
border: 3px dashed #667eea;
|
||
border-radius: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 100;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.drag-drop-content {
|
||
text-align: center;
|
||
padding: 40px;
|
||
}
|
||
|
||
.files-container.drag-over {
|
||
background: rgba(102, 126, 234, 0.05);
|
||
border-radius: 12px;
|
||
}
|
||
|
||
/* 健康检测状态颜色 */
|
||
.text-green-600 { color: #22c55e; }
|
||
.text-yellow-600 { color: #f59e0b; }
|
||
.text-red-600 { color: #ef4444; }
|
||
.text-blue-600 { color: #3b82f6; }
|
||
|
||
/* ========== 表格样式 ========== */
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
}
|
||
th, td {
|
||
padding: 12px;
|
||
text-align: left;
|
||
}
|
||
th {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
color: var(--text-secondary);
|
||
font-weight: 600;
|
||
border-bottom: 1px solid var(--glass-border);
|
||
}
|
||
tr {
|
||
border-bottom: 1px solid var(--glass-border);
|
||
}
|
||
tr:hover {
|
||
background: rgba(255, 255, 255, 0.02);
|
||
}
|
||
td {
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
/* 文件列表行样式 */
|
||
.file-list-row {
|
||
border-bottom: 1px solid var(--glass-border);
|
||
cursor: pointer;
|
||
transition: background 0.15s ease;
|
||
}
|
||
.file-list-row:hover {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
}
|
||
body.light-theme .file-list-row:hover {
|
||
background: rgba(0, 0, 0, 0.04);
|
||
}
|
||
|
||
.file-list-action-col {
|
||
display: none;
|
||
}
|
||
.file-row-mobile-more {
|
||
display: none;
|
||
border: 1px solid var(--glass-border);
|
||
background: rgba(255, 255, 255, 0.08);
|
||
color: var(--text-secondary);
|
||
padding: 6px 10px;
|
||
border-radius: 8px;
|
||
min-width: 36px;
|
||
justify-content: center;
|
||
align-items: center;
|
||
touch-action: manipulation;
|
||
}
|
||
|
||
/* ========== 危险按钮 ========== */
|
||
.btn-danger {
|
||
background: linear-gradient(135deg, #ef4444, #dc2626);
|
||
color: white;
|
||
border: none;
|
||
}
|
||
.btn-danger:hover {
|
||
box-shadow: 0 4px 15px rgba(239, 68, 68, 0.4);
|
||
}
|
||
|
||
/* ========== 信息面板样式 ========== */
|
||
.info-panel {
|
||
background: rgba(255, 255, 255, 0.03);
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 12px;
|
||
padding: 16px;
|
||
}
|
||
.info-label {
|
||
color: var(--text-muted);
|
||
font-size: 12px;
|
||
}
|
||
.info-value {
|
||
color: var(--text-primary);
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* ========== 状态标签 ========== */
|
||
.status-success {
|
||
color: #22c55e;
|
||
}
|
||
.status-warning {
|
||
color: #f59e0b;
|
||
}
|
||
.status-danger {
|
||
color: #ef4444;
|
||
}
|
||
.status-info {
|
||
color: #3b82f6;
|
||
}
|
||
|
||
/* ========== 通知栏样式 ========== */
|
||
.notice-info {
|
||
background: rgba(59, 130, 246, 0.1);
|
||
border-left: 4px solid var(--info);
|
||
border-radius: 8px;
|
||
padding: 12px 16px;
|
||
color: var(--text-secondary);
|
||
}
|
||
.notice-warning {
|
||
background: rgba(245, 158, 11, 0.1);
|
||
border-left: 4px solid var(--warning);
|
||
border-radius: 8px;
|
||
padding: 12px 16px;
|
||
color: var(--text-secondary);
|
||
}
|
||
.notice-success {
|
||
background: rgba(34, 197, 94, 0.1);
|
||
border-left: 4px solid var(--success);
|
||
border-radius: 8px;
|
||
padding: 12px 16px;
|
||
color: var(--text-secondary);
|
||
}
|
||
.notice-danger {
|
||
background: rgba(239, 68, 68, 0.1);
|
||
border-left: 4px solid var(--danger);
|
||
border-radius: 8px;
|
||
padding: 12px 16px;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
/* ========== 选择框和下拉样式 ========== */
|
||
select {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 8px;
|
||
padding: 8px 12px;
|
||
color: var(--text-primary);
|
||
cursor: pointer;
|
||
}
|
||
select:focus {
|
||
outline: none;
|
||
border-color: var(--accent-1);
|
||
}
|
||
select option {
|
||
background: var(--bg-secondary);
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
/* ========== 滚动条样式 ========== */
|
||
::-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);
|
||
}
|
||
|
||
/* 分享卡片布局 */
|
||
.share-card-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||
gap: 14px;
|
||
}
|
||
.share-card {
|
||
background: var(--bg-card);
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 12px;
|
||
padding: 14px;
|
||
box-shadow: 0 10px 30px rgba(0,0,0,0.12);
|
||
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
||
}
|
||
.share-card:hover {
|
||
transform: translateY(-2px);
|
||
border-color: var(--glass-border-hover);
|
||
box-shadow: 0 14px 36px rgba(0,0,0,0.18);
|
||
}
|
||
.share-card__title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
font-weight: 600;
|
||
font-size: 15px;
|
||
color: var(--text-primary);
|
||
margin-bottom: 8px;
|
||
word-break: break-all;
|
||
}
|
||
.share-card__chips {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
margin-bottom: 10px;
|
||
}
|
||
.share-chip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 4px 10px;
|
||
border-radius: 999px;
|
||
font-size: 12px;
|
||
border: 1px solid var(--glass-border);
|
||
background: rgba(255,255,255,0.04);
|
||
color: var(--text-secondary);
|
||
}
|
||
.share-chip.success { color: #22c55e; background: rgba(34,197,94,0.12); border-color: rgba(34,197,94,0.25); }
|
||
.share-chip.warn { color: #f59e0b; background: rgba(245,158,11,0.14); border-color: rgba(245,158,11,0.25); }
|
||
.share-chip.danger { color: #ef4444; background: rgba(239,68,68,0.14); border-color: rgba(239,68,68,0.25); }
|
||
.share-chip.info { color: var(--accent-1); background: rgba(102,126,234,0.14); border-color: rgba(102,126,234,0.25); }
|
||
.share-card__meta {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||
gap: 10px;
|
||
font-size: 12px;
|
||
color: var(--text-secondary);
|
||
margin-bottom: 12px;
|
||
}
|
||
.share-card__meta span {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
.share-card__actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.share-card__actions .btn {
|
||
padding: 8px 12px;
|
||
font-size: 13px;
|
||
}
|
||
.share-card__url-link {
|
||
color: var(--accent-1);
|
||
word-break: break-all;
|
||
user-select: text;
|
||
cursor: text;
|
||
text-decoration: none;
|
||
}
|
||
.share-list-link-text {
|
||
color: #667eea;
|
||
display: block;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
user-select: text;
|
||
cursor: text;
|
||
}
|
||
.share-list-actions {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.share-list-actions .btn {
|
||
padding: 6px 10px;
|
||
font-size: 12px;
|
||
}
|
||
.share-toolbar {
|
||
display: flex;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
}
|
||
.share-toolbar input,
|
||
.share-toolbar select {
|
||
background: rgba(255,255,255,0.05);
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 8px;
|
||
padding: 8px 12px;
|
||
color: var(--text-primary);
|
||
}
|
||
.share-toolbar input::placeholder {
|
||
color: var(--text-secondary);
|
||
}
|
||
.share-toolbar select {
|
||
min-width: 140px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.share-password-toggle {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 10px 12px;
|
||
border-radius: 10px;
|
||
border: 1px solid var(--glass-border);
|
||
background: rgba(255, 255, 255, 0.04);
|
||
color: var(--text-secondary);
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
}
|
||
.share-password-toggle input {
|
||
accent-color: var(--accent-1);
|
||
width: 16px;
|
||
height: 16px;
|
||
}
|
||
.share-password-hint {
|
||
display: block;
|
||
margin-top: 8px;
|
||
color: var(--text-muted);
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
}
|
||
.share-success-panel {
|
||
border: 1px solid rgba(34, 197, 94, 0.28);
|
||
border-radius: 12px;
|
||
padding: 14px;
|
||
background: rgba(34, 197, 94, 0.08);
|
||
color: var(--text-primary);
|
||
display: grid;
|
||
gap: 12px;
|
||
}
|
||
.share-success-head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
.share-success-head > i {
|
||
color: #22c55e;
|
||
font-size: 22px;
|
||
}
|
||
.share-success-title {
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
color: #22c55e;
|
||
}
|
||
.share-success-subtitle {
|
||
margin-top: 2px;
|
||
font-size: 13px;
|
||
color: var(--text-secondary);
|
||
word-break: break-all;
|
||
}
|
||
.share-success-link {
|
||
border-radius: 10px;
|
||
border: 1px dashed rgba(34, 197, 94, 0.35);
|
||
background: rgba(0, 0, 0, 0.12);
|
||
padding: 10px 12px;
|
||
font-size: 13px;
|
||
color: var(--text-primary);
|
||
word-break: break-all;
|
||
}
|
||
.share-success-actions {
|
||
display: flex;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.share-success-actions .btn {
|
||
flex: 1;
|
||
min-width: 96px;
|
||
padding: 9px 12px;
|
||
font-size: 13px;
|
||
}
|
||
.share-success-meta {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
}
|
||
.share-success-tip {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 12px;
|
||
padding: 10px 12px;
|
||
border-radius: 10px;
|
||
border: 1px solid transparent;
|
||
line-height: 1.5;
|
||
}
|
||
.share-success-tip.warn {
|
||
color: #f59e0b;
|
||
border-color: rgba(245, 158, 11, 0.28);
|
||
background: rgba(245, 158, 11, 0.12);
|
||
}
|
||
.share-success-tip.info {
|
||
color: #60a5fa;
|
||
border-color: rgba(59, 130, 246, 0.26);
|
||
background: rgba(59, 130, 246, 0.12);
|
||
}
|
||
body.light-theme .share-card {
|
||
box-shadow: 0 10px 30px rgba(0,0,0,0.08);
|
||
}
|
||
body.light-theme .share-card:hover {
|
||
box-shadow: 0 14px 36px rgba(0,0,0,0.12);
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.share-toolbar {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
.share-toolbar input,
|
||
.share-toolbar select {
|
||
width: 100%;
|
||
min-width: 0 !important;
|
||
}
|
||
.share-card-grid {
|
||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||
}
|
||
.share-card__actions .btn {
|
||
flex: 1;
|
||
min-width: 108px;
|
||
}
|
||
.share-list-actions {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
width: 100%;
|
||
}
|
||
.share-list-actions .btn {
|
||
width: 100%;
|
||
}
|
||
.share-success-actions {
|
||
flex-direction: column;
|
||
}
|
||
.share-success-actions .btn {
|
||
width: 100%;
|
||
min-width: 0;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.share-card-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
.share-card__meta {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
.share-card__actions .btn {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
</style>
|
||
</head>
|
||
<body class="enterprise-netdisk">
|
||
<div id="app" v-cloak>
|
||
<!-- 应用加载占位符(防止UI闪烁) -->
|
||
<div v-if="!appReady" class="app-loading">
|
||
<div class="loading-spinner">
|
||
<i class="fas fa-cloud"></i>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 应用主体内容 -->
|
||
<template v-else>
|
||
<div class="auth-container" v-if="!isLoggedIn">
|
||
<div class="auth-box">
|
||
<div class="auth-title">
|
||
<i class="fas fa-cloud"></i>
|
||
{{ isLogin ? '登录' : '注册' }}
|
||
</div>
|
||
<div v-if="errorMessage" class="alert alert-error">{{ errorMessage }}</div>
|
||
<div v-if="successMessage" class="alert alert-success">{{ successMessage }}</div>
|
||
<div v-if="verifyMessage" class="alert alert-info">{{ verifyMessage }}</div>
|
||
<form v-if="isLogin" @submit.prevent="handleLogin">
|
||
<div class="form-group">
|
||
<label class="form-label">用户名</label>
|
||
<input type="text" class="form-input" v-model="loginForm.username" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">密码</label>
|
||
<input type="password" class="form-input" v-model="loginForm.password" required>
|
||
</div>
|
||
<div v-if="showCaptcha" class="form-group">
|
||
<label class="form-label">验证码</label>
|
||
<div style="display: flex; gap: 10px; align-items: center;">
|
||
<input type="text" class="form-input" v-model="loginForm.captcha" required style="flex: 1;" placeholder="请输入验证码">
|
||
<div style="cursor: pointer; border: 1px solid var(--glass-border); border-radius: 8px; padding: 5px; background: rgba(255,255,255,0.05);" @click="refreshCaptcha">
|
||
<img :src="captchaUrl" alt="验证码" style="display: block; width: 120px; height: 40px;" />
|
||
</div>
|
||
</div>
|
||
<small style="color: var(--text-muted); font-size: 12px;">点击图片刷新验证码</small>
|
||
</div>
|
||
<div v-if="showResendVerify" class="alert alert-info" style="margin-bottom: 10px;">
|
||
<div>邮箱未验证?请输入验证码后重发激活邮件</div>
|
||
<div style="display: flex; gap: 10px; align-items: center; margin-top: 8px;">
|
||
<input type="text" class="form-input" v-model="resendVerifyCaptcha" placeholder="验证码" style="flex: 1; height: 40px;" @focus="!resendVerifyCaptchaUrl && refreshResendVerifyCaptcha()">
|
||
<img v-if="resendVerifyCaptchaUrl" :src="resendVerifyCaptchaUrl" @click="refreshResendVerifyCaptcha" style="height: 40px; cursor: pointer; border-radius: 4px;" title="点击刷新验证码">
|
||
<button type="button" class="btn btn-primary" @click="resendVerification" :disabled="resendingVerify" style="height: 40px; white-space: nowrap;">
|
||
<i v-if="resendingVerify" class="fas fa-spinner fa-spin"></i> {{ resendingVerify ? '发送中...' : '重发邮件' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div style="text-align: right; margin-bottom: 15px;">
|
||
<a @click="showForgotPasswordModal = true; refreshForgotPasswordCaptcha()" style="color: #667eea; cursor: pointer; font-size: 14px; text-decoration: none;">
|
||
忘记密码?
|
||
</a>
|
||
</div>
|
||
<button type="submit" class="btn btn-primary" :disabled="loginLoading">
|
||
<i :class="loginLoading ? 'fas fa-spinner fa-spin' : 'fas fa-right-to-bracket'"></i> {{ loginLoading ? '登录中...' : '登录' }}
|
||
</button>
|
||
</form>
|
||
<form v-else @submit.prevent="handleRegister" @focusin="!registerCaptchaUrl && refreshRegisterCaptcha()">
|
||
<div class="form-group">
|
||
<label class="form-label">用户名(3-20字符)</label>
|
||
<input type="text" class="form-input" v-model="registerForm.username" required minlength="3" maxlength="20">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">邮箱 (必填,用于激活)</label>
|
||
<input type="email" class="form-input" v-model="registerForm.email" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">密码 (至少6字符)</label>
|
||
<input type="password" class="form-input" v-model="registerForm.password" required minlength="6">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">验证码</label>
|
||
<div style="display: flex; gap: 10px; align-items: center;">
|
||
<input type="text" class="form-input" v-model="registerForm.captcha" placeholder="请输入验证码" required style="flex: 1;">
|
||
<img v-if="registerCaptchaUrl" :src="registerCaptchaUrl" @click="refreshRegisterCaptcha" style="height: 44px; cursor: pointer; border-radius: 4px;" title="点击刷新验证码">
|
||
</div>
|
||
</div>
|
||
<button type="submit" class="btn btn-primary" :disabled="registerLoading">
|
||
<i :class="registerLoading ? 'fas fa-spinner fa-spin' : 'fas fa-user-plus'"></i> {{ registerLoading ? '注册中...' : '注册' }}
|
||
</button>
|
||
</form>
|
||
<div class="auth-switch">
|
||
{{ isLogin ? '还没有账号?' : '已有账号?' }}
|
||
<a @click="toggleAuthMode">{{ isLogin ? '立即注册' : '去登录' }}</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 导航栏 -->
|
||
|
||
<div class="navbar" v-if="isLoggedIn">
|
||
<div class="navbar-brand">
|
||
<i class="fas fa-cloud"></i> 玩玩云
|
||
</div>
|
||
<div class="navbar-menu">
|
||
<div v-if="user && !user.is_admin" class="nav-item" :class="{active: currentView === 'files'}" @click="switchView('files')">
|
||
<i class="fas fa-folder"></i> 我的文件
|
||
</div>
|
||
<div v-if="user && !user.is_admin" class="nav-item" :class="{active: currentView === 'shares'}" @click="switchView('shares')">
|
||
<i class="fas fa-share-alt"></i> 我的分享
|
||
</div>
|
||
<div v-if="user && user.is_admin" class="nav-item" :class="{active: currentView === 'admin'}" @click="switchView('admin')">
|
||
<i class="fas fa-user-shield"></i> 管理员
|
||
</div>
|
||
<div class="nav-item" :class="{active: currentView === 'settings'}" @click="switchView('settings')">
|
||
|
||
<i class="fas fa-cog"></i> 设置
|
||
</div>
|
||
<div class="user-info">
|
||
<i class="fas fa-user-circle"></i>
|
||
<span>{{ user.username }}</span>
|
||
</div>
|
||
<button class="btn btn-danger" @click="logout">
|
||
<i class="fas fa-power-off"></i> 退出
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 文件视图 -->
|
||
<div v-if="isLoggedIn && currentView === 'files'" class="main-container">
|
||
<div class="card files-view-card">
|
||
|
||
<!-- 路径导航 (面包屑) -->
|
||
<div v-if="currentPath !== '/'" class="breadcrumb-bar">
|
||
<button class="btn-icon breadcrumb-home" @click="loadFiles('/')" title="返回根目录">
|
||
<i class="fas fa-home"></i>
|
||
</button>
|
||
<span class="breadcrumb-sep">/</span>
|
||
<template v-for="(part, index) in pathParts">
|
||
<span v-if="index < pathParts.length - 1"
|
||
@click="navigateToIndex(index)"
|
||
class="breadcrumb-link"
|
||
:title="'进入 ' + part">
|
||
{{ part }}
|
||
</span>
|
||
<span v-else class="breadcrumb-current">{{ part }}</span>
|
||
<span v-if="index < pathParts.length - 1" class="breadcrumb-sep">/</span>
|
||
</template>
|
||
<button class="btn btn-secondary breadcrumb-up" @click="navigateUp()" title="返回上一级">
|
||
<i class="fas fa-level-up-alt"></i> 返回上一级
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 隐藏的文件上传input -->
|
||
<input type="file" ref="fileUploadInput" @change="handleFileSelect" style="display: none;" multiple>
|
||
|
||
<!-- 加载中 -->
|
||
<div v-if="loading" class="loading">
|
||
<div class="spinner"></div>
|
||
<p>加载中...</p>
|
||
</div>
|
||
|
||
<!-- 文件列表 -->
|
||
<div v-else class="files-content-shell">
|
||
<div class="files-content-head files-content-head-compact files-content-head-actions-row">
|
||
<span class="files-content-title"><i class="fas fa-folder-tree"></i> 文件视图 · {{ fileStats.totalCount }} 项</span>
|
||
<div class="files-content-head-meta">
|
||
<span class="files-storage-badge files-head-storage-badge" :class="storageType === 'local' ? 'local' : 'oss'">
|
||
<i :class="storageType === 'local' ? 'fas fa-hard-drive' : 'fas fa-server'"></i>
|
||
{{ storageTypeText }}
|
||
</span>
|
||
<div class="files-head-usage-progress" :title="storageType === 'local' ? `本地:${localUsedFormatted} / ${localQuotaFormatted} (${quotaPercentage}%)` : `OSS:${ossUsedFormatted} / ${ossQuotaFormatted} (${ossQuotaPercentage}%)`">
|
||
<div class="files-head-usage-progress-bar" :style="{
|
||
width: (storageType === 'local' ? quotaPercentage : ossQuotaPercentage) + '%',
|
||
background: (storageType === 'local' ? quotaPercentage : ossQuotaPercentage) > 90 ? '#ef4444' : (storageType === 'local' ? quotaPercentage : ossQuotaPercentage) > 75 ? '#f59e0b' : '#22c55e'
|
||
}"></div>
|
||
<span class="files-head-usage-progress-text" v-if="storageType === 'local'">
|
||
本地 {{ localUsedFormatted }} / {{ localQuotaFormatted }} · {{ quotaPercentage }}%
|
||
</span>
|
||
<span class="files-head-usage-progress-text" v-else>
|
||
OSS {{ ossUsedFormatted }} / {{ ossQuotaFormatted }} · {{ ossQuotaPercentage }}%
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div class="files-content-head-actions">
|
||
<button class="btn btn-primary files-head-action-btn" @click="$refs.fileUploadInput.click()">
|
||
<i class="fas fa-upload"></i> 上传文件
|
||
</button>
|
||
<button class="btn btn-secondary files-head-action-btn files-head-folder-btn" @click="showCreateFolderModal = true">
|
||
<i class="fas fa-folder-plus"></i> 新建文件夹
|
||
</button>
|
||
<div class="view-toggle-group files-head-view-toggle">
|
||
<button class="btn" :class="fileViewMode === 'grid' ? 'btn-primary' : 'btn-secondary'" @click="fileViewMode = 'grid'">
|
||
<i class="fas fa-th-large"></i> 大图标
|
||
</button>
|
||
<button class="btn" :class="fileViewMode === 'list' ? 'btn-primary' : 'btn-secondary'" @click="fileViewMode = 'list'">
|
||
<i class="fas fa-list"></i> 列表
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div style="margin: 10px 0 14px 0; position: relative;">
|
||
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
|
||
<input
|
||
type="text"
|
||
class="form-input"
|
||
v-model="globalSearchKeyword"
|
||
@input="triggerGlobalSearch"
|
||
@focus="globalSearchVisible = !!globalSearchKeyword"
|
||
placeholder="全局搜索文件名(跨全部目录)"
|
||
style="flex: 1; min-width: 220px;">
|
||
<select class="form-input" v-model="globalSearchType" @change="runGlobalSearch" style="width: 120px;">
|
||
<option value="all">全部</option>
|
||
<option value="file">仅文件</option>
|
||
<option value="directory">仅文件夹</option>
|
||
</select>
|
||
<button class="btn btn-secondary" @click="runGlobalSearch" :disabled="globalSearchLoading" style="min-width: 86px;">
|
||
<i :class="globalSearchLoading ? 'fas fa-spinner fa-spin' : 'fas fa-search'"></i>
|
||
搜索
|
||
</button>
|
||
<button class="btn btn-secondary" @click="clearGlobalSearch()" style="min-width: 72px;">
|
||
清空
|
||
</button>
|
||
</div>
|
||
|
||
<div v-if="globalSearchVisible" style="position: absolute; left: 0; right: 0; top: calc(100% + 8px); z-index: 30; background: var(--bg-card); border: 1px solid var(--glass-border); border-radius: 10px; box-shadow: 0 8px 28px rgba(0,0,0,0.22); max-height: 320px; overflow: auto;">
|
||
<div v-if="globalSearchLoading" style="padding: 12px; color: var(--text-secondary);">
|
||
<i class="fas fa-spinner fa-spin"></i> 正在搜索...
|
||
</div>
|
||
<div v-else-if="globalSearchError" style="padding: 12px; color: #ef4444;">
|
||
<i class="fas fa-circle-exclamation"></i> {{ globalSearchError }}
|
||
</div>
|
||
<div v-else-if="globalSearchResults.length === 0" style="padding: 12px; color: var(--text-secondary);">
|
||
暂无匹配结果
|
||
</div>
|
||
<div v-else>
|
||
<button
|
||
v-for="item in globalSearchResults"
|
||
:key="item.path"
|
||
class="btn"
|
||
@click="jumpToSearchResult(item)"
|
||
style="display: block; width: 100%; border: none; border-bottom: 1px solid var(--glass-border); border-radius: 0; text-align: left; background: transparent; padding: 10px 12px;">
|
||
<div style="display: flex; justify-content: space-between; gap: 12px; align-items: center;">
|
||
<div style="min-width: 0;">
|
||
<div style="font-weight: 600; color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
||
<i class="fas" :class="item.isDirectory ? 'fa-folder' : 'fa-file'" style="margin-right: 6px; color: #667eea;"></i>
|
||
{{ item.name }}
|
||
</div>
|
||
<div style="font-size: 12px; color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
||
{{ item.path }}
|
||
</div>
|
||
</div>
|
||
<div style="font-size: 12px; color: var(--text-muted); white-space: nowrap;">
|
||
{{ item.isDirectory ? '文件夹' : (item.sizeFormatted || '-') }}
|
||
</div>
|
||
</div>
|
||
</button>
|
||
<div v-if="globalSearchMeta?.truncated" style="padding: 10px 12px; font-size: 12px; color: #f59e0b;">
|
||
结果已截断,请缩小关键词范围
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div @dragenter="handleDragEnter" @dragover="handleDragOver" @dragleave="handleDragLeave" @drop="handleDrop" class="files-container" :class="{ 'drag-over': isDragging }">
|
||
<div v-if="files.length === 0" class="empty-hint files-empty-state">
|
||
<i class="fas fa-folder-open"></i>
|
||
<div class="files-empty-title">当前目录暂无文件</div>
|
||
<div class="files-empty-desc">你可以先上传文件,或新建文件夹整理资料</div>
|
||
<div class="files-empty-actions">
|
||
<button class="btn btn-primary" @click="$refs.fileUploadInput.click()">
|
||
<i class="fas fa-upload"></i> 上传文件
|
||
</button>
|
||
<button class="btn btn-secondary" @click="showCreateFolderModal = true">
|
||
<i class="fas fa-folder-plus"></i> 新建文件夹
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<!-- 拖拽提示层 -->
|
||
<div v-if="isDragging && storageType === 'local'" class="drag-drop-overlay">
|
||
<div class="drag-drop-content">
|
||
<i class="fas fa-cloud-upload-alt" style="font-size: 64px; color: #667eea; margin-bottom: 20px;"></i>
|
||
<div style="font-size: 24px; font-weight: 600; color: var(--text-primary); margin-bottom: 10px;">拖放文件到这里上传</div>
|
||
<div style="font-size: 14px; color: var(--text-secondary);">松开鼠标即可开始上传</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 大图标视图 -->
|
||
<div v-else-if="fileViewMode === 'grid'" class="file-grid">
|
||
<div v-for="file in files" :key="file.name" class="file-grid-item" @click="handleFileClick(file)" @contextmenu.prevent.stop="showFileContextMenu(file, $event)" @touchstart="handleLongPressStart(file, $event)" @touchmove="handleLongPressMove($event)" @touchend="handleLongPressEnd" @touchcancel="handleLongPressEnd" @selectstart.prevent @dragstart.prevent>
|
||
<div class="file-icon">
|
||
<!-- 图片缩略图 -->
|
||
<img v-if="file.name.match(/\.(jpg|jpeg|png|gif|bmp|svg|webp)$/i) && getThumbnailUrl(file) && !isThumbnailLoadFailed(file)"
|
||
:src="getThumbnailUrl(file)"
|
||
:alt="file.name"
|
||
@error="markThumbnailLoadFailed(file)"
|
||
class="file-thumbnail">
|
||
<!-- 视频图标(不预加载,避免慢) -->
|
||
<div v-else-if="file.name.match(/\.(mp4|avi|mov|wmv|flv|mkv|webm)$/i)"
|
||
class="file-grid-video-icon">
|
||
<i class="fas fa-play-circle file-grid-video-play"></i>
|
||
</div>
|
||
<!-- 文件夹图标 -->
|
||
<i v-else-if="file.isDirectory" class="fas fa-folder file-grid-type-icon file-type-folder"></i>
|
||
<!-- 其他文件类型图标 -->
|
||
<i v-else-if="file.name.match(/\.(mp3|wav|flac|aac|ogg)$/i)" class="fas fa-file-audio file-grid-type-icon file-type-audio"></i>
|
||
<i v-else-if="file.name.match(/\.(pdf)$/i)" class="fas fa-file-pdf file-grid-type-icon file-type-pdf"></i>
|
||
<i v-else-if="file.name.match(/\.(doc|docx)$/i)" class="fas fa-file-word file-grid-type-icon file-type-word"></i>
|
||
<i v-else-if="file.name.match(/\.(xls|xlsx)$/i)" class="fas fa-file-excel file-grid-type-icon file-type-excel"></i>
|
||
<i v-else-if="file.name.match(/\.(zip|rar|7z|tar|gz)$/i)" class="fas fa-file-archive file-grid-type-icon file-type-archive"></i>
|
||
<i v-else class="fas fa-file file-grid-type-icon file-type-default"></i>
|
||
</div>
|
||
<div class="file-name" :title="getFileDisplayName(file)">{{ getFileDisplayName(file) }}</div>
|
||
<div class="file-size">{{ file.isDirectory ? '文件夹' : file.sizeFormatted }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 列表视图 -->
|
||
<div v-else class="file-list">
|
||
<table class="file-list-table">
|
||
<thead>
|
||
<tr>
|
||
<th class="file-col-name">文件名</th>
|
||
<th class="file-col-size">大小</th>
|
||
<th class="file-col-time">修改时间</th>
|
||
<th class="file-col-actions file-list-action-col">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="file in files" :key="file.name"
|
||
class="file-list-row"
|
||
@click="handleFileClick(file)"
|
||
@contextmenu.prevent.stop="showFileContextMenu(file, $event)"
|
||
@touchstart="handleLongPressStart(file, $event)"
|
||
@touchmove="handleLongPressMove($event)"
|
||
@touchend="handleLongPressEnd"
|
||
@touchcancel="handleLongPressEnd"
|
||
@selectstart.prevent
|
||
@dragstart.prevent>
|
||
<td class="file-list-name-cell">
|
||
<div class="file-list-name-wrap">
|
||
<!-- 图片缩略图 -->
|
||
<img v-if="file.name.match(/\.(jpg|jpeg|png|gif|bmp|svg|webp)$/i) && getThumbnailUrl(file) && !isThumbnailLoadFailed(file)"
|
||
:src="getThumbnailUrl(file)"
|
||
:alt="file.name"
|
||
@error="markThumbnailLoadFailed(file)"
|
||
class="file-list-thumb">
|
||
<!-- 视频图标 -->
|
||
<div v-else-if="file.name.match(/\.(mp4|avi|mov|wmv|flv|mkv|webm)$/i)"
|
||
class="file-list-video-icon">
|
||
<i class="fas fa-play"></i>
|
||
</div>
|
||
<!-- 文件夹图标 -->
|
||
<i v-else-if="file.isDirectory" class="fas fa-folder file-list-type-icon file-type-folder"></i>
|
||
<!-- 其他文件类型图标 -->
|
||
<i v-else-if="file.name.match(/\.(mp3|wav|flac|aac|ogg)$/i)" class="fas fa-file-audio file-list-type-icon file-type-audio"></i>
|
||
<i v-else-if="file.name.match(/\.(pdf)$/i)" class="fas fa-file-pdf file-list-type-icon file-type-pdf"></i>
|
||
<i v-else-if="file.name.match(/\.(doc|docx)$/i)" class="fas fa-file-word file-list-type-icon file-type-word"></i>
|
||
<i v-else-if="file.name.match(/\.(xls|xlsx)$/i)" class="fas fa-file-excel file-list-type-icon file-type-excel"></i>
|
||
<i v-else-if="file.name.match(/\.(zip|rar|7z|tar|gz)$/i)" class="fas fa-file-archive file-list-type-icon file-type-archive"></i>
|
||
<i v-else class="fas fa-file file-list-type-icon file-type-default"></i>
|
||
<div class="file-list-name-info">
|
||
<span class="file-list-name-text" :title="getFileDisplayName(file)">{{ getFileDisplayName(file) }}</span>
|
||
<span class="file-list-name-meta">{{ file.isDirectory ? '文件夹' : file.sizeFormatted }} · {{ formatDate(file.modifiedTime) }}</span>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
<td class="file-list-size-cell">{{ file.isDirectory ? '-' : file.sizeFormatted }}</td>
|
||
<td class="file-list-time-cell">{{ formatDate(file.modifiedTime) }}</td>
|
||
<td class="file-list-action-col">
|
||
<button
|
||
class="btn file-row-mobile-more"
|
||
@click.stop="openMobileFileActionSheet(file, $event)"
|
||
@touchstart.stop
|
||
@touchmove.stop
|
||
@touchend.stop
|
||
title="更多操作">
|
||
<i class="fas fa-ellipsis-h"></i>
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<!-- 重命名模态框 -->
|
||
<div v-if="showRenameModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showRenameModal', $event)">
|
||
<div class="modal-content" @click.stop>
|
||
<h3 style="margin-bottom: 20px;">重命名文件</h3>
|
||
<div class="form-group">
|
||
<label class="form-label">新文件名</label>
|
||
<input type="text" class="form-input" v-model="renameForm.newName" @keyup.enter="renameFile()">
|
||
</div>
|
||
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
||
<button class="btn btn-primary" @click="renameFile()" style="flex: 1;">
|
||
<i class="fas fa-check"></i> 确定
|
||
</button>
|
||
<button class="btn btn-secondary" @click="showRenameModal = false" style="flex: 1;">
|
||
<i class="fas fa-times"></i> 取消
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 新建文件夹模态框 -->
|
||
<div v-if="showCreateFolderModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showCreateFolderModal', $event)">
|
||
<div class="modal-content" @click.stop>
|
||
<h3 style="margin-bottom: 20px;">
|
||
<i class="fas fa-folder-plus"></i> 新建文件夹
|
||
</h3>
|
||
<div class="form-group">
|
||
<label class="form-label">文件夹名称</label>
|
||
<input type="text" class="form-input" v-model="createFolderForm.folderName" @keyup.enter="createFolder()" placeholder="请输入文件夹名称" autofocus>
|
||
</div>
|
||
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
||
<button class="btn btn-primary" @click="createFolder()" :disabled="creatingFolder" style="flex: 1;">
|
||
<i class="fas" :class="creatingFolder ? 'fa-spinner fa-spin' : 'fa-check'"></i> {{ creatingFolder ? '创建中...' : '创建' }}
|
||
</button>
|
||
<button class="btn btn-secondary" @click="showCreateFolderModal = false; createFolderForm.folderName = ''" style="flex: 1;">
|
||
<i class="fas fa-times"></i> 取消
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 文件夹详情模态框 -->
|
||
<div v-if="showFolderInfoModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showFolderInfoModal', $event)">
|
||
<div class="modal-content" @click.stop>
|
||
<h3 style="margin-bottom: 20px;">
|
||
<i class="fas fa-folder"></i> 文件夹详情
|
||
</h3>
|
||
<div v-if="folderInfo" style="background: rgba(255,255,255,0.03); padding: 20px; border-radius: 8px;">
|
||
<div style="margin-bottom: 15px;">
|
||
<strong style="color: var(--text-secondary);">名称:</strong>
|
||
<div style="margin-top: 5px; font-size: 16px;">{{ folderInfo.name }}</div>
|
||
</div>
|
||
<div style="margin-bottom: 15px;">
|
||
<strong style="color: var(--text-secondary);">路径:</strong>
|
||
<div style="margin-top: 5px; color: #667eea;">{{ folderInfo.path }}</div>
|
||
</div>
|
||
<div style="margin-bottom: 15px;">
|
||
<strong style="color: var(--text-secondary);">总大小:</strong>
|
||
<div style="margin-top: 5px; font-size: 18px; font-weight: 600; color: #667eea;">
|
||
{{ formatFileSize(folderInfo.size) }}
|
||
</div>
|
||
</div>
|
||
<div style="display: flex; gap: 20px;">
|
||
<div style="flex: 1;">
|
||
<strong style="color: var(--text-secondary);">文件数:</strong>
|
||
<div style="margin-top: 5px; font-size: 16px;">{{ folderInfo.fileCount }} 个</div>
|
||
</div>
|
||
<div style="flex: 1;">
|
||
<strong style="color: var(--text-secondary);">子文件夹:</strong>
|
||
<div style="margin-top: 5px; font-size: 16px;">{{ folderInfo.folderCount }} 个</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-else style="text-align: center; padding: 40px; color: var(--text-muted);">
|
||
<i class="fas fa-spinner fa-spin" style="font-size: 32px;"></i>
|
||
<div style="margin-top: 10px;">加载中...</div>
|
||
</div>
|
||
<div style="margin-top: 20px;">
|
||
<button class="btn btn-secondary" @click="showFolderInfoModal = false; folderInfo = null" style="width: 100%;">
|
||
<i class="fas fa-times"></i> 关闭
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 分享单个文件模态框 -->
|
||
<div v-if="showShareFileModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showShareFileModal', $event)">
|
||
<div class="modal-content" @click.stop>
|
||
<h3 style="margin-bottom: 20px;">分享文件</h3>
|
||
<p style="color: var(--text-secondary); margin-bottom: 15px;">文件: <strong>{{ shareFileForm.fileName }}</strong></p>
|
||
<div class="form-group">
|
||
<label class="form-label">访问保护</label>
|
||
<label class="share-password-toggle">
|
||
<input type="checkbox" v-model="shareFileForm.enablePassword" @change="toggleSharePassword('file')">
|
||
<span>{{ shareFileForm.enablePassword ? '已启用密码保护(访问时必须输入)' : '公开访问(不需要密码)' }}</span>
|
||
</label>
|
||
</div>
|
||
<div v-if="shareFileForm.enablePassword" class="form-group">
|
||
<label class="form-label">访问密码(必填)</label>
|
||
<input type="password" class="form-input" v-model="shareFileForm.password" maxlength="32" autocomplete="new-password" placeholder="请输入访问密码(创建时必填)">
|
||
<small class="share-password-hint">请通过安全渠道单独发送密码,不要和链接放在一起。</small>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">有效期</label>
|
||
<select class="form-input" v-model="shareFileForm.expiryType">
|
||
<option value="never">永久</option>
|
||
<option value="7">7天</option>
|
||
<option value="30">30天</option>
|
||
<option value="custom">自定义</option>
|
||
</select>
|
||
</div>
|
||
<div v-if="shareFileForm.expiryType === 'custom'" class="form-group">
|
||
<label class="form-label">自定义天数</label>
|
||
<input type="number" class="form-input" v-model.number="shareFileForm.customDays" min="1" max="365">
|
||
</div>
|
||
|
||
<div class="form-group" style="margin-top: 12px;">
|
||
<label class="share-password-toggle">
|
||
<input type="checkbox" v-model="shareFileForm.enableAdvancedSecurity">
|
||
<span>{{ shareFileForm.enableAdvancedSecurity ? '已启用高级安全策略' : '高级安全策略(可选)' }}</span>
|
||
</label>
|
||
</div>
|
||
|
||
<div v-if="shareFileForm.enableAdvancedSecurity" style="padding: 12px; border: 1px dashed var(--glass-border); border-radius: 10px; margin-top: 8px;">
|
||
<div class="form-group" style="margin-bottom: 10px;">
|
||
<label class="share-password-toggle">
|
||
<input type="checkbox" v-model="shareFileForm.maxDownloadsEnabled">
|
||
<span>限制下载次数</span>
|
||
</label>
|
||
<input v-if="shareFileForm.maxDownloadsEnabled" type="number" class="form-input" v-model.number="shareFileForm.maxDownloads" min="1" max="1000000" style="margin-top: 8px;" placeholder="例如 10 次">
|
||
</div>
|
||
|
||
<div class="form-group" style="margin-bottom: 10px;">
|
||
<label class="form-label">IP 白名单(可选)</label>
|
||
<textarea class="form-input" v-model="shareFileForm.ipWhitelist" rows="2" placeholder="支持逗号/空格分隔,例如:1.2.3.4, 5.6.7.*"></textarea>
|
||
</div>
|
||
|
||
<div class="form-group" style="margin-bottom: 10px;">
|
||
<label class="form-label">设备限制</label>
|
||
<select class="form-input" v-model="shareFileForm.deviceLimit">
|
||
<option value="all">全部设备</option>
|
||
<option value="mobile">仅移动端</option>
|
||
<option value="desktop">仅桌面端</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group" style="margin-bottom: 0;">
|
||
<label class="share-password-toggle">
|
||
<input type="checkbox" v-model="shareFileForm.accessTimeEnabled">
|
||
<span>限制访问时段</span>
|
||
</label>
|
||
<div v-if="shareFileForm.accessTimeEnabled" style="display: flex; gap: 8px; margin-top: 8px;">
|
||
<input type="time" class="form-input" v-model="shareFileForm.accessTimeStart" style="flex: 1;">
|
||
<input type="time" class="form-input" v-model="shareFileForm.accessTimeEnd" style="flex: 1;">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="shareResult" class="share-success-panel" style="margin-top: 15px;">
|
||
<div class="share-success-head">
|
||
<i class="fas fa-circle-check"></i>
|
||
<div>
|
||
<div class="share-success-title">分享创建成功</div>
|
||
<div class="share-success-subtitle">{{ shareResult.target_name || shareFileForm.fileName }} · {{ shareResult.has_password ? '密码访问' : '公开访问' }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="share-success-link" :title="shareResult.share_url">{{ shareResult.share_url }}</div>
|
||
|
||
<div class="share-success-actions">
|
||
<button class="btn btn-primary" @click="copyShareLink(shareResult.share_url)">
|
||
<i class="fas fa-copy"></i> 复制链接
|
||
</button>
|
||
<button class="btn btn-secondary" @click="openShare(shareResult.share_url)">
|
||
<i class="fas fa-up-right-from-square"></i> 打开链接
|
||
</button>
|
||
<button v-if="shareResult.has_password && shareResult.share_password_plain" class="btn btn-secondary" @click="copySharePassword(shareResult.share_password_plain)">
|
||
<i class="fas fa-key"></i> 复制密码
|
||
</button>
|
||
</div>
|
||
|
||
<div class="share-success-meta">
|
||
<span class="share-chip" :class="shareResult.has_password ? 'warn' : 'success'">
|
||
<i class="fas" :class="shareResult.has_password ? 'fa-lock' : 'fa-lock-open'"></i>
|
||
{{ shareResult.has_password ? '需要密码' : '无需密码' }}
|
||
</span>
|
||
<span class="share-chip warn" v-if="shareResult.has_password && shareResult.share_password_plain">
|
||
<i class="fas fa-key"></i> 密码:{{ shareResult.share_password_plain }}
|
||
</span>
|
||
<span class="share-chip info" v-if="shareResult.share_code">
|
||
<i class="fas fa-hashtag"></i> {{ shareResult.share_code }}
|
||
</span>
|
||
<span class="share-chip" :class="shareResult.expires_at ? (isExpiringSoon(shareResult.expires_at) ? 'warn' : 'success') : 'info'">
|
||
<i class="fas fa-clock"></i>
|
||
{{ shareResult.expires_at ? formatExpireTime(shareResult.expires_at) : '永久有效' }}
|
||
</span>
|
||
</div>
|
||
|
||
<div class="share-success-tip" :class="shareResult.has_password ? 'warn' : 'info'">
|
||
<i class="fas" :class="shareResult.has_password ? 'fa-shield-halved' : 'fa-circle-info'"></i>
|
||
{{ shareResult.has_password ? '请单独发送访问密码,避免与链接一并泄露。' : '当前是公开分享,任何拿到链接的人都可以访问。' }}
|
||
</div>
|
||
</div>
|
||
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
||
<button class="btn btn-primary" @click="createShareFile()" :disabled="creatingShare" style="flex: 1;">
|
||
<i class="fas" :class="creatingShare ? 'fa-spinner fa-spin' : 'fa-share'"></i> {{ creatingShare ? '创建中...' : '创建分享' }}
|
||
</button>
|
||
<button class="btn btn-secondary" @click="showShareFileModal = false; shareResult = null" style="flex: 1;">
|
||
<i class="fas fa-times"></i> 关闭
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 生成直链模态框 -->
|
||
<div v-if="showDirectLinkModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showDirectLinkModal', $event)">
|
||
<div class="modal-content" @click.stop>
|
||
<h3 style="margin-bottom: 20px;">生成文件直链</h3>
|
||
<p style="color: var(--text-secondary); margin-bottom: 15px;">文件: <strong>{{ directLinkForm.fileName }}</strong></p>
|
||
<p style="color: var(--text-secondary); margin-bottom: 15px; word-break: break-all;">路径: {{ directLinkForm.filePath }}</p>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">有效期</label>
|
||
<select class="form-input" v-model="directLinkForm.expiryType">
|
||
<option value="never">永久</option>
|
||
<option value="7">7天</option>
|
||
<option value="30">30天</option>
|
||
<option value="custom">自定义</option>
|
||
</select>
|
||
</div>
|
||
<div v-if="directLinkForm.expiryType === 'custom'" class="form-group">
|
||
<label class="form-label">自定义天数</label>
|
||
<input type="number" class="form-input" v-model.number="directLinkForm.customDays" min="1" max="365">
|
||
</div>
|
||
|
||
<div v-if="directLinkResult" class="share-success-panel" style="margin-top: 15px;">
|
||
<div class="share-success-head">
|
||
<i class="fas fa-circle-check"></i>
|
||
<div>
|
||
<div class="share-success-title">直链创建成功</div>
|
||
<div class="share-success-subtitle">{{ directLinkResult.target_name || directLinkForm.fileName }} · 直链下载</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="share-success-link" :title="directLinkResult.direct_url">{{ directLinkResult.direct_url }}</div>
|
||
|
||
<div class="share-success-actions">
|
||
<button class="btn btn-primary" @click="copyDirectLink(directLinkResult.direct_url)">
|
||
<i class="fas fa-copy"></i> 复制直链
|
||
</button>
|
||
<button class="btn btn-secondary" @click="openShare(directLinkResult.direct_url)">
|
||
<i class="fas fa-up-right-from-square"></i> 打开直链
|
||
</button>
|
||
</div>
|
||
|
||
<div class="share-success-meta">
|
||
<span class="share-chip info" v-if="directLinkResult.link_code">
|
||
<i class="fas fa-hashtag"></i> {{ directLinkResult.link_code }}
|
||
</span>
|
||
<span class="share-chip" :class="directLinkResult.expires_at ? (isExpiringSoon(directLinkResult.expires_at) ? 'warn' : 'success') : 'info'">
|
||
<i class="fas fa-clock"></i>
|
||
{{ directLinkResult.expires_at ? formatExpireTime(directLinkResult.expires_at) : '永久有效' }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
||
<button class="btn btn-primary" @click="createDirectLink()" :disabled="creatingDirectLink" style="flex: 1;">
|
||
<i class="fas" :class="creatingDirectLink ? 'fa-spinner fa-spin' : 'fa-link'"></i> {{ creatingDirectLink ? '生成中...' : '生成直链' }}
|
||
</button>
|
||
<button class="btn btn-secondary" @click="showDirectLinkModal = false; directLinkResult = null" style="flex: 1;">
|
||
<i class="fas fa-times"></i> 关闭
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 删除确认模态框(替代浏览器 confirm,避免误删) -->
|
||
<div v-if="showDeleteConfirmModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showDeleteConfirmModal', $event)">
|
||
<div class="modal-content" @click.stop style="max-width: 440px;">
|
||
<h3 style="margin-bottom: 12px; color: #ef4444;">
|
||
<i class="fas fa-triangle-exclamation"></i> {{ deleteConfirm.title || '确认删除' }}
|
||
</h3>
|
||
<p style="margin: 0; color: var(--text-secondary); line-height: 1.7;">
|
||
{{ deleteConfirm.message || '确认执行删除操作?' }}
|
||
</p>
|
||
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
||
<button class="btn btn-secondary" @click="closeDeleteConfirmModal()" :disabled="deleteConfirm.loading" style="flex: 1;">
|
||
<i class="fas fa-times"></i> 取消
|
||
</button>
|
||
<button class="btn" style="background: #ef4444; color: white; flex: 1;" @click="confirmDeleteAction()" :disabled="deleteConfirm.loading">
|
||
<i class="fas" :class="deleteConfirm.loading ? 'fa-spinner fa-spin' : 'fa-trash'"></i>
|
||
{{ deleteConfirm.loading ? '删除中...' : '确定删除' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- OSS 配置引导弹窗 -->
|
||
<div v-if="showOssGuideModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showOssGuideModal', $event)">
|
||
<div class="modal-content" @click.stop style="max-width: 520px; border-radius: 16px; overflow: hidden;">
|
||
<div style="background: linear-gradient(135deg,#667eea,#764ba2); color: white; padding: 18px;">
|
||
<div style="display: flex; align-items: center; gap: 10px;">
|
||
<i class="fas fa-cloud" style="font-size: 20px;"></i>
|
||
<h3 style="margin: 0; font-size: 20px;">切换到 OSS 存储</h3>
|
||
</div>
|
||
<p style="margin: 8px 0 0 0; opacity: 0.9; font-size: 14px;">先配置云服务信息,再切换到你的专属 OSS 空间。</p>
|
||
</div>
|
||
<div style="padding: 18px;">
|
||
<p style="color: var(--text-secondary); line-height: 1.6; margin-bottom: 16px;">
|
||
支持阿里云 OSS、腾讯云 COS、AWS S3 等兼容 S3 协议的云存储服务。
|
||
</p>
|
||
<div style="display: flex; gap: 10px; justify-content: flex-end;">
|
||
<button class="btn btn-secondary" @click="closeOssGuideModal">稍后再说</button>
|
||
<button class="btn btn-primary" @click="proceedOssGuide">
|
||
<i class="fas fa-tools"></i> 去配置 OSS
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- OSS 配置弹窗 -->
|
||
<div v-if="showOssConfigModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showOssConfigModal', $event)">
|
||
<div class="modal-content" @click.stop style="max-width: 720px; border-radius: 16px;">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
|
||
<div>
|
||
<h3 style="margin: 0 0 6px 0;">配置 OSS 存储</h3>
|
||
<p style="margin: 0; color: var(--text-muted); font-size: 13px;">填写云服务配置信息,保存后即可切换到 OSS 模式。</p>
|
||
</div>
|
||
<button class="btn btn-secondary" style="padding: 6px 10px;" @click="closeOssConfigModal">
|
||
<i class="fas fa-times"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<form @submit.prevent="updateOssConfig" style="display: grid; gap: 12px;">
|
||
<div class="form-group">
|
||
<label class="form-label">云服务商</label>
|
||
<select class="form-input" v-model="ossConfigForm.oss_provider" required style="cursor: pointer;">
|
||
<option value="aliyun">阿里云 OSS</option>
|
||
<option value="tencent">腾讯云 COS</option>
|
||
<option value="aws">AWS S3</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">地域</label>
|
||
<input type="text" class="form-input" v-model="ossConfigForm.oss_region" placeholder="如: oss-cn-hangzhou / ap-guangzhou / us-east-1" required>
|
||
<small style="color: var(--text-secondary); font-size: 12px; margin-top: 5px; display: block;">
|
||
阿里云: oss-cn-hangzhou, 腾讯云: ap-guangzhou, AWS: us-east-1
|
||
</small>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Access Key ID</label>
|
||
<input type="text" class="form-input" v-model="ossConfigForm.oss_access_key_id" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Access Key Secret (留空保留现有密钥)</label>
|
||
<input type="password" class="form-input" v-model="ossConfigForm.oss_access_key_secret" placeholder="留空保留现有密钥">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">存储桶名称</label>
|
||
<input type="text" class="form-input" v-model="ossConfigForm.oss_bucket" placeholder="如: my-storage-bucket" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">自定义 Endpoint (可选)</label>
|
||
<input type="text" class="form-input" v-model="ossConfigForm.oss_endpoint" placeholder="兼容 S3 的服务可填写自定义地址">
|
||
<small style="color: var(--text-secondary); font-size: 12px; margin-top: 5px; display: block;">
|
||
一般不需要填写,仅在使用自定义 S3 兼容服务时需要。
|
||
</small>
|
||
</div>
|
||
<div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 4px;">
|
||
<button type="button" class="btn btn-secondary" @click="closeOssConfigModal">取消</button>
|
||
<button type="submit" class="btn btn-primary" :disabled="ossConfigSaving" :style="{ opacity: ossConfigSaving ? 0.7 : 1 }">
|
||
<i class="fas" :class="ossConfigSaving ? 'fa-spinner fa-spin' : 'fa-save'"></i>
|
||
{{ ossConfigSaving ? '保存中...' : '保存配置' }}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 设置视图 -->
|
||
<div v-if="isLoggedIn && currentView === 'settings'" class="main-container">
|
||
<div class="card">
|
||
<div class="settings-page-head">
|
||
<h2 class="settings-page-title">
|
||
<i class="fas fa-sliders-h"></i> 设置中心
|
||
</h2>
|
||
<p class="settings-page-subtitle">管理存储策略、主题外观与账号安全</p>
|
||
</div>
|
||
|
||
<!-- 存储管理 - 仅用户可选择 -->
|
||
<div v-if="user && !user.is_admin && storagePermission === 'user_choice'" class="settings-section settings-storage-choice" style="margin-bottom: 40px;">
|
||
<h3 class="settings-section-title" style="margin-bottom: 20px;">
|
||
<i class="fas fa-database"></i> 存储管理
|
||
</h3>
|
||
|
||
<div class="settings-panel settings-storage-panel" style="background: var(--bg-card); backdrop-filter: blur(20px); padding: 22px; border-radius: 14px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); border: 1px solid var(--glass-border);">
|
||
<div class="settings-storage-head" style="display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap;">
|
||
<div style="display: flex; align-items: center; gap: 10px;">
|
||
<span style="font-weight: 700; color: var(--text-primary);">当前模式</span>
|
||
<span :style="{
|
||
padding: '6px 12px',
|
||
borderRadius: '999px',
|
||
background: storageType === 'local' ? 'rgba(40,167,69,0.12)' : 'rgba(102,126,234,0.12)',
|
||
color: storageType === 'local' ? '#1c7c3d' : '#4b5fc9',
|
||
fontWeight: 700,
|
||
display: 'inline-flex',
|
||
alignItems: 'center',
|
||
gap: '6px'
|
||
}">
|
||
<i :class="storageType === 'local' ? 'fas fa-hard-drive' : 'fas fa-server'"></i>
|
||
{{ storageTypeText }}
|
||
</span>
|
||
</div>
|
||
<div v-if="storageSwitching" style="color: #4b5fc9; font-weight: 600; display: inline-flex; align-items: center; gap: 8px;">
|
||
<i class="fas fa-sync-alt fa-spin"></i>
|
||
正在切换到 {{ storageSwitchTarget === 'oss' ? 'OSS 存储' : '本地存储' }}...
|
||
</div>
|
||
<div v-else style="color: var(--text-secondary); font-size: 13px;">本地存储适合快速读写,OSS 适合云存储扩展</div>
|
||
</div>
|
||
|
||
<div class="settings-storage-switch" style="margin-top: 16px; background: var(--bg-secondary); border-radius: 12px; padding: 12px; border: 1px solid var(--glass-border);">
|
||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; align-items: center;">
|
||
<div style="height: 4px; background: rgba(255,255,255,0.1); border-radius: 999px; position: relative; overflow: hidden;">
|
||
<div :style="{
|
||
position: 'absolute',
|
||
left: storageType === 'local' ? '6%' : '52%',
|
||
width: '42%',
|
||
height: '100%',
|
||
background: 'linear-gradient(90deg,#667eea,#764ba2)',
|
||
borderRadius: '999px',
|
||
transition: 'left .35s ease'
|
||
}"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="settings-storage-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 14px; margin-top: 14px; align-items: stretch;">
|
||
<div class="settings-storage-option local" style="background: var(--bg-secondary); border: 1px solid var(--glass-border); border-radius: 12px; padding: 16px; box-shadow: 0 6px 20px rgba(0,0,0,0.15); display: flex; flex-direction: column; height: 100%;">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||
<div style="font-weight: 700; color: var(--text-primary); display: flex; gap: 8px; align-items: center;">
|
||
<i class="fas fa-hard-drive" style="color: var(--accent-1);"></i> 本地存储
|
||
</div>
|
||
<span v-if="storageType === 'local'" style="font-size: 12px; color: #22c55e; background: rgba(40,167,69,0.12); padding: 4px 8px; border-radius: 999px;">当前</span>
|
||
</div>
|
||
<div style="color: var(--text-secondary); font-size: 13px; margin-bottom: 10px;">更快的读写,适合日常上传下载。</div>
|
||
<div style="margin-bottom: 10px;">
|
||
<div style="font-size: 12px; color: var(--text-muted); margin-bottom: 6px;">配额使用</div>
|
||
<div style="font-weight: 600; color: var(--text-primary);">{{ localUsedFormatted }} / {{ localQuotaFormatted }}</div>
|
||
<div style="margin-top: 6px; width: 100%; height: 10px; background: rgba(255,255,255,0.1); border-radius: 5px; overflow: hidden;">
|
||
<div :style="{
|
||
width: quotaPercentage + '%',
|
||
height: '100%',
|
||
background: quotaPercentage > 90 ? '#dc3545' : quotaPercentage > 75 ? '#ffc107' : '#28a745',
|
||
transition: 'width 0.35s ease'
|
||
}"></div>
|
||
</div>
|
||
</div>
|
||
<div style="margin-top: auto;">
|
||
<button class="btn btn-primary" style="width: 100%; border-radius: 10px;" :disabled="storageType === 'local' || storageSwitching" @click="switchStorage('local')">
|
||
<i class="fas fa-bolt"></i> 用本地存储
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="settings-storage-option oss" style="background: var(--bg-secondary); border: 1px solid var(--glass-border); border-radius: 12px; padding: 16px; box-shadow: 0 6px 20px rgba(0,0,0,0.15); display: flex; flex-direction: column; height: 100%;">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||
<div style="font-weight: 700; color: var(--text-primary); display: flex; gap: 8px; align-items: center;">
|
||
<i class="fas fa-cloud" style="color: var(--accent-1);"></i> OSS 存储
|
||
</div>
|
||
<span v-if="storageType === 'oss'" style="font-size: 12px; color: var(--accent-1); background: rgba(102,126,234,0.15); padding: 4px 8px; border-radius: 999px;">当前</span>
|
||
</div>
|
||
<div style="color: var(--text-secondary); font-size: 13px; margin-bottom: 10px;">使用云存储服务,安全可靠扩展性强。</div>
|
||
<div v-if="user?.oss_config_source !== 'none'" style="font-size: 13px; color: var(--text-primary); margin-bottom: 10px;">
|
||
<i class="fas fa-check-circle" style="color: var(--accent-1);"></i>
|
||
<span v-if="user?.oss_config_source === 'unified'">系统级OSS配置已启用</span>
|
||
<span v-else>已配置: {{ user.oss_provider }} / {{ user.oss_bucket }}</span>
|
||
</div>
|
||
<div v-else style="font-size: 13px; color: #f59e0b; background: rgba(245, 158, 11, 0.1); border: 1px dashed rgba(245,158,11,0.4); padding: 10px; border-radius: 8px; margin-bottom: 10px;">
|
||
<i class="fas fa-exclamation-circle"></i> 先填写 OSS 配置信息再切换
|
||
</div>
|
||
<!-- OSS空间使用统计(user_choice模式) -->
|
||
<div v-if="user?.oss_config_source !== 'none'" style="margin-bottom: 10px; padding: 10px; background: rgba(255,255,255,0.03); border-radius: 8px; border: 1px solid var(--glass-border);">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
||
<span style="font-size: 12px; color: var(--text-muted);">空间统计</span>
|
||
<button
|
||
style="background: none; border: none; color: #4b5fc9; cursor: pointer; font-size: 12px; padding: 2px 6px;"
|
||
@click.stop="loadOssUsage()"
|
||
:disabled="ossUsageLoading">
|
||
<i :class="ossUsageLoading ? 'fas fa-sync-alt fa-spin' : 'fas fa-sync-alt'"></i>
|
||
</button>
|
||
</div>
|
||
<div v-if="ossUsageLoading && !ossUsage" style="text-align: center; color: #667eea; font-size: 12px;">
|
||
<i class="fas fa-spinner fa-spin"></i> 统计中...
|
||
</div>
|
||
<div v-else-if="ossUsageError" style="font-size: 12px; color: #ef4444;">
|
||
<i class="fas fa-exclamation-triangle"></i> {{ ossUsageError }}
|
||
</div>
|
||
<div v-else-if="ossUsage" style="font-size: 13px; font-weight: 600; color: #4b5fc9;">
|
||
{{ ossUsage.totalSizeFormatted }}
|
||
<span style="font-weight: 400; color: var(--text-muted); font-size: 12px;">
|
||
/ {{ ossUsage.quotaFormatted }}
|
||
</span>
|
||
</div>
|
||
<div v-else style="font-size: 12px; color: var(--text-muted);">点击刷新查看</div>
|
||
</div>
|
||
<div style="margin-top: auto;">
|
||
<button
|
||
class="btn"
|
||
:class="user?.oss_config_source !== 'none' ? 'btn-primary' : 'btn-secondary'"
|
||
style="width: 100%; border-radius: 10px;"
|
||
:disabled="storageType === 'oss' || storageSwitching"
|
||
@click="switchStorage('oss')">
|
||
<i class="fas fa-random"></i>
|
||
{{ user?.oss_config_source !== 'none' ? '切到 OSS 存储' : '去配置 OSS' }}
|
||
</button>
|
||
<div v-if="user?.is_admin" style="margin-top: 8px; text-align: center;">
|
||
<a style="color: #4b5fc9; font-size: 13px; text-decoration: none; cursor: pointer;" @click.prevent="openOssConfigModal">
|
||
<i class="fas fa-tools"></i> 配置 / 修改 OSS
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="settings-inline-tip" style="margin-top: 12px; padding: 10px 12px; background: rgba(255,255,255,0.05); border-radius: 10px; font-size: 13px; color: var(--text-secondary);">
|
||
<i class="fas fa-info-circle" style="color: #4b5fc9;"></i>
|
||
本地存储速度快但受配额限制;OSS 支持多家云服务商,切换过程中可继续查看文件列表。
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 本地存储信息 - 仅本地存储权限 -->
|
||
<div v-if="user && !user.is_admin && storagePermission === 'local_only'" class="settings-section settings-local-only" style="margin-bottom: 40px;">
|
||
<h3 class="settings-section-title" style="margin-bottom: 20px;">
|
||
<i class="fas fa-hard-drive"></i> 本地存储
|
||
</h3>
|
||
|
||
<div class="settings-panel settings-local-panel" style="background: rgba(255,255,255,0.03); padding: 20px; border-radius: 8px;">
|
||
<div style="margin-bottom: 15px;">
|
||
<span style="font-weight: 600; color: var(--text-primary);">存储方式: </span>
|
||
<span style="color: #667eea; font-weight: 600;">本地存储</span>
|
||
<span style="margin-left: 10px; padding: 4px 12px; background: #22c55e; color: white; border-radius: 12px; font-size: 12px;">
|
||
<i class="fas fa-lock"></i> 仅本地
|
||
</span>
|
||
</div>
|
||
|
||
<div style="margin-bottom: 15px;">
|
||
<span style="font-weight: 600; color: var(--text-primary);">配额使用: </span>
|
||
<span>{{ localUsedFormatted }} / {{ localQuotaFormatted }} ({{ quotaPercentage }}%)</span>
|
||
<div style="margin-top: 8px; width: 100%; height: 18px; background: rgba(255,255,255,0.1); border-radius: 9px; overflow: hidden;">
|
||
<div :style="{
|
||
width: quotaPercentage + '%',
|
||
height: '100%',
|
||
background: quotaPercentage > 90 ? '#dc3545' : quotaPercentage > 75 ? '#ffc107' : '#28a745',
|
||
transition: 'width 0.3s'
|
||
}"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="settings-inline-tip settings-local-tip" style="padding: 10px; background: rgba(59, 130, 246, 0.15); border-left: 4px solid #3b82f6; border-radius: 6px; font-size: 13px; color: #93c5fd;">
|
||
<i class="fas fa-info-circle"></i>
|
||
<strong>说明:</strong> 管理员已将您的存储权限设置为"仅本地存储",您的文件存储在服务器本地,速度快但有配额限制。如需使用OSS存储,请联系管理员修改权限设置。
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- OSS 概览 / 配置入口 - 仅OSS权限 -->
|
||
<div v-if="user && !user.is_admin && storagePermission === 'oss_only'" class="settings-section settings-oss-only" style="margin-bottom: 40px;">
|
||
<h3 class="settings-section-title" style="margin-bottom: 20px;">
|
||
<i class="fas fa-cloud"></i> OSS存储
|
||
</h3>
|
||
<div class="settings-panel settings-oss-panel" style="background: rgba(255,255,255,0.03); padding: 20px; border-radius: 12px; border: 1px solid var(--glass-border);">
|
||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 12px;">
|
||
<div>
|
||
<div style="font-weight: 700; color: var(--text-primary); display: flex; gap: 8px; align-items: center;">
|
||
<i class="fas fa-shield-alt"></i>
|
||
仅 OSS 模式
|
||
</div>
|
||
<div style="color: var(--text-secondary); font-size: 13px; margin-top: 6px;">
|
||
{{ user?.oss_config_source !== 'none' ? '已配置系统级 OSS,可正常使用 OSS 存储。' : '还未配置 OSS,请先填写配置信息。' }}
|
||
</div>
|
||
</div>
|
||
<!-- 仅在用户有个人OSS配置时显示修改按钮 -->
|
||
<button v-if="user?.has_oss_config" class="btn btn-primary" @click="openOssConfigModal()" style="border-radius: 10px;">
|
||
<i class="fas fa-tools"></i> 修改个人 OSS 配置
|
||
</button>
|
||
</div>
|
||
|
||
<!-- OSS服务器信息 -->
|
||
<div v-if="user?.oss_config_source !== 'none'" class="settings-subpanel" style="margin-bottom: 12px; padding: 12px; background: var(--bg-secondary); border-radius: 10px; border: 1px solid var(--glass-border);">
|
||
<div style="font-weight: 600; color: var(--text-primary); margin-bottom: 8px;">
|
||
<i class="fas fa-cloud" style="color: var(--accent-1);"></i> 云服务信息
|
||
</div>
|
||
<div style="color: var(--text-secondary); font-size: 14px;">
|
||
{{ user.oss_provider }} / {{ user.oss_bucket }}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- OSS空间使用统计 -->
|
||
<div v-if="user?.oss_config_source !== 'none'" class="settings-subpanel settings-oss-usage-panel" style="margin-bottom: 12px; padding: 12px; background: var(--bg-secondary); border-radius: 10px; border: 1px solid var(--glass-border);">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||
<div style="font-weight: 600; color: var(--text-primary);">
|
||
<i class="fas fa-chart-pie" style="color: #667eea;"></i> 空间使用统计
|
||
</div>
|
||
<button
|
||
class="btn btn-secondary"
|
||
style="padding: 4px 10px; font-size: 12px; border-radius: 6px;"
|
||
@click="loadOssUsage()"
|
||
:disabled="ossUsageLoading">
|
||
<i :class="ossUsageLoading ? 'fas fa-sync-alt fa-spin' : 'fas fa-sync-alt'"></i>
|
||
{{ ossUsageLoading ? '统计中...' : '刷新' }}
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 加载中 -->
|
||
<div v-if="ossUsageLoading && !ossUsage" style="text-align: center; padding: 20px; color: #667eea;">
|
||
<i class="fas fa-spinner fa-spin" style="font-size: 24px;"></i>
|
||
<div style="margin-top: 8px; font-size: 13px;">正在统计 OSS 空间使用情况...</div>
|
||
<div style="margin-top: 4px; font-size: 12px; color: var(--text-muted);">(文件较多时可能需要一些时间)</div>
|
||
</div>
|
||
|
||
<!-- 错误提示 -->
|
||
<div v-else-if="ossUsageError" style="padding: 12px; background: rgba(239, 68, 68, 0.1); border-radius: 8px; color: #ef4444; font-size: 13px;">
|
||
<i class="fas fa-exclamation-triangle"></i> {{ ossUsageError }}
|
||
</div>
|
||
|
||
<!-- 统计结果 -->
|
||
<div v-else-if="ossUsage">
|
||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px;">
|
||
<div style="text-align: center; padding: 12px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 10px; color: white;">
|
||
<div style="font-size: 20px; font-weight: 700;">{{ ossUsage.totalSizeFormatted }}</div>
|
||
<div style="font-size: 12px; opacity: 0.9; margin-top: 4px;">总使用空间</div>
|
||
</div>
|
||
<div style="text-align: center; padding: 12px; background: rgba(59, 130, 246, 0.1); border-radius: 10px; border: 1px solid rgba(59,130,246,0.2);">
|
||
<div style="font-size: 20px; font-weight: 700; color: #3b82f6;">{{ ossUsage.quotaFormatted }}</div>
|
||
<div style="font-size: 12px; color: var(--text-muted); margin-top: 4px;">OSS 配额</div>
|
||
</div>
|
||
<div style="text-align: center; padding: 12px; background: rgba(34, 197, 94, 0.1); border-radius: 10px; border: 1px solid rgba(34,197,94,0.22);">
|
||
<div style="font-size: 20px; font-weight: 700; color: #16a34a;">{{ ossUsage.usagePercentage + '%' }}</div>
|
||
<div style="font-size: 12px; color: var(--text-muted); margin-top: 4px;">配额使用率</div>
|
||
</div>
|
||
</div>
|
||
<div style="margin-top: 10px;">
|
||
<div style="display: flex; justify-content: space-between; font-size: 12px; color: var(--text-secondary); margin-bottom: 6px;">
|
||
<span>剩余空间</span>
|
||
<span>{{ ossUsage.remainingSizeFormatted }}</span>
|
||
</div>
|
||
<div style="width: 100%; height: 10px; background: rgba(148,163,184,0.24); border-radius: 999px; overflow: hidden;">
|
||
<div :style="{ width: Math.min(100, ossUsage.usagePercentage) + '%', height: '100%', background: ossUsage.usagePercentage > 90 ? '#ef4444' : (ossUsage.usagePercentage > 75 ? '#f59e0b' : '#22c55e') }"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 未统计提示 -->
|
||
<div v-else style="text-align: center; padding: 16px; color: var(--text-muted); font-size: 13px;">
|
||
<i class="fas fa-database" style="font-size: 24px; color: var(--text-muted); margin-bottom: 8px; display: block;"></i>
|
||
点击"刷新"按钮统计 OSS 空间使用情况
|
||
</div>
|
||
</div>
|
||
|
||
<div class="settings-inline-tip settings-oss-tip" style="padding: 10px; background: rgba(102, 126, 234, 0.1); border-radius: 10px; color: var(--text-secondary); font-size: 13px;">
|
||
<i class="fas fa-info-circle" style="color: #4b5fc9;"></i>
|
||
数据存储在云服务上,安全可靠扩展性强。如需切换回本地请联系管理员调整权限。
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 下载流量额度与统计 -->
|
||
<div v-if="user && !user.is_admin" class="settings-section settings-download-traffic" style="margin-bottom: 40px;">
|
||
<h3 class="settings-section-title" style="margin-bottom: 20px;">
|
||
<i class="fas fa-tachometer-alt"></i> 下载流量额度与统计
|
||
</h3>
|
||
<div class="settings-panel" style="background: var(--bg-card); border: 1px solid var(--glass-border); border-radius: 12px; padding: 18px;">
|
||
<div style="display: flex; justify-content: space-between; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 12px;">
|
||
<div style="font-size: 13px; color: var(--text-secondary);">
|
||
管理员设置的下载流量限制会在这里实时展示,单位自动按 B/KB/MB/GB/TB 切换
|
||
</div>
|
||
<div class="download-traffic-range-actions">
|
||
<button
|
||
class="btn btn-secondary download-traffic-range-btn"
|
||
:class="{ 'download-traffic-range-btn-active': downloadTrafficReport.days === 7 }"
|
||
@click="setDownloadTrafficReportDays(7)">
|
||
近7天
|
||
</button>
|
||
<button
|
||
class="btn btn-secondary download-traffic-range-btn"
|
||
:class="{ 'download-traffic-range-btn-active': downloadTrafficReport.days === 30 }"
|
||
@click="setDownloadTrafficReportDays(30)">
|
||
近30天
|
||
</button>
|
||
<button
|
||
class="btn btn-secondary download-traffic-range-btn"
|
||
:class="{ 'download-traffic-range-btn-active': downloadTrafficReport.days === 90 }"
|
||
@click="setDownloadTrafficReportDays(90)">
|
||
近90天
|
||
</button>
|
||
<button
|
||
class="btn btn-secondary download-traffic-range-btn"
|
||
:class="{ 'download-traffic-range-btn-active': downloadTrafficReport.days === 180 }"
|
||
@click="setDownloadTrafficReportDays(180)">
|
||
近180天
|
||
</button>
|
||
<button class="btn btn-secondary download-traffic-range-btn download-traffic-range-refresh" @click="loadDownloadTrafficReport(downloadTrafficReport.days)" :disabled="downloadTrafficReport.loading">
|
||
<i :class="downloadTrafficReport.loading ? 'fas fa-sync-alt fa-spin' : 'fas fa-sync-alt'"></i>
|
||
{{ downloadTrafficReport.loading ? '加载中...' : '刷新' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="downloadTrafficReport.error" style="margin-bottom: 10px; padding: 10px 12px; border-radius: 8px; background: rgba(239,68,68,0.1); color: #ef4444; font-size: 13px;">
|
||
<i class="fas fa-exclamation-triangle"></i> {{ downloadTrafficReport.error }}
|
||
</div>
|
||
|
||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 10px; margin-bottom: 12px;">
|
||
<div style="padding: 12px; border-radius: 10px; background: rgba(59,130,246,0.1); border: 1px solid rgba(59,130,246,0.25);">
|
||
<div style="font-size: 12px; color: var(--text-muted); margin-bottom: 6px;">当前限制</div>
|
||
<div style="font-size: 18px; font-weight: 700; color: #3b82f6;">
|
||
{{ downloadTrafficIsUnlimited ? '不限' : formatBytes(downloadTrafficQuotaBytes) }}
|
||
</div>
|
||
</div>
|
||
<div style="padding: 12px; border-radius: 10px; background: rgba(245,158,11,0.1); border: 1px solid rgba(245,158,11,0.28);">
|
||
<div style="font-size: 12px; color: var(--text-muted); margin-bottom: 6px;">已使用</div>
|
||
<div style="font-size: 18px; font-weight: 700; color: #f59e0b;">
|
||
{{ formatBytes(downloadTrafficUsedBytes) }}
|
||
</div>
|
||
</div>
|
||
<div style="padding: 12px; border-radius: 10px; background: rgba(16,185,129,0.1); border: 1px solid rgba(16,185,129,0.26);">
|
||
<div style="font-size: 12px; color: var(--text-muted); margin-bottom: 6px;">剩余可用</div>
|
||
<div style="font-size: 18px; font-weight: 700; color: #10b981;">
|
||
{{ downloadTrafficIsUnlimited ? '不限' : formatBytes(downloadTrafficRemainingBytes || 0) }}
|
||
</div>
|
||
</div>
|
||
<div style="padding: 12px; border-radius: 10px; background: rgba(99,102,241,0.12); border: 1px solid rgba(99,102,241,0.28);">
|
||
<div style="font-size: 12px; color: var(--text-muted); margin-bottom: 6px;">策略</div>
|
||
<div style="font-size: 14px; font-weight: 700; color: #6366f1;">
|
||
{{ getDownloadResetCycleText(downloadTrafficResetCycle) }}
|
||
</div>
|
||
<div style="font-size: 12px; color: var(--text-secondary); margin-top: 4px;">
|
||
到期: {{ downloadTrafficExpiresAt ? formatDate(downloadTrafficExpiresAt) : '无' }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="!downloadTrafficIsUnlimited" style="margin-bottom: 12px;">
|
||
<div style="display: flex; justify-content: space-between; font-size: 12px; color: var(--text-secondary); margin-bottom: 6px;">
|
||
<span>额度使用率</span>
|
||
<span>{{ downloadTrafficUsagePercentage }}%</span>
|
||
</div>
|
||
<div style="height: 10px; background: rgba(148,163,184,0.24); border-radius: 999px; overflow: hidden;">
|
||
<div :style="{ width: downloadTrafficUsagePercentage + '%', height: '100%', background: downloadTrafficUsagePercentage > 90 ? '#ef4444' : (downloadTrafficUsagePercentage > 75 ? '#f59e0b' : '#22c55e') }"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 10px; margin-bottom: 12px;">
|
||
<div style="padding: 10px; border-radius: 8px; background: var(--bg-secondary); border: 1px solid var(--glass-border);">
|
||
<div style="font-size: 12px; color: var(--text-muted);">今天使用</div>
|
||
<div style="font-weight: 700; color: var(--text-primary); margin-top: 4px;">
|
||
{{ formatBytes(downloadTrafficReport.summary?.today?.bytes_used || 0) }}
|
||
</div>
|
||
</div>
|
||
<div style="padding: 10px; border-radius: 8px; background: var(--bg-secondary); border: 1px solid var(--glass-border);">
|
||
<div style="font-size: 12px; color: var(--text-muted);">近7天</div>
|
||
<div style="font-weight: 700; color: var(--text-primary); margin-top: 4px;">
|
||
{{ formatBytes(downloadTrafficReport.summary?.last_7_days?.bytes_used || 0) }}
|
||
</div>
|
||
</div>
|
||
<div style="padding: 10px; border-radius: 8px; background: var(--bg-secondary); border: 1px solid var(--glass-border);">
|
||
<div style="font-size: 12px; color: var(--text-muted);">近30天</div>
|
||
<div style="font-weight: 700; color: var(--text-primary); margin-top: 4px;">
|
||
{{ formatBytes(downloadTrafficReport.summary?.last_30_days?.bytes_used || 0) }}
|
||
</div>
|
||
</div>
|
||
<div style="padding: 10px; border-radius: 8px; background: var(--bg-secondary); border: 1px solid var(--glass-border);">
|
||
<div style="font-size: 12px; color: var(--text-muted);">所选区间</div>
|
||
<div style="font-weight: 700; color: var(--text-primary); margin-top: 4px;">
|
||
{{ formatBytes(downloadTrafficReport.summary?.selected_range?.bytes_used || 0) }}
|
||
</div>
|
||
</div>
|
||
<div style="padding: 10px; border-radius: 8px; background: var(--bg-secondary); border: 1px solid var(--glass-border);">
|
||
<div style="font-size: 12px; color: var(--text-muted);">历史累计</div>
|
||
<div style="font-weight: 700; color: var(--text-primary); margin-top: 4px;">
|
||
{{ formatBytes(downloadTrafficReport.summary?.all_time?.bytes_used || 0) }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="border: 1px solid var(--glass-border); border-radius: 10px; overflow: hidden;">
|
||
<div style="padding: 10px 12px; background: var(--bg-secondary); border-bottom: 1px solid var(--glass-border); font-size: 13px; color: var(--text-secondary); display: flex; justify-content: space-between; align-items: center;">
|
||
<span>按天用量明细(最近 {{ downloadTrafficReport.days }} 天)</span>
|
||
<span v-if="downloadTrafficReport.summary?.peak_day">峰值: {{ formatReportDateLabel(downloadTrafficReport.summary.peak_day.date) }} / {{ formatBytes(downloadTrafficReport.summary.peak_day.bytes_used || 0) }}</span>
|
||
</div>
|
||
<div style="padding: 8px 12px; border-bottom: 1px solid var(--glass-border); font-size: 12px; color: var(--text-muted);">
|
||
说明:预览/在线播放同样计入下载流量;OSS 明细来自访问日志,通常有 5-15 分钟延迟。
|
||
</div>
|
||
|
||
<div v-if="downloadTrafficReport.loading && downloadTrafficDailyRowsDesc.length === 0" style="padding: 16px; text-align: center; color: var(--text-muted);">
|
||
<i class="fas fa-spinner fa-spin"></i> 报表加载中...
|
||
</div>
|
||
<div v-else-if="downloadTrafficDailyRowsDesc.length === 0" style="padding: 16px; text-align: center; color: var(--text-muted);">
|
||
暂无下载流量记录
|
||
</div>
|
||
<div v-else style="max-height: 280px; overflow: auto;">
|
||
<table style="width: 100%; border-collapse: collapse;">
|
||
<thead>
|
||
<tr style="background: rgba(148,163,184,0.12);">
|
||
<th style="padding: 10px; text-align: left; font-size: 12px; color: var(--text-secondary);">日期</th>
|
||
<th style="padding: 10px; text-align: right; font-size: 12px; color: var(--text-secondary);">下载流量</th>
|
||
<th style="padding: 10px; text-align: right; font-size: 12px; color: var(--text-secondary);">下载次数</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="row in downloadTrafficDailyRowsDesc" :key="row.date" style="border-top: 1px solid var(--glass-border);">
|
||
<td style="padding: 9px 10px; color: var(--text-primary); font-size: 13px;">{{ formatReportDateLabel(row.date) }}</td>
|
||
<td style="padding: 9px 10px; text-align: right; color: var(--text-primary); font-size: 13px; font-weight: 600;">{{ formatBytes(row.bytes_used || 0) }}</td>
|
||
<td style="padding: 9px 10px; text-align: right; color: var(--text-secondary); font-size: 13px;">{{ row.download_count || 0 }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 在线设备 -->
|
||
<h3 class="settings-section-title settings-section-gap" style="margin: 40px 0 20px 0;">
|
||
<i class="fas fa-laptop-house"></i> 在线设备
|
||
</h3>
|
||
<div class="settings-panel" style="background: var(--bg-card); padding: 20px; border-radius: 12px; border: 1px solid var(--glass-border); margin-bottom: 30px;">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 12px;">
|
||
<div style="color: var(--text-secondary); font-size: 13px;">
|
||
可查看当前账号已登录设备,并支持远程强制下线
|
||
</div>
|
||
<button class="btn btn-secondary" @click="loadOnlineDevices()" :disabled="onlineDevices.loading || !!onlineDevices.kickingSessionId">
|
||
<i :class="onlineDevices.loading ? 'fas fa-sync-alt fa-spin' : 'fas fa-sync-alt'"></i>
|
||
{{ onlineDevices.loading ? '刷新中...' : '刷新设备列表' }}
|
||
</button>
|
||
</div>
|
||
|
||
<div v-if="onlineDevices.error" style="margin-bottom: 12px; padding: 10px 12px; border-radius: 8px; background: rgba(239,68,68,0.1); color: #ef4444; font-size: 13px;">
|
||
<i class="fas fa-exclamation-triangle"></i> {{ onlineDevices.error }}
|
||
</div>
|
||
|
||
<div v-if="onlineDevices.loading && onlineDevices.items.length === 0" style="text-align: center; padding: 20px; color: var(--text-muted);">
|
||
<i class="fas fa-spinner fa-spin"></i> 正在加载设备列表...
|
||
</div>
|
||
<div v-else-if="onlineDevices.items.length === 0" style="text-align: center; padding: 20px; color: var(--text-muted);">
|
||
暂无在线设备记录
|
||
</div>
|
||
<div v-else style="display: grid; gap: 10px;">
|
||
<div
|
||
v-for="device in onlineDevices.items"
|
||
:key="device.session_id"
|
||
style="border: 1px solid var(--glass-border); border-radius: 10px; background: var(--bg-secondary); padding: 12px; display: grid; gap: 8px;"
|
||
>
|
||
<div style="display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap;">
|
||
<div style="display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap;">
|
||
<strong style="color: var(--text-primary);">{{ device.device_name || '未知设备' }}</strong>
|
||
<span style="font-size: 12px; padding: 3px 8px; border-radius: 999px; background: rgba(99,102,241,0.16); color: #6366f1;">
|
||
{{ formatOnlineDeviceType(device.client_type) }}
|
||
</span>
|
||
<span v-if="device.is_current" style="font-size: 12px; padding: 3px 8px; border-radius: 999px; background: rgba(34,197,94,0.16); color: #16a34a;">
|
||
本机
|
||
</span>
|
||
</div>
|
||
<button
|
||
class="btn btn-danger"
|
||
style="padding: 6px 12px; border-radius: 8px;"
|
||
:disabled="onlineDevices.kickingSessionId === device.session_id"
|
||
@click="kickOnlineDevice(device)"
|
||
>
|
||
<i :class="onlineDevices.kickingSessionId === device.session_id ? 'fas fa-spinner fa-spin' : 'fas fa-power-off'"></i>
|
||
{{ device.is_current ? '下线本机' : '踢下线' }}
|
||
</button>
|
||
</div>
|
||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 8px; font-size: 12px; color: var(--text-secondary);">
|
||
<div>平台:{{ device.platform || '-' }}</div>
|
||
<div>IP:{{ device.ip_address || '-' }}</div>
|
||
<div>最近活跃:{{ formatDate(device.last_active_at) }}</div>
|
||
<div>登录时间:{{ formatDate(device.created_at) }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 界面设置 -->
|
||
<h3 class="settings-section-title settings-section-gap" style="margin: 40px 0 20px 0;"><i class="fas fa-palette"></i> 界面设置</h3>
|
||
<div class="settings-panel settings-theme-panel" style="background: var(--bg-card); padding: 20px; border-radius: 12px; border: 1px solid var(--glass-border); margin-bottom: 30px;">
|
||
<div style="margin-bottom: 15px;">
|
||
<span style="font-weight: 600; color: var(--text-primary);">主题模式</span>
|
||
<span style="color: var(--text-secondary); font-size: 13px; margin-left: 10px;">选择你喜欢的界面风格</span>
|
||
</div>
|
||
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
|
||
<button
|
||
class="btn"
|
||
:class="userThemePreference === null ? 'btn-primary' : 'btn-secondary'"
|
||
@click="setUserTheme(null)"
|
||
style="flex: 1; min-width: 120px; padding: 12px 16px;">
|
||
<i class="fas fa-globe"></i> 跟随全局
|
||
<span v-if="userThemePreference === null" style="margin-left: 8px; font-size: 12px; opacity: 0.8;">({{ globalTheme === 'dark' ? '暗色' : '亮色' }})</span>
|
||
</button>
|
||
<button
|
||
class="btn"
|
||
:class="userThemePreference === 'dark' ? 'btn-primary' : 'btn-secondary'"
|
||
@click="setUserTheme('dark')"
|
||
style="flex: 1; min-width: 120px; padding: 12px 16px;">
|
||
<i class="fas fa-moon"></i> 暗色主题
|
||
</button>
|
||
<button
|
||
class="btn"
|
||
:class="userThemePreference === 'light' ? 'btn-primary' : 'btn-secondary'"
|
||
@click="setUserTheme('light')"
|
||
style="flex: 1; min-width: 120px; padding: 12px 16px;">
|
||
<i class="fas fa-sun"></i> 亮色主题
|
||
</button>
|
||
</div>
|
||
<div style="margin-top: 12px; font-size: 13px; color: var(--text-muted);">
|
||
<i class="fas fa-info-circle"></i> 主题设置会影响你的文件页面和分享页面的外观
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 账号设置 -->
|
||
<h3 class="settings-section-title settings-section-gap" style="margin: 40px 0 20px 0;"><i class="fas fa-user-cog"></i> 账号设置</h3>
|
||
|
||
<div class="settings-panel settings-account-panel">
|
||
<!-- 管理员可以改用户名 -->
|
||
<form v-if="user && user.is_admin" @submit.prevent="updateUsername" style="margin-bottom: 30px;">
|
||
<div class="form-group">
|
||
<label class="form-label">用户名</label>
|
||
<input type="text" class="form-input" v-model="usernameForm.newUsername" :placeholder="user.username" minlength="3" maxlength="20" required>
|
||
</div>
|
||
<button type="submit" class="btn btn-primary" :disabled="usernameChanging">
|
||
<i :class="usernameChanging ? 'fas fa-spinner fa-spin' : 'fas fa-save'"></i> {{ usernameChanging ? '保存中...' : '修改用户名' }}
|
||
</button>
|
||
</form>
|
||
|
||
<!-- 所有用户都可以改密码 -->
|
||
<form @submit.prevent="changePassword">
|
||
<div class="form-group">
|
||
<label class="form-label">当前密码</label>
|
||
<input type="password" class="form-input" v-model="changePasswordForm.current_password" placeholder="输入当前密码" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">新密码 (至少6字符)</label>
|
||
<input type="password" class="form-input" v-model="changePasswordForm.new_password" placeholder="输入新密码" minlength="6" required>
|
||
</div>
|
||
<button type="submit" class="btn btn-primary" :disabled="passwordChanging">
|
||
<i :class="passwordChanging ? 'fas fa-spinner fa-spin' : 'fas fa-key'"></i> {{ passwordChanging ? '修改中...' : '修改密码' }}
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 分享视图 -->
|
||
<div v-if="isLoggedIn && currentView === 'shares'" class="main-container">
|
||
<div class="card">
|
||
<!-- 标题和工具栏 -->
|
||
<div class="shares-page-head" style="margin-bottom: 16px; display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap;">
|
||
<h3 class="shares-page-title" style="margin: 0; display: flex; align-items: center; gap: 8px;">
|
||
<i class="fas fa-share-alt"></i> 我的分享
|
||
</h3>
|
||
<div class="shares-page-actions" style="display: flex; gap: 8px;">
|
||
<button class="btn" :class="shareViewMode === 'grid' ? 'btn-primary' : 'btn-secondary'" @click="shareViewMode = 'grid'">
|
||
<i class="fas fa-th-large"></i> 卡片
|
||
</button>
|
||
<button class="btn" :class="shareViewMode === 'list' ? 'btn-primary' : 'btn-secondary'" @click="shareViewMode = 'list'">
|
||
<i class="fas fa-list"></i> 列表
|
||
</button>
|
||
<button class="btn btn-secondary" @click="refreshShareResources">
|
||
<i class="fas fa-sync-alt"></i> 刷新
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 筛选/搜索 -->
|
||
<div class="share-toolbar shares-toolbar">
|
||
<input type="text" v-model="shareFilters.keyword" placeholder="搜索路径 / 链接 / 分享码" style="flex: 1; min-width: 180px;">
|
||
<select v-model="shareFilters.type">
|
||
<option value="all">全部类型</option>
|
||
<option value="file">文件</option>
|
||
<option value="directory">文件夹</option>
|
||
<option value="all_files">全部文件</option>
|
||
</select>
|
||
<select v-model="shareFilters.status">
|
||
<option value="all">全部状态</option>
|
||
<option value="active">有效</option>
|
||
<option value="expiring">即将到期</option>
|
||
<option value="expired">已过期</option>
|
||
<option value="protected">已加密</option>
|
||
<option value="public">公开</option>
|
||
</select>
|
||
<select v-model="shareFilters.sort">
|
||
<option value="created_desc">最新创建</option>
|
||
<option value="created_asc">最早创建</option>
|
||
<option value="views_desc">访问最多</option>
|
||
<option value="downloads_desc">下载最多</option>
|
||
<option value="expire_asc">最早到期</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- 空状态 -->
|
||
<div v-if="shares.length === 0" class="alert alert-info">
|
||
还没有创建任何分享
|
||
</div>
|
||
<div v-else-if="filteredShares.length === 0" class="alert alert-warning">
|
||
没有符合筛选条件的分享,试试清空搜索/筛选。
|
||
</div>
|
||
|
||
<!-- 大图标视图 -->
|
||
<div v-else-if="shareViewMode === 'grid'" class="share-card-grid">
|
||
<div v-for="share in filteredShares" :key="share.id" class="share-card">
|
||
<div class="share-card__title">
|
||
<i class="fas" :class="getShareTypeIcon(share)" style="color: var(--accent-1);"></i>
|
||
<span :title="share.share_path">{{ getPathBaseName(share.share_path) }}</span>
|
||
</div>
|
||
<div class="share-card__chips">
|
||
<span :class="['share-chip', getShareStatus(share).class]">
|
||
<i class="fas" :class="getShareStatus(share).icon"></i> {{ getShareStatus(share).text }}
|
||
</span>
|
||
<span class="share-chip info">
|
||
<i class="fas fa-tag"></i> {{ getShareTypeLabel(share) }}
|
||
</span>
|
||
<span class="share-chip info">
|
||
<i class="fas" :class="getShareProtection(share).icon"></i> {{ getShareProtection(share).text }}
|
||
</span>
|
||
<span class="share-chip info" v-if="share.storage_type">
|
||
<i class="fas fa-hdd"></i> {{ getStorageLabel(share.storage_type) }}
|
||
</span>
|
||
<span class="share-chip info">
|
||
<i class="fas fa-barcode"></i> {{ share.share_code }}
|
||
</span>
|
||
</div>
|
||
<div class="share-card__url" style="font-size: 13px; color: var(--text-secondary); margin-bottom: 10px; word-break: break-all;">
|
||
<i class="fas fa-link"></i>
|
||
<span class="share-card__url-link" :title="share.share_url">{{ share.share_url }}</span>
|
||
</div>
|
||
<div class="share-card__meta">
|
||
<span><i class="fas fa-eye"></i> 访问 {{ share.view_count }}</span>
|
||
<span><i class="fas fa-download"></i> 下载 {{ share.download_count }}</span>
|
||
<span><i class="fas fa-clock"></i> {{ share.expires_at ? formatExpireTime(share.expires_at) : '永久有效' }}</span>
|
||
<span><i class="fas fa-calendar-alt"></i> 创建 {{ formatDateTime(share.created_at) }}</span>
|
||
</div>
|
||
<div class="share-card__actions">
|
||
<button class="btn btn-secondary" @click.stop="openShare(share.share_url)">
|
||
<i class="fas fa-external-link-alt"></i> 打开
|
||
</button>
|
||
<button class="btn btn-secondary" @click.stop="copyShareLink(share.share_url)">
|
||
<i class="fas fa-copy"></i> 复制链接
|
||
</button>
|
||
<button class="btn share-card__delete-btn" style="background: #ef4444; color: white;" @click.stop="requestDeleteShare(share.id, $event)">
|
||
<i class="fas fa-trash"></i> 删除
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 列表视图 -->
|
||
<table v-else class="share-list-table">
|
||
<thead>
|
||
<tr style="border-bottom: 2px solid #ddd;">
|
||
<th style="padding: 10px; text-align: left; width: 18%;">文件名</th>
|
||
<th style="padding: 10px; text-align: left; width: 30%;">链接地址</th>
|
||
<th style="padding: 10px; text-align: center; width: 8%;">访问次数</th>
|
||
<th style="padding: 10px; text-align: center; width: 8%;">下载次数</th>
|
||
<th style="padding: 10px; text-align: center; width: 16%;">到期时间</th>
|
||
<th style="padding: 10px; text-align: center; width: 20%;">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="share in filteredShares" :key="share.id" style="border-bottom: 1px solid #eee;">
|
||
<td style="padding: 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" :title="share.share_path">{{ getPathBaseName(share.share_path) }}</td>
|
||
<td style="padding: 10px; overflow: hidden;">
|
||
<span class="share-list-link-text" :title="share.share_url">{{ share.share_url }}</span>
|
||
</td>
|
||
<td style="padding: 10px; text-align: center;">{{ share.view_count }}</td>
|
||
<td style="padding: 10px; text-align: center;">{{ share.download_count }}</td>
|
||
<td style="padding: 10px; text-align: center;">
|
||
<span v-if="!share.expires_at" style="color: #22c55e;"><i class="fas fa-infinity"></i> 永久有效</span>
|
||
<span v-else :style="{color: isExpiringSoon(share.expires_at) ? '#ffc107' : isExpired(share.expires_at) ? '#dc3545' : '#667eea'}" :title="share.expires_at"><i class="fas fa-clock"></i> {{ formatExpireTime(share.expires_at) }}</span>
|
||
</td>
|
||
<td style="padding: 10px; text-align: center;">
|
||
<div class="share-list-actions">
|
||
<button class="btn btn-secondary" @click.stop="openShare(share.share_url)">
|
||
<i class="fas fa-up-right-from-square"></i> 打开
|
||
</button>
|
||
<button class="btn btn-secondary" @click.stop="copyShareLink(share.share_url)">
|
||
<i class="fas fa-copy"></i> 复制链接
|
||
</button>
|
||
<button class="btn" style="background: #ef4444; color: white;" @click.stop="requestDeleteShare(share.id, $event)">
|
||
<i class="fas fa-trash"></i> 删除
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
<!-- 直链管理(独立于普通分享) -->
|
||
<div style="margin-top: 24px;">
|
||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 10px;">
|
||
<h4 style="margin: 0; display: flex; align-items: center; gap: 8px;">
|
||
<i class="fas fa-link"></i> 我的直链
|
||
</h4>
|
||
<small style="color: var(--text-secondary);">提示:可在文件右键菜单中生成直链</small>
|
||
</div>
|
||
|
||
<div v-if="directLinksLoading" class="alert alert-info">
|
||
正在加载直链列表...
|
||
</div>
|
||
<div v-else-if="directLinks.length === 0" class="alert alert-info">
|
||
还没有创建直链
|
||
</div>
|
||
<div v-else-if="filteredDirectLinks.length === 0" class="alert alert-warning">
|
||
没有符合搜索条件的直链
|
||
</div>
|
||
<table v-else class="share-list-table">
|
||
<thead>
|
||
<tr style="border-bottom: 2px solid #ddd;">
|
||
<th style="padding: 10px; text-align: left; width: 18%;">文件名</th>
|
||
<th style="padding: 10px; text-align: left; width: 30%;">链接地址</th>
|
||
<th style="padding: 10px; text-align: center; width: 8%;">访问次数</th>
|
||
<th style="padding: 10px; text-align: center; width: 8%;">下载次数</th>
|
||
<th style="padding: 10px; text-align: center; width: 16%;">到期时间</th>
|
||
<th style="padding: 10px; text-align: center; width: 20%;">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="link in filteredDirectLinks" :key="`direct-${link.id}`" style="border-bottom: 1px solid #eee;">
|
||
<td style="padding: 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" :title="link.file_path">{{ getPathBaseName(link.file_path, link.file_name) }}</td>
|
||
<td style="padding: 10px; overflow: hidden;">
|
||
<span class="share-list-link-text" :title="link.direct_url">{{ link.direct_url }}</span>
|
||
</td>
|
||
<td style="padding: 10px; text-align: center; color: var(--text-muted);" title="直链暂不统计访问次数">—</td>
|
||
<td style="padding: 10px; text-align: center;">{{ link.download_count || 0 }}</td>
|
||
<td style="padding: 10px; text-align: center;">
|
||
<span v-if="!link.expires_at" style="color: #22c55e;"><i class="fas fa-infinity"></i> 永久有效</span>
|
||
<span v-else :style="{color: isExpiringSoon(link.expires_at) ? '#ffc107' : isExpired(link.expires_at) ? '#dc3545' : '#667eea'}" :title="link.expires_at"><i class="fas fa-clock"></i> {{ formatExpireTime(link.expires_at) }}</span>
|
||
</td>
|
||
<td style="padding: 10px; text-align: center;">
|
||
<div class="share-list-actions">
|
||
<button class="btn btn-secondary" @click.stop="openShare(link.direct_url)">
|
||
<i class="fas fa-up-right-from-square"></i> 打开
|
||
</button>
|
||
<button class="btn btn-secondary" @click.stop="copyDirectLink(link.direct_url)">
|
||
<i class="fas fa-copy"></i> 复制链接
|
||
</button>
|
||
<button class="btn" style="background: #ef4444; color: white;" @click.stop="requestDeleteDirectLink(link.id, $event)">
|
||
<i class="fas fa-trash"></i> 删除
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 管理员视图 -->
|
||
<div v-if="isLoggedIn && currentView === 'admin' && user && user.is_admin" class="main-container">
|
||
<!-- 管理员标签页导航 -->
|
||
<div class="card admin-tabs-card" style="margin-bottom: 20px; padding: 0;">
|
||
<div class="admin-tabs-nav">
|
||
<button class="admin-tab-btn" :class="{active: adminTab === 'overview'}" @click="adminTab = 'overview'">
|
||
<i class="fas fa-tachometer-alt"></i> 概览
|
||
</button>
|
||
<button class="admin-tab-btn" :class="{active: adminTab === 'settings'}" @click="adminTab = 'settings'">
|
||
<i class="fas fa-cog"></i> 设置
|
||
</button>
|
||
<button class="admin-tab-btn" :class="{active: adminTab === 'monitor'}" @click="openMonitorTab">
|
||
<i class="fas fa-chart-line"></i> 监控
|
||
</button>
|
||
<button class="admin-tab-btn" :class="{active: adminTab === 'users'}" @click="adminTab = 'users'">
|
||
<i class="fas fa-users"></i> 用户
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ========== 概览标签页 ========== -->
|
||
<div v-show="adminTab === 'overview'">
|
||
<!-- 调试模式开关 -->
|
||
<div class="card" style="margin-bottom: 30px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;">
|
||
<div class="admin-debug-row" style="display: flex; align-items: center; justify-content: space-between;">
|
||
<div>
|
||
<h3 style="margin-bottom: 10px; color: white;">
|
||
<i class="fas fa-bug"></i> 调试模式
|
||
</h3>
|
||
<p style="margin: 0; font-size: 14px; opacity: 0.9;">
|
||
{{ debugMode ? '已启用 - F12和开发者工具已解锁' : '已禁用 - F12和开发者工具被锁定' }}
|
||
</p>
|
||
</div>
|
||
<button @click="toggleDebugMode" class="btn admin-debug-toggle-btn" :style="{background: debugMode ? '#28a745' : '#dc3545', color: 'white', border: 'none', padding: '12px 24px', fontSize: '16px', fontWeight: '600', cursor: 'pointer', borderRadius: '8px', boxShadow: '0 4px 12px rgba(0,0,0,0.2)'}">
|
||
<i :class="debugMode ? 'fas fa-toggle-on' : 'fas fa-toggle-off'"></i>
|
||
{{ debugMode ? '关闭调试' : '开启调试' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 服务器存储统计 -->
|
||
<div class="card" style="margin-bottom: 30px;">
|
||
<h3 style="margin-bottom: 20px;">
|
||
<i class="fas fa-hdd"></i> 服务器存储统计
|
||
</h3>
|
||
|
||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px;">
|
||
<!-- 磁盘总容量 -->
|
||
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 20px; border-radius: 12px; color: white;">
|
||
<div style="display: flex; align-items: center; justify-content: space-between;">
|
||
<div>
|
||
<div style="font-size: 14px; opacity: 0.9; margin-bottom: 8px;">磁盘总容量</div>
|
||
<div style="font-size: 28px; font-weight: 700;">{{ formatBytes(serverStorageStats.totalDisk) }}</div>
|
||
</div>
|
||
<i class="fas fa-database" style="font-size: 48px; opacity: 0.3;"></i>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 已使用空间 -->
|
||
<div style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); padding: 20px; border-radius: 12px; color: white;">
|
||
<div style="display: flex; align-items: center; justify-content: space-between;">
|
||
<div>
|
||
<div style="font-size: 14px; opacity: 0.9; margin-bottom: 8px;">已使用空间</div>
|
||
<div style="font-size: 28px; font-weight: 700;">{{ formatBytes(serverStorageStats.usedDisk) }}</div>
|
||
<div style="font-size: 12px; opacity: 0.8; margin-top: 4px;">
|
||
{{ serverStorageStats.totalDisk > 0 ? Math.round((serverStorageStats.usedDisk / serverStorageStats.totalDisk) * 100) : 0 }}% 使用率
|
||
</div>
|
||
</div>
|
||
<i class="fas fa-chart-pie" style="font-size: 48px; opacity: 0.3;"></i>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 可用空间 -->
|
||
<div style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); padding: 20px; border-radius: 12px; color: white;">
|
||
<div style="display: flex; align-items: center; justify-content: space-between;">
|
||
<div>
|
||
<div style="font-size: 14px; opacity: 0.9; margin-bottom: 8px;">可用空间</div>
|
||
<div style="font-size: 28px; font-weight: 700;">{{ formatBytes(serverStorageStats.availableDisk) }}</div>
|
||
</div>
|
||
<i class="fas fa-folder-open" style="font-size: 48px; opacity: 0.3;"></i>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 用户配额总和 -->
|
||
<div style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); padding: 20px; border-radius: 12px; color: white;">
|
||
<div style="display: flex; align-items: center; justify-content: space-between;">
|
||
<div>
|
||
<div style="font-size: 14px; opacity: 0.9; margin-bottom: 8px;">用户配额总和</div>
|
||
<div style="font-size: 28px; font-weight: 700;">{{ formatBytes(serverStorageStats.totalUserQuotas) }}</div>
|
||
<div style="font-size: 12px; opacity: 0.8; margin-top: 4px;">
|
||
{{ serverStorageStats.totalUsers }} 个用户
|
||
</div>
|
||
</div>
|
||
<i class="fas fa-users" style="font-size: 48px; opacity: 0.3;"></i>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 用户实际使用 -->
|
||
<div style="background: linear-gradient(135deg, #30cfd0 0%, #330867 100%); padding: 20px; border-radius: 12px; color: white;">
|
||
<div style="display: flex; align-items: center; justify-content: space-between;">
|
||
<div>
|
||
<div style="font-size: 14px; opacity: 0.9; margin-bottom: 8px;">用户实际使用</div>
|
||
<div style="font-size: 28px; font-weight: 700;">{{ formatBytes(serverStorageStats.totalUserUsed) }}</div>
|
||
<div style="font-size: 12px; opacity: 0.8; margin-top: 4px;">
|
||
{{ serverStorageStats.totalUserQuotas > 0 ? Math.round((serverStorageStats.totalUserUsed / serverStorageStats.totalUserQuotas) * 100) : 0 }}% 配额使用率
|
||
</div>
|
||
</div>
|
||
<i class="fas fa-cloud-upload-alt" style="font-size: 48px; opacity: 0.3;"></i>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 配额剩余 -->
|
||
<div style="background: linear-gradient(135deg, rgba(102, 126, 234, 0.2), rgba(118, 75, 162, 0.2)); padding: 20px; border-radius: 12px; color: var(--text-primary); border: 1px solid var(--glass-border);">
|
||
<div style="display: flex; align-items: center; justify-content: space-between;">
|
||
<div>
|
||
<div style="font-size: 14px; opacity: 0.8; margin-bottom: 8px;">安全可分配配额</div>
|
||
<div style="font-size: 28px; font-weight: 700;">
|
||
{{ formatBytes(Math.max(0, serverStorageStats.availableDisk - (serverStorageStats.totalUserQuotas - serverStorageStats.totalUserUsed))) }}
|
||
</div>
|
||
<div style="font-size: 12px; opacity: 0.7; margin-top: 4px;">
|
||
可用空间 - 未使用的配额
|
||
</div>
|
||
</div>
|
||
<i class="fas fa-boxes" style="font-size: 48px; opacity: 0.2;"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 存储警告提示 -->
|
||
<div v-if="serverStorageStats.totalDisk > 0 && ((serverStorageStats.usedDisk / serverStorageStats.totalDisk) > 0.9)"
|
||
style="margin-top: 20px; padding: 15px; background: rgba(245, 158, 11, 0.15); border-left: 4px solid #f59e0b; border-radius: 6px; color: #fbbf24;">
|
||
<i class="fas fa-exclamation-triangle"></i>
|
||
<strong>警告:</strong> 磁盘使用率已超过90%,建议及时清理空间或扩容!
|
||
</div>
|
||
|
||
<div v-if="serverStorageStats.totalUserQuotas > serverStorageStats.totalDisk"
|
||
style="margin-top: 20px; padding: 15px; background: rgba(239, 68, 68, 0.15); border-left: 4px solid #ef4444; border-radius: 6px; color: #fca5a5;">
|
||
<i class="fas fa-exclamation-circle"></i>
|
||
<strong>配额超分配:</strong> 用户配额总和 ({{ formatBytes(serverStorageStats.totalUserQuotas) }}) 已超过磁盘总容量 ({{ formatBytes(serverStorageStats.totalDisk) }})!
|
||
</div>
|
||
</div>
|
||
</div><!-- 概览标签页结束 -->
|
||
|
||
<!-- ========== 设置标签页 ========== -->
|
||
<div v-show="adminTab === 'settings'">
|
||
<!-- 系统设置 -->
|
||
<div class="card" style="margin-bottom: 30px;">
|
||
<h3 style="margin-bottom: 20px;">
|
||
<i class="fas fa-sliders-h"></i> 系统设置
|
||
</h3>
|
||
<div style="display: flex; align-items: flex-end; gap: 16px; flex-wrap: wrap;">
|
||
<div class="form-group" style="margin-bottom: 0;">
|
||
<label class="form-label">最大上传大小 (MB)</label>
|
||
<input type="number" class="form-input" v-model.number="systemSettings.maxUploadSizeMB" min="1" style="width: 150px;">
|
||
</div>
|
||
<button class="btn btn-primary" @click="updateSystemSettings" style="height: 40px;">
|
||
<i class="fas fa-save"></i> 保存
|
||
</button>
|
||
<span style="color: var(--text-secondary); font-size: 13px;">修改后需要重启服务才能生效</span>
|
||
</div>
|
||
|
||
<hr style="margin: 20px 0;">
|
||
<h4 style="margin-bottom: 12px;"><i class="fas fa-shield-alt"></i> 下载安全策略(无感防刷)</h4>
|
||
<div class="alert alert-info" style="margin-bottom: 15px;">
|
||
触发策略时对用户返回“当前网络繁忙,请稍后再试”,避免暴露具体风控规则。
|
||
</div>
|
||
|
||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 14px;">
|
||
<div style="padding: 12px; border: 1px solid var(--glass-border); border-radius: 10px;">
|
||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 10px;">
|
||
<input type="checkbox" id="ds-enabled" v-model="systemSettings.downloadSecurity.enabled">
|
||
<label for="ds-enabled" style="margin: 0; font-weight: 600;">启用下载安全策略总开关</label>
|
||
</div>
|
||
<div style="font-size: 12px; color: var(--text-secondary);">
|
||
关闭后不做下载频率限制。
|
||
</div>
|
||
</div>
|
||
|
||
<div style="padding: 12px; border: 1px solid var(--glass-border); border-radius: 10px;">
|
||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 10px;">
|
||
<input type="checkbox" id="ds-same-file" v-model="systemSettings.downloadSecurity.same_ip_same_file.enabled">
|
||
<label for="ds-same-file" style="margin: 0; font-weight: 600;">同IP + 同文件限频</label>
|
||
</div>
|
||
<div style="display: flex; gap: 8px; align-items: center; flex-wrap: wrap;">
|
||
<span style="font-size: 12px; color: var(--text-secondary);">5分钟</span>
|
||
<input type="number" class="form-input" v-model.number="systemSettings.downloadSecurity.same_ip_same_file.limit_5m" min="1" style="width: 80px;">
|
||
<span style="font-size: 12px; color: var(--text-secondary);">1小时</span>
|
||
<input type="number" class="form-input" v-model.number="systemSettings.downloadSecurity.same_ip_same_file.limit_1h" min="1" style="width: 80px;">
|
||
<span style="font-size: 12px; color: var(--text-secondary);">1天</span>
|
||
<input type="number" class="form-input" v-model.number="systemSettings.downloadSecurity.same_ip_same_file.limit_1d" min="1" style="width: 80px;">
|
||
</div>
|
||
</div>
|
||
|
||
<div style="padding: 12px; border: 1px solid var(--glass-border); border-radius: 10px;">
|
||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 10px;">
|
||
<input type="checkbox" id="ds-same-user" v-model="systemSettings.downloadSecurity.same_ip_same_user.enabled">
|
||
<label for="ds-same-user" style="margin: 0; font-weight: 600;">扩展:同IP + 同用户总限频</label>
|
||
</div>
|
||
<div style="display: flex; gap: 8px; align-items: center; flex-wrap: wrap;">
|
||
<span style="font-size: 12px; color: var(--text-secondary);">1小时</span>
|
||
<input type="number" class="form-input" v-model.number="systemSettings.downloadSecurity.same_ip_same_user.limit_1h" min="1" style="width: 90px;">
|
||
<span style="font-size: 12px; color: var(--text-secondary);">1天</span>
|
||
<input type="number" class="form-input" v-model.number="systemSettings.downloadSecurity.same_ip_same_user.limit_1d" min="1" style="width: 90px;">
|
||
</div>
|
||
</div>
|
||
|
||
<div style="padding: 12px; border: 1px solid var(--glass-border); border-radius: 10px;">
|
||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 10px;">
|
||
<input type="checkbox" id="ds-min-interval" v-model="systemSettings.downloadSecurity.same_ip_same_file_min_interval.enabled">
|
||
<label for="ds-min-interval" style="margin: 0; font-weight: 600;">扩展:同IP + 同文件最小间隔</label>
|
||
</div>
|
||
<div style="display: flex; gap: 8px; align-items: center;">
|
||
<span style="font-size: 12px; color: var(--text-secondary);">间隔秒数</span>
|
||
<input type="number" class="form-input" v-model.number="systemSettings.downloadSecurity.same_ip_same_file_min_interval.seconds" min="1" style="width: 90px;">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<hr style="margin: 20px 0;">
|
||
<h4 style="margin-bottom: 12px;"><i class="fas fa-palette"></i> 全局主题设置</h4>
|
||
<div style="margin-bottom: 15px;">
|
||
<span style="color: var(--text-secondary); font-size: 13px;">设置系统默认主题,用户可以在个人设置中覆盖此设置</span>
|
||
</div>
|
||
<div style="display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 20px;">
|
||
<button
|
||
class="btn"
|
||
:class="globalTheme === 'dark' ? 'btn-primary' : 'btn-secondary'"
|
||
@click="setGlobalTheme('dark')"
|
||
style="padding: 12px 24px;">
|
||
<i class="fas fa-moon"></i> 暗色主题(默认)
|
||
</button>
|
||
<button
|
||
class="btn"
|
||
:class="globalTheme === 'light' ? 'btn-primary' : 'btn-secondary'"
|
||
@click="setGlobalTheme('light')"
|
||
style="padding: 12px 24px;">
|
||
<i class="fas fa-sun"></i> 亮色主题
|
||
</button>
|
||
</div>
|
||
<div style="font-size: 13px; color: var(--text-muted); background: var(--bg-card); padding: 12px; border-radius: 8px; border: 1px solid var(--glass-border);">
|
||
<i class="fas fa-info-circle"></i> 当前全局主题: <strong>{{ globalTheme === 'dark' ? '暗色' : '亮色' }}</strong>。
|
||
未设置个人偏好的用户将使用此主题。分享页面也会默认使用分享者的主题设置。
|
||
</div>
|
||
|
||
<hr style="margin: 20px 0;">
|
||
<h4 style="margin-bottom: 12px;"><i class="fas fa-envelope"></i> SMTP 邮件配置(用于注册激活和找回密码)</h4>
|
||
<div class="alert alert-info" style="margin-bottom: 15px;">
|
||
支持 QQ/163/企业邮箱等。QQ 邮箱示例:主机 <code>smtp.qq.com</code>,端口 <code>465</code>,勾选 SSL,用户名=邮箱地址,密码=授权码。
|
||
</div>
|
||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 16px;">
|
||
<div class="form-group">
|
||
<label class="form-label">SMTP 主机</label>
|
||
<input type="text" class="form-input" v-model="systemSettings.smtp.host" placeholder="如 smtp.qq.com">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">端口</label>
|
||
<input type="number" class="form-input" v-model.number="systemSettings.smtp.port" placeholder="465/587">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">SSL/TLS</label>
|
||
<div style="display: flex; align-items: center; gap: 8px;">
|
||
<input type="checkbox" id="smtp-secure" v-model="systemSettings.smtp.secure">
|
||
<label for="smtp-secure" style="margin: 0;">使用 SSL(465 通常需要)</label>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">用户名(邮箱)</label>
|
||
<input type="text" class="form-input" v-model="systemSettings.smtp.user" placeholder="your@qq.com">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">发件人 From(可选)</label>
|
||
<input type="text" class="form-input" v-model="systemSettings.smtp.from" placeholder="显示名称 <your@qq.com>">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">密码/授权码</label>
|
||
<input type="password" class="form-input" v-model="systemSettings.smtp.password" :placeholder="systemSettings.smtp.has_password ? '已配置,留空则不修改' : '请输入授权码'">
|
||
</div>
|
||
</div>
|
||
<div style="display: flex; gap: 10px; flex-wrap: wrap; margin-top: 15px;">
|
||
<button class="btn btn-primary" @click="updateSystemSettings">
|
||
<i class="fas fa-save"></i> 保存设置
|
||
</button>
|
||
<button class="btn btn-secondary" @click="testSmtp">
|
||
<i class="fas fa-envelope"></i> 发送测试邮件
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 管理员 OSS 配置 -->
|
||
<div class="card" style="margin-bottom: 30px;">
|
||
<h3 style="margin-bottom: 20px;">
|
||
<i class="fas fa-cloud"></i> 管理员 OSS 配置
|
||
</h3>
|
||
<div style="margin-bottom: 15px;">
|
||
<span style="color: var(--text-secondary); font-size: 13px;">配置管理员账号的 OSS 云存储,用于文件存储和管理。</span>
|
||
</div>
|
||
|
||
<!-- OSS 配置状态 -->
|
||
<div v-if="user?.oss_config_source !== 'none'" style="margin-bottom: 20px; padding: 15px; background: rgba(34, 197, 94, 0.1); border-left: 4px solid #22c55e; border-radius: 8px;">
|
||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||
<div>
|
||
<div style="font-weight: 600; color: var(--text-primary); margin-bottom: 4px;">
|
||
<i class="fas fa-check-circle" style="color: #22c55e;"></i> OSS 已配置
|
||
</div>
|
||
<div style="font-size: 13px; color: var(--text-secondary);">
|
||
{{ user.oss_provider }} / {{ user.oss_bucket }}
|
||
</div>
|
||
</div>
|
||
<button class="btn btn-primary" @click="openOssConfigModal" style="padding: 8px 16px;">
|
||
<i class="fas fa-edit"></i> 修改配置
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- OSS 空间统计 -->
|
||
<div v-if="user?.oss_config_source !== 'none'" style="margin-bottom: 20px;">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||
<span style="font-weight: 600; color: var(--text-primary);">
|
||
<i class="fas fa-chart-pie"></i> 空间使用统计
|
||
</span>
|
||
<button class="btn btn-secondary" @click="loadOssUsage" :disabled="ossUsageLoading" style="padding: 6px 12px; font-size: 12px;">
|
||
<i :class="ossUsageLoading ? 'fas fa-spinner fa-spin' : 'fas fa-sync-alt'"></i> 刷新
|
||
</button>
|
||
</div>
|
||
<div v-if="ossUsage" style="padding: 12px; background: var(--bg-card); border-radius: 8px; border: 1px solid var(--glass-border);">
|
||
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; font-size: 13px;">
|
||
<div>
|
||
<span style="color: var(--text-muted);">总大小</span>
|
||
<div style="font-weight: 600; color: var(--text-primary);">{{ ossUsage.totalSizeFormatted || '-' }}</div>
|
||
</div>
|
||
<div>
|
||
<span style="color: var(--text-muted);">文件数</span>
|
||
<div style="font-weight: 600; color: var(--text-primary);">{{ ossUsage.fileCount || '-' }}</div>
|
||
</div>
|
||
<div>
|
||
<span style="color: var(--text-muted);">文件夹数</span>
|
||
<div style="font-weight: 600; color: var(--text-primary);">{{ ossUsage.dirCount || '-' }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-else-if="ossUsageLoading" style="padding: 12px; text-align: center; color: var(--text-muted);">
|
||
<i class="fas fa-spinner fa-spin"></i> 正在加载...
|
||
</div>
|
||
<div v-else style="padding: 12px; text-align: center; color: var(--text-muted);">
|
||
暂无数据,点击刷新查看
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 未配置 OSS 时显示配置按钮 -->
|
||
<div v-if="user?.oss_config_source === 'none'" style="padding: 30px; text-align: center; background: var(--bg-card); border-radius: 12px; border: 1px solid var(--glass-border);">
|
||
<i class="fas fa-cloud-upload-alt" style="font-size: 48px; color: var(--text-muted); margin-bottom: 15px;"></i>
|
||
<div style="margin-bottom: 15px; color: var(--text-secondary);">尚未配置 OSS 存储</div>
|
||
<button class="btn btn-primary" @click="openOssConfigModal" style="padding: 12px 30px;">
|
||
<i class="fas fa-plus"></i> 配置 OSS 存储
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 存储模式切换 -->
|
||
<div v-if="user" style="margin-top: 20px; padding: 15px; background: var(--bg-card); border-radius: 12px; border: 1px solid var(--glass-border);">
|
||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||
<div>
|
||
<div style="font-weight: 600; color: var(--text-primary); margin-bottom: 5px;">
|
||
<i class="fas fa-database"></i> 当前存储模式
|
||
</div>
|
||
<div style="font-size: 13px; color: var(--text-secondary);">
|
||
<span v-if="user.current_storage_type === 'local'" style="color: #667eea;">
|
||
<i class="fas fa-hard-drive"></i> 本地存储
|
||
</span>
|
||
<span v-else style="color: #6c757d;">
|
||
<i class="fas fa-cloud"></i> OSS 存储
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<button v-if="user?.oss_config_source !== 'none'" class="btn" :class="user.current_storage_type === 'oss' ? 'btn-secondary' : 'btn-primary'"
|
||
@click="switchStorage(user.current_storage_type === 'local' ? 'oss' : 'local')"
|
||
:disabled="storageSwitching" style="padding: 8px 16px;">
|
||
<i :class="storageSwitching ? 'fas fa-spinner fa-spin' : 'fas fa-random'"></i>
|
||
{{ storageSwitching ? '切换中...' : (user.current_storage_type === 'local' ? '切换到 OSS' : '切换到本地') }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div><!-- 设置标签页结束 -->
|
||
|
||
<!-- ========== 监控标签页 ========== -->
|
||
<div v-show="adminTab === 'monitor'">
|
||
<div v-if="monitorTabLoading" class="card" style="margin-bottom: 30px; text-align: center; padding: 40px;">
|
||
<i class="fas fa-spinner fa-spin" style="font-size: 36px; margin-bottom: 10px;"></i>
|
||
<div style="color: var(--text-secondary); font-size: 14px;">正在加载监控数据...</div>
|
||
</div>
|
||
<template v-else>
|
||
<!-- 健康检测 -->
|
||
<div class="card" style="margin-bottom: 30px;">
|
||
<div class="admin-health-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||
<h3 style="margin: 0;">
|
||
<i class="fas fa-heartbeat"></i> 系统健康检测
|
||
</h3>
|
||
<button class="btn btn-primary" @click="loadHealthCheck" :disabled="healthCheck.loading">
|
||
<i class="fas" :class="healthCheck.loading ? 'fa-spinner fa-spin' : 'fa-sync'"></i>
|
||
{{ healthCheck.loading ? '检测中...' : '刷新检测' }}
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 整体状态 -->
|
||
<div v-if="healthCheck.overallStatus" style="margin-bottom: 20px;">
|
||
<div style="display: flex; align-items: center; gap: 15px; flex-wrap: wrap;">
|
||
<div style="font-size: 18px; font-weight: bold;" :class="getOverallStatusColor(healthCheck.overallStatus)">
|
||
<i class="fas" :class="{
|
||
'fa-check-circle': healthCheck.overallStatus === 'healthy',
|
||
'fa-exclamation-triangle': healthCheck.overallStatus === 'warning',
|
||
'fa-times-circle': healthCheck.overallStatus === 'critical'
|
||
}"></i>
|
||
{{ getOverallStatusText(healthCheck.overallStatus) }}
|
||
</div>
|
||
<div class="admin-health-summary" style="display: flex; gap: 12px; font-size: 13px;">
|
||
<span style="color: #22c55e;"><i class="fas fa-check"></i> 通过: {{ healthCheck.summary.pass }}</span>
|
||
<span style="color: #ffc107;"><i class="fas fa-exclamation"></i> 警告: {{ healthCheck.summary.warning }}</span>
|
||
<span style="color: #ef4444;"><i class="fas fa-times"></i> 失败: {{ healthCheck.summary.fail }}</span>
|
||
<span style="color: #17a2b8;"><i class="fas fa-info"></i> 信息: {{ healthCheck.summary.info }}</span>
|
||
</div>
|
||
</div>
|
||
<div v-if="healthCheck.lastCheck" style="font-size: 12px; color: var(--text-muted); margin-top: 8px;">
|
||
上次检测: {{ new Date(healthCheck.lastCheck).toLocaleString() }}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 检测项列表 -->
|
||
<div v-if="healthCheck.checks.length > 0">
|
||
<!-- 按分类分组 -->
|
||
<div style="margin-bottom: 20px;">
|
||
<h4 style="margin-bottom: 12px; color: var(--text-primary);"><i class="fas fa-shield-alt"></i> 安全配置</h4>
|
||
<div style="display: grid; gap: 10px;">
|
||
<div v-for="check in healthCheck.checks.filter(c => c.category === 'security')" :key="check.name"
|
||
style="display: flex; align-items: flex-start; gap: 12px; padding: 12px; background: rgba(255,255,255,0.03); border-radius: 8px;">
|
||
<span style="font-size: 18px; width: 24px; text-align: center;"
|
||
:class="{
|
||
'text-green-600': check.status === 'pass',
|
||
'text-yellow-600': check.status === 'warning',
|
||
'text-red-600': check.status === 'fail',
|
||
'text-blue-600': check.status === 'info'
|
||
}">
|
||
{{ getHealthStatusIcon(check.status) }}
|
||
</span>
|
||
<div style="flex: 1;">
|
||
<div style="font-weight: 500; margin-bottom: 4px;">{{ check.name }}</div>
|
||
<div style="font-size: 13px; color: var(--text-secondary);">{{ check.message }}</div>
|
||
<div v-if="check.suggestion" style="font-size: 12px; color: #e67e22; margin-top: 4px;">
|
||
<i class="fas fa-lightbulb"></i> {{ check.suggestion }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="margin-bottom: 20px;">
|
||
<h4 style="margin-bottom: 12px; color: var(--text-primary);"><i class="fas fa-server"></i> 服务状态</h4>
|
||
<div style="display: grid; gap: 10px;">
|
||
<div v-for="check in healthCheck.checks.filter(c => c.category === 'service')" :key="check.name"
|
||
style="display: flex; align-items: flex-start; gap: 12px; padding: 12px; background: rgba(255,255,255,0.03); border-radius: 8px;">
|
||
<span style="font-size: 18px; width: 24px; text-align: center;"
|
||
:class="{
|
||
'text-green-600': check.status === 'pass',
|
||
'text-yellow-600': check.status === 'warning',
|
||
'text-red-600': check.status === 'fail',
|
||
'text-blue-600': check.status === 'info'
|
||
}">
|
||
{{ getHealthStatusIcon(check.status) }}
|
||
</span>
|
||
<div style="flex: 1;">
|
||
<div style="font-weight: 500; margin-bottom: 4px;">{{ check.name }}</div>
|
||
<div style="font-size: 13px; color: var(--text-secondary);">{{ check.message }}</div>
|
||
<div v-if="check.suggestion" style="font-size: 12px; color: #e67e22; margin-top: 4px;">
|
||
<i class="fas fa-lightbulb"></i> {{ check.suggestion }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<h4 style="margin-bottom: 12px; color: var(--text-primary);"><i class="fas fa-cog"></i> 运行配置</h4>
|
||
<div style="display: grid; gap: 10px;">
|
||
<div v-for="check in healthCheck.checks.filter(c => c.category === 'config')" :key="check.name"
|
||
style="display: flex; align-items: flex-start; gap: 12px; padding: 12px; background: rgba(255,255,255,0.03); border-radius: 8px;">
|
||
<span style="font-size: 18px; width: 24px; text-align: center;"
|
||
:class="{
|
||
'text-green-600': check.status === 'pass',
|
||
'text-yellow-600': check.status === 'warning',
|
||
'text-red-600': check.status === 'fail',
|
||
'text-blue-600': check.status === 'info'
|
||
}">
|
||
{{ getHealthStatusIcon(check.status) }}
|
||
</span>
|
||
<div style="flex: 1;">
|
||
<div style="font-weight: 500; margin-bottom: 4px;">{{ check.name }}</div>
|
||
<div style="font-size: 13px; color: var(--text-secondary);">{{ check.message }}</div>
|
||
<div v-if="check.suggestion" style="font-size: 12px; color: #e67e22; margin-top: 4px;">
|
||
<i class="fas fa-lightbulb"></i> {{ check.suggestion }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 加载中 -->
|
||
<div v-else-if="healthCheck.loading" style="text-align: center; padding: 40px; color: var(--text-muted);">
|
||
<i class="fas fa-spinner fa-spin" style="font-size: 48px; margin-bottom: 15px;"></i>
|
||
<p>正在检测系统健康状态...</p>
|
||
</div>
|
||
|
||
<!-- 未检测提示 -->
|
||
<div v-else style="text-align: center; padding: 40px; color: var(--text-muted);">
|
||
<i class="fas fa-stethoscope" style="font-size: 48px; margin-bottom: 15px;"></i>
|
||
<p>点击"刷新检测"按钮开始系统健康检测</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 下载预扣运维面板 -->
|
||
<div class="card" style="margin-bottom: 30px;">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 14px;">
|
||
<h3 style="margin: 0;"><i class="fas fa-gauge-high"></i> 下载预扣运维面板</h3>
|
||
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
|
||
<button class="btn btn-secondary" @click="cleanupReservations" :disabled="reservationMonitor.cleaning">
|
||
<i :class="reservationMonitor.cleaning ? 'fas fa-spinner fa-spin' : 'fas fa-broom'"></i>
|
||
{{ reservationMonitor.cleaning ? '清理中...' : '清理历史' }}
|
||
</button>
|
||
<button class="btn btn-primary" @click="loadDownloadReservationMonitor(reservationMonitor.page)" :disabled="reservationMonitor.loading">
|
||
<i :class="reservationMonitor.loading ? 'fas fa-spinner fa-spin' : 'fas fa-sync'"></i>
|
||
刷新
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 12px;">
|
||
<select class="form-input" v-model="reservationMonitor.filters.status" @change="loadDownloadReservationMonitor(1)" style="width: 140px;">
|
||
<option value="">全部状态</option>
|
||
<option value="pending">待确认</option>
|
||
<option value="confirmed">已确认</option>
|
||
<option value="expired">已过期</option>
|
||
<option value="cancelled">已取消</option>
|
||
</select>
|
||
<input class="form-input" v-model="reservationMonitor.filters.userId" @keyup.enter="loadDownloadReservationMonitor(1)" placeholder="用户ID" style="width: 120px;">
|
||
<input class="form-input" v-model="reservationMonitor.filters.keyword" @input="triggerReservationKeywordSearch" placeholder="用户名 / 对象Key / 来源" style="flex: 1; min-width: 220px;">
|
||
</div>
|
||
|
||
<div style="display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; font-size: 12px;">
|
||
<span class="share-chip info">总数 {{ reservationMonitor.summary?.total || 0 }}</span>
|
||
<span class="share-chip warn">待确认 {{ reservationMonitor.summary?.pending || 0 }}</span>
|
||
<span class="share-chip success">已确认 {{ reservationMonitor.summary?.confirmed || 0 }}</span>
|
||
<span class="share-chip danger">已过期 {{ reservationMonitor.summary?.expired || 0 }}</span>
|
||
<span class="share-chip info">待确认剩余 {{ formatBytes(reservationMonitor.summary?.pending_remaining_bytes || 0) }}</span>
|
||
<span class="share-chip info">即将过期 {{ reservationMonitor.summary?.pending_expiring_soon || 0 }}</span>
|
||
</div>
|
||
|
||
<div v-if="reservationMonitor.loading" style="padding: 24px; text-align: center; color: var(--text-muted);">
|
||
<i class="fas fa-spinner fa-spin"></i> 正在加载预扣数据...
|
||
</div>
|
||
|
||
<div v-else-if="reservationMonitor.rows.length === 0" style="padding: 24px; text-align: center; color: var(--text-muted);">
|
||
暂无预扣记录
|
||
</div>
|
||
|
||
<div v-else style="overflow-x: auto;">
|
||
<table class="share-list-table" style="min-width: 920px;">
|
||
<thead>
|
||
<tr>
|
||
<th style="padding: 8px;">ID</th>
|
||
<th style="padding: 8px;">用户</th>
|
||
<th style="padding: 8px;">来源</th>
|
||
<th style="padding: 8px;">已预扣</th>
|
||
<th style="padding: 8px;">剩余</th>
|
||
<th style="padding: 8px;">状态</th>
|
||
<th style="padding: 8px;">到期</th>
|
||
<th style="padding: 8px;">对象</th>
|
||
<th style="padding: 8px;">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="row in reservationMonitor.rows" :key="row.id">
|
||
<td style="padding: 8px; text-align: center;">{{ row.id }}</td>
|
||
<td style="padding: 8px; text-align: center;">
|
||
<div>#{{ row.user_id }}</div>
|
||
<div style="font-size: 12px; color: var(--text-secondary);">{{ row.username || '-' }}</div>
|
||
</td>
|
||
<td style="padding: 8px; text-align: center;">{{ row.source || '-' }}</td>
|
||
<td style="padding: 8px; text-align: center;">{{ formatBytes(row.reserved_bytes || 0) }}</td>
|
||
<td style="padding: 8px; text-align: center;">{{ formatBytes(row.remaining_bytes || 0) }}</td>
|
||
<td style="padding: 8px; text-align: center; font-weight: 600;" :style="{ color: getReservationStatusColor(row.status) }">
|
||
{{ getReservationStatusText(row.status) }}
|
||
</td>
|
||
<td style="padding: 8px; text-align: center; white-space: nowrap;">{{ formatDate(row.expires_at) }}</td>
|
||
<td style="padding: 8px;">
|
||
<span style="display: inline-block; max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" :title="row.object_key || '-'">
|
||
{{ row.object_key || '-' }}
|
||
</span>
|
||
</td>
|
||
<td style="padding: 8px; text-align: center;">
|
||
<button v-if="row.status === 'pending'" class="btn btn-secondary" @click="cancelReservation(row)" style="padding: 4px 10px; font-size: 12px;">
|
||
<i class="fas fa-ban"></i> 释放
|
||
</button>
|
||
<span v-else style="color: var(--text-muted); font-size: 12px;">-</span>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div v-if="reservationMonitor.totalPages > 1" style="margin-top: 12px; display: flex; justify-content: center; gap: 8px;">
|
||
<button class="btn btn-secondary" @click="changeReservationPage(reservationMonitor.page - 1)" :disabled="reservationMonitor.page <= 1">
|
||
上一页
|
||
</button>
|
||
<span style="display: flex; align-items: center; color: var(--text-secondary);">
|
||
{{ reservationMonitor.page }} / {{ reservationMonitor.totalPages }}
|
||
</span>
|
||
<button class="btn btn-secondary" @click="changeReservationPage(reservationMonitor.page + 1)" :disabled="reservationMonitor.page >= reservationMonitor.totalPages">
|
||
下一页
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 系统日志 -->
|
||
<div class="card" style="margin-bottom: 30px;">
|
||
<div class="admin-log-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||
<h3 style="margin: 0;">
|
||
<i class="fas fa-clipboard-list"></i> 系统日志
|
||
</h3>
|
||
<div class="admin-log-actions" style="display: flex; gap: 8px; align-items: center;">
|
||
<button class="btn btn-secondary" @click="cleanupLogs" title="清理90天前的日志" style="padding: 6px 12px; font-size: 12px;">
|
||
<i class="fas fa-trash"></i> 清理
|
||
</button>
|
||
<button class="btn btn-primary" @click="loadSystemLogs(1)" :disabled="systemLogs.loading" style="padding: 6px 12px; font-size: 12px;">
|
||
<i class="fas" :class="systemLogs.loading ? 'fa-spinner fa-spin' : 'fa-sync'"></i>
|
||
刷新
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 筛选器 -->
|
||
<div class="admin-log-filters" style="display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 15px; padding: 15px; background: rgba(255,255,255,0.03); border-radius: 8px;">
|
||
<div class="admin-log-filter-item" style="display: flex; align-items: center; gap: 8px;">
|
||
<label style="font-size: 13px; color: var(--text-secondary);">级别:</label>
|
||
<select v-model="systemLogs.filters.level" @change="filterLogs" style="padding: 6px 10px; border: 1px solid #ddd; border-radius: 4px;">
|
||
<option value="">全部</option>
|
||
<option value="info">信息</option>
|
||
<option value="warn">警告</option>
|
||
<option value="error">错误</option>
|
||
</select>
|
||
</div>
|
||
<div class="admin-log-filter-item" style="display: flex; align-items: center; gap: 8px;">
|
||
<label style="font-size: 13px; color: var(--text-secondary);">分类:</label>
|
||
<select v-model="systemLogs.filters.category" @change="filterLogs" style="padding: 6px 10px; border: 1px solid #ddd; border-radius: 4px;">
|
||
<option value="">全部</option>
|
||
<option value="auth">认证</option>
|
||
<option value="user">用户</option>
|
||
<option value="file">文件</option>
|
||
<option value="share">分享</option>
|
||
<option value="system">系统</option>
|
||
<option value="security">安全</option>
|
||
</select>
|
||
</div>
|
||
<div class="admin-log-filter-item admin-log-filter-search" style="display: flex; align-items: center; gap: 8px; flex: 1; min-width: 200px;">
|
||
<label style="font-size: 13px; color: var(--text-secondary);">搜索:</label>
|
||
<input type="text" v-model="systemLogs.filters.keyword" @keyup.enter="filterLogs"
|
||
placeholder="搜索日志内容..." style="flex: 1; padding: 6px 10px; border: 1px solid #ddd; border-radius: 4px;">
|
||
</div>
|
||
<button class="btn btn-secondary" @click="clearLogFilters" style="padding: 6px 12px;">
|
||
<i class="fas fa-times"></i> 清除筛选
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 日志统计 -->
|
||
<div v-if="systemLogs.total > 0" style="margin-bottom: 15px; font-size: 13px; color: var(--text-secondary);">
|
||
共 {{ systemLogs.total }} 条日志,第 {{ systemLogs.page }}/{{ systemLogs.totalPages }} 页
|
||
</div>
|
||
|
||
<!-- 日志列表 -->
|
||
<div v-if="systemLogs.logs.length > 0" class="admin-log-list" style="max-height: 500px; overflow-y: auto;">
|
||
<div v-for="log in systemLogs.logs" :key="log.id"
|
||
class="admin-log-row" style="display: flex; gap: 12px; padding: 12px; border-bottom: 1px solid #eee; align-items: flex-start;">
|
||
<!-- 时间 -->
|
||
<div class="admin-log-time" style="width: 140px; flex-shrink: 0; font-size: 12px; color: var(--text-muted);">
|
||
{{ formatLogTime(log.created_at) }}
|
||
</div>
|
||
<!-- 级别标签 -->
|
||
<div class="admin-log-level" style="width: 50px; flex-shrink: 0;">
|
||
<span :style="getLogLevelColor(log.level)" style="padding: 2px 8px; border-radius: 4px; font-size: 11px;">
|
||
{{ getLogLevelText(log.level) }}
|
||
</span>
|
||
</div>
|
||
<!-- 分类图标 -->
|
||
<div class="admin-log-category" style="width: 70px; flex-shrink: 0; display: flex; align-items: center; gap: 4px; font-size: 12px; color: var(--text-secondary);">
|
||
<i class="fas" :class="getLogCategoryIcon(log.category)"></i>
|
||
{{ getLogCategoryText(log.category) }}
|
||
</div>
|
||
<!-- 内容 -->
|
||
<div class="admin-log-content" style="flex: 1; min-width: 0;">
|
||
<div style="font-weight: 500; margin-bottom: 4px;">{{ log.action }}</div>
|
||
<div style="font-size: 13px; color: var(--text-secondary);">{{ log.message }}</div>
|
||
<div v-if="log.username || log.ip_address" style="font-size: 11px; color: var(--text-muted); margin-top: 4px;">
|
||
<span v-if="log.username"><i class="fas fa-user"></i> {{ log.username }}</span>
|
||
<span v-if="log.ip_address" style="margin-left: 10px;"><i class="fas fa-globe"></i> {{ log.ip_address }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 加载中 -->
|
||
<div v-else-if="systemLogs.loading" style="text-align: center; padding: 40px; color: var(--text-muted);">
|
||
<i class="fas fa-spinner fa-spin" style="font-size: 24px;"></i>
|
||
<p>加载中...</p>
|
||
</div>
|
||
|
||
<!-- 空状态 -->
|
||
<div v-else style="text-align: center; padding: 40px; color: var(--text-muted);">
|
||
<i class="fas fa-clipboard" style="font-size: 48px; margin-bottom: 15px;"></i>
|
||
<p>暂无日志记录</p>
|
||
</div>
|
||
|
||
<!-- 分页 -->
|
||
<div v-if="systemLogs.totalPages > 1" class="admin-log-pager" style="display: flex; justify-content: center; gap: 8px; margin-top: 15px;">
|
||
<button class="btn btn-secondary" @click="loadSystemLogs(systemLogs.page - 1)" :disabled="systemLogs.page <= 1" style="padding: 6px 12px;">
|
||
<i class="fas fa-chevron-left"></i> 上一页
|
||
</button>
|
||
<span style="display: flex; align-items: center; padding: 0 15px; color: var(--text-secondary);">
|
||
{{ systemLogs.page }} / {{ systemLogs.totalPages }}
|
||
</span>
|
||
<button class="btn btn-secondary" @click="loadSystemLogs(systemLogs.page + 1)" :disabled="systemLogs.page >= systemLogs.totalPages" style="padding: 6px 12px;">
|
||
下一页 <i class="fas fa-chevron-right"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div><!-- 监控标签页结束 -->
|
||
|
||
<!-- ========== 用户标签页 ========== -->
|
||
<div v-show="adminTab === 'users'">
|
||
<div class="card">
|
||
<div class="admin-users-header">
|
||
<h3 style="margin: 0;">用户管理</h3>
|
||
<button class="btn btn-secondary" @click="loadUsers" :disabled="adminUsersLoading" style="min-width: 92px;">
|
||
<i :class="adminUsersLoading ? 'fas fa-spinner fa-spin' : 'fas fa-rotate'"></i> 刷新
|
||
</button>
|
||
</div>
|
||
|
||
<div class="admin-users-toolbar">
|
||
<div class="admin-users-filter admin-users-filter-search">
|
||
<label>搜索</label>
|
||
<input type="text" v-model.trim="adminUserFilters.keyword" @input="triggerAdminUsersKeywordSearch" placeholder="ID / 用户名 / 邮箱">
|
||
</div>
|
||
<div class="admin-users-filter">
|
||
<label>角色</label>
|
||
<select v-model="adminUserFilters.role" @change="handleAdminUsersFilterChange">
|
||
<option value="all">全部</option>
|
||
<option value="admin">管理员</option>
|
||
<option value="user">普通用户</option>
|
||
</select>
|
||
</div>
|
||
<div class="admin-users-filter">
|
||
<label>状态</label>
|
||
<select v-model="adminUserFilters.status" @change="handleAdminUsersFilterChange">
|
||
<option value="all">全部</option>
|
||
<option value="active">正常</option>
|
||
<option value="banned">已封禁</option>
|
||
<option value="unverified">未激活</option>
|
||
<option value="download_blocked">下载受限</option>
|
||
</select>
|
||
</div>
|
||
<div class="admin-users-filter">
|
||
<label>存储</label>
|
||
<select v-model="adminUserFilters.storage" @change="handleAdminUsersFilterChange">
|
||
<option value="all">全部</option>
|
||
<option value="local">当前本地</option>
|
||
<option value="oss">当前OSS</option>
|
||
<option value="local_only">仅本地</option>
|
||
<option value="oss_only">仅OSS</option>
|
||
<option value="user_choice">用户选择</option>
|
||
</select>
|
||
</div>
|
||
<div class="admin-users-filter">
|
||
<label>排序</label>
|
||
<select v-model="adminUserFilters.sort" @change="handleAdminUsersFilterChange">
|
||
<option value="created_desc">注册时间(新到旧)</option>
|
||
<option value="created_asc">注册时间(旧到新)</option>
|
||
<option value="username_asc">用户名(A-Z)</option>
|
||
<option value="username_desc">用户名(Z-A)</option>
|
||
<option value="storage_usage_desc">存储使用率(高到低)</option>
|
||
<option value="download_usage_desc">下载流量使用率(高到低)</option>
|
||
</select>
|
||
</div>
|
||
<div class="admin-users-filter admin-users-filter-page-size">
|
||
<label>每页</label>
|
||
<select v-model.number="adminUsersPageSize" @change="handleAdminUsersPageSizeChange">
|
||
<option :value="20">20</option>
|
||
<option :value="50">50</option>
|
||
<option :value="100">100</option>
|
||
</select>
|
||
</div>
|
||
<div class="admin-users-filter admin-users-filter-reset">
|
||
<button class="btn btn-secondary" @click="resetAdminUserFilters">
|
||
<i class="fas fa-filter-circle-xmark"></i> 重置
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="admin-users-stats">
|
||
<span class="admin-users-stat-chip">总用户 {{ adminUsersGlobalCount }}</span>
|
||
<span class="admin-users-stat-chip">筛选后 {{ adminUsersFilteredCount }}</span>
|
||
<span class="admin-users-stat-chip">正常 {{ adminUserStats.active }}</span>
|
||
<span class="admin-users-stat-chip">封禁 {{ adminUserStats.banned }}</span>
|
||
<span class="admin-users-stat-chip">未激活 {{ adminUserStats.unverified }}</span>
|
||
<span class="admin-users-stat-chip">下载受限 {{ adminUserStats.download_blocked }}</span>
|
||
</div>
|
||
|
||
<div v-if="adminUsersLoading" class="admin-users-empty-state">
|
||
<i class="fas fa-spinner fa-spin"></i>
|
||
<span>正在加载用户数据...</span>
|
||
</div>
|
||
|
||
<template v-else>
|
||
<div v-if="adminUsersFilteredCount > 0" class="admin-users-table-wrap" style="overflow-x: auto;">
|
||
<table class="admin-users-table" style="width: 100%; border-collapse: collapse; table-layout: fixed; min-width: 960px;">
|
||
<thead>
|
||
<tr style="background: rgba(255,255,255,0.05);">
|
||
<th style="padding: 10px; text-align: left; width: 5%;">ID</th>
|
||
<th style="padding: 10px; text-align: left; width: 11%;">用户名</th>
|
||
<th style="padding: 10px; text-align: center; width: 9%;">角色</th>
|
||
<th style="padding: 10px; text-align: left; width: 15%;">邮箱</th>
|
||
<th style="padding: 10px; text-align: center; width: 9%;">存储权限</th>
|
||
<th style="padding: 10px; text-align: center; width: 8%;">当前存储</th>
|
||
<th style="padding: 10px; text-align: center; width: 12%;">存储配额</th>
|
||
<th style="padding: 10px; text-align: center; width: 12%;">下载流量</th>
|
||
<th style="padding: 10px; text-align: center; width: 8%;">状态</th>
|
||
<th style="padding: 10px; text-align: center; width: 11%;">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="u in adminUsers" :key="u.id" style="border-bottom: 1px solid #eee;">
|
||
<td style="padding: 10px;">{{ u.id }}</td>
|
||
<td style="padding: 10px; overflow: hidden;">
|
||
<div style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" :title="u.username" v-html="getHighlightedText(u.username, adminUserFilters.keyword)"></div>
|
||
</td>
|
||
<td style="padding: 10px; text-align: center; font-size: 12px;">
|
||
<span v-if="u.is_admin" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 3px 10px; border-radius: 4px; white-space: nowrap;">
|
||
<i class="fas fa-crown"></i> 管理员
|
||
</span>
|
||
<span v-else style="background: rgba(255,255,255,0.1); color: var(--text-secondary); padding: 3px 10px; border-radius: 4px; white-space: nowrap;">
|
||
<i class="fas fa-user"></i> 用户
|
||
</span>
|
||
</td>
|
||
<td style="padding: 10px; font-size: 12px; color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" :title="u.email || '-'" v-html="getHighlightedText(u.email || '-', adminUserFilters.keyword)"></td>
|
||
<td style="padding: 10px; text-align: center; font-size: 12px;">
|
||
<span v-if="u.storage_permission === 'local_only'" style="background: #667eea; color: white; padding: 3px 8px; border-radius: 4px;">仅本地</span>
|
||
<span v-else-if="u.storage_permission === 'oss_only'" style="background: #6c757d; color: white; padding: 3px 8px; border-radius: 4px;">仅OSS</span>
|
||
<span v-else style="background: #22c55e; color: white; padding: 3px 8px; border-radius: 4px;">用户选择</span>
|
||
</td>
|
||
<td style="padding: 10px; text-align: center; font-size: 12px;">
|
||
<span v-if="u.current_storage_type === 'local'" style="color: #667eea;">
|
||
<i class="fas fa-hard-drive"></i> 本地
|
||
</span>
|
||
<span v-else style="color: #6c757d;">
|
||
<i class="fas fa-cloud"></i> OSS
|
||
</span>
|
||
</td>
|
||
<td style="padding: 10px; text-align: center; font-size: 12px;">
|
||
<div v-if="u.current_storage_type === 'local'">
|
||
<div>{{ formatBytes(u.local_storage_used) }} / {{ formatBytes(u.local_storage_quota) }}</div>
|
||
<div style="font-size: 11px; color: var(--text-muted);">
|
||
{{ getAdminUserQuotaPercentage(u) }}%
|
||
</div>
|
||
</div>
|
||
<div v-else>
|
||
<div>{{ formatBytes(u.storage_used || 0) }} / {{ formatBytes(u.oss_storage_quota) }}</div>
|
||
<div style="font-size: 11px; color: var(--text-muted);">
|
||
{{ getAdminUserOssQuotaPercentage(u) }}%
|
||
</div>
|
||
</div>
|
||
</td>
|
||
<td style="padding: 10px; text-align: center; font-size: 12px;">
|
||
<div v-if="u.download_traffic_quota >= 0">
|
||
<div>{{ formatBytes(u.download_traffic_used || 0) }} / {{ formatBytes(u.download_traffic_quota) }}</div>
|
||
<div style="font-size: 11px; color: var(--text-muted);">
|
||
{{ u.download_traffic_quota === 0 ? '已禁用下载' : (getAdminUserDownloadQuotaPercentage(u) + '%') }}
|
||
</div>
|
||
</div>
|
||
<div v-else>
|
||
<div>{{ formatBytes(u.download_traffic_used || 0) }} / 不限</div>
|
||
<div style="font-size: 11px; color: var(--text-muted);">
|
||
不限
|
||
</div>
|
||
</div>
|
||
<div style="font-size: 11px; color: var(--text-muted); margin-top: 2px;">
|
||
{{ getDownloadResetCycleText(u.download_traffic_reset_cycle || 'none') }}
|
||
</div>
|
||
<div v-if="u.download_traffic_quota_expires_at" style="font-size: 11px; color: #f59e0b; margin-top: 2px;">
|
||
到期: {{ formatDate(u.download_traffic_quota_expires_at) }}
|
||
</div>
|
||
</td>
|
||
<td style="padding: 10px; text-align: center;">
|
||
<span class="admin-user-status-tag" :class="'status-' + getAdminUserStatusTag(u)">
|
||
{{ getAdminUserStatusLabel(u) }}
|
||
</span>
|
||
</td>
|
||
<td style="padding: 10px; text-align: center;">
|
||
<div class="admin-user-actions" style="display: flex; gap: 3px; justify-content: center; flex-wrap: wrap;">
|
||
<button class="btn admin-user-action-btn" style="background: #667eea; color: white; font-size: 11px; padding: 5px 10px;" @click="openEditStorageModal(u)" title="存储设置">
|
||
<i class="fas fa-database"></i> 存储
|
||
</button>
|
||
<button v-if="!u.is_banned" class="btn admin-user-action-btn" style="background: #f59e0b; color: #000; font-size: 11px; padding: 5px 10px;" @click="banUser(u.id, true)">
|
||
<i class="fas fa-ban"></i> 封禁
|
||
</button>
|
||
<button v-else class="btn admin-user-action-btn" style="background: #22c55e; color: white; font-size: 11px; padding: 5px 10px;" @click="banUser(u.id, false)">
|
||
<i class="fas fa-check"></i> 解封
|
||
</button>
|
||
<button v-if="u.oss_config_source !== 'none'" class="btn admin-user-action-btn" style="background: #3b82f6; color: white; font-size: 11px; padding: 5px 10px;" @click="openFileInspection(u)">
|
||
<i class="fas fa-folder-open"></i> 文件
|
||
</button>
|
||
<button class="btn admin-user-action-btn" style="background: #ef4444; color: white; font-size: 11px; padding: 5px 10px;" @click="deleteUser(u.id)">
|
||
<i class="fas fa-trash"></i>
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="admin-users-pagination" v-if="adminUsersFilteredCount > 0">
|
||
<div class="admin-users-pagination-info">
|
||
显示 {{ adminUsersPageStart }}-{{ adminUsersPageEnd }} 条,共 {{ adminUsersFilteredCount }} 条(总 {{ adminUsersGlobalCount }} 条)
|
||
</div>
|
||
<div class="admin-users-pagination-actions">
|
||
<button class="btn btn-secondary" @click="setAdminUsersPage(1)" :disabled="adminUsersCurrentPage <= 1">
|
||
首页
|
||
</button>
|
||
<button class="btn btn-secondary" @click="setAdminUsersPage(adminUsersCurrentPage - 1)" :disabled="adminUsersCurrentPage <= 1">
|
||
<i class="fas fa-chevron-left"></i> 上一页
|
||
</button>
|
||
<span class="admin-users-page-indicator">{{ adminUsersCurrentPage }} / {{ adminUsersTotalPages }}</span>
|
||
<button class="btn btn-secondary" @click="setAdminUsersPage(adminUsersCurrentPage + 1)" :disabled="adminUsersCurrentPage >= adminUsersTotalPages">
|
||
下一页 <i class="fas fa-chevron-right"></i>
|
||
</button>
|
||
<button class="btn btn-secondary" @click="setAdminUsersPage(adminUsersTotalPages)" :disabled="adminUsersCurrentPage >= adminUsersTotalPages">
|
||
末页
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else class="admin-users-empty-state">
|
||
<i class="fas fa-users-slash"></i>
|
||
<span v-if="adminUsersGlobalCount > 0">没有符合当前筛选条件的用户</span>
|
||
<span v-else>暂无用户数据</span>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div><!-- 用户标签页结束 -->
|
||
</div><!-- 管理员视图结束 -->
|
||
|
||
<!-- 忘记密码模态框 -->
|
||
<div v-if="showForgotPasswordModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showForgotPasswordModal', $event)">
|
||
<div class="modal-content" @click.stop @focusin="!forgotPasswordCaptchaUrl && refreshForgotPasswordCaptcha()">
|
||
<h3 style="margin-bottom: 20px;">忘记密码 - 邮箱重置</h3>
|
||
<p style="color: var(--text-secondary); margin-bottom: 15px; font-size: 14px;">
|
||
请输入注册邮箱,我们会发送重置链接到您的邮箱
|
||
</p>
|
||
<div class="form-group">
|
||
<label class="form-label">邮箱</label>
|
||
<input type="email" class="form-input" v-model="forgotPasswordForm.email" placeholder="请输入注册邮箱" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">验证码</label>
|
||
<div style="display: flex; gap: 10px; align-items: center;">
|
||
<input type="text" class="form-input" v-model="forgotPasswordForm.captcha" placeholder="请输入验证码" required style="flex: 1;">
|
||
<img v-if="forgotPasswordCaptchaUrl" :src="forgotPasswordCaptchaUrl" @click="refreshForgotPasswordCaptcha" style="height: 44px; cursor: pointer; border-radius: 4px;" title="点击刷新验证码">
|
||
</div>
|
||
</div>
|
||
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
||
<button class="btn btn-primary" @click="requestPasswordReset" :disabled="passwordResetting" style="flex: 1;">
|
||
<i :class="passwordResetting ? 'fas fa-spinner fa-spin' : 'fas fa-paper-plane'"></i> {{ passwordResetting ? '发送中...' : '发送重置邮件' }}
|
||
</button>
|
||
<button class="btn btn-secondary" @click="showForgotPasswordModal = false; forgotPasswordForm = {email: '', captcha: ''}; forgotPasswordCaptchaUrl = ''" :disabled="passwordResetting" style="flex: 1;">
|
||
<i class="fas fa-times"></i> 取消
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 邮件重置密码模态框 -->
|
||
<div v-if="showResetPasswordModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showResetPasswordModal', $event)">
|
||
<div class="modal-content" @click.stop>
|
||
<h3 style="margin-bottom: 20px;">设置新密码</h3>
|
||
<p style="color: var(--text-secondary); margin-bottom: 15px; font-size: 14px;">
|
||
重置链接已验证,请输入新密码
|
||
</p>
|
||
<div class="form-group">
|
||
<label class="form-label">新密码 (至少6字符)</label>
|
||
<input type="password" class="form-input" v-model="resetPasswordForm.new_password" placeholder="输入新密码" minlength="6" required>
|
||
</div>
|
||
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
||
<button class="btn btn-primary" @click="submitResetPassword" :disabled="passwordResetting" style="flex: 1;">
|
||
<i :class="passwordResetting ? 'fas fa-spinner fa-spin' : 'fas fa-unlock'"></i> {{ passwordResetting ? '重置中...' : '重置密码' }}
|
||
</button>
|
||
<button class="btn btn-secondary" @click="showResetPasswordModal = false; resetPasswordForm = {token: '', new_password: ''}" :disabled="passwordResetting" style="flex: 1;">
|
||
<i class="fas fa-times"></i> 取消
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 文件审查模态框 -->
|
||
<div v-if="showFileInspectionModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showFileInspectionModal', $event)">
|
||
<div class="modal-content" @click.stop style="max-width: 900px; max-height: 80vh; overflow-y: auto;">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||
<h3 style="margin: 0;">
|
||
<i class="fas fa-eye"></i> 文件审查 - {{ inspectionUser?.username }}
|
||
<span style="background: #f59e0b; color: #000; padding: 4px 8px; border-radius: 4px; font-size: 12px; margin-left: 10px;">只读模式</span>
|
||
</h3>
|
||
<button class="btn-icon" @click="showFileInspectionModal = false" style="font-size: 20px;">
|
||
<i class="fas fa-times"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 路径导航和视图切换 -->
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; gap: 15px;">
|
||
<div style="background: rgba(255,255,255,0.03); padding: 10px; border-radius: 6px; display: flex; align-items: center; gap: 10px; flex: 1;">
|
||
<button class="btn-icon" @click="navigateInspectionToRoot" title="返回根目录">
|
||
<i class="fas fa-home"></i>
|
||
</button>
|
||
<button class="btn-icon" @click="navigateInspectionUp" :disabled="inspectionPath === '/'" title="上一级">
|
||
<i class="fas fa-arrow-up"></i>
|
||
</button>
|
||
<span style="flex: 1; color: var(--text-secondary); font-family: monospace;">{{ inspectionPath }}</span>
|
||
</div>
|
||
<div style="display: flex; gap: 10px;">
|
||
<button class="btn" :class="inspectionViewMode === 'grid' ? 'btn-primary' : 'btn-secondary'" @click="inspectionViewMode = 'grid'">
|
||
<i class="fas fa-th-large"></i> 大图标
|
||
</button>
|
||
<button class="btn" :class="inspectionViewMode === 'list' ? 'btn-primary' : 'btn-secondary'" @click="inspectionViewMode = 'list'">
|
||
<i class="fas fa-list"></i> 列表
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 加载中 -->
|
||
<div v-if="inspectionLoading" class="loading">
|
||
<div class="spinner"></div>
|
||
<p>加载中...</p>
|
||
</div>
|
||
|
||
<!-- 文件列表 -->
|
||
<div v-else>
|
||
<p v-if="inspectionFiles.length === 0" class="empty-hint">文件夹是空的</p>
|
||
|
||
<!-- 大图标视图 -->
|
||
<div v-else-if="inspectionViewMode === 'grid'" class="file-grid">
|
||
<div v-for="file in inspectionFiles" :key="file.name" class="file-grid-item" @dblclick="handleInspectionFileClick(file)">
|
||
<div class="file-icon">
|
||
<i v-if="file.isDirectory" class="fas fa-folder" style="font-size: 64px; color: #FFC107;"></i>
|
||
<i v-else-if="file.name.match(/\.(jpg|jpeg|png|gif|bmp|svg)$/i)" class="fas fa-file-image" style="font-size: 64px; color: #4CAF50;"></i>
|
||
<i v-else-if="file.name.match(/\.(mp4|avi|mov|wmv|flv|mkv)$/i)" class="fas fa-file-video" style="font-size: 64px; color: #9C27B0;"></i>
|
||
<i v-else-if="file.name.match(/\.(mp3|wav|flac|aac|ogg)$/i)" class="fas fa-file-audio" style="font-size: 64px; color: #FF5722;"></i>
|
||
<i v-else-if="file.name.match(/\.(pdf)$/i)" class="fas fa-file-pdf" style="font-size: 64px; color: #F44336;"></i>
|
||
<i v-else-if="file.name.match(/\.(doc|docx)$/i)" class="fas fa-file-word" style="font-size: 64px; color: #2196F3;"></i>
|
||
<i v-else-if="file.name.match(/\.(xls|xlsx)$/i)" class="fas fa-file-excel" style="font-size: 64px; color: #4CAF50;"></i>
|
||
<i v-else-if="file.name.match(/\.(zip|rar|7z|tar|gz)$/i)" class="fas fa-file-archive" style="font-size: 64px; color: #795548;"></i>
|
||
<i v-else class="fas fa-file" style="font-size: 64px; color: #9E9E9E;"></i>
|
||
</div>
|
||
<div class="file-name" :title="file.name">{{ file.name }}</div>
|
||
<div class="file-size">{{ file.isDirectory ? '文件夹' : file.sizeFormatted }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 列表视图 -->
|
||
<div v-else class="file-list">
|
||
<table style="width: 100%; border-collapse: collapse; table-layout: fixed;">
|
||
<thead>
|
||
<tr style="background: rgba(255,255,255,0.05);">
|
||
<th style="padding: 12px; text-align: left; width: 50%;">文件名</th>
|
||
<th style="padding: 12px; text-align: left; width: 25%;">大小</th>
|
||
<th style="padding: 12px; text-align: left; width: 25%;">修改时间</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="file in inspectionFiles" :key="file.name"
|
||
style="border-bottom: 1px solid #eee; cursor: pointer;"
|
||
@dblclick="handleInspectionFileClick(file)"
|
||
@mouseover="$event.currentTarget.style.background='#f9f9f9'"
|
||
@mouseout="$event.currentTarget.style.background='white'">
|
||
<td style="padding: 10px;">
|
||
<div style="display: flex; align-items: center; gap: 10px; overflow: hidden;">
|
||
<i v-if="file.isDirectory" class="fas fa-folder" style="font-size: 20px; color: #FFC107; flex-shrink: 0;"></i>
|
||
<i v-else-if="file.name.match(/\.(jpg|jpeg|png|gif|bmp|svg)$/i)" class="fas fa-file-image" style="font-size: 20px; color: #4CAF50;"></i>
|
||
<i v-else-if="file.name.match(/\.(mp4|avi|mov|wmv|flv|mkv)$/i)" class="fas fa-file-video" style="font-size: 20px; color: #9C27B0;"></i>
|
||
<i v-else-if="file.name.match(/\.(mp3|wav|flac|aac|ogg)$/i)" class="fas fa-file-audio" style="font-size: 20px; color: #FF5722; flex-shrink: 0;"></i>
|
||
<i v-else-if="file.name.match(/\.(pdf)$/i)" class="fas fa-file-pdf" style="font-size: 20px; color: #F44336; flex-shrink: 0;"></i>
|
||
<i v-else-if="file.name.match(/\.(doc|docx)$/i)" class="fas fa-file-word" style="font-size: 20px; color: #2196F3; flex-shrink: 0;"></i>
|
||
<i v-else-if="file.name.match(/\.(xls|xlsx)$/i)" class="fas fa-file-excel" style="font-size: 20px; color: #4CAF50; flex-shrink: 0;"></i>
|
||
<i v-else-if="file.name.match(/\.(zip|rar|7z|tar|gz)$/i)" class="fas fa-file-archive" style="font-size: 20px; color: #795548; flex-shrink: 0;"></i>
|
||
<i v-else class="fas fa-file" style="font-size: 20px; color: #9E9E9E; flex-shrink: 0;"></i>
|
||
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0;" :title="file.name">{{ file.name }}</span>
|
||
</div>
|
||
</td>
|
||
<td style="padding: 10px; color: var(--text-secondary);">{{ file.isDirectory ? '-' : file.sizeFormatted }}</td>
|
||
<td style="padding: 10px; color: var(--text-secondary);">{{ formatDate(file.modifiedAt) }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="margin-top: 15px; padding: 10px; background: rgba(245, 158, 11, 0.15); border-radius: 6px; color: #fbbf24;">
|
||
<i class="fas fa-info-circle"></i> 只读模式:双击文件夹可进入,无法下载、修改或删除文件
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Toast 通知容器 -->
|
||
<div class="toast-stack">
|
||
<div v-for="toast in toasts" :key="toast.id"
|
||
class="app-toast"
|
||
:class="`toast-${toast.type}`"
|
||
:style="{
|
||
animation: toast.hiding ? 'slideOut 0.5s ease-out forwards' : 'slideIn 0.5s ease-out',
|
||
opacity: toast.hiding ? 0 : 1,
|
||
transform: toast.hiding ? 'translateX(400px)' : 'translateX(0)'
|
||
}">
|
||
<i :class="toast.icon" class="toast-icon"></i>
|
||
<div class="toast-content">
|
||
<div class="toast-title">{{ toast.title }}</div>
|
||
<div class="toast-message">{{ toast.message }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 上传进度条 -->
|
||
<div v-if="uploadProgress > 0 && uploadProgress < 100"
|
||
class="upload-progress-panel">
|
||
<div class="upload-progress-header">
|
||
<i class="fas fa-cloud-upload-alt upload-progress-icon"></i>
|
||
<div class="upload-progress-meta">
|
||
<div class="upload-progress-title">正在上传文件</div>
|
||
<div class="upload-progress-name">{{ uploadingFileName }}</div>
|
||
<div v-if="totalBytes > 0" class="upload-progress-size">{{ formatFileSize(uploadedBytes) }} / {{ formatFileSize(totalBytes) }}</div>
|
||
</div>
|
||
<div class="upload-progress-percent">{{ uploadProgress }}%</div>
|
||
</div>
|
||
<div class="upload-progress-bar">
|
||
<div class="upload-progress-value" :style="{ width: uploadProgress + '%' }"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右键菜单 -->
|
||
<div v-if="showContextMenu" class="context-menu" :style="{
|
||
left: contextMenuX + 'px',
|
||
top: contextMenuY + 'px'
|
||
}" @click.stop>
|
||
<div v-if="isPreviewable(contextMenuFile)" class="context-menu-item" @click="contextMenuAction('preview')">
|
||
<i class="fas fa-eye"></i> 预览
|
||
</div>
|
||
<!-- 文件夹不显示下载和分享按钮 -->
|
||
<div v-if="!contextMenuFile.isDirectory" class="context-menu-item" @click="contextMenuAction('download')">
|
||
<i class="fas fa-download"></i> 下载
|
||
</div>
|
||
<div class="context-menu-item" @click="contextMenuAction('rename')">
|
||
<i class="fas fa-edit"></i> 重命名
|
||
</div>
|
||
<div v-if="contextMenuFile.isDirectory" class="context-menu-item" @click="contextMenuAction('info')">
|
||
<i class="fas fa-info-circle"></i> 查看详情
|
||
</div>
|
||
<div class="context-menu-item" @click="contextMenuAction('share')">
|
||
<i class="fas fa-share"></i> 分享
|
||
</div>
|
||
<div v-if="!contextMenuFile.isDirectory" class="context-menu-item" @click="contextMenuAction('direct_link')">
|
||
<i class="fas fa-link"></i> 生成直链
|
||
</div>
|
||
<div class="context-menu-divider"></div>
|
||
<div class="context-menu-item context-menu-item-danger" @click="contextMenuAction('delete')">
|
||
<i class="fas fa-trash"></i> 删除
|
||
</div>
|
||
</div>
|
||
<!-- 移动端文件操作面板 -->
|
||
<div v-if="showMobileFileActionSheet && mobileActionFile" class="modal-overlay mobile-file-sheet-overlay" @click="closeMobileFileActionSheet">
|
||
<div class="mobile-file-sheet" @click.stop>
|
||
<div class="mobile-file-sheet-handle"></div>
|
||
<div class="mobile-file-sheet-title" :title="getFileDisplayName(mobileActionFile)">
|
||
{{ getFileDisplayName(mobileActionFile) }}
|
||
</div>
|
||
|
||
<div class="mobile-file-sheet-actions">
|
||
<button v-if="isPreviewable(mobileActionFile)" class="mobile-file-sheet-btn" @click="mobileFileAction('preview')">
|
||
<i class="fas fa-eye"></i> 预览
|
||
</button>
|
||
<button v-if="!mobileActionFile.isDirectory" class="mobile-file-sheet-btn" @click="mobileFileAction('download')">
|
||
<i class="fas fa-download"></i> 下载
|
||
</button>
|
||
<button class="mobile-file-sheet-btn" @click="mobileFileAction('rename')">
|
||
<i class="fas fa-edit"></i> 重命名
|
||
</button>
|
||
<button v-if="mobileActionFile.isDirectory" class="mobile-file-sheet-btn" @click="mobileFileAction('info')">
|
||
<i class="fas fa-info-circle"></i> 查看详情
|
||
</button>
|
||
<button class="mobile-file-sheet-btn" @click="mobileFileAction('share')">
|
||
<i class="fas fa-share"></i> 分享
|
||
</button>
|
||
<button v-if="!mobileActionFile.isDirectory" class="mobile-file-sheet-btn" @click="mobileFileAction('direct_link')">
|
||
<i class="fas fa-link"></i> 生成直链
|
||
</button>
|
||
<button class="mobile-file-sheet-btn danger" @click="mobileFileAction('delete')">
|
||
<i class="fas fa-trash"></i> 删除
|
||
</button>
|
||
<button class="mobile-file-sheet-btn cancel" @click="closeMobileFileActionSheet">
|
||
<i class="fas fa-times"></i> 取消
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 管理员:编辑用户存储权限模态框 -->
|
||
<div v-if="showEditStorageModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showEditStorageModal', $event)">
|
||
<div class="modal-content" @click.stop>
|
||
<h3 style="margin-bottom: 20px;">
|
||
<i class="fas fa-database"></i> 存储权限设置 - {{ editStorageForm.username }}
|
||
</h3>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">存储权限</label>
|
||
<select class="form-input" v-model="editStorageForm.storage_permission">
|
||
<option value="local_only">仅本地存储</option>
|
||
<option value="oss_only">仅OSS存储</option>
|
||
<option value="user_choice">用户选择</option>
|
||
</select>
|
||
<small style="color: var(--text-secondary); font-size: 12px; margin-top: 5px; display: block;">
|
||
仅本地:用户只能使用本地存储 | 仅OSS:用户只能使用OSS | 用户选择:用户可自由切换
|
||
</small>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">本地存储配额</label>
|
||
<div style="display: flex; gap: 10px;">
|
||
<input type="number" class="form-input" v-model.number="editStorageForm.local_storage_quota_value" min="1" max="102400" step="1" style="flex: 1;">
|
||
<select class="form-input" v-model="editStorageForm.quota_unit" style="width: 100px;">
|
||
<option value="MB">MB</option>
|
||
<option value="GB">GB</option>
|
||
</select>
|
||
</div>
|
||
<small style="color: var(--text-secondary); font-size: 12px; margin-top: 5px; display: block;">
|
||
配额范围: 1MB - 100GB | 建议: 大配额使用GB,小配额使用MB
|
||
</small>
|
||
</div>
|
||
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">OSS存储配额</label>
|
||
<div style="display: flex; gap: 10px; margin-bottom: 8px;">
|
||
<input
|
||
type="number"
|
||
class="form-input"
|
||
v-model.number="editStorageForm.oss_storage_quota_value"
|
||
min="1"
|
||
max="10240"
|
||
step="1"
|
||
style="flex: 1;">
|
||
<select class="form-input" v-model="editStorageForm.oss_quota_unit" style="width: 100px;">
|
||
<option value="MB">MB</option>
|
||
<option value="GB">GB</option>
|
||
<option value="TB">TB</option>
|
||
</select>
|
||
</div>
|
||
<small style="color: var(--text-secondary); font-size: 12px; margin-top: 5px; display: block;">
|
||
OSS 配额范围: 1MB - 10TB(未配置时默认 1GB)
|
||
</small>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">下载流量配额</label>
|
||
<div style="display: flex; gap: 10px; margin-bottom: 8px;">
|
||
<select class="form-input" v-model="editStorageForm.download_quota_operation" style="flex: 1;">
|
||
<option value="set">直接设置</option>
|
||
<option value="increase">增加额度</option>
|
||
<option value="decrease">减少额度</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div v-if="editStorageForm.download_quota_operation === 'set'">
|
||
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 8px;">
|
||
<label style="display: inline-flex; align-items: center; gap: 6px; font-size: 13px; color: var(--text-secondary);">
|
||
<input type="checkbox" v-model="editStorageForm.download_quota_unlimited">
|
||
不限流量(-1)
|
||
</label>
|
||
</div>
|
||
<div v-if="!editStorageForm.download_quota_unlimited" style="display: flex; gap: 10px;">
|
||
<input
|
||
type="number"
|
||
class="form-input"
|
||
v-model.number="editStorageForm.download_traffic_quota_value"
|
||
min="0"
|
||
max="10240"
|
||
step="1"
|
||
style="flex: 1;">
|
||
<select class="form-input" v-model="editStorageForm.download_quota_unit" style="width: 100px;">
|
||
<option value="MB">MB</option>
|
||
<option value="GB">GB</option>
|
||
<option value="TB">TB</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else style="display: flex; gap: 10px;">
|
||
<input
|
||
type="number"
|
||
class="form-input"
|
||
v-model.number="editStorageForm.download_quota_adjust_value"
|
||
min="1"
|
||
max="10240"
|
||
step="1"
|
||
style="flex: 1;">
|
||
<select class="form-input" v-model="editStorageForm.download_quota_adjust_unit" style="width: 100px;">
|
||
<option value="MB">MB</option>
|
||
<option value="GB">GB</option>
|
||
<option value="TB">TB</option>
|
||
</select>
|
||
</div>
|
||
|
||
<small style="color: var(--text-secondary); font-size: 12px; margin-top: 5px; display: block;">
|
||
下载流量支持直接设置、增减操作,范围: 0B - 10TB;勾选“不限流量(-1)”表示不限制
|
||
</small>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">下载流量到期时间(可选)</label>
|
||
<input type="datetime-local" class="form-input" v-model="editStorageForm.download_quota_expires_at">
|
||
<small style="color: var(--text-secondary); font-size: 12px; margin-top: 5px; display: block;">
|
||
到期后自动恢复为 0 并清零已用流量;留空表示永不过期
|
||
</small>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">下载流量重置周期</label>
|
||
<select class="form-input" v-model="editStorageForm.download_quota_reset_cycle">
|
||
<option value="none">不自动重置</option>
|
||
<option value="daily">每日重置</option>
|
||
<option value="weekly">每周重置</option>
|
||
<option value="monthly">每月重置</option>
|
||
</select>
|
||
<label style="display: inline-flex; align-items: center; gap: 6px; font-size: 13px; color: var(--text-secondary); margin-top: 8px;">
|
||
<input type="checkbox" v-model="editStorageForm.reset_download_used_now">
|
||
保存时立即将已用流量清零
|
||
</label>
|
||
<small style="color: var(--text-secondary); font-size: 12px; margin-top: 5px; display: block;">
|
||
可用于按日/周/月重置下载流量使用量
|
||
</small>
|
||
</div>
|
||
|
||
<div style="padding: 12px; background: rgba(255,255,255,0.03); border-radius: 6px; margin-bottom: 20px;">
|
||
<div style="font-size: 13px; color: var(--text-secondary); line-height: 1.6;">
|
||
<strong style="color: var(--text-primary);">配额说明:</strong><br>
|
||
• 本地默认配额: 1GB<br>
|
||
• 当前本地配额: {{ editStorageForm.local_storage_quota_value }} {{ editStorageForm.quota_unit }}
|
||
({{ editStorageForm.quota_unit === 'GB' ? (editStorageForm.local_storage_quota_value * 1024).toFixed(0) : editStorageForm.local_storage_quota_value }} MB)<br>
|
||
• 当前 OSS 配额: {{ editStorageForm.oss_storage_quota_value + ' ' + editStorageForm.oss_quota_unit }}<br>
|
||
• 已用下载流量: {{ formatBytes(editStorageForm.download_traffic_used || 0) }}<br>
|
||
• 下载策略: {{ editStorageForm.download_quota_operation === 'set' ? '直接设置' : (editStorageForm.download_quota_operation === 'increase' ? '增加额度' : '减少额度') }}<br>
|
||
• 当前下载流量配额: {{ editStorageForm.download_quota_unlimited ? '不限' : (editStorageForm.download_traffic_quota_value + ' ' + editStorageForm.download_quota_unit) }}<br>
|
||
• 自动重置: {{ getDownloadResetCycleText(editStorageForm.download_quota_reset_cycle) }}<br>
|
||
• 到期时间: {{ editStorageForm.download_quota_expires_at ? editStorageForm.download_quota_expires_at.replace('T', ' ') : '无' }}
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
||
<button class="btn btn-primary" @click="updateUserStorage" style="flex: 1;">
|
||
<i class="fas fa-save"></i> 保存设置
|
||
</button>
|
||
<button class="btn btn-secondary" @click="showEditStorageModal = false" style="flex: 1;">
|
||
<i class="fas fa-times"></i> 取消
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 图片预览模态框 -->
|
||
<div v-if="showImageViewer" class="modal-overlay" @click="closeMediaViewer">
|
||
<div class="media-viewer-content" @click.stop>
|
||
<div class="media-viewer-header">
|
||
<span class="media-viewer-title">{{ currentMediaName }}</span>
|
||
<div class="media-viewer-actions">
|
||
<button class="media-viewer-btn" @click="downloadCurrentMedia" title="下载">
|
||
<i class="fas fa-download"></i>
|
||
</button>
|
||
<button class="media-viewer-btn" @click="closeMediaViewer" title="关闭">
|
||
<i class="fas fa-times"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="media-viewer-body">
|
||
<div v-if="mediaPreviewLoading" class="media-preview-loading">
|
||
<i class="fas fa-spinner fa-spin"></i>
|
||
<span>正在加载预览...</span>
|
||
</div>
|
||
<img :src="currentMediaUrl" :alt="currentMediaName" class="media-viewer-image" @load="handleMediaPreviewLoaded" @error="handleMediaPreviewError('image')">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 视频播放器模态框 -->
|
||
<div v-if="showVideoPlayer" class="modal-overlay" @click="closeMediaViewer">
|
||
<div class="media-viewer-content" @click.stop>
|
||
<div class="media-viewer-header">
|
||
<span class="media-viewer-title">{{ currentMediaName }}</span>
|
||
<div class="media-viewer-actions">
|
||
<button class="media-viewer-btn" @click="downloadCurrentMedia" title="下载">
|
||
<i class="fas fa-download"></i>
|
||
</button>
|
||
<button class="media-viewer-btn" @click="closeMediaViewer" title="关闭">
|
||
<i class="fas fa-times"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="media-viewer-body">
|
||
<div v-if="mediaPreviewLoading" class="media-preview-loading">
|
||
<i class="fas fa-spinner fa-spin"></i>
|
||
<span>正在缓冲视频...</span>
|
||
</div>
|
||
<video controls preload="metadata" playsinline :src="currentMediaUrl" class="media-viewer-video" @loadedmetadata="handleMediaPreviewLoaded" @canplay="handleMediaPreviewLoaded" @waiting="handleMediaPreviewWaiting" @playing="handleMediaPreviewPlaying" @error="handleMediaPreviewError('video')">
|
||
您的浏览器不支持视频播放
|
||
</video>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 音频播放器模态框 -->
|
||
<div v-if="showAudioPlayer" class="modal-overlay" @click="closeMediaViewer">
|
||
<div class="media-viewer-content audio-player" @click.stop>
|
||
<div class="media-viewer-header">
|
||
<span class="media-viewer-title">{{ currentMediaName }}</span>
|
||
<div class="media-viewer-actions">
|
||
<button class="media-viewer-btn" @click="downloadCurrentMedia" title="下载">
|
||
<i class="fas fa-download"></i>
|
||
</button>
|
||
<button class="media-viewer-btn" @click="closeMediaViewer" title="关闭">
|
||
<i class="fas fa-times"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="media-viewer-body">
|
||
<div class="audio-player-icon">
|
||
<i class="fas fa-music"></i>
|
||
</div>
|
||
<div v-if="mediaPreviewLoading" class="media-preview-loading">
|
||
<i class="fas fa-spinner fa-spin"></i>
|
||
<span>正在缓冲音频...</span>
|
||
</div>
|
||
<audio controls preload="metadata" :src="currentMediaUrl" class="media-viewer-audio" @loadedmetadata="handleMediaPreviewLoaded" @canplay="handleMediaPreviewLoaded" @waiting="handleMediaPreviewWaiting" @playing="handleMediaPreviewPlaying" @error="handleMediaPreviewError('audio')">
|
||
您的浏览器不支持音频播放
|
||
</audio>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<style>
|
||
@keyframes slideIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateX(400px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateX(0);
|
||
}
|
||
}
|
||
@keyframes slideOut {
|
||
from {
|
||
opacity: 1;
|
||
transform: translateX(0);
|
||
}
|
||
to {
|
||
opacity: 0;
|
||
transform: translateX(400px);
|
||
}
|
||
}
|
||
|
||
/* 右键菜单样式 */
|
||
.context-menu {
|
||
position: fixed;
|
||
background: var(--bg-secondary);
|
||
backdrop-filter: blur(20px);
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 12px;
|
||
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
|
||
min-width: 160px;
|
||
z-index: 10000;
|
||
overflow: hidden;
|
||
animation: contextMenuFadeIn 0.15s ease-out;
|
||
}
|
||
|
||
@keyframes contextMenuFadeIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: scale(0.95);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: scale(1);
|
||
}
|
||
}
|
||
|
||
.context-menu-item {
|
||
padding: 12px 16px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
color: var(--text-primary);
|
||
font-size: 14px;
|
||
}
|
||
|
||
.context-menu-item:hover {
|
||
background: rgba(102, 126, 234, 0.15);
|
||
color: var(--accent-1);
|
||
}
|
||
|
||
.context-menu-item i {
|
||
width: 16px;
|
||
text-align: center;
|
||
color: var(--accent-1);
|
||
}
|
||
|
||
.context-menu-item-danger {
|
||
color: #ef4444;
|
||
}
|
||
|
||
.context-menu-item-danger i {
|
||
color: #ef4444;
|
||
}
|
||
|
||
.context-menu-item-danger:hover {
|
||
background: rgba(239, 68, 68, 0.15);
|
||
color: #ef4444;
|
||
}
|
||
|
||
.context-menu-divider {
|
||
height: 1px;
|
||
background: var(--glass-border);
|
||
margin: 4px 0;
|
||
}
|
||
|
||
.mobile-file-sheet-overlay {
|
||
align-items: flex-end;
|
||
z-index: 1100;
|
||
}
|
||
|
||
.mobile-file-sheet {
|
||
width: min(100%, 560px);
|
||
background: var(--bg-secondary);
|
||
border: 1px solid var(--glass-border);
|
||
border-bottom: none;
|
||
border-radius: 16px 16px 0 0;
|
||
box-shadow: 0 -12px 40px rgba(0, 0, 0, 0.45);
|
||
padding: 10px 14px calc(14px + env(safe-area-inset-bottom));
|
||
max-height: 78vh;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.mobile-file-sheet-handle {
|
||
width: 44px;
|
||
height: 4px;
|
||
border-radius: 999px;
|
||
background: rgba(255, 255, 255, 0.25);
|
||
margin: 2px auto 10px;
|
||
}
|
||
|
||
.mobile-file-sheet-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: var(--text-primary);
|
||
margin-bottom: 10px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
text-align: center;
|
||
padding: 0 4px;
|
||
}
|
||
|
||
.mobile-file-sheet-actions {
|
||
display: grid;
|
||
gap: 8px;
|
||
}
|
||
|
||
.mobile-file-sheet-btn {
|
||
width: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
padding: 10px 12px;
|
||
border-radius: 10px;
|
||
border: 1px solid var(--glass-border);
|
||
background: rgba(255, 255, 255, 0.06);
|
||
color: var(--text-primary);
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.mobile-file-sheet-btn.danger {
|
||
color: #ef4444;
|
||
border-color: rgba(239, 68, 68, 0.4);
|
||
background: rgba(239, 68, 68, 0.12);
|
||
}
|
||
|
||
.upload-progress-panel {
|
||
width: min(92vw, 350px);
|
||
right: max(12px, env(safe-area-inset-right));
|
||
bottom: calc(12px + env(safe-area-inset-bottom));
|
||
}
|
||
|
||
.mobile-file-sheet-btn.cancel {
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
@media (min-width: 769px) {
|
||
.mobile-file-sheet-overlay {
|
||
display: none !important;
|
||
}
|
||
}
|
||
|
||
/* 移动端优化 */
|
||
@media (max-width: 768px) {
|
||
.context-menu {
|
||
min-width: 180px;
|
||
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
|
||
}
|
||
|
||
.context-menu-item {
|
||
padding: 14px 18px;
|
||
font-size: 15px;
|
||
}
|
||
}
|
||
|
||
/* 媒体预览器样式 */
|
||
.media-viewer-content {
|
||
background: var(--bg-secondary);
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 16px;
|
||
max-width: 90vw;
|
||
max-height: 90vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
box-shadow: 0 20px 60px rgba(0,0,0,0.5);
|
||
}
|
||
|
||
.media-viewer-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 15px 20px;
|
||
background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
|
||
color: white;
|
||
}
|
||
|
||
.media-viewer-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
flex: 1;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
margin-right: 15px;
|
||
}
|
||
|
||
.media-viewer-actions {
|
||
display: flex;
|
||
gap: 10px;
|
||
}
|
||
|
||
.media-viewer-btn {
|
||
background: rgba(255,255,255,0.2);
|
||
border: none;
|
||
color: white;
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.2s;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.media-viewer-btn:hover {
|
||
background: rgba(255,255,255,0.3);
|
||
}
|
||
|
||
.media-viewer-body {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 20px;
|
||
background: rgba(255,255,255,0.03);
|
||
flex: 1;
|
||
overflow: auto;
|
||
position: relative;
|
||
}
|
||
|
||
.media-preview-loading {
|
||
position: absolute;
|
||
inset: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 10px;
|
||
background: rgba(15, 23, 42, 0.45);
|
||
color: #fff;
|
||
font-size: 14px;
|
||
z-index: 2;
|
||
pointer-events: none;
|
||
backdrop-filter: blur(2px);
|
||
}
|
||
|
||
.media-preview-loading i {
|
||
font-size: 20px;
|
||
}
|
||
|
||
.media-viewer-image {
|
||
max-width: 100%;
|
||
max-height: 75vh;
|
||
object-fit: contain;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.media-viewer-video {
|
||
max-width: 100%;
|
||
max-height: 75vh;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.media-viewer-audio {
|
||
width: 100%;
|
||
max-width: 500px;
|
||
}
|
||
|
||
.audio-player .media-viewer-body {
|
||
flex-direction: column;
|
||
gap: 30px;
|
||
}
|
||
|
||
.audio-player-icon {
|
||
font-size: 80px;
|
||
color: #667eea;
|
||
}
|
||
|
||
.audio-player-icon i {
|
||
animation: pulse 2s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0%, 100% {
|
||
opacity: 1;
|
||
}
|
||
50% {
|
||
opacity: 0.6;
|
||
}
|
||
}
|
||
|
||
/* 移动端适配 */
|
||
@media (max-width: 768px) {
|
||
.media-viewer-content {
|
||
max-width: 95vw;
|
||
max-height: 95vh;
|
||
}
|
||
|
||
.media-viewer-header {
|
||
padding: 12px 15px;
|
||
}
|
||
|
||
.media-viewer-title {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.media-viewer-btn {
|
||
width: 32px;
|
||
height: 32px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.media-viewer-body {
|
||
padding: 15px;
|
||
}
|
||
|
||
.media-preview-loading {
|
||
font-size: 13px;
|
||
}
|
||
|
||
.media-preview-loading i {
|
||
font-size: 18px;
|
||
}
|
||
|
||
.media-viewer-image,
|
||
.media-viewer-video {
|
||
max-height: 70vh;
|
||
}
|
||
|
||
.audio-player-icon {
|
||
font-size: 64px;
|
||
}
|
||
}
|
||
|
||
/* 拖拽上传样式 */
|
||
.files-container {
|
||
position: relative;
|
||
min-height: 200px;
|
||
}
|
||
|
||
.drag-drop-overlay {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(102, 126, 234, 0.1);
|
||
backdrop-filter: blur(2px);
|
||
border: 3px dashed #667eea;
|
||
border-radius: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 100;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.drag-drop-content {
|
||
text-align: center;
|
||
padding: 40px;
|
||
}
|
||
|
||
.files-container.drag-over {
|
||
background: rgba(102, 126, 234, 0.05);
|
||
border-radius: 12px;
|
||
}
|
||
|
||
/* 拖拽上传样式 */
|
||
.files-container {
|
||
position: relative;
|
||
min-height: 200px;
|
||
}
|
||
|
||
.drag-drop-overlay {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(102, 126, 234, 0.1);
|
||
backdrop-filter: blur(2px);
|
||
border: 3px dashed #667eea;
|
||
border-radius: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 100;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.drag-drop-content {
|
||
text-align: center;
|
||
padding: 40px;
|
||
}
|
||
|
||
.files-container.drag-over {
|
||
background: rgba(102, 126, 234, 0.05);
|
||
border-radius: 12px;
|
||
}
|
||
</style>
|
||
|
||
<style>
|
||
/* ===== Enterprise Netdisk UI Rebuild (Classic Cloud Disk) ===== */
|
||
body.enterprise-netdisk,
|
||
body.enterprise-netdisk.light-theme,
|
||
body.enterprise-netdisk: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.16);
|
||
--danger: #dc2626;
|
||
--success: #16a34a;
|
||
--warning: #d97706;
|
||
background: linear-gradient(180deg, #eef3fb 0%, #f8fafe 34%, #edf2fb 100%) !important;
|
||
color: var(--text-primary) !important;
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', Roboto, sans-serif;
|
||
line-height: 1.5;
|
||
background-attachment: fixed;
|
||
}
|
||
|
||
body.enterprise-netdisk::before {
|
||
display: none !important;
|
||
}
|
||
|
||
body.enterprise-netdisk #app {
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
body.enterprise-netdisk a {
|
||
color: var(--accent-1);
|
||
}
|
||
|
||
body.enterprise-netdisk .app-loading {
|
||
background: var(--bg-primary);
|
||
}
|
||
|
||
body.enterprise-netdisk .navbar {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 50;
|
||
min-height: 62px;
|
||
padding: 10px 18px;
|
||
gap: 10px;
|
||
background: var(--bg-secondary) !important;
|
||
border-bottom: 1px solid var(--glass-border) !important;
|
||
backdrop-filter: none !important;
|
||
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.05);
|
||
}
|
||
|
||
body.enterprise-netdisk .navbar-brand {
|
||
color: var(--accent-1);
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.2px;
|
||
}
|
||
|
||
body.enterprise-netdisk .navbar-menu {
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
justify-content: flex-end;
|
||
align-items: center;
|
||
}
|
||
|
||
body.enterprise-netdisk .nav-item {
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 8px;
|
||
background: var(--bg-card-hover);
|
||
color: var(--text-secondary);
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
padding: 7px 12px;
|
||
transition: all .2s ease;
|
||
}
|
||
|
||
body.enterprise-netdisk .nav-item:hover {
|
||
border-color: var(--glass-border-hover);
|
||
color: var(--accent-1);
|
||
background: #eef4ff;
|
||
}
|
||
|
||
body.enterprise-netdisk .nav-item.active {
|
||
border-color: var(--accent-1);
|
||
background: var(--accent-1);
|
||
color: #fff;
|
||
box-shadow: none;
|
||
}
|
||
|
||
body.enterprise-netdisk .user-info {
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 8px;
|
||
background: var(--bg-card-hover);
|
||
color: var(--text-primary);
|
||
gap: 8px;
|
||
padding: 6px 10px;
|
||
}
|
||
|
||
body.enterprise-netdisk .main-container {
|
||
max-width: 1320px;
|
||
margin: 18px auto;
|
||
padding: 0 14px;
|
||
}
|
||
|
||
body.enterprise-netdisk .card {
|
||
background: linear-gradient(180deg, #fcfdff 0%, #f7fafe 100%);
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 10px;
|
||
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.08);
|
||
backdrop-filter: none;
|
||
padding: 20px;
|
||
}
|
||
|
||
body.enterprise-netdisk .auth-container {
|
||
min-height: 100vh;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 20px 12px;
|
||
background: var(--bg-primary);
|
||
}
|
||
|
||
body.enterprise-netdisk .auth-box {
|
||
width: min(460px, 100%);
|
||
padding: 24px;
|
||
background: var(--bg-secondary);
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 10px;
|
||
box-shadow: 0 6px 20px rgba(15, 23, 42, 0.08);
|
||
}
|
||
|
||
body.enterprise-netdisk .auth-title {
|
||
margin-bottom: 16px;
|
||
font-size: 24px;
|
||
color: var(--text-primary);
|
||
background: none;
|
||
-webkit-text-fill-color: currentColor;
|
||
}
|
||
|
||
body.enterprise-netdisk .alert {
|
||
border-radius: 8px;
|
||
font-size: 13px;
|
||
padding: 10px 12px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
body.enterprise-netdisk .form-label {
|
||
color: var(--text-secondary);
|
||
font-size: 13px;
|
||
margin-bottom: 6px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
body.enterprise-netdisk .form-input,
|
||
body.enterprise-netdisk select.form-input,
|
||
body.enterprise-netdisk textarea.form-input {
|
||
background: var(--bg-secondary);
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 8px;
|
||
color: var(--text-primary);
|
||
font-size: 14px;
|
||
padding: 10px 12px;
|
||
box-shadow: none;
|
||
}
|
||
|
||
body.enterprise-netdisk .form-input:focus,
|
||
body.enterprise-netdisk select.form-input:focus,
|
||
body.enterprise-netdisk textarea.form-input:focus {
|
||
border-color: var(--accent-1);
|
||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.14);
|
||
background: var(--bg-secondary);
|
||
outline: none;
|
||
}
|
||
|
||
body.enterprise-netdisk .btn {
|
||
border-radius: 8px;
|
||
border: 1px solid transparent;
|
||
box-shadow: none;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
line-height: 1.2;
|
||
padding: 9px 14px;
|
||
transition: all .2s ease;
|
||
}
|
||
|
||
body.enterprise-netdisk .btn-primary {
|
||
background: var(--accent-1);
|
||
border-color: var(--accent-1);
|
||
color: #fff;
|
||
}
|
||
|
||
body.enterprise-netdisk .btn-primary:hover {
|
||
background: var(--accent-2);
|
||
border-color: var(--accent-2);
|
||
transform: none;
|
||
box-shadow: none;
|
||
}
|
||
|
||
body.enterprise-netdisk .btn-secondary {
|
||
background: var(--bg-secondary);
|
||
border-color: var(--glass-border);
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
body.enterprise-netdisk .btn-secondary:hover {
|
||
background: var(--bg-card-hover);
|
||
border-color: var(--glass-border-hover);
|
||
}
|
||
|
||
body.enterprise-netdisk .btn-danger {
|
||
background: #ef4444;
|
||
border-color: #ef4444;
|
||
color: #fff;
|
||
}
|
||
|
||
body.enterprise-netdisk .btn-danger:hover {
|
||
background: #dc2626;
|
||
border-color: #dc2626;
|
||
}
|
||
|
||
body.enterprise-netdisk .btn-icon {
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 6px;
|
||
background: var(--bg-secondary);
|
||
color: var(--text-secondary);
|
||
padding: 7px 10px;
|
||
}
|
||
|
||
body.enterprise-netdisk .btn-icon:hover {
|
||
border-color: var(--glass-border-hover);
|
||
color: var(--accent-1);
|
||
background: var(--bg-card-hover);
|
||
}
|
||
|
||
body.enterprise-netdisk .files-page-head {
|
||
margin-bottom: 12px;
|
||
padding: 14px 16px;
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 8px;
|
||
background: linear-gradient(180deg, #ffffff, #f8fbff);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-page-title {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin: 0;
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
body.enterprise-netdisk .files-page-title i {
|
||
color: var(--accent-1);
|
||
font-size: 18px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-page-subtitle {
|
||
margin: 4px 0 0;
|
||
font-size: 12px;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
body.enterprise-netdisk .files-storage-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 999px;
|
||
background: #ffffff;
|
||
color: var(--text-secondary);
|
||
padding: 8px 12px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-storage-badge.oss {
|
||
color: #1d4ed8;
|
||
border-color: #bfdbfe;
|
||
background: #eff6ff;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-storage-badge.local {
|
||
color: #065f46;
|
||
border-color: #bbf7d0;
|
||
background: #f0fdf4;
|
||
}
|
||
|
||
body.enterprise-netdisk .storage-summary-card {
|
||
margin-bottom: 12px;
|
||
padding: 12px 14px;
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 8px;
|
||
background: var(--bg-card-hover);
|
||
display: grid;
|
||
gap: 8px;
|
||
}
|
||
|
||
body.enterprise-netdisk .quota-progress-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
font-size: 12px;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
body.enterprise-netdisk .quota-progress-row strong {
|
||
color: var(--text-primary);
|
||
font-weight: 700;
|
||
}
|
||
|
||
body.enterprise-netdisk .quota-progress-track {
|
||
width: 100%;
|
||
height: 10px;
|
||
border-radius: 999px;
|
||
background: #e2e8f0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
body.enterprise-netdisk .quota-progress-value {
|
||
height: 100%;
|
||
border-radius: 999px;
|
||
transition: width 0.3s ease;
|
||
}
|
||
|
||
|
||
body.enterprise-netdisk .quota-detail-row {
|
||
display: flex;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
}
|
||
|
||
body.enterprise-netdisk .quota-detail-chip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 5px 10px;
|
||
border-radius: 999px;
|
||
font-size: 12px;
|
||
line-height: 1.4;
|
||
color: var(--text-secondary);
|
||
border: 1px solid var(--glass-border);
|
||
background: var(--bg-secondary);
|
||
}
|
||
|
||
body.enterprise-netdisk .quota-detail-chip i {
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
body.enterprise-netdisk .quota-detail-chip.active {
|
||
color: var(--accent-1);
|
||
border-color: rgba(59, 130, 246, 0.35);
|
||
background: rgba(59, 130, 246, 0.1);
|
||
font-weight: 700;
|
||
}
|
||
|
||
body.enterprise-netdisk .quota-detail-chip.active i {
|
||
color: var(--accent-1);
|
||
}
|
||
|
||
body.enterprise-netdisk .breadcrumb-bar {
|
||
margin-bottom: 12px;
|
||
padding: 10px 12px;
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 8px;
|
||
background: var(--bg-card-hover);
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
}
|
||
|
||
body.enterprise-netdisk .breadcrumb-home {
|
||
padding: 7px 9px;
|
||
}
|
||
|
||
body.enterprise-netdisk .breadcrumb-sep {
|
||
color: var(--text-muted);
|
||
font-size: 12px;
|
||
}
|
||
|
||
body.enterprise-netdisk .breadcrumb-link {
|
||
color: var(--accent-1);
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
padding: 2px 0;
|
||
}
|
||
|
||
body.enterprise-netdisk .breadcrumb-link:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
body.enterprise-netdisk .breadcrumb-current {
|
||
color: var(--text-primary);
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
body.enterprise-netdisk .breadcrumb-up {
|
||
margin-left: auto;
|
||
padding: 7px 10px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-toolbar {
|
||
margin-bottom: 12px;
|
||
padding: 12px 14px;
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 8px;
|
||
background: var(--bg-card-hover);
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-toolbar-meta {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-toolbar-meta-main {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
min-width: 0;
|
||
flex: 1;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-toolbar-meta-path-label {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
font-weight: 600;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-toolbar-meta-path {
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
color: var(--text-primary);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
min-width: 0;
|
||
max-width: min(540px, 100%);
|
||
}
|
||
|
||
body.enterprise-netdisk .file-toolbar-meta-stats {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-toolbar-chip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 4px 10px;
|
||
border-radius: 999px;
|
||
border: 1px solid var(--glass-border);
|
||
background: var(--bg-secondary);
|
||
color: var(--text-secondary);
|
||
font-size: 12px;
|
||
line-height: 1.3;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-toolbar-chip i {
|
||
color: var(--accent-1);
|
||
font-size: 11px;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-toolbar-main {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
width: 100%;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-toolbar-left,
|
||
body.enterprise-netdisk .file-toolbar-right {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-toolbar-left {
|
||
flex-wrap: nowrap;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-toolbar-right {
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-toolbar-primary {
|
||
min-width: 114px;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-toolbar-secondary {
|
||
min-width: 120px;
|
||
background: #ffffff;
|
||
}
|
||
|
||
body.enterprise-netdisk .view-toggle-total {
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
font-weight: 600;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
body.enterprise-netdisk .view-toggle-label {
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
margin-right: 2px;
|
||
font-weight: 600;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
body.enterprise-netdisk .view-toggle-group {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 4px;
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 8px;
|
||
background: var(--bg-secondary);
|
||
}
|
||
|
||
body.enterprise-netdisk .view-toggle-group .btn {
|
||
min-width: 90px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-content-shell {
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 10px;
|
||
overflow: hidden;
|
||
background: var(--bg-secondary);
|
||
}
|
||
|
||
body.enterprise-netdisk .files-content-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
padding: 10px 12px;
|
||
border-bottom: 1px solid var(--glass-border);
|
||
background: var(--bg-card-hover);
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-content-title {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 13px;
|
||
color: var(--text-primary);
|
||
font-weight: 700;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-content-title i {
|
||
color: var(--accent-1);
|
||
font-size: 12px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-content-tip {
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
body.enterprise-netdisk .files-container {
|
||
background: var(--bg-secondary);
|
||
border: none;
|
||
border-radius: 0;
|
||
padding: 6px;
|
||
min-height: 220px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-container.drag-over {
|
||
background: rgba(37, 99, 235, 0.06);
|
||
}
|
||
|
||
body.enterprise-netdisk .files-content-shell .empty-hint {
|
||
padding: 64px 0;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-empty-state {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
padding: 56px 14px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-empty-state > i {
|
||
font-size: 38px;
|
||
color: #94a3b8;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-empty-title {
|
||
color: var(--text-primary);
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-empty-desc {
|
||
color: var(--text-muted);
|
||
font-size: 12px;
|
||
text-align: center;
|
||
max-width: 360px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-empty-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
margin-top: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-empty-actions .btn {
|
||
min-width: 118px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-content-shell .file-list {
|
||
border-radius: 6px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-content-shell .file-list-table {
|
||
border: none;
|
||
border-radius: 0;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-content-shell .file-list-table th,
|
||
body.enterprise-netdisk .files-content-shell .file-list-table td {
|
||
border-left: none;
|
||
border-right: none;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-content-shell .file-grid {
|
||
padding: 8px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-content-shell .file-grid-item {
|
||
border-color: #e2e8f0;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-content-shell .file-grid-item:hover {
|
||
border-color: #93c5fd;
|
||
background: #f8fbff;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-content-shell .file-name,
|
||
body.enterprise-netdisk .files-content-shell .file-size {
|
||
text-align: center;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-col-name {
|
||
width: 48%;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-col-size {
|
||
width: 16%;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-col-time {
|
||
width: 26%;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-col-actions {
|
||
width: 10%;
|
||
text-align: center !important;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-list-name-cell {
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
body.enterprise-netdisk .file-list-name-wrap {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
overflow: hidden;
|
||
min-width: 0;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-list-name-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
min-width: 0;
|
||
flex: 1;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-list-name-text {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
min-width: 0;
|
||
color: var(--text-primary);
|
||
font-size: 13px;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-list-name-meta {
|
||
display: none;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
color: var(--text-muted);
|
||
font-size: 11px;
|
||
line-height: 1.25;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-list-thumb {
|
||
width: 34px;
|
||
height: 34px;
|
||
object-fit: cover;
|
||
border-radius: 6px;
|
||
border: 1px solid var(--glass-border);
|
||
flex-shrink: 0;
|
||
background: #ffffff;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-list-video-icon {
|
||
width: 34px;
|
||
height: 34px;
|
||
border-radius: 6px;
|
||
border: 1px solid #bfdbfe;
|
||
background: #eff6ff;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
color: var(--accent-1);
|
||
}
|
||
|
||
body.enterprise-netdisk .file-list-video-icon i {
|
||
font-size: 14px;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-list-type-icon {
|
||
font-size: 20px;
|
||
min-width: 24px;
|
||
text-align: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-grid-type-icon {
|
||
font-size: 52px;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-grid-video-icon {
|
||
width: 64px;
|
||
height: 64px;
|
||
border-radius: 10px;
|
||
border: 1px solid #bfdbfe;
|
||
background: #eff6ff;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-grid-video-play {
|
||
color: var(--accent-1);
|
||
font-size: 30px;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-type-folder {
|
||
color: #d97706;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-type-audio {
|
||
color: #ea580c;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-type-pdf {
|
||
color: #dc2626;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-type-word {
|
||
color: #2563eb;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-type-excel {
|
||
color: #16a34a;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-type-archive {
|
||
color: #7c3aed;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-type-default {
|
||
color: #64748b;
|
||
}
|
||
|
||
body.enterprise-netdisk .empty-hint {
|
||
color: var(--text-muted);
|
||
font-size: 13px;
|
||
padding: 54px 0;
|
||
text-align: center;
|
||
}
|
||
|
||
body.enterprise-netdisk .loading {
|
||
color: var(--text-secondary);
|
||
font-size: 13px;
|
||
padding: 32px;
|
||
}
|
||
|
||
body.enterprise-netdisk .spinner {
|
||
border: 3px solid rgba(148, 163, 184, 0.25);
|
||
border-top: 3px solid var(--accent-1);
|
||
width: 30px;
|
||
height: 30px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-grid {
|
||
padding: 6px;
|
||
gap: 10px;
|
||
grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
|
||
}
|
||
|
||
body.enterprise-netdisk .file-grid-item {
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 8px;
|
||
background: var(--bg-secondary);
|
||
min-height: 160px;
|
||
padding: 12px 10px;
|
||
transition: all .2s ease;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-grid-item:hover {
|
||
border-color: #93c5fd;
|
||
background: #f8fbff;
|
||
transform: none;
|
||
box-shadow: none;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-name {
|
||
font-size: 13px;
|
||
min-height: 34px;
|
||
line-height: 1.35;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
body.enterprise-netdisk .file-size {
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
margin-top: 5px;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-list-size-cell,
|
||
body.enterprise-netdisk .file-list-time-cell {
|
||
color: var(--text-secondary);
|
||
font-size: 13px;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-list-action-col {
|
||
text-align: center !important;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-list-table td.file-list-action-col {
|
||
overflow: visible;
|
||
text-overflow: clip;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-list-action-col .btn {
|
||
margin: 0 auto;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-list-table,
|
||
body.enterprise-netdisk .share-list-table,
|
||
body.enterprise-netdisk .admin-users-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
table-layout: fixed;
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
background: var(--bg-secondary);
|
||
}
|
||
|
||
body.enterprise-netdisk .file-list-table thead tr,
|
||
body.enterprise-netdisk .share-list-table thead tr,
|
||
body.enterprise-netdisk .admin-users-table thead tr {
|
||
background: var(--bg-card-hover) !important;
|
||
border-bottom: 1px solid var(--glass-border) !important;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-list-table th,
|
||
body.enterprise-netdisk .share-list-table th,
|
||
body.enterprise-netdisk .admin-users-table th {
|
||
color: var(--text-secondary);
|
||
font-weight: 600;
|
||
font-size: 13px;
|
||
padding: 10px 12px !important;
|
||
border-bottom: 1px solid var(--glass-border);
|
||
text-align: left;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-list-table td,
|
||
body.enterprise-netdisk .share-list-table td,
|
||
body.enterprise-netdisk .admin-users-table td {
|
||
font-size: 13px;
|
||
color: var(--text-primary);
|
||
padding: 10px 12px !important;
|
||
border-bottom: 1px solid var(--glass-border);
|
||
vertical-align: middle;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-list-table td {
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-list-table .file-list-name-cell {
|
||
white-space: normal;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-list-row:hover,
|
||
body.enterprise-netdisk .share-list-table tbody tr:hover,
|
||
body.enterprise-netdisk .admin-users-table tbody tr:hover {
|
||
background: var(--bg-card-hover);
|
||
}
|
||
|
||
body.enterprise-netdisk .file-row-mobile-more {
|
||
min-width: 34px;
|
||
min-height: 34px;
|
||
padding: 0;
|
||
border: 1px solid var(--glass-border);
|
||
background: var(--bg-secondary);
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
body.enterprise-netdisk .file-row-mobile-more:hover {
|
||
color: var(--accent-1);
|
||
border-color: var(--glass-border-hover);
|
||
background: var(--bg-card-hover);
|
||
}
|
||
|
||
body.enterprise-netdisk .modal-overlay {
|
||
background: rgba(15, 23, 42, 0.42);
|
||
backdrop-filter: blur(1px);
|
||
}
|
||
|
||
body.enterprise-netdisk .modal-content {
|
||
width: min(700px, 92vw);
|
||
max-height: 90vh;
|
||
overflow-y: auto;
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 10px;
|
||
background: var(--bg-secondary);
|
||
box-shadow: 0 16px 30px rgba(15, 23, 42, 0.15);
|
||
padding: 20px;
|
||
}
|
||
|
||
body.enterprise-netdisk .share-toolbar {
|
||
margin-bottom: 12px;
|
||
gap: 8px;
|
||
align-items: center;
|
||
}
|
||
|
||
body.enterprise-netdisk .share-toolbar input,
|
||
body.enterprise-netdisk .share-toolbar select {
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 8px;
|
||
background: var(--bg-secondary);
|
||
color: var(--text-primary);
|
||
font-size: 13px;
|
||
padding: 7px 8px;
|
||
}
|
||
|
||
body.enterprise-netdisk .share-card {
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 8px;
|
||
background: var(--bg-secondary);
|
||
box-shadow: none;
|
||
padding: 12px;
|
||
transition: all .2s ease;
|
||
}
|
||
|
||
body.enterprise-netdisk .share-card:hover {
|
||
border-color: #93c5fd;
|
||
background: #f8fbff;
|
||
transform: none;
|
||
box-shadow: none;
|
||
}
|
||
|
||
body.enterprise-netdisk .share-chip {
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 999px;
|
||
background: var(--bg-card-hover);
|
||
color: var(--text-secondary);
|
||
font-size: 12px;
|
||
padding: 4px 9px;
|
||
}
|
||
|
||
body.enterprise-netdisk .share-password-toggle {
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 8px;
|
||
background: var(--bg-card-hover);
|
||
color: var(--text-secondary);
|
||
font-size: 13px;
|
||
padding: 8px 10px;
|
||
}
|
||
|
||
body.enterprise-netdisk .share-success-panel {
|
||
border: 1px solid #bfdbfe;
|
||
border-radius: 8px;
|
||
background: #eff6ff;
|
||
box-shadow: none;
|
||
padding: 12px;
|
||
gap: 10px;
|
||
}
|
||
|
||
body.enterprise-netdisk .share-success-head > i,
|
||
body.enterprise-netdisk .share-success-title {
|
||
color: #1d4ed8;
|
||
}
|
||
|
||
body.enterprise-netdisk .share-success-link {
|
||
border: 1px dashed #93c5fd;
|
||
border-radius: 8px;
|
||
background: #fff;
|
||
color: #1e3a8a;
|
||
padding: 9px 10px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
body.enterprise-netdisk .share-success-tip.warn {
|
||
border-color: #fcd34d;
|
||
background: #fffbeb;
|
||
color: #b45309;
|
||
}
|
||
|
||
body.enterprise-netdisk .share-success-tip.info {
|
||
border-color: #bfdbfe;
|
||
background: #eff6ff;
|
||
color: #1d4ed8;
|
||
}
|
||
|
||
body.enterprise-netdisk .admin-tabs-card {
|
||
padding: 0;
|
||
overflow: hidden;
|
||
border-radius: 10px;
|
||
}
|
||
|
||
body.enterprise-netdisk .admin-tabs-nav {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
padding: 10px;
|
||
border-bottom: 1px solid var(--glass-border);
|
||
background: var(--bg-card-hover);
|
||
}
|
||
|
||
body.enterprise-netdisk .admin-tab-btn {
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 8px;
|
||
padding: 8px 14px;
|
||
background: var(--bg-secondary);
|
||
color: var(--text-secondary);
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all .2s ease;
|
||
}
|
||
|
||
body.enterprise-netdisk .admin-tab-btn:hover {
|
||
border-color: var(--glass-border-hover);
|
||
color: var(--accent-1);
|
||
background: #eef4ff;
|
||
}
|
||
|
||
body.enterprise-netdisk .admin-tab-btn.active {
|
||
background: var(--accent-1);
|
||
border-color: var(--accent-1);
|
||
color: #fff;
|
||
}
|
||
|
||
body.enterprise-netdisk .admin-health-summary,
|
||
body.enterprise-netdisk .admin-log-filters,
|
||
body.enterprise-netdisk .admin-log-list,
|
||
body.enterprise-netdisk .admin-log-pager,
|
||
body.enterprise-netdisk .admin-users-table-wrap {
|
||
background: var(--bg-secondary);
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 8px;
|
||
}
|
||
|
||
body.enterprise-netdisk .admin-users-toolbar,
|
||
body.enterprise-netdisk .admin-users-pagination,
|
||
body.enterprise-netdisk .admin-users-empty-state {
|
||
background: var(--bg-secondary);
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 8px;
|
||
}
|
||
|
||
body.enterprise-netdisk .admin-users-filter input,
|
||
body.enterprise-netdisk .admin-users-filter select {
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 8px;
|
||
background: var(--bg-secondary);
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
body.enterprise-netdisk .admin-users-stat-chip {
|
||
background: var(--bg-card-hover);
|
||
border: 1px solid var(--glass-border);
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
body.enterprise-netdisk .admin-users-table thead th {
|
||
background: var(--bg-card-hover) !important;
|
||
backdrop-filter: none;
|
||
}
|
||
|
||
body.enterprise-netdisk .admin-user-status-tag.status-active {
|
||
color: #15803d;
|
||
background: #dcfce7;
|
||
border-color: #86efac;
|
||
}
|
||
|
||
body.enterprise-netdisk .admin-user-status-tag.status-banned {
|
||
color: #b91c1c;
|
||
background: #fee2e2;
|
||
border-color: #fca5a5;
|
||
}
|
||
|
||
body.enterprise-netdisk .admin-user-status-tag.status-unverified {
|
||
color: #b45309;
|
||
background: #fef3c7;
|
||
border-color: #fcd34d;
|
||
}
|
||
|
||
body.enterprise-netdisk .admin-user-status-tag.status-download_blocked {
|
||
color: #c2410c;
|
||
background: #ffedd5;
|
||
border-color: #fdba74;
|
||
}
|
||
|
||
body.enterprise-netdisk .admin-search-hit {
|
||
background: #fde68a;
|
||
color: #7c2d12;
|
||
}
|
||
|
||
body.enterprise-netdisk .admin-log-row {
|
||
border-bottom: 1px solid var(--glass-border);
|
||
}
|
||
|
||
body.enterprise-netdisk .admin-log-row:hover {
|
||
background: var(--bg-card-hover);
|
||
}
|
||
|
||
body.enterprise-netdisk .context-menu {
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 8px;
|
||
background: var(--bg-secondary);
|
||
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.18);
|
||
backdrop-filter: none;
|
||
}
|
||
|
||
body.enterprise-netdisk .context-menu-item {
|
||
font-size: 13px;
|
||
padding: 10px 12px;
|
||
gap: 10px;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
body.enterprise-netdisk .context-menu-item i {
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
body.enterprise-netdisk .context-menu-item:hover {
|
||
background: var(--bg-card-hover);
|
||
color: var(--accent-1);
|
||
}
|
||
|
||
body.enterprise-netdisk .context-menu-item:hover i {
|
||
color: var(--accent-1);
|
||
}
|
||
|
||
body.enterprise-netdisk .context-menu-item-danger,
|
||
body.enterprise-netdisk .context-menu-item-danger i {
|
||
color: #dc2626;
|
||
}
|
||
|
||
body.enterprise-netdisk .context-menu-item-danger:hover {
|
||
background: #fef2f2;
|
||
}
|
||
|
||
body.enterprise-netdisk .mobile-file-sheet {
|
||
border: 1px solid var(--glass-border);
|
||
border-bottom: none;
|
||
border-radius: 10px 10px 0 0;
|
||
box-shadow: 0 -10px 24px rgba(15, 23, 42, 0.2);
|
||
background: var(--bg-secondary);
|
||
}
|
||
|
||
body.enterprise-netdisk .mobile-file-sheet-btn {
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 8px;
|
||
background: var(--bg-secondary);
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
body.enterprise-netdisk .mobile-file-sheet-btn.cancel {
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
body.enterprise-netdisk .media-viewer-content {
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 10px;
|
||
background: var(--bg-secondary);
|
||
box-shadow: 0 16px 34px rgba(15, 23, 42, 0.24);
|
||
}
|
||
|
||
body.enterprise-netdisk .media-viewer-header {
|
||
background: var(--bg-card-hover);
|
||
color: var(--text-primary);
|
||
border-bottom: 1px solid var(--glass-border);
|
||
padding: 12px 14px;
|
||
}
|
||
|
||
body.enterprise-netdisk .media-viewer-btn {
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 6px;
|
||
background: var(--bg-secondary);
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
body.enterprise-netdisk .media-viewer-btn:hover {
|
||
border-color: var(--glass-border-hover);
|
||
color: var(--accent-1);
|
||
background: var(--bg-card-hover);
|
||
}
|
||
|
||
body.enterprise-netdisk .media-viewer-body {
|
||
background: var(--bg-secondary);
|
||
}
|
||
|
||
body.enterprise-netdisk .audio-player-icon {
|
||
color: var(--accent-1);
|
||
}
|
||
|
||
body.enterprise-netdisk .toast-stack {
|
||
position: fixed;
|
||
top: 14px;
|
||
right: 14px;
|
||
z-index: 2000;
|
||
width: min(360px, calc(100vw - 20px));
|
||
display: grid;
|
||
gap: 10px;
|
||
}
|
||
|
||
body.enterprise-netdisk .app-toast {
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 8px;
|
||
background: var(--bg-secondary);
|
||
color: var(--text-primary);
|
||
box-shadow: 0 10px 20px rgba(15, 23, 42, 0.12);
|
||
padding: 12px;
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 10px;
|
||
}
|
||
|
||
body.enterprise-netdisk .app-toast.toast-success {
|
||
border-color: #86efac;
|
||
background: #f0fdf4;
|
||
color: #166534;
|
||
}
|
||
|
||
body.enterprise-netdisk .app-toast.toast-error {
|
||
border-color: #fca5a5;
|
||
background: #fef2f2;
|
||
color: #991b1b;
|
||
}
|
||
|
||
body.enterprise-netdisk .app-toast.toast-info,
|
||
body.enterprise-netdisk .app-toast.toast-warning {
|
||
border-color: #bfdbfe;
|
||
background: #eff6ff;
|
||
color: #1d4ed8;
|
||
}
|
||
|
||
body.enterprise-netdisk .toast-icon {
|
||
font-size: 18px;
|
||
margin-top: 1px;
|
||
}
|
||
|
||
body.enterprise-netdisk .toast-title {
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
body.enterprise-netdisk .toast-message {
|
||
font-size: 13px;
|
||
line-height: 1.45;
|
||
}
|
||
|
||
body.enterprise-netdisk .upload-progress-panel {
|
||
position: fixed;
|
||
z-index: 2000;
|
||
right: max(12px, env(safe-area-inset-right));
|
||
bottom: calc(12px + env(safe-area-inset-bottom));
|
||
width: min(92vw, 360px);
|
||
background: var(--bg-secondary);
|
||
border: 1px solid var(--glass-border);
|
||
border-radius: 8px;
|
||
box-shadow: 0 10px 26px rgba(15, 23, 42, 0.16);
|
||
padding: 14px;
|
||
animation: slideIn .3s ease-out;
|
||
}
|
||
|
||
body.enterprise-netdisk .upload-progress-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
body.enterprise-netdisk .upload-progress-icon {
|
||
font-size: 20px;
|
||
color: var(--accent-1);
|
||
}
|
||
|
||
body.enterprise-netdisk .upload-progress-meta {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
body.enterprise-netdisk .upload-progress-title {
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
body.enterprise-netdisk .upload-progress-name,
|
||
body.enterprise-netdisk .upload-progress-size {
|
||
font-size: 12px;
|
||
color: var(--text-secondary);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
body.enterprise-netdisk .upload-progress-percent {
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
color: var(--accent-1);
|
||
}
|
||
|
||
body.enterprise-netdisk .upload-progress-bar {
|
||
width: 100%;
|
||
height: 8px;
|
||
border-radius: 999px;
|
||
background: #dbeafe;
|
||
overflow: hidden;
|
||
}
|
||
|
||
body.enterprise-netdisk .upload-progress-value {
|
||
height: 100%;
|
||
border-radius: 999px;
|
||
background: var(--accent-1);
|
||
transition: width .3s ease;
|
||
}
|
||
|
||
body.enterprise-netdisk .drag-drop-overlay {
|
||
background: rgba(37, 99, 235, 0.08);
|
||
border: 2px dashed var(--accent-1);
|
||
border-radius: 8px;
|
||
backdrop-filter: none;
|
||
}
|
||
|
||
body.enterprise-netdisk .drag-drop-content i,
|
||
body.enterprise-netdisk .drag-drop-content div {
|
||
color: var(--accent-1) !important;
|
||
}
|
||
|
||
body.enterprise-netdisk #app [style*="linear-gradient("] {
|
||
background: #ffffff !important;
|
||
color: #1f2937 !important;
|
||
border: 1px solid var(--glass-border) !important;
|
||
box-shadow: none !important;
|
||
}
|
||
|
||
body.enterprise-netdisk #app [style*="background: rgba(255,255,255,0.03)"],
|
||
body.enterprise-netdisk #app [style*="background: rgba(255,255,255,0.05)"],
|
||
body.enterprise-netdisk #app [style*="background: rgba(255,255,255,0.1)"] {
|
||
background: var(--bg-card-hover) !important;
|
||
}
|
||
|
||
body.enterprise-netdisk #app [style*="color: #667eea"] {
|
||
color: var(--accent-1) !important;
|
||
}
|
||
|
||
body.enterprise-netdisk #app [style*="border: 1px solid var(--glass-border)"] {
|
||
border-color: var(--glass-border) !important;
|
||
}
|
||
|
||
@media (max-width: 1024px) {
|
||
body.enterprise-netdisk .main-container {
|
||
margin: 14px auto;
|
||
padding: 0 10px;
|
||
}
|
||
|
||
body.enterprise-netdisk .navbar {
|
||
position: static;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-toolbar {
|
||
align-items: stretch;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-toolbar-meta,
|
||
body.enterprise-netdisk .file-toolbar-main,
|
||
body.enterprise-netdisk .file-toolbar-left,
|
||
body.enterprise-netdisk .file-toolbar-right {
|
||
width: 100%;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-toolbar-meta {
|
||
align-items: flex-start;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-toolbar-meta-path {
|
||
max-width: 100%;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-toolbar-meta-stats {
|
||
width: 100%;
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-toolbar-main {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-toolbar-right {
|
||
justify-content: space-between;
|
||
}
|
||
|
||
body.enterprise-netdisk .view-toggle-total,
|
||
body.enterprise-netdisk .view-toggle-label {
|
||
display: none;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-toolbar-left .btn {
|
||
flex: 1;
|
||
}
|
||
|
||
body.enterprise-netdisk .view-toggle-group {
|
||
width: 100%;
|
||
}
|
||
|
||
body.enterprise-netdisk .view-toggle-group .btn {
|
||
flex: 1;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-page-head {
|
||
padding: 12px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-page-title {
|
||
font-size: 18px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-page-subtitle {
|
||
font-size: 12px;
|
||
}
|
||
|
||
body.enterprise-netdisk .breadcrumb-up {
|
||
width: 100%;
|
||
margin-left: 0;
|
||
}
|
||
|
||
body.enterprise-netdisk .share-toolbar {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
|
||
body.enterprise-netdisk .share-toolbar input,
|
||
body.enterprise-netdisk .share-toolbar select {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
body.enterprise-netdisk .navbar {
|
||
padding: 8px;
|
||
}
|
||
|
||
body.enterprise-netdisk .navbar-brand {
|
||
font-size: 18px;
|
||
}
|
||
|
||
body.enterprise-netdisk .nav-item,
|
||
body.enterprise-netdisk .btn {
|
||
font-size: 12px;
|
||
}
|
||
|
||
body.enterprise-netdisk .card {
|
||
padding: 14px;
|
||
}
|
||
|
||
body.enterprise-netdisk .storage-summary-card,
|
||
body.enterprise-netdisk .breadcrumb-bar,
|
||
body.enterprise-netdisk .files-content-head {
|
||
padding: 10px;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-grid {
|
||
grid-template-columns: repeat(auto-fill, minmax(108px, 1fr));
|
||
gap: 8px;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-grid-item {
|
||
min-height: 140px;
|
||
padding: 10px 8px;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-grid-type-icon {
|
||
font-size: 44px;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-grid-video-icon {
|
||
width: 54px;
|
||
height: 54px;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-grid-video-play {
|
||
font-size: 24px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-empty-actions {
|
||
width: 100%;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-empty-actions .btn {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-list-name-wrap {
|
||
gap: 8px;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-list-name-meta {
|
||
display: block;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-col-time,
|
||
body.enterprise-netdisk .file-list-time-cell {
|
||
display: none;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-col-name {
|
||
width: 58%;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-col-size {
|
||
width: 24%;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-col-actions {
|
||
width: 18%;
|
||
}
|
||
|
||
body.enterprise-netdisk .admin-tabs-nav {
|
||
padding: 8px;
|
||
}
|
||
|
||
body.enterprise-netdisk .admin-tab-btn {
|
||
flex: 1;
|
||
min-width: 78px;
|
||
text-align: center;
|
||
padding: 8px 10px;
|
||
}
|
||
|
||
body.enterprise-netdisk .toast-stack {
|
||
top: 8px;
|
||
right: 8px;
|
||
width: calc(100vw - 16px);
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
body.enterprise-netdisk .file-col-size,
|
||
body.enterprise-netdisk .file-list-size-cell {
|
||
display: none;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-toolbar-meta-path {
|
||
font-size: 12px;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-toolbar-chip {
|
||
font-size: 11px;
|
||
padding: 4px 8px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-empty-actions {
|
||
flex-direction: column;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-empty-actions .btn {
|
||
width: 100%;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-col-name {
|
||
width: auto;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-col-actions {
|
||
width: 74px;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-list-thumb,
|
||
body.enterprise-netdisk .file-list-video-icon {
|
||
width: 30px;
|
||
height: 30px;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-list-name-text {
|
||
font-size: 12px;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-list-name-meta {
|
||
font-size: 10px;
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<style>
|
||
/* ===== Enterprise Mobile Polish v2 ===== */
|
||
body.enterprise-netdisk {
|
||
background:
|
||
radial-gradient(circle at 12% -8%, rgba(59, 130, 246, 0.16), transparent 38%),
|
||
radial-gradient(circle at 88% -4%, rgba(56, 189, 248, 0.14), transparent 34%),
|
||
linear-gradient(180deg, #eef3fb 0%, #f8fafe 34%, #edf2fb 100%) !important;
|
||
}
|
||
|
||
body.enterprise-netdisk .card {
|
||
background: linear-gradient(180deg, #ffffff 0%, #f7fafe 64%, #f3f7ff 100%);
|
||
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.07);
|
||
}
|
||
|
||
body.enterprise-netdisk .files-content-shell {
|
||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.45);
|
||
}
|
||
|
||
body.enterprise-netdisk .files-container {
|
||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.88), rgba(245, 250, 255, 0.96));
|
||
}
|
||
|
||
body.enterprise-netdisk .files-content-shell .file-grid-item {
|
||
transition: transform .16s ease, box-shadow .16s ease, border-color .16s ease;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-list-row:active {
|
||
background: #edf3ff;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
body.enterprise-netdisk .main-container {
|
||
margin: 12px auto;
|
||
padding: 0 10px 14px;
|
||
}
|
||
|
||
body.enterprise-netdisk .navbar {
|
||
padding-top: calc(env(safe-area-inset-top, 0px) + 8px);
|
||
}
|
||
|
||
body.enterprise-netdisk .navbar-menu {
|
||
width: 100%;
|
||
flex-wrap: nowrap;
|
||
overflow-x: auto;
|
||
justify-content: flex-start;
|
||
-webkit-overflow-scrolling: touch;
|
||
scrollbar-width: none;
|
||
padding-bottom: 2px;
|
||
}
|
||
|
||
body.enterprise-netdisk .navbar-menu::-webkit-scrollbar {
|
||
display: none;
|
||
}
|
||
|
||
body.enterprise-netdisk .nav-item,
|
||
body.enterprise-netdisk .user-info,
|
||
body.enterprise-netdisk .navbar-menu .btn-danger {
|
||
flex: 0 0 auto;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-page-head {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
gap: 8px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-page-head-right {
|
||
width: 100%;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-storage-badge {
|
||
width: 100%;
|
||
justify-content: center;
|
||
}
|
||
|
||
body.enterprise-netdisk .storage-summary-card {
|
||
padding: 10px 12px;
|
||
}
|
||
|
||
body.enterprise-netdisk .quota-progress-row {
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
body.enterprise-netdisk .quota-progress-row strong {
|
||
width: 100%;
|
||
text-align: right;
|
||
font-size: 12px;
|
||
}
|
||
|
||
body.enterprise-netdisk .quota-detail-row {
|
||
width: 100%;
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
body.enterprise-netdisk .quota-detail-chip {
|
||
width: 100%;
|
||
justify-content: center;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-toolbar {
|
||
padding: 10px;
|
||
gap: 8px;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-toolbar-left {
|
||
width: 100%;
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 8px;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-toolbar-left .btn {
|
||
min-width: 0;
|
||
justify-content: center;
|
||
padding: 9px 8px;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-toolbar-right {
|
||
gap: 8px;
|
||
}
|
||
|
||
body.enterprise-netdisk .view-toggle-group {
|
||
width: 100%;
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
body.enterprise-netdisk .view-toggle-group .btn {
|
||
min-width: 0;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-content-head {
|
||
padding: 9px 10px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-content-tip {
|
||
width: 100%;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-container {
|
||
padding: 6px 4px 4px;
|
||
min-height: 260px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-content-shell .file-grid {
|
||
padding: 4px;
|
||
gap: 10px;
|
||
grid-template-columns: repeat(auto-fill, minmax(112px, 1fr));
|
||
}
|
||
|
||
body.enterprise-netdisk .file-grid-item {
|
||
min-height: 148px;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-list-row td {
|
||
padding-top: 11px;
|
||
padding-bottom: 11px;
|
||
}
|
||
|
||
body.enterprise-netdisk .mobile-file-sheet {
|
||
padding-bottom: calc(10px + env(safe-area-inset-bottom, 0px));
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
body.enterprise-netdisk .main-container {
|
||
padding: 0 8px 12px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-view-card {
|
||
padding: 9px 8px 7px;
|
||
}
|
||
|
||
body.enterprise-netdisk .card {
|
||
border-radius: 10px;
|
||
padding: 12px;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-toolbar-left .btn {
|
||
font-size: 12px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-content-shell .file-grid {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
body.enterprise-netdisk .quota-detail-row {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
body.enterprise-netdisk .navbar {
|
||
padding-left: 8px;
|
||
padding-right: 8px;
|
||
}
|
||
|
||
body.enterprise-netdisk .navbar-menu {
|
||
gap: 6px;
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<style>
|
||
/* ===== Files View Compact Header v6 ===== */
|
||
body.enterprise-netdisk .files-view-card {
|
||
padding: 10px 12px 10px;
|
||
}
|
||
|
||
body.enterprise-netdisk .breadcrumb-bar {
|
||
margin-bottom: 8px;
|
||
padding: 7px 9px;
|
||
gap: 6px;
|
||
}
|
||
|
||
body.enterprise-netdisk .breadcrumb-up {
|
||
padding: 6px 8px;
|
||
font-size: 11px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-content-head.files-content-head-compact {
|
||
min-height: 40px;
|
||
padding: 6px 8px;
|
||
gap: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-content-head.files-content-head-compact .files-content-title {
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
white-space: nowrap;
|
||
flex: 0 0 auto;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-content-head-meta {
|
||
margin-left: auto;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
flex-wrap: nowrap;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-storage-badge {
|
||
padding: 6px 9px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-usage-progress {
|
||
position: relative;
|
||
width: 260px;
|
||
height: 24px;
|
||
border-radius: 999px;
|
||
background: #e2e8f0;
|
||
border: 1px solid #d6dfec;
|
||
overflow: hidden;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-usage-progress-bar {
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
border-radius: 999px;
|
||
transition: width .25s ease;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-usage-progress-text {
|
||
position: relative;
|
||
z-index: 1;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
color: #1f2937;
|
||
white-space: nowrap;
|
||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.65);
|
||
padding: 0 10px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-content-head-actions {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
flex-wrap: wrap;
|
||
margin-left: auto;
|
||
justify-content: flex-end;
|
||
min-width: 0;
|
||
flex: 1 1 auto;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-content-head-actions > * {
|
||
min-width: 0;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-action-btn {
|
||
min-width: 0;
|
||
padding: 6px 10px;
|
||
font-size: 12px;
|
||
white-space: nowrap;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-folder-btn {
|
||
min-width: 116px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-view-toggle {
|
||
padding: 2px;
|
||
gap: 4px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-view-toggle .btn {
|
||
min-width: 70px;
|
||
padding: 6px 8px;
|
||
font-size: 12px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
@media (max-width: 1180px) {
|
||
body.enterprise-netdisk .files-content-head-meta {
|
||
order: 2;
|
||
width: 100%;
|
||
margin-left: 0;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-usage-progress {
|
||
flex: 1;
|
||
min-width: 210px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-content-head-actions {
|
||
order: 3;
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
body.enterprise-netdisk .files-view-card {
|
||
padding: 9px 9px 8px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-content-head.files-content-head-compact {
|
||
padding: 6px;
|
||
gap: 6px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-content-head.files-content-head-compact .files-content-title {
|
||
width: 100%;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-content-head-meta {
|
||
width: 100%;
|
||
display: grid;
|
||
grid-template-columns: 1fr;
|
||
gap: 6px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-storage-badge {
|
||
justify-content: center;
|
||
width: 100%;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-usage-progress {
|
||
width: 100%;
|
||
min-width: 0;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-usage-progress-text {
|
||
font-size: 10px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-content-head-actions {
|
||
width: 100%;
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 6px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-action-btn,
|
||
body.enterprise-netdisk .files-head-folder-btn {
|
||
width: 100%;
|
||
min-width: 0;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-view-toggle {
|
||
grid-column: 1 / -1;
|
||
width: 100%;
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-view-toggle .btn {
|
||
min-width: 0;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
body.enterprise-netdisk .files-content-head.files-content-head-compact .files-content-title {
|
||
font-size: 11px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-action-btn {
|
||
font-size: 11px;
|
||
padding: 6px 8px;
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<style>
|
||
/* ===== Responsive Scale & Overflow Fix v1 ===== */
|
||
body.enterprise-netdisk,
|
||
body.enterprise-netdisk #app {
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-content-head,
|
||
body.enterprise-netdisk .files-content-head-meta,
|
||
body.enterprise-netdisk .files-content-head-actions,
|
||
body.enterprise-netdisk .files-head-view-toggle,
|
||
body.enterprise-netdisk .files-content-shell {
|
||
min-width: 0;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-usage-progress {
|
||
max-width: 100%;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-usage-progress-text {
|
||
display: block;
|
||
width: 100%;
|
||
max-width: 100%;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
text-align: center;
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
body.enterprise-netdisk .files-content-head-actions {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-view-toggle {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-action-btn,
|
||
body.enterprise-netdisk .files-head-folder-btn,
|
||
body.enterprise-netdisk .files-head-view-toggle .btn {
|
||
min-width: 0;
|
||
}
|
||
|
||
body.enterprise-netdisk .modal-content {
|
||
max-width: calc(100vw - 16px);
|
||
}
|
||
}
|
||
|
||
@media (min-width: 1920px) {
|
||
body.enterprise-netdisk .main-container {
|
||
max-width: 1680px;
|
||
}
|
||
|
||
body.enterprise-netdisk .card {
|
||
padding: 24px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-view-card {
|
||
padding: 14px 16px 12px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-content-head.files-content-head-compact .files-content-title {
|
||
font-size: 14px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-action-btn,
|
||
body.enterprise-netdisk .files-head-view-toggle .btn {
|
||
font-size: 13px;
|
||
padding: 7px 12px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-usage-progress {
|
||
width: 320px;
|
||
height: 26px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-usage-progress-text {
|
||
font-size: 12px;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-grid {
|
||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||
gap: 14px;
|
||
}
|
||
}
|
||
|
||
@media (min-width: 2560px) {
|
||
body.enterprise-netdisk .main-container {
|
||
max-width: 1980px;
|
||
}
|
||
|
||
body.enterprise-netdisk .navbar {
|
||
padding: 14px 28px;
|
||
}
|
||
|
||
body.enterprise-netdisk .card {
|
||
padding: 28px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-usage-progress {
|
||
width: 360px;
|
||
height: 28px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-usage-progress-text {
|
||
font-size: 13px;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-grid {
|
||
grid-template-columns: repeat(auto-fill, minmax(168px, 1fr));
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<style>
|
||
/* ===== App True Large-Screen Scaling v2 ===== */
|
||
@media (min-width: 1920px) {
|
||
body.enterprise-netdisk {
|
||
font-size: 16px;
|
||
}
|
||
|
||
body.enterprise-netdisk .navbar {
|
||
min-height: 76px;
|
||
padding: 12px 26px;
|
||
}
|
||
|
||
body.enterprise-netdisk .main-container {
|
||
max-width: 1840px;
|
||
}
|
||
|
||
body.enterprise-netdisk .card {
|
||
padding: 26px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-view-card {
|
||
padding: 16px 18px 14px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-content-head.files-content-head-compact {
|
||
min-height: 48px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-content-head.files-content-head-compact .files-content-title {
|
||
font-size: 15px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-action-btn,
|
||
body.enterprise-netdisk .files-head-view-toggle .btn {
|
||
font-size: 14px;
|
||
padding: 8px 13px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-usage-progress {
|
||
width: 360px;
|
||
height: 30px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-usage-progress-text {
|
||
font-size: 13px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-container {
|
||
min-height: 420px;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-grid {
|
||
grid-template-columns: repeat(auto-fill, minmax(165px, 1fr));
|
||
gap: 16px;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-grid-item {
|
||
min-height: 205px;
|
||
padding: 16px 10px 12px;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-name {
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
|
||
@media (min-width: 2560px) {
|
||
body.enterprise-netdisk {
|
||
font-size: 17px;
|
||
}
|
||
|
||
body.enterprise-netdisk .navbar {
|
||
min-height: 86px;
|
||
padding: 15px 34px;
|
||
}
|
||
|
||
body.enterprise-netdisk .main-container {
|
||
max-width: 2200px;
|
||
}
|
||
|
||
body.enterprise-netdisk .card {
|
||
padding: 32px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-view-card {
|
||
padding: 20px 24px 18px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-content-head.files-content-head-compact .files-content-title {
|
||
font-size: 17px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-action-btn,
|
||
body.enterprise-netdisk .files-head-view-toggle .btn {
|
||
font-size: 15px;
|
||
padding: 9px 15px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-usage-progress {
|
||
width: 420px;
|
||
height: 34px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-usage-progress-text {
|
||
font-size: 14px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-container {
|
||
min-height: 520px;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-grid {
|
||
grid-template-columns: repeat(auto-fill, minmax(182px, 1fr));
|
||
gap: 18px;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-grid-item {
|
||
min-height: 228px;
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<style>
|
||
/* ===== Files Header Action Layout v3 ===== */
|
||
@media (min-width: 992px) {
|
||
body.enterprise-netdisk .files-content-head.files-content-head-compact {
|
||
display: grid;
|
||
grid-template-columns: max-content minmax(220px, 1fr) minmax(420px, 560px);
|
||
align-items: center;
|
||
column-gap: 10px;
|
||
row-gap: 8px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-content-head-meta {
|
||
width: 100%;
|
||
margin-left: 0;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-content-head-actions {
|
||
width: 100%;
|
||
margin-left: 0;
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
gap: 8px;
|
||
align-items: stretch;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-action-btn,
|
||
body.enterprise-netdisk .files-head-folder-btn,
|
||
body.enterprise-netdisk .files-head-view-toggle {
|
||
width: 100%;
|
||
min-width: 0;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-action-btn,
|
||
body.enterprise-netdisk .files-head-view-toggle .btn {
|
||
height: 36px;
|
||
padding: 0 10px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-view-toggle {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 6px;
|
||
padding: 0;
|
||
border: none;
|
||
background: transparent;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-view-toggle .btn {
|
||
min-width: 0;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 991px) and (min-width: 769px) {
|
||
body.enterprise-netdisk .files-content-head-actions {
|
||
width: 100%;
|
||
margin-left: 0;
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
gap: 8px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-action-btn,
|
||
body.enterprise-netdisk .files-head-folder-btn,
|
||
body.enterprise-netdisk .files-head-view-toggle {
|
||
width: 100%;
|
||
min-width: 0;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-action-btn,
|
||
body.enterprise-netdisk .files-head-view-toggle .btn {
|
||
height: 34px;
|
||
padding: 0 8px;
|
||
}
|
||
|
||
body.enterprise-netdisk .files-head-view-toggle {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 6px;
|
||
padding: 0;
|
||
border: none;
|
||
background: transparent;
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<style>
|
||
/* ===== Mobile Overflow Guard v1 ===== */
|
||
@media (max-width: 768px) {
|
||
body.enterprise-netdisk .card,
|
||
body.enterprise-netdisk .files-view-card,
|
||
body.enterprise-netdisk .files-content-shell,
|
||
body.enterprise-netdisk .files-content-head,
|
||
body.enterprise-netdisk .files-content-head-meta,
|
||
body.enterprise-netdisk .files-content-head-actions,
|
||
body.enterprise-netdisk .settings-section,
|
||
body.enterprise-netdisk .settings-panel,
|
||
body.enterprise-netdisk .settings-subpanel,
|
||
body.enterprise-netdisk .settings-inline-tip,
|
||
body.enterprise-netdisk .settings-storage-switch,
|
||
body.enterprise-netdisk .settings-storage-grid,
|
||
body.enterprise-netdisk .settings-storage-option {
|
||
width: 100%;
|
||
max-width: 100%;
|
||
min-width: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body.enterprise-netdisk .settings-storage-grid {
|
||
grid-template-columns: 1fr !important;
|
||
gap: 10px !important;
|
||
}
|
||
|
||
body.enterprise-netdisk .settings-storage-head {
|
||
align-items: flex-start !important;
|
||
}
|
||
|
||
body.enterprise-netdisk .settings-storage-head > div {
|
||
width: 100%;
|
||
min-width: 0;
|
||
}
|
||
|
||
body.enterprise-netdisk .settings-storage-option [style*="display: flex"][style*="justify-content: space-between"] {
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
align-items: flex-start !important;
|
||
}
|
||
|
||
body.enterprise-netdisk .settings-storage-option button,
|
||
body.enterprise-netdisk .settings-oss-panel button,
|
||
body.enterprise-netdisk .settings-local-panel button {
|
||
max-width: 100%;
|
||
}
|
||
|
||
body.enterprise-netdisk .settings-page-subtitle,
|
||
body.enterprise-netdisk .settings-inline-tip,
|
||
body.enterprise-netdisk .files-content-title,
|
||
body.enterprise-netdisk .files-head-usage-progress-text {
|
||
overflow-wrap: anywhere;
|
||
word-break: break-word;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-list {
|
||
overflow-x: auto;
|
||
-webkit-overflow-scrolling: touch;
|
||
}
|
||
|
||
body.enterprise-netdisk .file-list-table {
|
||
width: 100%;
|
||
min-width: 0;
|
||
}
|
||
|
||
body.enterprise-netdisk .modal-content {
|
||
width: calc(100vw - 16px);
|
||
max-width: calc(100vw - 16px);
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
body.enterprise-netdisk .settings-panel,
|
||
body.enterprise-netdisk .settings-subpanel {
|
||
padding: 10px !important;
|
||
}
|
||
|
||
body.enterprise-netdisk .settings-inline-tip {
|
||
font-size: 12px !important;
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<script src="app.js?v=20260218002"></script>
|
||
</body>
|
||
</html>
|