(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 };
})();