feat: 统一文件上传接口 + 模板图片展示 + 导航页 + 侧边栏调整
This commit is contained in:
390
web/dist/admin/index.html
vendored
390
web/dist/admin/index.html
vendored
@@ -139,11 +139,61 @@ tbody tr:last-child td{border-bottom:none}
|
||||
.spinner{display:inline-block;width:32px;height:32px;border:3px solid var(--border);border-top-color:var(--primary);border-radius:50%;animation:spin .8s linear infinite}
|
||||
@keyframes spin{to{transform:rotate(360deg)}}
|
||||
|
||||
/* Content nav tabs */
|
||||
.content-tabs{display:flex;gap:4px;margin-bottom:16px;flex-wrap:wrap}
|
||||
.content-tab{padding:8px 16px;border-radius:var(--radius);background:#fff;border:1px solid var(--border);cursor:pointer;font-size:13px;font-weight:600;transition:all .2s;color:var(--text-muted)}
|
||||
.content-tab:hover{background:#f8fafc}
|
||||
.content-tab.active{background:var(--primary);color:#fff;border-color:var(--primary)}
|
||||
/* Content layout: sidebar + main */
|
||||
.content-layout{display:flex;gap:16px;align-items:flex-start}
|
||||
.content-sidebar{width:220px;flex-shrink:0;background:var(--card-bg);border-radius:var(--radius);box-shadow:var(--shadow);overflow:hidden;position:sticky;top:80px}
|
||||
.content-group{border-bottom:1px solid var(--border)}
|
||||
.content-group:last-child{border-bottom:none}
|
||||
.content-group-header{display:flex;align-items:center;gap:8px;padding:12px 16px;cursor:pointer;font-size:13px;font-weight:700;color:var(--text);transition:background .2s;user-select:none}
|
||||
.content-group-header:hover{background:#f8fafc}
|
||||
.content-group-header .arrow{margin-left:auto;transition:transform .25s;font-size:12px;color:var(--text-muted)}
|
||||
.content-group.expanded .content-group-header .arrow{transform:rotate(90deg)}
|
||||
.content-group-items{max-height:0;overflow:hidden;transition:max-height .3s ease}
|
||||
.content-group.expanded .content-group-items{max-height:400px}
|
||||
.content-item{padding:8px 16px 8px 40px;cursor:pointer;font-size:13px;color:var(--text-muted);transition:all .2s;border-left:3px solid transparent}
|
||||
.content-item:hover{background:var(--primary-light);color:var(--primary)}
|
||||
.content-item.active{background:var(--primary);color:#fff;border-left-color:#818cf8;font-weight:600}
|
||||
.content-item .count{float:right;font-size:11px;opacity:.7}
|
||||
.content-main{flex:1;min-width:0}
|
||||
|
||||
/* Upload page */
|
||||
.upload-area{border:2px dashed var(--border);border-radius:12px;padding:40px;text-align:center;cursor:pointer;transition:all .2s;background:#fafbfc}
|
||||
.upload-area:hover{border-color:var(--primary);background:var(--primary-light)}
|
||||
.upload-area.dragover{border-color:var(--primary);background:var(--primary-light);transform:scale(1.01)}
|
||||
.upload-area .upload-icon{font-size:48px;margin-bottom:12px}
|
||||
.upload-area .upload-text{font-size:15px;color:var(--text-muted)}
|
||||
.upload-area .upload-hint{font-size:12px;color:var(--text-muted);margin-top:8px}
|
||||
.upload-formats{display:flex;gap:8px;justify-content:center;margin-top:12px}
|
||||
.fmt-tag{display:inline-block;padding:3px 12px;border-radius:6px;font-size:12px;font-weight:700;letter-spacing:.5px}
|
||||
.fmt-tag:nth-child(1){background:#fef3c7;color:#92400e}
|
||||
.fmt-tag:nth-child(2){background:#dbeafe;color:#1e40af}
|
||||
.fmt-tag:nth-child(3){background:#fce7f3;color:#9d174d}
|
||||
.fmt-tag:nth-child(4){background:#d1fae5;color:#065f46}
|
||||
.fmt-tag:nth-child(5){background:#ede9fe;color:#5b21b6}
|
||||
.fmt-tag:nth-child(6){background:#ffedd5;color:#9a3412}
|
||||
.fmt-tag:nth-child(7){background:#fee2e2;color:#991b1b}
|
||||
.module-tabs{display:flex;gap:4px;margin-bottom:16px;flex-wrap:wrap}
|
||||
.module-tab{padding:8px 16px;border-radius:var(--radius);background:#fff;border:1px solid var(--border);cursor:pointer;font-size:13px;font-weight:600;transition:all .2s;color:var(--text-muted)}
|
||||
.module-tab:hover{background:#f8fafc}
|
||||
.module-tab.active{background:var(--primary);color:#fff;border-color:var(--primary)}
|
||||
.file-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px;margin-top:16px}
|
||||
.file-card{background:var(--card-bg);border-radius:var(--radius);box-shadow:var(--shadow);overflow:hidden;transition:transform .2s,box-shadow .2s;position:relative}
|
||||
.file-card:hover{transform:translateY(-2px);box-shadow:var(--shadow-lg)}
|
||||
.file-card .file-preview{width:100%;height:120px;background:#f8fafc;display:flex;align-items:center;justify-content:center;overflow:hidden}
|
||||
.file-card .file-preview img{width:100%;height:100%;object-fit:cover}
|
||||
.file-card .file-preview .file-icon{font-size:36px}
|
||||
.file-card .file-info{padding:8px 12px}
|
||||
.file-card .file-name{font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.file-card .file-meta{font-size:11px;color:var(--text-muted);margin-top:2px}
|
||||
.file-card .file-actions{position:absolute;top:4px;right:4px;display:flex;gap:4px;opacity:0;transition:opacity .2s}
|
||||
.file-card:hover .file-actions{opacity:1}
|
||||
.file-card .file-actions button{background:rgba(255,255,255,.9);border:none;border-radius:4px;width:28px;height:28px;cursor:pointer;font-size:14px;display:flex;align-items:center;justify-content:center}
|
||||
.file-card .file-actions button:hover{background:#fff}
|
||||
.file-card .copy-url{position:absolute;bottom:4px;right:4px;opacity:0;transition:opacity .2s}
|
||||
.file-card:hover .copy-url{opacity:1}
|
||||
.upload-progress{margin-top:12px}
|
||||
.upload-progress-bar{height:6px;background:var(--border);border-radius:3px;overflow:hidden}
|
||||
.upload-progress-fill{height:100%;background:var(--primary);transition:width .3s}
|
||||
|
||||
/* Image cell */
|
||||
.cell-img{width:40px;height:40px;border-radius:6px;object-fit:cover}
|
||||
@@ -190,10 +240,6 @@ tbody tr:last-child td{border-bottom:none}
|
||||
<h2><span class="logo">G</span> Gotest</h2>
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
<div class="sidebar-section">概览</div>
|
||||
<a class="sidebar-link" onclick="navigate('dashboard')" data-page="dashboard">
|
||||
<span class="icon">📊</span> 仪表盘
|
||||
</a>
|
||||
<div class="sidebar-section">管理</div>
|
||||
<a class="sidebar-link" onclick="navigate('users')" data-page="users">
|
||||
<span class="icon">👥</span> 用户管理
|
||||
@@ -219,7 +265,7 @@ tbody tr:last-child td{border-bottom:none}
|
||||
<header class="topbar">
|
||||
<div class="topbar-left">
|
||||
<button class="sidebar-toggle" onclick="toggleSidebar()">☰</button>
|
||||
<h3 id="pageTitle">仪表盘</h3>
|
||||
<h3 id="pageTitle">内容管理</h3>
|
||||
</div>
|
||||
<div class="topbar-right">
|
||||
<div class="topbar-user">
|
||||
@@ -336,7 +382,7 @@ function showApp() {
|
||||
document.getElementById('app').style.display = 'flex';
|
||||
document.getElementById('topbarUsername').textContent = currentUser.username;
|
||||
document.getElementById('topbarAvatar').textContent = currentUser.username[0].toUpperCase();
|
||||
navigate('dashboard');
|
||||
navigate('content');
|
||||
}
|
||||
|
||||
// ===== 导航 =====
|
||||
@@ -345,10 +391,9 @@ function navigate(page, params = {}) {
|
||||
document.querySelectorAll('.sidebar-link').forEach(el => el.classList.remove('active'));
|
||||
const link = document.querySelector(`[data-page="${page}"]`);
|
||||
if (link) link.classList.add('active');
|
||||
const titles = { dashboard: '仪表盘', users: '用户管理', content: '内容管理', settings: '系统设置', sites: '站点导航' };
|
||||
const titles = { users: '用户管理', content: '内容管理', settings: '系统设置', sites: '站点导航' };
|
||||
document.getElementById('pageTitle').textContent = titles[page] || page;
|
||||
if (page === 'dashboard') renderDashboard();
|
||||
else if (page === 'users') renderUsers();
|
||||
if (page === 'users') renderUsers();
|
||||
else if (page === 'content') renderContent(params.table);
|
||||
else if (page === 'settings') renderSettings();
|
||||
else if (page === 'sites') renderSites();
|
||||
@@ -357,69 +402,6 @@ function navigate(page, params = {}) {
|
||||
|
||||
function toggleSidebar() { document.getElementById('sidebar').classList.toggle('open'); }
|
||||
|
||||
// ===== 仪表盘 =====
|
||||
async function renderDashboard() {
|
||||
const el = document.getElementById('content');
|
||||
el.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
||||
try {
|
||||
const data = await api('/admin/dashboard/stats');
|
||||
const g = data.data.grouped;
|
||||
const iconMap = { enterprise: '🏢', shop: '🛒', food: '🍜', game: '🎮', app: '📱', music: '🎵' };
|
||||
const colorMap = { enterprise: 'bg-indigo', shop: 'bg-green', food: 'bg-orange', game: 'bg-purple', app: 'bg-blue', music: 'bg-pink' };
|
||||
|
||||
let statsHtml = `
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon bg-cyan">👥</div>
|
||||
<div class="stat-value">${data.data.total_counts.users || 0}</div>
|
||||
<div class="stat-label">总用户数</div>
|
||||
</div>`;
|
||||
|
||||
let groupedHtml = '';
|
||||
for (const [key, val] of Object.entries(g)) {
|
||||
const items = val.items;
|
||||
let total = 0;
|
||||
for (const v of Object.values(items)) total += (v || 0);
|
||||
statsHtml += `
|
||||
<div class="stat-card" onclick="navigate('content',{table:'${Object.keys(items)[0]}'})" style="cursor:pointer">
|
||||
<div class="stat-icon ${colorMap[key]}">${iconMap[key]}</div>
|
||||
<div class="stat-value">${total}</div>
|
||||
<div class="stat-label">${val.label}</div>
|
||||
</div>`;
|
||||
|
||||
groupedHtml += `
|
||||
<div class="table-card" style="margin-bottom:16px">
|
||||
<div class="section-header" style="padding:16px">
|
||||
<h4>${iconMap[key]} ${val.label}</h4>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<tbody>`;
|
||||
const itemLabels = {
|
||||
company: '企业信息', news: '新闻', services: '服务', team: '团队',
|
||||
products: '商品', categories: '分类', restaurants: '餐厅',
|
||||
menu_items: '菜品', menu_categories: '菜品分类',
|
||||
games: '游戏', apps: '应用', songs: '歌曲'
|
||||
};
|
||||
for (const [itemKey, itemVal] of Object.entries(items)) {
|
||||
const tableKey = key + '_' + itemKey;
|
||||
groupedHtml += `
|
||||
<tr style="cursor:pointer" onclick="navigate('content',{table:'${tableKey}'})">
|
||||
<td style="font-weight:600">${itemLabels[itemKey] || itemKey}</td>
|
||||
<td style="text-align:right"><span class="badge-status badge-active">${itemVal || 0} 条</span></td>
|
||||
<td style="text-align:right;width:60px">→</td>
|
||||
</tr>`;
|
||||
}
|
||||
groupedHtml += `</tbody></table></div></div>`;
|
||||
}
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="stats-grid">${statsHtml}</div>
|
||||
<div class="section-header"><h4>📈 各模块数据概览</h4></div>
|
||||
${groupedHtml}
|
||||
`;
|
||||
} catch (e) { el.innerHTML = '<div class="loading">加载失败,请刷新重试</div>'; }
|
||||
}
|
||||
|
||||
// ===== 用户管理 =====
|
||||
let userPage = 1, userPageSize = 20, userKeyword = '';
|
||||
async function renderUsers(page) {
|
||||
@@ -582,15 +564,27 @@ function renderContent(table) {
|
||||
|
||||
const el = document.getElementById('content');
|
||||
|
||||
// 渲染 tabs
|
||||
let tabsHtml = '';
|
||||
// 构建左侧可下拉分组导航
|
||||
let sidebarHtml = '';
|
||||
for (const mod of contentModules) {
|
||||
// 判断当前选中表是否属于该模块,属于则默认展开
|
||||
const isActive = mod.tables.some(t => t.key === currentTable);
|
||||
sidebarHtml += `<div class="content-group ${isActive?'expanded':''}">
|
||||
<div class="content-group-header" onclick="this.parentElement.classList.toggle('expanded')">
|
||||
<span>${mod.icon}</span> <span>${mod.group}</span>
|
||||
<span class="arrow">▶</span>
|
||||
</div>
|
||||
<div class="content-group-items">`;
|
||||
for (const t of mod.tables) {
|
||||
tabsHtml += `<div class="content-tab ${t.key===currentTable?'active':''}" onclick="navigate('content',{table:'${t.key}'})">${mod.icon} ${t.label}</div>`;
|
||||
sidebarHtml += `<div class="content-item ${t.key===currentTable?'active':''}" onclick="navigate('content',{table:'${t.key}'})">${t.label}</div>`;
|
||||
}
|
||||
sidebarHtml += `</div></div>`;
|
||||
}
|
||||
|
||||
el.innerHTML = `<div class="content-tabs">${tabsHtml}</div><div id="contentTable"><div class="loading"><div class="spinner"></div></div></div>`;
|
||||
el.innerHTML = `<div class="content-layout">
|
||||
<div class="content-sidebar">${sidebarHtml}</div>
|
||||
<div class="content-main"><div id="contentTable"><div class="loading"><div class="spinner"></div></div></div></div>
|
||||
</div>`;
|
||||
loadContentTable();
|
||||
}
|
||||
|
||||
@@ -661,15 +655,26 @@ function openContentModal(id) {
|
||||
const fields = allKeys.length > 0 ? allKeys : ['name'];
|
||||
|
||||
let formHtml = '';
|
||||
const imageFields = ['image','cover','logo','avatar','icon','images','screenshots'];
|
||||
for (const key of fields) {
|
||||
const val = item ? item[key] : '';
|
||||
const isLongText = ['description','content','bio','vision','mission','notice','lyrics'].includes(key);
|
||||
const isNumber = ['price','original_price','stock','sales','rating','sort','status','category_id','restaurant_id','plays','downloads','reviews','play_count','like_count','duration','delivery_time','min_order','delivery_fee','founded_year'].includes(key);
|
||||
const isBool = ['is_hot','is_new','is_featured'].includes(key);
|
||||
const isImage = imageFields.includes(key);
|
||||
|
||||
if (isBool) {
|
||||
formHtml += `<div class="form-group"><label>${formatKey(key)}</label>
|
||||
<select id="f_${key}"><option value="false" ${val!==true?'selected':''}>否</option><option value="true" ${val===true?'selected':''}>是</option></select></div>`;
|
||||
} else if (isImage) {
|
||||
const mod = currentTable.split('_')[0];
|
||||
formHtml += `<div class="form-group"><label>${formatKey(key)}</label>
|
||||
<div style="display:flex;gap:8px;align-items:center">
|
||||
<input type="text" id="f_${key}" value="${val??''}" placeholder="输入URL或点击上传" style="flex:1">
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="uploadToField('${key}','${mod}')" title="支持 JPG/PNG/GIF/WebP/SVG/MP4/PDF,最大 10MB">📤 上传</button>
|
||||
</div>
|
||||
<div style="font-size:11px;color:var(--text-muted);margin-top:4px">支持格式: JPG · PNG · GIF · WebP · SVG · MP4 · PDF,最大 10MB</div>
|
||||
${val ? `<img id="preview_${key}" src="${val}" style="margin-top:8px;max-width:120px;max-height:80px;border-radius:6px;border:1px solid var(--border)" onerror="this.style.display='none'">` : ''}</div>`;
|
||||
} else if (isLongText) {
|
||||
formHtml += `<div class="form-group"><label>${formatKey(key)}</label><textarea id="f_${key}">${val||''}</textarea></div>`;
|
||||
} else if (isNumber) {
|
||||
@@ -724,6 +729,229 @@ async function deleteContent(id) {
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// ===== 文件上传 =====
|
||||
let uploadModule = 'common';
|
||||
const moduleList = [
|
||||
{ key: 'enterprise', label: '🏢 企业官网' },
|
||||
{ key: 'shop', label: '🛒 跨境电商' },
|
||||
{ key: 'food', label: '🍜 外卖平台' },
|
||||
{ key: 'game', label: '🎮 游戏平台' },
|
||||
{ key: 'app', label: '📱 应用商店' },
|
||||
{ key: 'music', label: '🎵 音乐平台' },
|
||||
{ key: 'common', label: '📂 公共资源' },
|
||||
];
|
||||
|
||||
async function renderUploads() {
|
||||
const el = document.getElementById('content');
|
||||
let tabsHtml = '';
|
||||
for (const m of moduleList) {
|
||||
tabsHtml += `<div class="module-tab ${m.key===uploadModule?'active':''}" onclick="switchUploadModule('${m.key}')">${m.label}</div>`;
|
||||
}
|
||||
el.innerHTML = `
|
||||
<div class="module-tabs">${tabsHtml}</div>
|
||||
<div class="table-card">
|
||||
<div style="padding:24px">
|
||||
<div class="upload-area" id="uploadArea" onclick="document.getElementById('fileInput').click()">
|
||||
<div class="upload-icon">📤</div>
|
||||
<div class="upload-text">点击或拖拽文件到此处上传</div>
|
||||
<div class="upload-formats">
|
||||
<span class="fmt-tag">JPG</span>
|
||||
<span class="fmt-tag">PNG</span>
|
||||
<span class="fmt-tag">GIF</span>
|
||||
<span class="fmt-tag">WebP</span>
|
||||
<span class="fmt-tag">SVG</span>
|
||||
<span class="fmt-tag">MP4</span>
|
||||
<span class="fmt-tag">PDF</span>
|
||||
</div>
|
||||
<div class="upload-hint">单个文件最大 10MB · 可多选</div>
|
||||
</div>
|
||||
<input type="file" id="fileInput" multiple style="display:none" accept="image/jpeg,image/png,image/gif,image/webp,image/svg+xml,image/x-icon,image/bmp,video/mp4,video/webm,application/pdf">
|
||||
<div class="upload-progress" id="uploadProgress" style="display:none">
|
||||
<div class="upload-progress-bar"><div class="upload-progress-fill" id="progressFill" style="width:0%"></div></div>
|
||||
<div style="text-align:center;margin-top:6px;font-size:12px;color:var(--text-muted)" id="progressText">上传中...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-header" style="margin-top:24px"><h4>📋 已上传文件</h4></div>
|
||||
<div id="fileGrid"><div class="loading"><div class="spinner"></div></div></div>
|
||||
`;
|
||||
|
||||
// 绑定文件选择和拖拽事件
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const uploadArea = document.getElementById('uploadArea');
|
||||
fileInput.addEventListener('change', function() { handleFileUpload(this.files); });
|
||||
uploadArea.addEventListener('dragover', function(e) { e.preventDefault(); this.classList.add('dragover'); });
|
||||
uploadArea.addEventListener('dragleave', function(e) { e.preventDefault(); this.classList.remove('dragover'); });
|
||||
uploadArea.addEventListener('drop', function(e) {
|
||||
e.preventDefault();
|
||||
this.classList.remove('dragover');
|
||||
handleFileUpload(e.dataTransfer.files);
|
||||
});
|
||||
|
||||
loadFileList();
|
||||
}
|
||||
|
||||
function switchUploadModule(mod) {
|
||||
uploadModule = mod;
|
||||
renderUploads();
|
||||
}
|
||||
|
||||
async function handleFileUpload(files) {
|
||||
if (!files || files.length === 0) return;
|
||||
const progressEl = document.getElementById('uploadProgress');
|
||||
const fillEl = document.getElementById('progressFill');
|
||||
const textEl = document.getElementById('progressText');
|
||||
progressEl.style.display = 'block';
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
textEl.textContent = `上传中 (${i+1}/${files.length}): ${file.name}`;
|
||||
fillEl.style.width = ((i / files.length) * 100) + '%';
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
try {
|
||||
const resp = await fetch(API_BASE + '/upload?module=' + uploadModule, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': 'Bearer ' + token },
|
||||
body: formData
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.code !== 0) {
|
||||
toast('上传失败: ' + data.message, 'error');
|
||||
} else {
|
||||
toast('上传成功: ' + file.name, 'success');
|
||||
}
|
||||
} catch (e) {
|
||||
toast('网络错误: ' + file.name, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
fillEl.style.width = '100%';
|
||||
textEl.textContent = '上传完成';
|
||||
setTimeout(() => { progressEl.style.display = 'none'; fillEl.style.width = '0%'; }, 1500);
|
||||
document.getElementById('fileInput').value = '';
|
||||
loadFileList();
|
||||
}
|
||||
|
||||
async function loadFileList() {
|
||||
const el = document.getElementById('fileGrid');
|
||||
if (!el) return;
|
||||
el.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
||||
try {
|
||||
const data = await api('/upload/list?module=' + uploadModule);
|
||||
const files = data.data.list || [];
|
||||
if (files.length === 0) {
|
||||
el.innerHTML = '<div class="table-card"><div class="empty-row" style="padding:40px">暂无文件,请上传</div></div>';
|
||||
return;
|
||||
}
|
||||
let html = '<div class="file-grid">';
|
||||
for (const f of files) {
|
||||
const isImage = f.url.match(/\.(jpg|jpeg|png|gif|webp)$/i);
|
||||
const isVideo = f.url.match(/\.(mp4|webm)$/i);
|
||||
let preview = '';
|
||||
if (isImage) {
|
||||
preview = `<img src="${f.url}" onerror="this.parentElement.innerHTML='<div class=\\'file-icon\\'>📄</div>'">`;
|
||||
} else if (isVideo) {
|
||||
preview = `<div class="file-icon">🎬</div>`;
|
||||
} else {
|
||||
preview = `<div class="file-icon">📄</div>`;
|
||||
}
|
||||
html += `
|
||||
<div class="file-card">
|
||||
<div class="file-preview">${preview}</div>
|
||||
<div class="file-actions">
|
||||
<button title="复制链接" onclick="copyFileUrl('${f.url}')">🔗</button>
|
||||
<button title="删除" onclick="deleteFile('${f.url}')">🗑</button>
|
||||
</div>
|
||||
<div class="file-info">
|
||||
<div class="file-name" title="${f.name}">${f.name}</div>
|
||||
<div class="file-meta">${formatFileSize(f.size)} · ${f.mod_time}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
el.innerHTML = html;
|
||||
} catch (e) {
|
||||
el.innerHTML = '<div class="loading">加载失败</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function formatFileSize(bytes) {
|
||||
if (!bytes) return '0 B';
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
return (bytes / 1024 / 1024).toFixed(1) + ' MB';
|
||||
}
|
||||
|
||||
function copyFileUrl(url) {
|
||||
const fullUrl = window.location.origin + url;
|
||||
navigator.clipboard.writeText(fullUrl).then(() => {
|
||||
toast('链接已复制: ' + fullUrl, 'success');
|
||||
}).catch(() => {
|
||||
// 降级方案
|
||||
const input = document.createElement('input');
|
||||
input.value = fullUrl;
|
||||
document.body.appendChild(input);
|
||||
input.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(input);
|
||||
toast('链接已复制: ' + fullUrl, 'success');
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteFile(fileUrl) {
|
||||
if (!confirm('确定删除该文件?')) return;
|
||||
// 从 URL 中提取模块相对路径
|
||||
const path = fileUrl.replace('/uploads/', '');
|
||||
try {
|
||||
await api('/upload?module=' + uploadModule + '&filename=' + encodeURIComponent(path), { method: 'DELETE' });
|
||||
toast('删除成功', 'success');
|
||||
loadFileList();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// 内容编辑弹窗中的字段上传
|
||||
function uploadToField(fieldKey, mod) {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'image/jpeg,image/png,image/gif,image/webp,image/svg+xml,image/x-icon,image/bmp,video/mp4,video/webm,application/pdf';
|
||||
input.onchange = async function() {
|
||||
const file = this.files[0];
|
||||
if (!file) return;
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
try {
|
||||
const resp = await fetch(API_BASE + '/upload?module=' + mod, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': 'Bearer ' + token },
|
||||
body: formData
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.code !== 0) {
|
||||
toast('上传失败: ' + data.message, 'error');
|
||||
return;
|
||||
}
|
||||
const urlInput = document.getElementById('f_' + fieldKey);
|
||||
urlInput.value = data.data.url;
|
||||
// 更新预览
|
||||
let preview = document.getElementById('preview_' + fieldKey);
|
||||
if (!preview) {
|
||||
preview = document.createElement('img');
|
||||
preview.id = 'preview_' + fieldKey;
|
||||
preview.style.cssText = 'margin-top:8px;max-width:120px;max-height:80px;border-radius:6px;border:1px solid var(--border)';
|
||||
urlInput.parentElement.parentElement.appendChild(preview);
|
||||
}
|
||||
preview.src = data.data.url;
|
||||
preview.style.display = '';
|
||||
toast('上传成功', 'success');
|
||||
} catch (e) {
|
||||
toast('网络错误', 'error');
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
|
||||
// ===== 系统设置 =====
|
||||
async function renderSettings() {
|
||||
const el = document.getElementById('content');
|
||||
|
||||
Reference in New Issue
Block a user