feat: add user download traffic reports and restore OSS direct downloads

This commit is contained in:
2026-02-17 17:36:26 +08:00
parent 7687397954
commit 3a22b88f23
4 changed files with 600 additions and 8 deletions

View File

@@ -2330,6 +2330,163 @@
</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 style="display: flex; gap: 8px; align-items: center; flex-wrap: wrap;">
<button
class="btn btn-secondary"
style="padding: 5px 10px; font-size: 12px;"
:class="{ 'btn-primary': downloadTrafficReport.days === 7 }"
@click="setDownloadTrafficReportDays(7)">
近7天
</button>
<button
class="btn btn-secondary"
style="padding: 5px 10px; font-size: 12px;"
:class="{ 'btn-primary': downloadTrafficReport.days === 30 }"
@click="setDownloadTrafficReportDays(30)">
近30天
</button>
<button
class="btn btn-secondary"
style="padding: 5px 10px; font-size: 12px;"
:class="{ 'btn-primary': downloadTrafficReport.days === 90 }"
@click="setDownloadTrafficReportDays(90)">
近90天
</button>
<button
class="btn btn-secondary"
style="padding: 5px 10px; font-size: 12px;"
:class="{ 'btn-primary': downloadTrafficReport.days === 180 }"
@click="setDownloadTrafficReportDays(180)">
近180天
</button>
<button class="btn btn-secondary" style="padding: 5px 10px; font-size: 12px;" @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 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-palette"></i> 界面设置</h3>