(function () { 'use strict'; var Api = function () { return window.AdminApi; }; var Ui = function () { return window.AdminUi; }; var I = function () { return window.AdminI18n; }; var Form = function () { return window.AdminForm; }; var MODAL_OPTS = { area: ['480px', 'auto'], skin: 'soon-layer soon-layer--tpl', maxWidth: '94vw' }; function activeBadge(v) { var on = v === true || parseInt(v, 10) === 1; return I().badgeStatus(on ? 'active' : 'disabled'); } function typeLabel(type) { return Number(type) === 2 ? '双面' : '单面'; } function formatBytes(n) { n = parseInt(n, 10) || 0; if (!n) return '—'; if (n < 1024) return n + ' B'; if (n < 1048576) return (n / 1024).toFixed(1) + ' KB'; return (n / 1048576).toFixed(1) + ' MB'; } function getAdminBase() { return ((window.SOON_DEPLOY_CONFIG && window.SOON_DEPLOY_CONFIG.api_admin_base) || '').replace(/\/+$/, ''); } function revokeBlob(state) { if (state && state._blobUrl) { try { URL.revokeObjectURL(state._blobUrl); } catch (e) { /* ignore */ } state._blobUrl = ''; } } function nameFromFileName(fileName) { return String(fileName || '').replace(/\.soon$/i, '').replace(/[_-]+/g, ' ').trim(); } function parseSoonMeta(text, fileName) { var data; try { data = JSON.parse(text); } catch (e) { throw new Error('invalid_json'); } if (!data || typeof data !== 'object') throw new Error('invalid_json'); var type = 1; if (Number(data.soonType) === 2 || data.backBlackPic) type = 2; var thumbRaw = ''; if (data.frontDisplayPic && typeof data.frontDisplayPic === 'string') { var candidate = data.frontDisplayPic.trim(); if (candidate.indexOf('data:image/') === 0) thumbRaw = candidate; } var name = ''; if (typeof data.name === 'string' && data.name.trim()) name = data.name.trim(); else if (typeof data.title === 'string' && data.title.trim()) name = data.title.trim(); else name = nameFromFileName(fileName); if (!name) name = '未命名模板'; return { name: name, type: type, thumbRaw: thumbRaw, content: text }; } function shrinkDataUrl(dataUrl, maxW, quality, done) { if (!dataUrl) { done(''); return; } var img = new Image(); img.onload = function () { var w = img.naturalWidth || img.width; var h = img.naturalHeight || img.height; if (!w || !h) { done(dataUrl); return; } if (w <= maxW) { done(dataUrl); return; } var nh = Math.max(1, Math.round(h * (maxW / w))); var canvas = document.createElement('canvas'); canvas.width = maxW; canvas.height = nh; canvas.getContext('2d').drawImage(img, 0, 0, maxW, nh); try { done(canvas.toDataURL('image/jpeg', quality)); } catch (e) { done(dataUrl); } }; img.onerror = function () { done(''); }; img.src = dataUrl; } function setPreviewImage(modalState, src, renderPreview) { modalState.thumbView = src || ''; renderPreview(); } function applyThumbRaw(modalState, thumbRaw, renderPreview) { if (!thumbRaw) { setPreviewImage(modalState, '', renderPreview); return; } if (thumbRaw.length < 700000) { setPreviewImage(modalState, thumbRaw, renderPreview); return; } shrinkDataUrl(thumbRaw, 360, 0.82, function (small) { setPreviewImage(modalState, small || thumbRaw, renderPreview); }); } function readFileText(input) { return new Promise(function (resolve, reject) { var file = input && input.files && input.files[0]; if (!file) { reject(new Error('no_file')); return; } var reader = new FileReader(); reader.onload = function () { resolve({ text: String(reader.result || ''), fileName: file.name }); }; reader.onerror = function () { reject(new Error('read_failed')); }; reader.readAsText(file, 'UTF-8'); }); } function fetchAdminThumb(plan, modalState, renderPreview) { if (!plan || !plan.id || !Api().getToken()) { renderPreview(); return; } revokeBlob(modalState); window.fetch(getAdminBase() + '/templates/' + plan.id + '/thumb?_=' + Date.now(), { headers: { Authorization: 'Bearer ' + Api().getToken() }, }).then(function (r) { if (!r.ok) throw new Error('thumb'); return r.blob(); }).then(function (blob) { modalState._blobUrl = URL.createObjectURL(blob); setPreviewImage(modalState, modalState._blobUrl, renderPreview); }).catch(function () { renderPreview(); }); } function openThumbPreview(src, title) { if (!src || typeof layer === 'undefined') return; layer.photos({ photos: { title: title || '模板预览', start: 0, data: [{ alt: title || '模板预览', src: src }], }, anim: 5, shade: 0.88, }); } function modalHtml(opts) { opts = opts || {}; var isEdit = opts.mode === 'edit'; var plan = opts.plan || {}; var pickLabel = isEdit ? '替换文件' : '选择文件'; return '
' + '' + '' + '
' + '' + '
' + '
' + '
'; } function bindTplModal(opts, modalState) { var fileEl = Ui().el('tplFile'); var thumbBtn = Ui().el('tplThumbBtn'); var thumbEl = Ui().el('tplThumb'); var thumbEmptyEl = Ui().el('tplThumbEmpty'); var thumbHintEl = Ui().el('tplThumbHint'); var nameEl = Ui().el('tplNameDisp'); var typeEl = Ui().el('tplTypeDisp'); var labelEl = Ui().el('tplFileLabel'); var plan = opts.plan; function renderPreview() { var hasMeta = !!(modalState.name || modalState.type); var src = modalState.thumbView || ''; nameEl.textContent = modalState.name || '—'; typeEl.textContent = hasMeta ? typeLabel(modalState.type) : '—'; if (src) { thumbEl.src = src; thumbEl.hidden = false; thumbEmptyEl.hidden = true; thumbHintEl.hidden = false; thumbBtn.disabled = false; thumbBtn.classList.add('is-ready'); } else { thumbEl.hidden = true; thumbEl.removeAttribute('src'); thumbEmptyEl.hidden = false; thumbEmptyEl.textContent = hasMeta ? '无缩略图' : '选择 .soon 后显示预览'; thumbHintEl.hidden = true; thumbBtn.disabled = true; thumbBtn.classList.remove('is-ready'); } } if (thumbBtn) { thumbBtn.onclick = function () { if (!modalState.thumbView) return; openThumbPreview(modalState.thumbView, modalState.name); }; } if (opts.mode === 'edit' && plan) { modalState.name = plan.name; modalState.type = plan.type; modalState.thumbView = ''; modalState.content = null; renderPreview(); fetchAdminThumb(plan, modalState, renderPreview); } if (!fileEl) return; fileEl.onchange = function () { var file = fileEl.files && fileEl.files[0]; if (!file) return; labelEl.textContent = file.name; readFileText(fileEl).then(function (payload) { var meta = parseSoonMeta(payload.text, payload.fileName); modalState.name = meta.name; modalState.type = meta.type; modalState.content = meta.content; applyThumbRaw(modalState, meta.thumbRaw, renderPreview); }).catch(function (e) { fileEl.value = ''; labelEl.textContent = opts.mode === 'edit' ? (plan.name + '.soon') : '未选择'; if (e && e.message === 'invalid_json') soonToast('.soon 须为有效 JSON', 'warn'); else soonToast('文件读取失败', 'error'); }); }; } function openCreateModal() { var modalState = { content: null, name: '', type: 1, thumbView: '', _blobUrl: '' }; Ui().openFormModal('上传模板', modalHtml({ mode: 'create' }), function () { if (!modalState.content) { soonToast('请先选择 .soon 文件', 'warn'); return false; } return Api().postJson('templates', { name: modalState.name, content: modalState.content, sort_order: parseInt(Ui().el('tplSort').value, 10) || 0, is_active: Ui().el('tplActive').checked ? 1 : 0, }).then(function (r) { if (r) { revokeBlob(modalState); layer.closeAll(); soonToast('已上传', 'success'); render(); } }); }, Object.assign({}, MODAL_OPTS, { success: function () { bindTplModal({ mode: 'create' }, modalState); }, })); } function openEditModal(plan) { var modalState = { content: null, name: plan.name, type: plan.type, thumbView: '', _blobUrl: '' }; Ui().openFormModal('编辑模板', modalHtml({ mode: 'edit', plan: plan }), function () { var body = { name: modalState.content ? modalState.name : plan.name, sort_order: parseInt(Ui().el('tplSort').value, 10) || 0, is_active: Ui().el('tplActive').checked ? 1 : 0, }; if (modalState.content) body.content = modalState.content; return Api().putJson('templates/' + plan.id, body).then(function (r) { if (r) { revokeBlob(modalState); layer.closeAll(); soonToast('已保存', 'success'); render(); } }); }, Object.assign({}, MODAL_OPTS, { success: function () { bindTplModal({ mode: 'edit', plan: plan }, modalState); }, })); } function deleteTemplate(plan) { Ui().confirm('确定删除模板「' + plan.name + '」?', function () { Api().del('templates/' + plan.id).then(function (r) { if (r) { soonToast('已删除', 'success'); render(); } }); }); } function render() { var panel = Ui().el('panel-templates'); if (!panel) return; panel.innerHTML = '
' + Ui().pageHeader('模板库', '上传 .soon 后自动识别名称、类型与缩略图', '') + Ui().skeletonTable() + '
'; Api().get('templates').then(function (j) { if (!j) return; var items = j.data.items || []; var rows = items.map(function (p) { return '' + p.id + '' + I().esc(p.name) + '' + typeLabel(p.type) + '' + formatBytes(p.file_size) + '' + p.sort_order + '' + (p.has_thumb ? '有' : '—') + '' + activeBadge(p.is_active) + '' + ' ' + ''; }).join(''); panel.innerHTML = '
' + Ui().pageHeader('模板库', '上传 .soon 后自动识别名称、类型与缩略图', '') + Ui().dataTable( ['ID', '名称', '类型', '大小', '排序', '缩略图', '状态', '操作'], rows ) + '
'; var planMap = {}; items.forEach(function (p) { planMap[p.id] = p; }); panel.querySelector('#tplCreate').onclick = openCreateModal; panel.querySelectorAll('.tpl-edit').forEach(function (btn) { btn.onclick = function () { var id = parseInt(btn.closest('tr').dataset.id, 10); if (planMap[id]) openEditModal(planMap[id]); }; }); panel.querySelectorAll('.tpl-del').forEach(function (btn) { btn.onclick = function () { var id = parseInt(btn.closest('tr').dataset.id, 10); if (planMap[id]) deleteTemplate(planMap[id]); }; }); }); } window.AdminViews = window.AdminViews || {}; window.AdminViews.templates = { render: render }; })();