Files
SoonDesign/frontend-web/pages/admin/assets/js/views/plans.js
T
24kycj 88c6ce8ccc 重构 monorepo 并完善网页端订阅与首页体验
- 迁移为 frontend-web、frontend-electron、backend-web 与 docker 部署结构
- 网页端:订阅门禁二次弹窗、套餐/支付组件化、顶栏分组对齐
- 首页:最近文件与模板库布局优化,缩略图对齐,下载与删除操作
- 新增管理后台、支付与云端文件 API,更新 README 与项目规范

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 18:17:39 +08:00

254 lines
10 KiB
JavaScript
Raw 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 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 (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 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 }),
free ? '免费版填 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: 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) {
layer.closeAll();
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 };
})();