sync(theme-3): 同步 main 最新 API/Admin 与玩家端逻辑
从 main 同步站内信手动发送、媒体库选择、列表子路由重构等 API/Admin 改动;玩家端仅合并 ADMIN_CUSTOM 富文本、消息预览与充值截图压缩逻辑,保留 theme-3 样式。
This commit is contained in:
@@ -41,6 +41,74 @@ const fileInputRef = ref<HTMLInputElement | null>(null);
|
||||
|
||||
const unusedCount = computed(() => files.value.filter((f) => !f.inUse).length);
|
||||
|
||||
const storageStats = ref<{
|
||||
categories: Array<{ category: string; count: number; sizeBytes: number }>;
|
||||
total: { count: number; sizeBytes: number };
|
||||
} | null>(null);
|
||||
|
||||
const cleanupConfig = ref({ enabled: false, keepDays: 180 });
|
||||
const manualCleanupBefore = ref('');
|
||||
|
||||
function getCategoryLabel(cat: string) {
|
||||
if (cat === 'deposits') return t('media.deposits_on_disk');
|
||||
return categoryLabel(cat);
|
||||
}
|
||||
|
||||
async function loadStorageStats() {
|
||||
try {
|
||||
const res = await api.get('/admin/files/storage-stats');
|
||||
storageStats.value = res.data.data;
|
||||
} catch (err) {
|
||||
console.error('Failed to load storage stats:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCleanupConfig() {
|
||||
try {
|
||||
const res = await api.get('/admin/deposits/screenshot-cleanup-config');
|
||||
cleanupConfig.value = res.data.data;
|
||||
} catch (err) {
|
||||
console.error('Failed to load cleanup config:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveCleanupConfig() {
|
||||
try {
|
||||
const res = await api.put('/admin/deposits/screenshot-cleanup-config', {
|
||||
enabled: cleanupConfig.value.enabled,
|
||||
keepDays: cleanupConfig.value.keepDays,
|
||||
});
|
||||
cleanupConfig.value = res.data.data;
|
||||
ElMessage.success(t('msg.saved'));
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err?.response?.data?.message || t('msg.save_failed'));
|
||||
}
|
||||
}
|
||||
|
||||
async function runManualCleanup() {
|
||||
if (!manualCleanupBefore.value) return;
|
||||
const beforeDate = manualCleanupBefore.value;
|
||||
await ElMessageBox.confirm(
|
||||
`${t('media.delete_confirm')} (${beforeDate} ${t('common.to')})`,
|
||||
{ type: 'warning' }
|
||||
);
|
||||
try {
|
||||
const res = await api.delete(`/admin/deposits/screenshots?before=${beforeDate}T00:00:00.000Z`);
|
||||
const cleaned = res.data.data.cleaned;
|
||||
const freedBytes = res.data.data.freedBytes;
|
||||
|
||||
const msg = t('media.cleanup_result')
|
||||
.replace('{cleaned}', String(cleaned))
|
||||
.replace('{size}', formatSize(freedBytes));
|
||||
|
||||
ElMessage.success(msg);
|
||||
loadStorageStats();
|
||||
loadFiles();
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err?.response?.data?.message || t('msg.delete_failed'));
|
||||
}
|
||||
}
|
||||
|
||||
function categoryLabel(cat: string) {
|
||||
const key = `media.category.${cat}` as const;
|
||||
return t(key as any) || cat;
|
||||
@@ -64,6 +132,7 @@ async function loadFiles(opts?: { silent?: boolean }) {
|
||||
const res = await api.get('/admin/files', { params });
|
||||
files.value = res.data.data.items;
|
||||
total.value = res.data.data.total;
|
||||
void loadStorageStats();
|
||||
} catch {
|
||||
ElMessage.error(t('common.loading'));
|
||||
} finally {
|
||||
@@ -80,9 +149,15 @@ watch(currentPage, () => {
|
||||
void loadFiles();
|
||||
});
|
||||
|
||||
onMounted(() => void loadFiles());
|
||||
onMounted(() => {
|
||||
void loadFiles();
|
||||
void loadCleanupConfig();
|
||||
});
|
||||
onActivated(() => {
|
||||
if (files.value.length > 0) void loadFiles({ silent: true });
|
||||
if (files.value.length > 0) {
|
||||
void loadFiles({ silent: true });
|
||||
void loadCleanupConfig();
|
||||
}
|
||||
});
|
||||
|
||||
async function confirmDelete(file: MediaFile) {
|
||||
@@ -197,6 +272,58 @@ async function doUpload() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Storage Stats -->
|
||||
<div class="stats-banner" v-if="storageStats">
|
||||
<div v-for="item in storageStats.categories" :key="item.category" class="stat-card">
|
||||
<span class="stat-label">{{ getCategoryLabel(item.category) }}</span>
|
||||
<span class="stat-value">{{ item.count }} <span class="stat-unit">{{ t('common.times') }}</span></span>
|
||||
<span class="stat-size">{{ formatSize(item.sizeBytes) }}</span>
|
||||
</div>
|
||||
<div class="stat-card total-card">
|
||||
<span class="stat-label">{{ t('media.storage_stats') }}</span>
|
||||
<span class="stat-value">{{ storageStats.total.count }} <span class="stat-unit">{{ t('common.times') }}</span></span>
|
||||
<span class="stat-size">{{ formatSize(storageStats.total.sizeBytes) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cleanup Panel -->
|
||||
<div class="cleanup-card">
|
||||
<div class="cleanup-title">
|
||||
<span>{{ t('media.screenshot_cleanup') }}</span>
|
||||
</div>
|
||||
<div class="cleanup-grid">
|
||||
<!-- Auto Cleanup Config -->
|
||||
<div class="cleanup-section">
|
||||
<h4 class="section-subtitle">{{ t('media.cleanup_auto_enabled') }}</h4>
|
||||
<div class="config-row">
|
||||
<label class="switch-container">
|
||||
<input type="checkbox" v-model="cleanupConfig.enabled" @change="saveCleanupConfig" />
|
||||
<span class="switch-slider"></span>
|
||||
</label>
|
||||
<div class="days-input-group" v-if="cleanupConfig.enabled">
|
||||
<span>{{ t('media.cleanup_keep_days') }}</span>
|
||||
<input type="number" v-model.number="cleanupConfig.keepDays" min="1" class="num-input" />
|
||||
<button class="btn btn-ghost btn-sm" @click="saveCleanupConfig">{{ t('common.save') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Manual Cleanup -->
|
||||
<div class="cleanup-section manual-section">
|
||||
<h4 class="section-subtitle">{{ t('media.cleanup_before_date') }}</h4>
|
||||
<div class="config-row">
|
||||
<input type="date" v-model="manualCleanupBefore" class="date-input" />
|
||||
<button class="btn btn-primary" :disabled="!manualCleanupBefore" @click="runManualCleanup">
|
||||
{{ t('media.cleanup_run_now') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cleanup-tip">
|
||||
* 仅清理【已同意】或【已拒绝】的充值订单截图,【待处理】的截图绝不会被删除。清理后文件会被替换为已过期占位图。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File grid -->
|
||||
<div v-if="loading" class="state-center">{{ t('common.loading') }}</div>
|
||||
<div v-else-if="files.length === 0" class="state-center muted">{{ t('media.no_files') }}</div>
|
||||
@@ -309,6 +436,185 @@ async function doUpload() {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* ── Storage Stats ── */
|
||||
.stats-banner {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.stat-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
box-shadow: var(--shadow);
|
||||
transition: all 0.15s ease-in-out;
|
||||
}
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(56, 49, 37, 0.06);
|
||||
}
|
||||
.total-card {
|
||||
border-color: var(--primary);
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 550;
|
||||
}
|
||||
.stat-value {
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
color: var(--text);
|
||||
}
|
||||
.stat-unit {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.stat-size {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ── Cleanup Panel ── */
|
||||
.cleanup-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
box-shadow: var(--shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.cleanup-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
color: var(--text);
|
||||
border-bottom: 1px solid var(--border-soft);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.cleanup-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
.cleanup-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.section-subtitle {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
}
|
||||
.config-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.days-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.num-input {
|
||||
width: 75px;
|
||||
padding: 5px 8px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
}
|
||||
.num-input:focus {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
.date-input {
|
||||
padding: 5px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
}
|
||||
.date-input:focus {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
.btn-sm {
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.cleanup-tip {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.4;
|
||||
padding: 8px 12px;
|
||||
background: var(--accent-hover);
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid var(--primary);
|
||||
}
|
||||
|
||||
/* ── Custom Toggle Switch ── */
|
||||
.switch-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 44px;
|
||||
height: 22px;
|
||||
}
|
||||
.switch-container input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
.switch-slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #d5cfc3;
|
||||
transition: .2s;
|
||||
border-radius: 22px;
|
||||
}
|
||||
.switch-slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background-color: white;
|
||||
transition: .2s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
input:checked + .switch-slider {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
input:checked + .switch-slider:before {
|
||||
transform: translateX(22px);
|
||||
}
|
||||
|
||||
/* ── Toolbar ── */
|
||||
.toolbar {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user