feat: refine web workspace UI

This commit is contained in:
237899745
2026-07-21 17:11:18 +08:00
parent 465be72579
commit d07c08c609
4 changed files with 1121 additions and 233 deletions

8
design-qa.md Normal file
View File

@@ -0,0 +1,8 @@
# Design QA
final result: passed
- File workspace: verified at 1280x720 with a full-height drop target and unboxed icon layout.
- Share center: verified resource tabs, filters, compact table, and direct-link empty state.
- Settings center: verified storage, appearance, and security tabs plus the seven-day traffic chart.
- Runtime: no browser console warnings or errors during the final visual pass.

View File

@@ -2014,6 +2014,10 @@
</button>
</div>
</div>
<div v-if="files.length > 0 && !isDragging" class="files-drop-hint" aria-hidden="true">
<i class="fas fa-cloud-upload-alt"></i>
<span>拖放文件到此区域即可上传</span>
</div>
<!-- 拖拽提示层 -->
<div v-if="isDragging" class="drag-drop-overlay">
<div class="drag-drop-content">
@@ -2507,9 +2511,9 @@
</div>
</div>
<!-- 设置视图 -->
<!-- 设置视图 -->
<div v-if="isLoggedIn && currentView === 'settings'" class="main-container">
<div class="card">
<div class="card settings-page-card">
<div class="settings-page-head">
<h2 class="settings-page-title">
<i class="fas fa-sliders-h"></i> 设置中心
@@ -2517,8 +2521,20 @@
<p class="settings-page-subtitle">管理存储策略、主题外观与账号安全</p>
</div>
<nav class="settings-tabs" aria-label="设置分类">
<button v-if="user && !user.is_admin" class="settings-tab" :class="{ active: settingsTab === 'storage' }" @click="settingsTab = 'storage'">
<i class="fas fa-database"></i> 存储管理
</button>
<button class="settings-tab" :class="{ active: settingsTab === 'appearance' }" @click="settingsTab = 'appearance'">
<i class="fas fa-palette"></i> 外观与主题
</button>
<button class="settings-tab" :class="{ active: settingsTab === 'security' }" @click="settingsTab = 'security'">
<i class="fas fa-shield-alt"></i> 账号安全
</button>
</nav>
<!-- 存储管理 - 仅用户可选择 -->
<div v-if="user && !user.is_admin && storagePermission === 'user_choice'" class="settings-section settings-storage-choice" style="margin-bottom: 40px;">
<div v-if="user && !user.is_admin && storagePermission === 'user_choice'" v-show="settingsTab === 'storage'" 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>
@@ -2660,7 +2676,7 @@
</div>
<!-- 本地存储信息 - 仅本地存储权限 -->
<div v-if="user && !user.is_admin && storagePermission === 'local_only'" class="settings-section settings-local-only" style="margin-bottom: 40px;">
<div v-if="user && !user.is_admin && storagePermission === 'local_only'" v-show="settingsTab === 'storage'" 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>
@@ -2695,7 +2711,7 @@
</div>
<!-- OSS 概览 / 配置入口 - 仅OSS权限 -->
<div v-if="user && !user.is_admin && storagePermission === 'oss_only'" class="settings-section settings-oss-only" style="margin-bottom: 40px;">
<div v-if="user && !user.is_admin && storagePermission === 'oss_only'" v-show="settingsTab === 'storage'" 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>
@@ -2796,12 +2812,12 @@
</div>
<!-- 下载流量额度与统计 -->
<div v-if="user && !user.is_admin" class="settings-section settings-download-traffic" style="margin-bottom: 40px;">
<div v-if="user && !user.is_admin" v-show="settingsTab === 'storage'" 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 class="settings-panel settings-traffic-panel" style="background: var(--bg-card); border: 1px solid var(--glass-border); border-radius: 12px; padding: 18px;">
<div class="settings-traffic-toolbar" 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>
@@ -2841,7 +2857,7 @@
<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 class="traffic-kpi-grid" 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;">
@@ -2871,7 +2887,7 @@
</div>
</div>
<div v-if="!downloadTrafficIsUnlimited" style="margin-bottom: 12px;">
<div v-if="!downloadTrafficIsUnlimited" class="traffic-quota-progress" 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>
@@ -2881,7 +2897,7 @@
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 10px; margin-bottom: 12px;">
<div class="traffic-summary-grid" 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;">
@@ -2914,48 +2930,72 @@
</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 class="traffic-visual-grid">
<section class="traffic-chart-card">
<div class="traffic-card-head">
<div>
<strong>近 7 天流量趋势</strong>
<span>每天的下载流量变化</span>
</div>
<span v-if="downloadTrafficReport.summary?.peak_day" class="traffic-peak-pill">
峰值 {{ formatBytes(downloadTrafficReport.summary.peak_day.bytes_used || 0) }}
</span>
</div>
<div v-if="downloadTrafficReport.loading && downloadTrafficChartRows.length === 0" class="traffic-empty-state">
<i class="fas fa-spinner fa-spin"></i> 报表加载中...
</div>
<div v-else-if="downloadTrafficChartRows.length === 0" class="traffic-empty-state">暂无下载流量记录</div>
<div v-else class="traffic-chart-bars">
<div v-for="row in downloadTrafficChartRows" :key="`chart-${row.date}`" class="traffic-bar-column">
<span class="traffic-bar-value">{{ formatBytes(row.bytes_used || 0) }}</span>
<div class="traffic-bar-track" :title="formatReportDateLabel(row.date) + ' · ' + formatBytes(row.bytes_used || 0)">
<span :style="{ height: (Number(row.bytes_used || 0) / downloadTrafficChartMax * 100) + '%' }"></span>
</div>
<span class="traffic-bar-label">{{ formatReportDateLabel(row.date) }}</span>
</div>
</div>
</section>
<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>
<section class="traffic-table-card">
<div class="traffic-card-head">
<div>
<strong>按天用量明细</strong>
<span>最近 {{ downloadTrafficReport.days }} 天</span>
</div>
</div>
<div v-if="downloadTrafficDailyRowsDesc.length === 0" class="traffic-empty-state">暂无下载流量记录</div>
<div v-else class="traffic-table-scroll">
<table>
<thead>
<tr>
<th>日期</th>
<th>下载流量</th>
<th>次数</th>
</tr>
</thead>
<tbody>
<tr v-for="row in downloadTrafficDailyRowsDesc.slice(0, 7)" :key="row.date">
<td>{{ formatReportDateLabel(row.date) }}</td>
<td>{{ formatBytes(row.bytes_used || 0) }}</td>
<td>{{ row.download_count || 0 }}</td>
</tr>
</tbody>
</table>
</div>
</section>
</div>
<div class="traffic-report-note">
<i class="fas fa-info-circle"></i>
预览和在线播放同样计入下载流量OSS 明细来自访问日志,通常有 5-15 分钟延迟。
</div>
</div>
</div>
<!-- 在线设备 -->
<h3 class="settings-section-title settings-section-gap" style="margin: 40px 0 20px 0;">
<h3 v-show="settingsTab === 'security'" 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 v-show="settingsTab === 'security'" class="settings-panel settings-devices-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;">
可查看当前账号已登录设备,并支持远程强制下线
@@ -3013,8 +3053,8 @@
</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;">
<h3 v-show="settingsTab === 'appearance'" class="settings-section-title settings-section-gap" style="margin: 40px 0 20px 0;"><i class="fas fa-palette"></i> 界面设置</h3>
<div v-show="settingsTab === 'appearance'" 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>
@@ -3049,9 +3089,9 @@
</div>
<!-- 账号设置 -->
<h3 class="settings-section-title settings-section-gap" style="margin: 40px 0 20px 0;"><i class="fas fa-user-cog"></i> 账号设置</h3>
<h3 v-show="settingsTab === 'security'" 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">
<div v-show="settingsTab === 'security'" class="settings-panel settings-account-panel">
<!-- 管理员可以改用户名 -->
<form v-if="user && user.is_admin" @submit.prevent="updateUsername" style="margin-bottom: 30px;">
<div class="form-group">
@@ -3091,204 +3131,166 @@
<p>集中查看分享状态、访问数据和文件直链。</p>
</div>
<div class="shares-page-actions">
<div class="view-toggle-group shares-view-toggle" aria-label="分享视图切换">
<button class="btn" :class="shareViewMode === 'grid' ? 'btn-primary' : 'btn-secondary'" @click="shareViewMode = 'grid'" title="卡片视图">
<i class="fas fa-th-large"></i><span>卡片</span>
</button>
<button class="btn" :class="shareViewMode === 'list' ? 'btn-primary' : 'btn-secondary'" @click="shareViewMode = 'list'" title="列表视图">
<i class="fas fa-list"></i><span>列表</span>
</button>
</div>
<button class="btn btn-secondary" @click="refreshShareResources">
<i class="fas fa-sync-alt"></i> 刷新
</button>
<button class="btn btn-primary" @click="switchView('files')">
<i class="fas fa-plus"></i> 创建分享
</button>
</div>
</header>
<div class="share-metrics">
<div class="share-metric">
<span class="share-metric-icon blue"><i class="fas fa-share-alt"></i></span>
<span><small>分享链接</small><strong>{{ shares.length }}</strong></span>
</div>
<div class="share-metric">
<span class="share-metric-icon teal"><i class="fas fa-link"></i></span>
<span><small>文件直链</small><strong>{{ directLinks.length }}</strong></span>
</div>
<div class="share-metric share-metric-wide">
<span class="share-metric-icon amber"><i class="fas fa-layer-group"></i></span>
<span><small>共享资源总数</small><strong>{{ shares.length + directLinks.length }}</strong></span>
</div>
</div>
<nav class="share-resource-tabs" aria-label="分享资源分类">
<button :class="{ active: shareResourceTab === 'shares' }" @click="shareResourceTab = 'shares'">
<i class="fas fa-share-alt"></i> 分享链接
<span>{{ shares.length }}</span>
</button>
<button :class="{ active: shareResourceTab === 'directLinks' }" @click="shareResourceTab = 'directLinks'">
<i class="fas fa-link"></i> 文件直链
<span>{{ directLinks.length }}</span>
</button>
</nav>
<!-- 筛选/搜索 -->
<div class="share-toolbar shares-toolbar">
<div class="share-search-field">
<i class="fas fa-search"></i>
<input type="text" v-model="shareFilters.keyword" placeholder="搜索文件名、链接或分享码">
<section class="share-resource-card">
<div class="share-inline-stats">
<span><i class="fas fa-layer-group"></i> 共享资源 <strong>{{ shares.length + directLinks.length }}</strong></span>
<span><i class="fas fa-eye"></i> 累计访问 <strong>{{ shareTotalViews }}</strong></span>
<span><i class="fas fa-shield-alt"></i> 状态 <strong>运行正常</strong></span>
</div>
<select v-model="shareFilters.type" aria-label="分享类型">
<option value="all">全部类型</option>
<option value="file">文件</option>
<option value="directory">文件夹</option>
<option value="all_files">全部文件</option>
</select>
<select v-model="shareFilters.status" aria-label="分享状态">
<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" aria-label="分享排序">
<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>
<section class="share-section">
<div class="share-section-head">
<div>
<span class="share-section-icon"><i class="fas fa-share-alt"></i></span>
<span><strong>分享链接</strong><small>可设置有效期、访问密码并查看使用情况</small></span>
<div class="share-toolbar shares-toolbar">
<div class="share-search-field">
<i class="fas fa-search"></i>
<input type="text" v-model="shareFilters.keyword" :placeholder="shareResourceTab === 'shares' ? '搜索文件名、链接或分享码' : '搜索文件名或直链地址'">
</div>
<span class="share-section-count">{{ filteredShares.length }} 项</span>
<template v-if="shareResourceTab === 'shares'">
<select v-model="shareFilters.type" aria-label="分享类型">
<option value="all">全部类型</option>
<option value="file">文件</option>
<option value="directory">文件夹</option>
<option value="all_files">全部文件</option>
</select>
<select v-model="shareFilters.status" aria-label="分享状态">
<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" aria-label="分享排序">
<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>
</template>
</div>
<div v-if="shares.length === 0" class="share-empty-state">
<span class="share-empty-icon"><i class="fas fa-paper-plane"></i></span>
<strong>还没有创建分享</strong>
<p>前往我的文件,选择文件或文件夹后即可生成分享链接。</p>
<button class="btn btn-primary" @click="switchView('files')"><i class="fas fa-folder-open"></i> 前往我的文件</button>
</div>
<div v-else-if="filteredShares.length === 0" class="share-empty-state compact">
<span class="share-empty-icon"><i class="fas fa-filter-circle-xmark"></i></span>
<strong>没有匹配的分享</strong>
<p>请调整上方的关键词或筛选条件。</p>
</div>
<!-- 大图标视图 -->
<div v-else-if="shareViewMode === 'grid'" class="share-card-grid">
<article v-for="share in filteredShares" :key="share.id" class="share-card">
<div class="share-card__title">
<span class="share-card__type"><i class="fas" :class="getShareTypeIcon(share)"></i></span>
<span :title="share.share_path">{{ getPathBaseName(share.share_path) }}</span>
<div v-if="shareResourceTab === 'shares'" class="share-resource-panel">
<div class="share-section-head">
<div>
<span class="share-section-icon"><i class="fas fa-share-alt"></i></span>
<span><strong>分享链接</strong><small>管理有效期、访问密码和使用数据</small></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>
</div>
<div class="share-card__url">
<i class="fas fa-link"></i>
<span class="share-card__url-link" :title="share.share_url">{{ share.share_url }}</span>
<span class="share-code">{{ share.share_code }}</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 btn-danger share-card__delete-btn" @click.stop="requestDeleteShare(share.id, $event)"><i class="fas fa-trash"></i> 删除</button>
</div>
</article>
</div>
<!-- 列表视图 -->
<div v-else class="share-table-wrap">
<table class="share-list-table">
<thead>
<tr>
<th class="share-col-name">文件名</th>
<th class="share-col-link">链接地址</th>
<th class="share-col-number">访问</th>
<th class="share-col-number">下载</th>
<th class="share-col-expire">有效期</th>
<th class="share-col-actions">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="share in filteredShares" :key="share.id">
<td :title="share.share_path"><span class="share-list-resource"><i class="fas" :class="getShareTypeIcon(share)"></i>{{ getPathBaseName(share.share_path) }}</span></td>
<td><span class="share-list-link-text" :title="share.share_url">{{ share.share_url }}</span></td>
<td class="share-number-cell">{{ share.view_count }}</td>
<td class="share-number-cell">{{ share.download_count }}</td>
<td class="share-expire-cell">
<span v-if="!share.expires_at" class="share-forever"><i class="fas fa-infinity"></i> 永久有效</span>
<span v-else :style="{color: isExpiringSoon(share.expires_at) ? '#d97706' : isExpired(share.expires_at) ? '#dc2626' : '#2878ff'}" :title="share.expires_at"><i class="fas fa-clock"></i> {{ formatExpireTime(share.expires_at) }}</span>
</td>
<td><div class="share-list-actions">
<button class="btn btn-secondary" @click.stop="openShare(share.share_url)" title="打开"><i class="fas fa-up-right-from-square"></i></button>
<button class="btn btn-secondary" @click.stop="copyShareLink(share.share_url)" title="复制链接"><i class="fas fa-copy"></i></button>
<button class="btn btn-danger" @click.stop="requestDeleteShare(share.id, $event)" title="删除"><i class="fas fa-trash"></i></button>
</div></td>
</tr>
</tbody>
</table>
</div>
</section>
<!-- 直链管理(独立于普通分享) -->
<section class="share-section direct-link-section">
<div class="share-section-head">
<div>
<span class="share-section-icon teal"><i class="fas fa-link"></i></span>
<span><strong>文件直链</strong><small>适合外部引用和直接下载,可在文件右键菜单中创建</small></span>
<span class="share-section-count">{{ filteredShares.length }} 项</span>
</div>
<div v-if="shares.length === 0" class="share-empty-state">
<span class="share-empty-icon"><i class="fas fa-paper-plane"></i></span>
<strong>还没有创建分享</strong>
<p>前往我的文件,选择文件或文件夹后即可生成分享链接。</p>
<button class="btn btn-primary" @click="switchView('files')"><i class="fas fa-folder-open"></i> 前往我的文件</button>
</div>
<div v-else-if="filteredShares.length === 0" class="share-empty-state compact">
<span class="share-empty-icon"><i class="fas fa-filter-circle-xmark"></i></span>
<strong>没有匹配的分享</strong>
<p>请调整上方的关键词或筛选条件。</p>
</div>
<div v-else class="share-table-wrap">
<table class="share-list-table">
<thead>
<tr>
<th class="share-col-name">文件名</th>
<th class="share-col-link">链接地址</th>
<th class="share-col-number">访问</th>
<th class="share-col-number">下载</th>
<th class="share-col-expire">有效期</th>
<th class="share-col-actions">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="share in filteredShares" :key="share.id">
<td :title="share.share_path"><span class="share-list-resource"><i class="fas" :class="getShareTypeIcon(share)"></i>{{ getPathBaseName(share.share_path) }}</span></td>
<td><span class="share-list-link-text" :title="share.share_url">{{ share.share_url }}</span></td>
<td class="share-number-cell">{{ share.view_count }}</td>
<td class="share-number-cell">{{ share.download_count }}</td>
<td class="share-expire-cell">
<span v-if="!share.expires_at" class="share-forever"><i class="fas fa-infinity"></i> 永久有效</span>
<span v-else :style="{color: isExpiringSoon(share.expires_at) ? '#d97706' : isExpired(share.expires_at) ? '#dc2626' : '#2878ff'}" :title="share.expires_at"><i class="fas fa-clock"></i> {{ formatExpireTime(share.expires_at) }}</span>
</td>
<td><div class="share-list-actions">
<button class="btn btn-secondary" @click.stop="openShare(share.share_url)" title="打开"><i class="fas fa-up-right-from-square"></i></button>
<button class="btn btn-secondary" @click.stop="copyShareLink(share.share_url)" title="复制链接"><i class="fas fa-copy"></i></button>
<button class="btn btn-danger" @click.stop="requestDeleteShare(share.id, $event)" title="删除"><i class="fas fa-trash"></i></button>
</div></td>
</tr>
</tbody>
</table>
</div>
<span class="share-section-count">{{ filteredDirectLinks.length }} 项</span>
</div>
<div v-if="directLinksLoading" class="share-loading-state"><i class="fas fa-spinner fa-spin"></i> 正在加载直链列表...</div>
<div v-else-if="directLinks.length === 0" class="share-empty-state compact">
<span class="share-empty-icon teal"><i class="fas fa-link-slash"></i></span>
<strong>还没有创建直链</strong>
<p>在文件列表中右键目标文件,选择“生成直链”。</p>
</div>
<div v-else-if="filteredDirectLinks.length === 0" class="share-empty-state compact">
<span class="share-empty-icon"><i class="fas fa-search"></i></span>
<strong>没有匹配的直链</strong>
<p>请调整上方搜索关键词。</p>
</div>
<div v-else class="share-table-wrap">
<table class="share-list-table">
<thead>
<tr>
<th class="share-col-name">文件名</th>
<th class="share-col-link">链接地址</th>
<th class="share-col-number">访问</th>
<th class="share-col-number">下载</th>
<th class="share-col-expire">有效期</th>
<th class="share-col-actions">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="link in filteredDirectLinks" :key="`direct-${link.id}`">
<td :title="link.file_path"><span class="share-list-resource"><i class="fas fa-file"></i>{{ getPathBaseName(link.file_path, link.file_name) }}</span></td>
<td><span class="share-list-link-text" :title="link.direct_url">{{ link.direct_url }}</span></td>
<td class="share-number-cell muted" title="直链暂不统计访问次数"></td>
<td class="share-number-cell">{{ link.download_count || 0 }}</td>
<td class="share-expire-cell">
<span v-if="!link.expires_at" class="share-forever"><i class="fas fa-infinity"></i> 永久有效</span>
<span v-else :style="{color: isExpiringSoon(link.expires_at) ? '#d97706' : isExpired(link.expires_at) ? '#dc2626' : '#2878ff'}" :title="link.expires_at"><i class="fas fa-clock"></i> {{ formatExpireTime(link.expires_at) }}</span>
</td>
<td><div class="share-list-actions">
<button class="btn btn-secondary" @click.stop="openShare(link.direct_url)" title="打开"><i class="fas fa-up-right-from-square"></i></button>
<button class="btn btn-secondary" @click.stop="copyDirectLink(link.direct_url)" title="复制链接"><i class="fas fa-copy"></i></button>
<button class="btn btn-danger" @click.stop="requestDeleteDirectLink(link.id, $event)" title="删除"><i class="fas fa-trash"></i></button>
</div></td>
</tr>
</tbody>
</table>
<div v-else class="share-resource-panel direct-link-section">
<div class="share-section-head">
<div>
<span class="share-section-icon teal"><i class="fas fa-link"></i></span>
<span><strong>文件直链</strong><small>适合外部引用和直接下载</small></span>
</div>
<span class="share-section-count">{{ filteredDirectLinks.length }} 项</span>
</div>
<div v-if="directLinksLoading" class="share-loading-state"><i class="fas fa-spinner fa-spin"></i> 正在加载直链列表...</div>
<div v-else-if="directLinks.length === 0" class="share-empty-state compact">
<span class="share-empty-icon teal"><i class="fas fa-link-slash"></i></span>
<strong>还没有创建直链</strong>
<p>在文件列表中右键目标文件,选择“生成直链”。</p>
</div>
<div v-else-if="filteredDirectLinks.length === 0" class="share-empty-state compact">
<span class="share-empty-icon"><i class="fas fa-search"></i></span>
<strong>没有匹配的直链</strong>
<p>请调整上方搜索关键词。</p>
</div>
<div v-else class="share-table-wrap">
<table class="share-list-table">
<thead>
<tr>
<th class="share-col-name">文件名</th>
<th class="share-col-link">链接地址</th>
<th class="share-col-number">访问</th>
<th class="share-col-number">下载</th>
<th class="share-col-expire">有效期</th>
<th class="share-col-actions">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="link in filteredDirectLinks" :key="`direct-${link.id}`">
<td :title="link.file_path"><span class="share-list-resource"><i class="fas fa-file"></i>{{ getPathBaseName(link.file_path, link.file_name) }}</span></td>
<td><span class="share-list-link-text" :title="link.direct_url">{{ link.direct_url }}</span></td>
<td class="share-number-cell muted" title="直链暂不统计访问次数"></td>
<td class="share-number-cell">{{ link.download_count || 0 }}</td>
<td class="share-expire-cell">
<span v-if="!link.expires_at" class="share-forever"><i class="fas fa-infinity"></i> 永久有效</span>
<span v-else :style="{color: isExpiringSoon(link.expires_at) ? '#d97706' : isExpired(link.expires_at) ? '#dc2626' : '#2878ff'}" :title="link.expires_at"><i class="fas fa-clock"></i> {{ formatExpireTime(link.expires_at) }}</span>
</td>
<td><div class="share-list-actions">
<button class="btn btn-secondary" @click.stop="openShare(link.direct_url)" title="打开"><i class="fas fa-up-right-from-square"></i></button>
<button class="btn btn-secondary" @click.stop="copyDirectLink(link.direct_url)" title="复制链接"><i class="fas fa-copy"></i></button>
<button class="btn btn-danger" @click.stop="requestDeleteDirectLink(link.id, $event)" title="删除"><i class="fas fa-trash"></i></button>
</div></td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
</div>
@@ -7669,7 +7671,7 @@
}
</style>
<link rel="stylesheet" href="workspace.css?v=20260721005">
<script src="app.js?v=20260721005"></script>
<link rel="stylesheet" href="workspace.css?v=20260721006">
<script src="app.js?v=20260721006"></script>
</body>
</html>

View File

@@ -29,6 +29,8 @@ createApp({
isLogin: true,
fileViewMode: 'grid', // 文件显示模式: grid 大图标, list 列表
shareViewMode: 'list', // 分享显示模式: grid 大图标, list 列表
shareResourceTab: 'shares', // 分享中心资源标签: shares / directLinks
settingsTab: 'storage', // 设置中心标签: storage / appearance / security
debugMode: false, // 调试模式(管理员可切换)
adminTab: initialAdminTab, // 管理员页面当前标签overview, settings, monitor, users
@@ -565,6 +567,24 @@ createApp({
return [...rows].reverse();
},
downloadTrafficChartRows() {
const rows = Array.isArray(this.downloadTrafficReport?.daily)
? this.downloadTrafficReport.daily
: [];
return rows.slice(-7);
},
downloadTrafficChartMax() {
return Math.max(
1,
...this.downloadTrafficChartRows.map(row => Number(row?.bytes_used || 0))
);
},
shareTotalViews() {
return this.shares.reduce((total, share) => total + Number(share?.view_count || 0), 0);
},
// 存储类型显示文本
storageTypeText() {
return this.storageType === 'local' ? '本地存储' : 'OSS存储';
@@ -4209,6 +4229,9 @@ handleDragLeave(e) {
}
break;
case 'settings':
if (this.user?.is_admin && this.settingsTab === 'storage') {
this.settingsTab = 'appearance';
}
this.loadOnlineDevices();
if (this.user && !this.user.is_admin) {
this.loadOssConfig();

View File

@@ -1837,3 +1837,858 @@ body.enterprise-netdisk .share-list-actions .btn {
animation-iteration-count: 1 !important;
}
}
/* Selected workspace direction: compact file canvas, tabbed shares and settings. */
body.enterprise-netdisk .files-view-card {
height: calc(100vh - 62px);
min-height: 680px;
display: flex;
flex-direction: column;
}
body.enterprise-netdisk .files-page-header {
flex: 0 0 auto;
padding: 22px 26px 20px;
}
body.enterprise-netdisk .files-page-header .workspace-page-heading h1 {
font-size: 27px;
}
body.enterprise-netdisk .files-content-shell {
min-height: 0;
flex: 1;
display: flex;
flex-direction: column;
padding: 16px 24px 22px;
}
body.enterprise-netdisk .files-command-bar,
body.enterprise-netdisk .files-search-wrap,
body.enterprise-netdisk .files-list-heading {
flex: 0 0 auto;
}
body.enterprise-netdisk .files-search-wrap {
margin-top: 11px;
}
body.enterprise-netdisk .files-search-row {
padding: 7px;
border-radius: 11px;
}
body.enterprise-netdisk .files-search-field,
body.enterprise-netdisk .files-search-type,
body.enterprise-netdisk .files-search-button {
min-height: 38px;
}
body.enterprise-netdisk .files-list-heading {
min-height: 43px;
}
body.enterprise-netdisk .files-container {
min-height: 360px;
flex: 1;
overflow: auto;
padding: 8px 10px 54px;
border-radius: 12px;
background:
linear-gradient(rgba(255, 255, 255, 0.96), rgba(255, 255, 255, 0.96)),
radial-gradient(circle at 50% 42%, rgba(40, 120, 255, 0.08), transparent 22rem);
transition: border-color 160ms ease, background 160ms ease, box-shadow 160ms ease;
}
body.enterprise-netdisk .files-container.drag-over {
border-color: #72a8ff;
background: #f5f9ff;
box-shadow: inset 0 0 0 2px rgba(40, 120, 255, 0.12);
}
body.enterprise-netdisk .files-content-shell .file-grid {
min-height: 100%;
padding: 18px 10px;
align-content: start;
justify-content: start;
grid-template-columns: repeat(auto-fill, 138px);
grid-auto-rows: 132px;
gap: 8px 14px;
}
body.enterprise-netdisk .files-content-shell .file-grid-item {
width: 138px;
min-height: 132px;
padding: 10px 8px 9px;
justify-content: flex-start;
border: 0;
border-radius: 10px;
background: transparent;
box-shadow: none;
transform: none;
}
body.enterprise-netdisk .files-content-shell .file-grid-item:hover {
border: 0;
background: #f3f7fc;
box-shadow: none;
transform: none;
}
body.enterprise-netdisk .files-content-shell .file-grid-item:active {
background: #eaf2ff;
}
body.enterprise-netdisk .file-icon {
min-height: 68px;
margin: 0 0 4px;
}
body.enterprise-netdisk .file-grid-type-icon {
font-size: 54px;
filter: none;
}
body.enterprise-netdisk .file-type-default {
color: #6c7d96 !important;
}
body.enterprise-netdisk .file-type-folder {
color: #e8ae42 !important;
}
body.enterprise-netdisk .file-type-pdf {
color: #e15d62 !important;
}
body.enterprise-netdisk .file-type-word {
color: #397bd8 !important;
}
body.enterprise-netdisk .file-type-excel {
color: #2b9c70 !important;
}
body.enterprise-netdisk .file-type-archive {
color: #b07d50 !important;
}
body.enterprise-netdisk .file-type-audio {
color: #7886c7 !important;
}
body.enterprise-netdisk .file-thumbnail {
width: 68px;
height: 62px;
border-radius: 9px;
box-shadow: 0 5px 14px rgba(38, 58, 90, 0.09);
}
body.enterprise-netdisk .files-content-shell .file-name {
min-height: 20px;
color: #10203a;
font-size: 13px;
font-weight: 750;
}
body.enterprise-netdisk .files-content-shell .file-size {
margin-top: 2px;
color: #72839d;
font-size: 11px;
}
body.enterprise-netdisk .files-drop-hint {
position: absolute;
left: 50%;
bottom: 14px;
z-index: 2;
padding: 6px 11px;
display: inline-flex;
align-items: center;
gap: 7px;
border: 1px solid #e1e9f5;
border-radius: 999px;
color: #8492a8;
background: rgba(249, 251, 254, 0.92);
font-size: 10px;
pointer-events: none;
transform: translateX(-50%);
}
body.enterprise-netdisk .files-drop-hint i {
color: #5f97ec;
}
body.enterprise-netdisk .drag-drop-overlay {
position: absolute !important;
inset: 0 !important;
z-index: 25 !important;
min-height: 100% !important;
display: grid !important;
place-items: center !important;
border: 0 !important;
border-radius: 11px !important;
background: rgba(240, 247, 255, 0.96) !important;
backdrop-filter: blur(3px);
}
body.enterprise-netdisk .drag-drop-content {
padding: 26px 34px;
text-align: center;
border: 1px dashed #79aaff;
border-radius: 18px;
background: #fff;
box-shadow: 0 18px 40px rgba(32, 84, 165, 0.12);
}
body.enterprise-netdisk .drag-drop-content > i {
margin-bottom: 12px !important;
color: var(--ww-blue) !important;
font-size: 44px !important;
}
body.enterprise-netdisk .drag-drop-content > div:first-of-type {
margin-bottom: 4px !important;
font-size: 17px !important;
}
/* Shares */
body.enterprise-netdisk .shares-page-card {
min-height: calc(100vh - 62px);
display: flex;
flex-direction: column;
}
body.enterprise-netdisk .shares-page-head {
padding: 24px 28px 20px;
}
body.enterprise-netdisk .share-resource-tabs {
min-height: 55px;
padding: 0 28px;
display: flex;
align-items: flex-end;
gap: 26px;
border-bottom: 1px solid var(--ww-line);
background: #fff;
}
body.enterprise-netdisk .share-resource-tabs button {
position: relative;
min-height: 54px;
padding: 0 2px;
display: inline-flex;
align-items: center;
gap: 8px;
border: 0;
color: #74839a;
background: transparent;
font-size: 13px;
font-weight: 700;
cursor: pointer;
}
body.enterprise-netdisk .share-resource-tabs button::after {
content: "";
position: absolute;
right: 0;
bottom: -1px;
left: 0;
height: 3px;
border-radius: 3px 3px 0 0;
background: transparent;
}
body.enterprise-netdisk .share-resource-tabs button.active {
color: var(--ww-blue);
}
body.enterprise-netdisk .share-resource-tabs button.active::after {
background: var(--ww-blue);
}
body.enterprise-netdisk .share-resource-tabs button > span {
min-width: 21px;
height: 20px;
padding: 0 6px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
color: #718097;
background: #edf2f8;
font-size: 10px;
}
body.enterprise-netdisk .share-resource-tabs button.active > span {
color: #1763e6;
background: #e8f1ff;
}
body.enterprise-netdisk .share-resource-card {
min-height: 0;
margin: 22px 28px 28px;
display: flex;
flex: 1;
flex-direction: column;
overflow: hidden;
border: 1px solid var(--ww-line);
border-radius: 14px;
background: #fff;
}
body.enterprise-netdisk .share-inline-stats {
min-height: 52px;
padding: 0 17px;
display: flex;
align-items: center;
gap: 24px;
border-bottom: 1px solid var(--ww-line);
color: #74839a;
background: #fbfcfe;
font-size: 11px;
}
body.enterprise-netdisk .share-inline-stats > span {
display: inline-flex;
align-items: center;
gap: 7px;
}
body.enterprise-netdisk .share-inline-stats i {
color: #7896c4;
}
body.enterprise-netdisk .share-inline-stats strong {
color: #23344f;
font-size: 12px;
}
body.enterprise-netdisk .share-resource-card .share-toolbar {
margin: 14px 16px;
padding: 7px;
border-radius: 11px;
}
body.enterprise-netdisk .share-resource-card .share-search-field,
body.enterprise-netdisk .share-resource-card .share-toolbar select {
min-height: 38px;
}
body.enterprise-netdisk .share-resource-card .share-search-field input,
body.enterprise-netdisk .share-resource-card .share-search-field input:focus {
min-height: 34px;
padding: 0 !important;
border: 0 !important;
border-radius: 0 !important;
outline: 0 !important;
background: transparent !important;
box-shadow: none !important;
}
body.enterprise-netdisk .share-resource-panel {
min-height: 260px;
flex: 1;
overflow: auto;
border-top: 1px solid var(--ww-line);
}
body.enterprise-netdisk .share-resource-panel .share-section-head {
min-height: 58px;
padding: 9px 16px;
position: sticky;
top: 0;
z-index: 3;
}
body.enterprise-netdisk .share-resource-panel .share-section-icon {
width: 34px;
height: 34px;
flex-basis: 34px;
}
body.enterprise-netdisk .share-resource-panel .share-list-table td {
height: 56px;
}
/* Settings */
body.enterprise-netdisk .settings-page-card {
min-height: calc(100vh - 62px);
padding: 0 !important;
overflow: hidden;
}
body.enterprise-netdisk .settings-page-head {
padding: 25px 30px 21px;
border-bottom: 1px solid var(--ww-line);
background:
radial-gradient(circle at 92% 0%, rgba(40, 120, 255, 0.07), transparent 19rem),
#fff;
}
body.enterprise-netdisk .settings-page-title {
margin: 0;
color: var(--ww-ink);
font-size: 27px;
font-weight: 800;
line-height: 1.2;
letter-spacing: -0.02em;
}
body.enterprise-netdisk .settings-page-title i {
margin-right: 8px;
color: var(--ww-blue);
font-size: 20px;
}
body.enterprise-netdisk .settings-page-subtitle {
margin: 6px 0 0;
color: var(--ww-muted);
font-size: 12px;
}
body.enterprise-netdisk .settings-tabs {
min-height: 57px;
padding: 0 30px;
display: flex;
align-items: flex-end;
gap: 28px;
border-bottom: 1px solid var(--ww-line);
background: #fff;
}
body.enterprise-netdisk .settings-tab {
position: relative;
min-height: 56px;
padding: 0 2px;
display: inline-flex;
align-items: center;
gap: 8px;
border: 0;
color: #74839a;
background: transparent;
font-size: 13px;
font-weight: 700;
cursor: pointer;
}
body.enterprise-netdisk .settings-tab::after {
content: "";
position: absolute;
right: 0;
bottom: -1px;
left: 0;
height: 3px;
border-radius: 3px 3px 0 0;
background: transparent;
}
body.enterprise-netdisk .settings-tab.active {
color: var(--ww-blue);
}
body.enterprise-netdisk .settings-tab.active::after {
background: var(--ww-blue);
}
body.enterprise-netdisk .settings-page-card > .settings-section,
body.enterprise-netdisk .settings-page-card > .settings-section-title,
body.enterprise-netdisk .settings-page-card > .settings-panel {
margin-right: 28px !important;
margin-left: 28px !important;
}
body.enterprise-netdisk .settings-page-card > .settings-section {
margin-top: 24px !important;
margin-bottom: 28px !important;
}
body.enterprise-netdisk .settings-page-card > .settings-section-title {
margin-top: 24px !important;
margin-bottom: 13px !important;
}
body.enterprise-netdisk .settings-section-title {
color: #24354f;
font-size: 14px;
font-weight: 800;
}
body.enterprise-netdisk .settings-section-title i {
width: 19px;
color: var(--ww-blue);
}
body.enterprise-netdisk .settings-storage-choice > .settings-section-title,
body.enterprise-netdisk .settings-download-traffic > .settings-section-title,
body.enterprise-netdisk .settings-local-only > .settings-section-title,
body.enterprise-netdisk .settings-oss-only > .settings-section-title {
margin-bottom: 13px !important;
}
body.enterprise-netdisk .settings-storage-panel,
body.enterprise-netdisk .settings-local-panel,
body.enterprise-netdisk .settings-oss-panel,
body.enterprise-netdisk .settings-traffic-panel,
body.enterprise-netdisk .settings-devices-panel,
body.enterprise-netdisk .settings-theme-panel,
body.enterprise-netdisk .settings-account-panel {
border: 1px solid var(--ww-line) !important;
border-radius: 14px !important;
background: #fff !important;
box-shadow: 0 10px 30px rgba(31, 49, 78, 0.06) !important;
}
body.enterprise-netdisk .settings-storage-panel {
padding: 18px !important;
}
body.enterprise-netdisk .settings-storage-switch {
padding: 9px !important;
border-color: #e6ebf3 !important;
background: #f8fafd !important;
}
body.enterprise-netdisk .settings-storage-grid {
grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
gap: 12px !important;
}
body.enterprise-netdisk .settings-storage-option {
padding: 15px !important;
border-color: var(--ww-line) !important;
border-radius: 11px !important;
background: #fbfcfe !important;
box-shadow: none !important;
}
body.enterprise-netdisk .settings-storage-option:has(button:disabled) {
border-color: #b9d1f6 !important;
background: #f6f9ff !important;
}
body.enterprise-netdisk .settings-inline-tip {
border: 0 !important;
color: #65758d !important;
background: #f7f9fc !important;
}
body.enterprise-netdisk .settings-traffic-panel {
padding: 18px !important;
}
body.enterprise-netdisk .settings-traffic-toolbar {
margin-bottom: 14px !important;
}
body.enterprise-netdisk .download-traffic-range-actions {
gap: 5px;
}
body.enterprise-netdisk .download-traffic-range-btn {
min-height: 32px;
padding: 0 10px;
border-radius: 8px;
font-size: 11px;
}
body.enterprise-netdisk .download-traffic-range-btn-active {
color: #fff !important;
border-color: var(--ww-blue) !important;
background: var(--ww-blue) !important;
}
body.enterprise-netdisk .traffic-kpi-grid {
grid-template-columns: repeat(4, minmax(0, 1fr)) !important;
gap: 9px !important;
}
body.enterprise-netdisk .traffic-kpi-grid > div {
min-height: 84px;
padding: 12px 13px !important;
border-radius: 10px !important;
box-shadow: none;
}
body.enterprise-netdisk .traffic-quota-progress {
padding: 8px 2px 4px;
}
body.enterprise-netdisk .traffic-summary-grid {
grid-template-columns: repeat(5, minmax(0, 1fr)) !important;
gap: 8px !important;
}
body.enterprise-netdisk .traffic-summary-grid > div {
padding: 9px 10px !important;
border-color: #e5eaf2 !important;
background: #fafbfd !important;
}
body.enterprise-netdisk .traffic-visual-grid {
display: grid;
grid-template-columns: minmax(0, 1.55fr) minmax(320px, 0.8fr);
gap: 10px;
}
body.enterprise-netdisk .traffic-chart-card,
body.enterprise-netdisk .traffic-table-card {
min-width: 0;
padding: 13px;
border: 1px solid var(--ww-line);
border-radius: 11px;
background: #fff;
}
body.enterprise-netdisk .traffic-card-head {
min-height: 39px;
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
body.enterprise-netdisk .traffic-card-head > div {
display: flex;
flex-direction: column;
}
body.enterprise-netdisk .traffic-card-head strong {
color: #263750;
font-size: 12px;
font-weight: 800;
}
body.enterprise-netdisk .traffic-card-head span {
color: #8a98ab;
font-size: 9px;
}
body.enterprise-netdisk .traffic-peak-pill {
padding: 4px 7px;
border-radius: 999px;
color: #1763e6 !important;
background: #eaf2ff;
font-weight: 700;
}
body.enterprise-netdisk .traffic-chart-bars {
height: 178px;
padding: 15px 4px 0;
display: grid;
grid-template-columns: repeat(7, minmax(30px, 1fr));
align-items: end;
gap: 8px;
border-bottom: 1px solid #e6ebf2;
background: repeating-linear-gradient(to bottom, transparent 0, transparent 43px, #f0f3f8 44px);
}
body.enterprise-netdisk .traffic-bar-column {
height: 160px;
min-width: 0;
display: grid;
grid-template-rows: 18px 116px 20px;
align-items: end;
justify-items: center;
}
body.enterprise-netdisk .traffic-bar-value,
body.enterprise-netdisk .traffic-bar-label {
max-width: 100%;
overflow: hidden;
color: #8795a9;
font-size: 8px;
text-overflow: ellipsis;
white-space: nowrap;
}
body.enterprise-netdisk .traffic-bar-track {
width: min(34px, 68%);
height: 112px;
display: flex;
align-items: flex-end;
border-radius: 6px 6px 0 0;
}
body.enterprise-netdisk .traffic-bar-track > span {
width: 100%;
min-height: 4px;
display: block;
border-radius: 6px 6px 2px 2px;
background: linear-gradient(180deg, #4f91ff 0%, #2878ff 100%);
box-shadow: 0 5px 12px rgba(40, 120, 255, 0.16);
}
body.enterprise-netdisk .traffic-table-scroll {
max-height: 193px;
overflow: auto;
}
body.enterprise-netdisk .traffic-table-scroll table {
width: 100%;
border-collapse: collapse;
}
body.enterprise-netdisk .traffic-table-scroll th,
body.enterprise-netdisk .traffic-table-scroll td {
height: 29px;
padding: 0 7px;
border-bottom: 1px solid #edf1f6;
color: #53637a;
font-size: 9px;
text-align: right;
}
body.enterprise-netdisk .traffic-table-scroll th {
color: #8a98ab;
background: #fafbfd;
font-weight: 700;
}
body.enterprise-netdisk .traffic-table-scroll th:first-child,
body.enterprise-netdisk .traffic-table-scroll td:first-child {
text-align: left;
}
body.enterprise-netdisk .traffic-table-scroll td:nth-child(2) {
color: #263750;
font-weight: 700;
}
body.enterprise-netdisk .traffic-empty-state {
min-height: 168px;
display: flex;
align-items: center;
justify-content: center;
gap: 7px;
color: var(--ww-muted);
font-size: 11px;
}
body.enterprise-netdisk .traffic-report-note {
margin-top: 9px;
color: #8492a7;
font-size: 10px;
}
body.enterprise-netdisk .traffic-report-note i {
margin-right: 5px;
color: #6797df;
}
body.enterprise-netdisk .settings-theme-panel,
body.enterprise-netdisk .settings-devices-panel,
body.enterprise-netdisk .settings-account-panel {
padding: 18px !important;
margin-bottom: 28px !important;
}
@media (max-width: 1180px) {
body.enterprise-netdisk .traffic-kpi-grid {
grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
}
body.enterprise-netdisk .traffic-summary-grid {
grid-template-columns: repeat(3, minmax(0, 1fr)) !important;
}
body.enterprise-netdisk .traffic-visual-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 900px) {
body.enterprise-netdisk .files-view-card,
body.enterprise-netdisk .shares-page-card,
body.enterprise-netdisk .settings-page-card {
height: auto;
min-height: calc(100vh - 94px);
}
body.enterprise-netdisk .files-content-shell .file-grid {
grid-template-columns: repeat(auto-fill, 128px);
}
body.enterprise-netdisk .files-content-shell .file-grid-item {
width: 128px;
}
body.enterprise-netdisk .settings-storage-grid {
grid-template-columns: 1fr !important;
}
}
@media (max-width: 680px) {
body.enterprise-netdisk .files-page-header,
body.enterprise-netdisk .shares-page-head,
body.enterprise-netdisk .settings-page-head {
padding: 19px 17px 17px;
}
body.enterprise-netdisk .files-content-shell {
padding: 13px 14px 16px;
}
body.enterprise-netdisk .files-content-shell .file-grid {
padding-inline: 0;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 6px;
}
body.enterprise-netdisk .files-content-shell .file-grid-item {
width: auto;
min-height: 126px;
}
body.enterprise-netdisk .files-drop-hint {
width: max-content;
max-width: calc(100% - 28px);
}
body.enterprise-netdisk .share-resource-tabs,
body.enterprise-netdisk .settings-tabs {
padding: 0 17px;
gap: 18px;
overflow-x: auto;
}
body.enterprise-netdisk .share-resource-card {
margin: 16px 14px 18px;
}
body.enterprise-netdisk .share-inline-stats {
min-height: 48px;
gap: 12px;
overflow-x: auto;
white-space: nowrap;
}
body.enterprise-netdisk .share-resource-card .share-toolbar {
display: grid;
grid-template-columns: 1fr;
}
body.enterprise-netdisk .share-resource-card .share-toolbar select {
width: 100%;
}
body.enterprise-netdisk .settings-page-card > .settings-section,
body.enterprise-netdisk .settings-page-card > .settings-section-title,
body.enterprise-netdisk .settings-page-card > .settings-panel {
margin-right: 14px !important;
margin-left: 14px !important;
}
body.enterprise-netdisk .traffic-kpi-grid,
body.enterprise-netdisk .traffic-summary-grid {
grid-template-columns: 1fr !important;
}
body.enterprise-netdisk .traffic-chart-bars {
gap: 4px;
}
body.enterprise-netdisk .traffic-bar-value {
display: none;
}
}