Files
24kycj 5fb269cc0a fix(web): 本地优先保存与 design 页体验修复
- 保存先写本地缓存,登录后可选云端同步;退出登录不再因文件操作跳转登录

- 修复 Layui 遮罩残留、design2 保存后线条、toast 被 finally 提前关闭

- 保存过程恢复 loading spinner;成功提示 2.5 秒

- 云端 payload 瘦身与体积超限提示;后端 schema 迁移与 FileService 容错

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 19:24:34 +08:00

260 lines
10 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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; };
function activeBadge(v) {
var on = v === true || parseInt(v, 10) === 1;
return I().badgeStatus(on ? 'active' : 'disabled');
}
function recBadge(v) {
if (!v) return '—';
return '<span class="soon-badge soon-badge--paid">推荐</span>';
}
function isFreePlan(plan) {
return plan && plan.code === 'free';
}
function isPaidMemberPlan(plan) {
return plan && plan.code !== 'free' && plan.price_cents > 0;
}
function isLifetimePlan(plan) {
return plan && plan.code === 'member_lifetime';
}
function editableFeatures(plan) {
if (plan.features_editable && plan.features_editable.length) {
return plan.features_editable.slice();
}
var lines = plan.features || [];
return lines.filter(function (l) {
return l.indexOf('订阅有效期') < 0
&& l.indexOf('订阅周期') < 0
&& l.indexOf('到期') < 0;
});
}
function featureRowsHtml(lines) {
var items = lines && lines.length ? lines : [''];
return items.map(function (text) {
return '<div class="soon-admin-feature-row">' +
'<input type="text" class="soon-input soon-input--sm soon-admin-feature-input" value="' +
I().esc(text) + '" placeholder="例如:预览成品效果">' +
'<button type="button" class="soon-btn soon-btn--xs soon-btn--ghost soon-admin-feature-del" title="删除">×</button></div>';
}).join('');
}
function bindFeatureEditor() {
var list = document.getElementById('peFeatures');
var addBtn = document.getElementById('peFeatureAdd');
if (!list) return;
function bindDel(btn) {
if (!btn || btn.dataset.bound) return;
btn.dataset.bound = '1';
btn.onclick = function () {
var rows = list.querySelectorAll('.soon-admin-feature-row');
if (rows.length <= 1) {
var inp = list.querySelector('.soon-admin-feature-input');
if (inp) inp.value = '';
return;
}
var row = btn.closest('.soon-admin-feature-row');
if (row) row.remove();
};
}
list.querySelectorAll('.soon-admin-feature-del').forEach(bindDel);
if (addBtn && !addBtn.dataset.bound) {
addBtn.dataset.bound = '1';
addBtn.onclick = function () {
var row = document.createElement('div');
row.className = 'soon-admin-feature-row';
row.innerHTML =
'<input type="text" class="soon-input soon-input--sm soon-admin-feature-input" placeholder="例如:预览成品效果">' +
'<button type="button" class="soon-btn soon-btn--xs soon-btn--ghost soon-admin-feature-del" title="删除">×</button>';
list.appendChild(row);
bindDel(row.querySelector('.soon-admin-feature-del'));
row.querySelector('.soon-admin-feature-input').focus();
};
}
}
function collectFeatures() {
var lines = [];
document.querySelectorAll('#peFeatures .soon-admin-feature-input').forEach(function (inp) {
var t = inp.value.trim();
if (t) lines.push(t);
});
return lines;
}
function periodHint(plan) {
if (isFreePlan(plan)) return '';
var days = parseInt(Ui().el('peDays') && Ui().el('peDays').value, 10);
if (!days || days <= 0) days = plan.duration_days || 0;
if (isLifetimePlan(plan)) return '永久激活方案,duration_days 固定为 0。';
if (days <= 0) return '付费方案保存后将自动追加周期说明。';
var label = days >= 365 ? '1 年' : (days === 90 ? '1 季' : (days >= 30 && days % 30 === 0 ? (days / 30) + ' 个月' : days + ' 天'));
return '保存后将自动追加周期说明(' + label + '),用户端不展示该行';
}
function openEditModal(plan) {
var free = isFreePlan(plan);
var paid = isPaidMemberPlan(plan);
var features = editableFeatures(plan);
var basicPanel = Form().panel('基本信息',
Form().field('代码', '<input class="soon-input soon-input--sm" value="' + I().esc(plan.code) + '" disabled>') +
Form().field('名称', Form().input('peName', { value: plan.name })) +
Form().field('简介', Form().input('peDesc', { value: plan.description || '' }))
);
var lifetime = isLifetimePlan(plan);
var pricePanel = Form().panel('价格与周期',
'<div class="soon-admin-form-grid">' +
Form().field('价格(元)',
Form().input('pePriceYuan', {
type: 'number',
value: (plan.price_cents / 100).toFixed(2),
disabled: free,
})) +
Form().field('有效天数',
Form().input('peDays', { type: 'number', value: plan.duration_days, disabled: lifetime }),
free ? '免费版填 0' : (lifetime ? '永久激活固定为 0' : '月付 30 / 季付 90 / 年付 365')) +
Form().field('排序',
Form().input('peSort', { type: 'number', value: plan.sort_order })) +
'</div>' +
'<div class="soon-admin-form-checks">' +
'<label class="soon-admin-check"><input type="checkbox" id="peRec"' +
(plan.is_recommended ? ' checked' : '') + (free ? ' disabled' : '') + '> 设为推荐</label>' +
'<label class="soon-admin-check"><input type="checkbox" id="peActive"' +
(plan.is_active ? ' checked' : '') + '> 上架展示</label>' +
(paid ? '<label class="soon-admin-check"><input type="checkbox" id="peSync" checked> 应用到全部付费方案</label>' : '') +
'</div>'
);
var quotaPanel = Form().panel('云端配额',
'<div class="soon-admin-form-grid">' +
Form().field('存储(MB', Form().input('peQuota', { type: 'number', value: plan.quota_mb })) +
Form().field('文件数上限', Form().input('peMaxFiles', { type: 'number', value: plan.max_files })) +
'</div>',
'云端存储与文件数量上限。'
);
var benefitTitle = free ? '免费版展示' : '会员权益';
var benefitHint = free
? '展示在用户端身份说明中。'
: periodHint(plan);
var benefitPanel = Form().panel(benefitTitle,
'<div class="soon-admin-feature-list" id="peFeatures">' + featureRowsHtml(features) + '</div>' +
'<button type="button" class="soon-btn soon-btn--xs soon-btn--ghost" id="peFeatureAdd">+ 添加权益</button>' +
(benefitHint ? '<p class="soon-admin-form-hint soon-admin-form-hint--tight">' + I().esc(benefitHint) + '</p>' : '')
);
var form = '<div class="soon-admin-form soon-admin-form--plan">' +
basicPanel + pricePanel + quotaPanel + benefitPanel +
Form().actions('保存', 'adminModalSave') +
'</div>';
Ui().openFormModal((free ? '免费版' : '会员激活') + ' · ' + plan.name, form, function () {
var name = Ui().el('peName').value.trim();
if (!name) {
soonToast('名称不能为空', 'warn');
return false;
}
var featureLines = collectFeatures();
if (!featureLines.length) {
soonToast('请至少填写一条权益', 'warn');
return false;
}
var yuan = free ? 0 : (parseFloat(Ui().el('pePriceYuan').value) || 0);
var body = {
code: plan.code,
name: name,
description: Ui().el('peDesc').value.trim(),
price_cents: Math.round(yuan * 100),
quota_mb: parseInt(Ui().el('peQuota').value, 10) || 0,
max_files: parseInt(Ui().el('peMaxFiles').value, 10) || 0,
duration_days: lifetime ? 0 : (parseInt(Ui().el('peDays').value, 10) || 0),
sort_order: parseInt(Ui().el('peSort').value, 10) || 0,
is_recommended: free ? 0 : (Ui().el('peRec').checked ? 1 : 0),
is_active: Ui().el('peActive').checked ? 1 : 0,
features: featureLines,
sync_member_benefits: paid && Ui().el('peSync') && Ui().el('peSync').checked ? 1 : 0,
};
return Api().postJson('plans', body).then(function (r) {
if (r) {
soonLayerDismiss();
soonToast('已保存', 'success');
render();
}
});
}, {
area: ['640px', '72vh'],
success: function () {
bindFeatureEditor();
var daysEl = Ui().el('peDays');
var hintEl = document.querySelector('.soon-admin-form-hint--tight');
if (daysEl && hintEl && paid) {
daysEl.oninput = function () {
hintEl.textContent = periodHint(plan);
};
}
},
});
}
function featureSummary(plan) {
var n = editableFeatures(plan).length;
return n > 0 ? n + ' 条' : '—';
}
function render() {
var panel = Ui().el('panel-plans');
if (!panel) return;
panel.innerHTML = '<div class="soon-admin-page">' +
Ui().pageHeader('会员激活', '配置激活价格、配额与用户端展示文案') +
Ui().skeletonTable() + '</div>';
Api().get('plans').then(function (j) {
if (!j) return;
var items = j.data.items || [];
var rows = items.map(function (p) {
return '<tr data-code="' + I().esc(p.code) + '"><td>' + I().esc(p.code) + '</td><td>' + I().esc(p.name) +
'</td><td>' + I().formatCents(p.price_cents) + '</td><td>' + featureSummary(p) + '</td><td>' +
p.quota_mb + ' MB</td><td>' + (p.duration_days > 0 ? p.duration_days + ' 天' : '永久') + '</td><td>' +
recBadge(p.is_recommended) + '</td><td>' + activeBadge(p.is_active) +
'</td><td><button type="button" class="soon-btn soon-btn--sm soon-btn--ghost plan-edit">编辑</button></td></tr>';
}).join('');
panel.innerHTML = '<div class="soon-admin-page">' +
Ui().pageHeader('会员激活', '配置激活价格、配额与用户端展示文案') +
Ui().dataTable(
['代码', '名称', '价格', '权益', '存储', '有效期', '推荐', '状态', '操作'],
rows
) + '</div>';
var planMap = {};
items.forEach(function (p) { planMap[p.code] = p; });
panel.querySelectorAll('.plan-edit').forEach(function (btn) {
btn.onclick = function () {
var code = btn.closest('tr').dataset.code;
if (planMap[code]) openEditModal(planMap[code]);
};
});
});
}
window.AdminViews = window.AdminViews || {};
window.AdminViews.plans = { render: render };
})();