feat: enhance download traffic quota lifecycle controls
This commit is contained in:
@@ -3190,6 +3190,12 @@
|
||||
不限
|
||||
</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 v-if="u.is_banned" style="color: #ef4444; font-weight: 600;">已封禁</span>
|
||||
@@ -3534,29 +3540,81 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">下载流量配额</label>
|
||||
<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">
|
||||
不限流量
|
||||
</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_unlimited" style="display: flex; gap: 10px;">
|
||||
|
||||
<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">
|
||||
不限流量
|
||||
</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="1"
|
||||
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_traffic_quota_value"
|
||||
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_unit" style="width: 100px;">
|
||||
<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;">
|
||||
下载流量范围: 不限 或 1MB - 10TB(按实际下载字节扣减)
|
||||
下载流量支持直接设置、增减操作,范围: 不限 或 1MB - 10TB
|
||||
</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;">
|
||||
到期后自动恢复为不限流量并清零已用流量;留空表示永不过期
|
||||
</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>
|
||||
|
||||
@@ -3568,7 +3626,10 @@
|
||||
({{ 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_unlimited ? '不限' : (editStorageForm.download_traffic_quota_value + ' ' + editStorageForm.download_quota_unit) }}
|
||||
• 下载策略: {{ 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>
|
||||
|
||||
|
||||
117
frontend/app.js
117
frontend/app.js
@@ -275,7 +275,13 @@ createApp({
|
||||
download_traffic_quota_value: 1, // 下载流量配额数值
|
||||
download_quota_unit: 'GB', // 下载流量单位:MB / GB / TB
|
||||
download_quota_unlimited: true, // 下载流量:true=不限
|
||||
download_traffic_used: 0 // 下载流量已使用(字节)
|
||||
download_traffic_used: 0, // 下载流量已使用(字节)
|
||||
download_quota_operation: 'set', // set/increase/decrease
|
||||
download_quota_adjust_value: 1, // 增减额度数值
|
||||
download_quota_adjust_unit: 'GB', // 增减额度单位
|
||||
download_quota_expires_at: '', // 到期时间(datetime-local)
|
||||
download_quota_reset_cycle: 'none', // none/daily/weekly/monthly
|
||||
reset_download_used_now: false // 保存时立即重置已用流量
|
||||
},
|
||||
|
||||
// 服务器存储统计
|
||||
@@ -2943,6 +2949,12 @@ handleDragLeave(e) {
|
||||
this.editStorageForm.download_traffic_used = Number.isFinite(downloadUsedBytes) && downloadUsedBytes > 0
|
||||
? Math.floor(downloadUsedBytes)
|
||||
: 0;
|
||||
this.editStorageForm.download_quota_operation = 'set';
|
||||
this.editStorageForm.download_quota_adjust_value = 1;
|
||||
this.editStorageForm.download_quota_adjust_unit = 'GB';
|
||||
this.editStorageForm.download_quota_reset_cycle = user.download_traffic_reset_cycle || 'none';
|
||||
this.editStorageForm.download_quota_expires_at = this.toDateTimeLocalInput(user.download_traffic_quota_expires_at);
|
||||
this.editStorageForm.reset_download_used_now = false;
|
||||
|
||||
this.editStorageForm.download_quota_unlimited = effectiveDownloadQuotaBytes <= 0;
|
||||
if (effectiveDownloadQuotaBytes <= 0) {
|
||||
@@ -2993,31 +3005,65 @@ handleDragLeave(e) {
|
||||
ossQuotaBytes = this.editStorageForm.oss_storage_quota_value * 1024 * 1024;
|
||||
}
|
||||
|
||||
// 计算下载流量配额(字节,0表示不限)
|
||||
let downloadQuotaBytes = 0;
|
||||
if (!this.editStorageForm.download_quota_unlimited) {
|
||||
if (!this.editStorageForm.download_traffic_quota_value || this.editStorageForm.download_traffic_quota_value < 1) {
|
||||
this.showToast('error', '参数错误', '下载流量配额必须大于 0,或选择不限流量');
|
||||
const toBytes = (value, unit) => {
|
||||
if (unit === 'TB') return value * 1024 * 1024 * 1024 * 1024;
|
||||
if (unit === 'GB') return value * 1024 * 1024 * 1024;
|
||||
return value * 1024 * 1024;
|
||||
};
|
||||
|
||||
// 下载流量:支持直接设置 / 增加 / 删减
|
||||
let downloadQuotaBytes = null;
|
||||
let downloadTrafficDelta = null;
|
||||
if (this.editStorageForm.download_quota_operation === 'set') {
|
||||
downloadQuotaBytes = 0;
|
||||
if (!this.editStorageForm.download_quota_unlimited) {
|
||||
if (!this.editStorageForm.download_traffic_quota_value || this.editStorageForm.download_traffic_quota_value < 1) {
|
||||
this.showToast('error', '参数错误', '下载流量配额必须大于 0,或选择不限流量');
|
||||
return;
|
||||
}
|
||||
downloadQuotaBytes = toBytes(
|
||||
this.editStorageForm.download_traffic_quota_value,
|
||||
this.editStorageForm.download_quota_unit
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (!this.editStorageForm.download_quota_adjust_value || this.editStorageForm.download_quota_adjust_value < 1) {
|
||||
this.showToast('error', '参数错误', '下载流量增减值必须大于 0');
|
||||
return;
|
||||
}
|
||||
const adjustBytes = toBytes(
|
||||
this.editStorageForm.download_quota_adjust_value,
|
||||
this.editStorageForm.download_quota_adjust_unit
|
||||
);
|
||||
downloadTrafficDelta = this.editStorageForm.download_quota_operation === 'increase'
|
||||
? adjustBytes
|
||||
: -adjustBytes;
|
||||
}
|
||||
|
||||
if (this.editStorageForm.download_quota_unit === 'TB') {
|
||||
downloadQuotaBytes = this.editStorageForm.download_traffic_quota_value * 1024 * 1024 * 1024 * 1024;
|
||||
} else if (this.editStorageForm.download_quota_unit === 'GB') {
|
||||
downloadQuotaBytes = this.editStorageForm.download_traffic_quota_value * 1024 * 1024 * 1024;
|
||||
} else {
|
||||
downloadQuotaBytes = this.editStorageForm.download_traffic_quota_value * 1024 * 1024;
|
||||
}
|
||||
const downloadQuotaExpiresAt = this.normalizeDateTimeLocalToApi(this.editStorageForm.download_quota_expires_at);
|
||||
if (this.editStorageForm.download_quota_expires_at && !downloadQuotaExpiresAt) {
|
||||
this.showToast('error', '参数错误', '下载流量到期时间格式无效');
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
storage_permission: this.editStorageForm.storage_permission,
|
||||
local_storage_quota: localQuotaBytes,
|
||||
oss_storage_quota: ossQuotaBytes,
|
||||
download_traffic_quota_expires_at: downloadQuotaExpiresAt,
|
||||
download_traffic_reset_cycle: this.editStorageForm.download_quota_reset_cycle || 'none',
|
||||
reset_download_traffic_used: !!this.editStorageForm.reset_download_used_now
|
||||
};
|
||||
if (downloadQuotaBytes !== null) {
|
||||
payload.download_traffic_quota = downloadQuotaBytes;
|
||||
}
|
||||
if (downloadTrafficDelta !== null) {
|
||||
payload.download_traffic_delta = downloadTrafficDelta;
|
||||
}
|
||||
|
||||
const response = await axios.post(
|
||||
`${this.apiBase}/api/admin/users/${this.editStorageForm.userId}/storage-permission`,
|
||||
{
|
||||
storage_permission: this.editStorageForm.storage_permission,
|
||||
local_storage_quota: localQuotaBytes,
|
||||
oss_storage_quota: ossQuotaBytes,
|
||||
download_traffic_quota: downloadQuotaBytes
|
||||
},
|
||||
payload,
|
||||
);
|
||||
|
||||
if (response.data.success) {
|
||||
@@ -3065,6 +3111,39 @@ handleDragLeave(e) {
|
||||
return Math.min(100, Math.round((used / quota) * 100));
|
||||
},
|
||||
|
||||
getDownloadResetCycleText(cycle) {
|
||||
if (cycle === 'daily') return '每日重置';
|
||||
if (cycle === 'weekly') return '每周重置';
|
||||
if (cycle === 'monthly') return '每月重置';
|
||||
return '不自动重置';
|
||||
},
|
||||
|
||||
toDateTimeLocalInput(dateString) {
|
||||
if (!dateString) return '';
|
||||
const normalized = String(dateString).trim().replace(' ', 'T');
|
||||
const match = normalized.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2})/);
|
||||
if (match) return match[1];
|
||||
|
||||
const parsed = new Date(normalized);
|
||||
if (Number.isNaN(parsed.getTime())) return '';
|
||||
const year = parsed.getFullYear();
|
||||
const month = String(parsed.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(parsed.getDate()).padStart(2, '0');
|
||||
const hours = String(parsed.getHours()).padStart(2, '0');
|
||||
const minutes = String(parsed.getMinutes()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
||||
},
|
||||
|
||||
normalizeDateTimeLocalToApi(localValue) {
|
||||
if (!localValue) return null;
|
||||
const normalized = String(localValue).trim();
|
||||
if (!normalized) return null;
|
||||
const fullMatch = normalized.match(/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})(?::(\d{2}))?$/);
|
||||
if (!fullMatch) return null;
|
||||
const seconds = fullMatch[3] || '00';
|
||||
return `${fullMatch[1]} ${fullMatch[2]}:${seconds}`;
|
||||
},
|
||||
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '-';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user