重构 monorepo 并完善网页端订阅与首页体验
- 迁移为 frontend-web、frontend-electron、backend-web 与 docker 部署结构 - 网页端:订阅门禁二次弹窗、套餐/支付组件化、顶栏分组对齐 - 首页:最近文件与模板库布局优化,缩略图对齐,下载与删除操作 - 新增管理后台、支付与云端文件 API,更新 README 与项目规范 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var GATE_FEATURES = ['高清预览', '成品打印', '云端保存', '导出设计文件'];
|
||||
var _gate = { index: null, payIndex: null, plans: [], selectedId: null, gateOpts: null };
|
||||
|
||||
var pay = window.SoonMemberPay;
|
||||
var plansUi = window.SoonMemberPlan;
|
||||
|
||||
function esc(s) {
|
||||
if (pay && pay.esc) return pay.esc(s);
|
||||
if (s == null) return '';
|
||||
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
function toast(msg, type) {
|
||||
if (typeof window.soonToast === 'function') window.soonToast(msg, type);
|
||||
}
|
||||
|
||||
function emptyGate() {
|
||||
return { index: null, payIndex: null, plans: [], selectedId: null, gateOpts: null };
|
||||
}
|
||||
|
||||
function ensureLayer(cb) {
|
||||
if (typeof layer !== 'undefined' && layer.open) {
|
||||
if (layer.config) layer.config({ skin: 'soon-layer' });
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
if (typeof layui !== 'undefined') {
|
||||
layui.use(['layer'], function () {
|
||||
window.layer = layui.layer;
|
||||
layer.config({ skin: 'soon-layer' });
|
||||
cb();
|
||||
});
|
||||
return;
|
||||
}
|
||||
toast('订阅功能暂不可用,请刷新页面后重试', 'warn');
|
||||
}
|
||||
|
||||
function actionCopy(actionLabel) {
|
||||
var label = (actionLabel || '使用').trim();
|
||||
if (label.indexOf('预览') >= 0 || label.indexOf('导出') >= 0) return '预览、打印与导出成品';
|
||||
if (label.indexOf('保存') >= 0 || label.indexOf('另存') >= 0) return '保存或另存到云端';
|
||||
if (label.indexOf('下载') >= 0) return '下载设计文件';
|
||||
return label;
|
||||
}
|
||||
|
||||
function shellHtml(actionLabel) {
|
||||
var feat = GATE_FEATURES.map(function (t) {
|
||||
return '<li>' + esc(t) + '</li>';
|
||||
}).join('');
|
||||
return '<div class="soon-subscribe-modal">' +
|
||||
'<header class="soon-subscribe-modal__head">' +
|
||||
'<span class="soon-subscribe-modal__eyebrow">订阅功能</span>' +
|
||||
'<h3 class="soon-subscribe-modal__title">开通订阅,解锁完整能力</h3>' +
|
||||
'<p class="soon-subscribe-modal__desc">免费版可使用设计工具;订阅后可' + esc(actionCopy(actionLabel)) + '。</p>' +
|
||||
'<ul class="soon-subscribe-modal__features">' + feat + '</ul>' +
|
||||
'</header>' +
|
||||
'<div class="soon-subscribe-modal__body">' +
|
||||
'<div class="soon-subscribe-modal__section-head">' +
|
||||
'<h4 class="soon-subscribe-modal__section-title"><span class="soon-subscribe-modal__section-mark"></span>订阅方案</h4>' +
|
||||
'<p class="soon-subscribe-modal__section-sub">选择订阅周期,功能相同,随时续订</p></div>' +
|
||||
'<section class="soon-plans-grid soon-subscribe-plans-grid" data-role="plan-picker">' +
|
||||
'<p class="soon-subscribe-plans__loading">正在加载订阅方案…</p></section>' +
|
||||
'</div>' +
|
||||
'<footer class="soon-subscribe-modal__foot">' +
|
||||
'<button type="button" class="soon-subscribe-modal__stay" data-action="stay">继续设计</button>' +
|
||||
'<button type="button" class="soon-btn soon-subscribe-modal__pay" data-action="go-pay" disabled>立即支付</button>' +
|
||||
'</footer></div>';
|
||||
}
|
||||
|
||||
function findPlan(id) {
|
||||
var pid = Number(id);
|
||||
for (var i = 0; i < _gate.plans.length; i++) {
|
||||
if (_gate.plans[i].id === pid) return _gate.plans[i];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function planPriceDisplay(plan) {
|
||||
if (!plan) return '';
|
||||
return plan.price_display || (plan.price_cents / 100).toFixed(2);
|
||||
}
|
||||
|
||||
function renderPlanPicker(modal) {
|
||||
var picker = modal.querySelector('[data-role="plan-picker"]');
|
||||
if (!picker || !plansUi) return;
|
||||
if (!_gate.plans.length) {
|
||||
picker.innerHTML = '<p class="soon-subscribe-plans__empty">暂无可用订阅方案</p>';
|
||||
return;
|
||||
}
|
||||
picker.innerHTML = plansUi.plansGridHtml(_gate.plans, {
|
||||
allPlans: _gate.plans,
|
||||
selectedId: _gate.selectedId,
|
||||
mode: 'gate',
|
||||
});
|
||||
}
|
||||
|
||||
function updatePayButton(modal) {
|
||||
var btn = modal.querySelector('[data-action="go-pay"]');
|
||||
var plan = findPlan(_gate.selectedId);
|
||||
if (!btn) return;
|
||||
if (!plan) {
|
||||
btn.disabled = true;
|
||||
btn.textContent = '立即支付';
|
||||
return;
|
||||
}
|
||||
btn.disabled = false;
|
||||
btn.textContent = '立即支付 ¥' + planPriceDisplay(plan);
|
||||
}
|
||||
|
||||
function selectPlan(modal, planId) {
|
||||
var plan = findPlan(planId);
|
||||
if (!plan) return;
|
||||
_gate.selectedId = plan.id;
|
||||
renderPlanPicker(modal);
|
||||
updatePayButton(modal);
|
||||
}
|
||||
|
||||
function closeGate(fireStay) {
|
||||
var idx = _gate.index;
|
||||
var payIdx = _gate.payIndex;
|
||||
var opts = _gate.gateOpts;
|
||||
_gate = emptyGate();
|
||||
if (typeof layer !== 'undefined') {
|
||||
if (idx != null) layer.close(idx);
|
||||
if (payIdx != null) layer.close(payIdx);
|
||||
}
|
||||
if (fireStay && opts && typeof opts.onStay === 'function') opts.onStay();
|
||||
}
|
||||
|
||||
function onPaySuccess() {
|
||||
var gateIdx = _gate.index;
|
||||
var opts = _gate.gateOpts;
|
||||
_gate = emptyGate();
|
||||
if (typeof layer !== 'undefined') {
|
||||
if (gateIdx != null) layer.close(gateIdx);
|
||||
}
|
||||
toast('恭喜您,订阅已成功生效!', 'success');
|
||||
if (typeof window.soonLoadMembership === 'function') {
|
||||
window.soonLoadMembership(true).then(function () {
|
||||
if (typeof window.soonRefreshPortalIdentity === 'function') window.soonRefreshPortalIdentity();
|
||||
});
|
||||
}
|
||||
if (opts && typeof opts.onSuccess === 'function') opts.onSuccess();
|
||||
}
|
||||
|
||||
function openPayStep(modal) {
|
||||
var plan = findPlan(_gate.selectedId);
|
||||
if (!plan || !pay || !pay.openPayModal) {
|
||||
toast('请先选择订阅方案', 'warn');
|
||||
return;
|
||||
}
|
||||
if (_gate.payIndex != null) {
|
||||
try { layer.close(_gate.payIndex); } catch (e) { /* ignore */ }
|
||||
_gate.payIndex = null;
|
||||
}
|
||||
var priceDisplay = planPriceDisplay(plan);
|
||||
_gate.payIndex = pay.openPayModal({
|
||||
planId: plan.id,
|
||||
planName: plan.name,
|
||||
priceDisplay: priceDisplay,
|
||||
shadeClose: true,
|
||||
onPaid: onPaySuccess,
|
||||
onClose: function () {
|
||||
_gate.payIndex = null;
|
||||
updatePayButton(modal);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function renderPlansError(modal, message) {
|
||||
var picker = modal.querySelector('[data-role="plan-picker"]');
|
||||
if (!picker) return;
|
||||
picker.innerHTML = '<p class="soon-subscribe-plans__empty">' + esc(message) +
|
||||
'<br><button type="button" class="soon-btn soon-btn--sm" data-action="retry-plans">重试</button></p>';
|
||||
updatePayButton(modal);
|
||||
}
|
||||
|
||||
function loadPlans(modal) {
|
||||
var picker = modal.querySelector('[data-role="plan-picker"]');
|
||||
if (!picker) return;
|
||||
if (!pay || !plansUi) {
|
||||
renderPlansError(modal, '订阅模块未加载,请刷新页面');
|
||||
return;
|
||||
}
|
||||
picker.innerHTML = '<p class="soon-subscribe-plans__loading">正在加载订阅方案…</p>';
|
||||
updatePayButton(modal);
|
||||
pay.apiGet('/plans').then(function (ps) {
|
||||
if (!ps.ok) {
|
||||
renderPlansError(modal, ps.message || '方案加载失败');
|
||||
return;
|
||||
}
|
||||
var paid = ((ps.data && ps.data.items) || []).filter(function (p) {
|
||||
return p.code !== 'free' && p.price_cents > 0;
|
||||
});
|
||||
if (!paid.length) {
|
||||
renderPlansError(modal, '暂无可用订阅方案');
|
||||
return;
|
||||
}
|
||||
_gate.plans = paid;
|
||||
var def = paid[0];
|
||||
for (var i = 0; i < paid.length; i++) {
|
||||
if (paid[i].is_recommended) { def = paid[i]; break; }
|
||||
}
|
||||
selectPlan(modal, def.id);
|
||||
}).catch(function () {
|
||||
renderPlansError(modal, '网络错误,请稍后重试');
|
||||
});
|
||||
}
|
||||
|
||||
function bindModal(modal) {
|
||||
modal.addEventListener('click', function (e) {
|
||||
var stay = e.target.closest('[data-action="stay"]');
|
||||
if (stay) {
|
||||
e.preventDefault();
|
||||
closeGate(true);
|
||||
return;
|
||||
}
|
||||
var retry = e.target.closest('[data-action="retry-plans"]');
|
||||
if (retry) {
|
||||
e.preventDefault();
|
||||
loadPlans(modal);
|
||||
return;
|
||||
}
|
||||
var goPay = e.target.closest('[data-action="go-pay"]');
|
||||
if (goPay) {
|
||||
e.preventDefault();
|
||||
if (!goPay.disabled) openPayStep(modal);
|
||||
return;
|
||||
}
|
||||
var pick = e.target.closest('[data-action="pick-plan"]');
|
||||
if (pick) {
|
||||
e.preventDefault();
|
||||
var id = Number(pick.dataset.id);
|
||||
if (id) selectPlan(modal, id);
|
||||
}
|
||||
});
|
||||
loadPlans(modal);
|
||||
}
|
||||
|
||||
function openGate(opts) {
|
||||
opts = opts || {};
|
||||
if (_gate.index != null) {
|
||||
try { layer.close(_gate.index); } catch (e) { /* ignore */ }
|
||||
}
|
||||
if (_gate.payIndex != null) {
|
||||
try { layer.close(_gate.payIndex); } catch (e) { /* ignore */ }
|
||||
}
|
||||
_gate = emptyGate();
|
||||
_gate.gateOpts = opts;
|
||||
|
||||
var width = Math.min(1000, window.innerWidth - 24);
|
||||
layer.open({
|
||||
type: 1,
|
||||
skin: 'soon-layer',
|
||||
title: false,
|
||||
closeBtn: 1,
|
||||
shadeClose: true,
|
||||
area: [width + 'px', 'auto'],
|
||||
content: shellHtml(opts.action),
|
||||
success: function (layero, index) {
|
||||
var layerEl = layero && layero[0] ? layero[0] : layero;
|
||||
if (layerEl && layerEl.classList) layerEl.classList.add('soon-layer--subscribe');
|
||||
var content = layerEl && layerEl.querySelector ? layerEl.querySelector('.layui-layer-content') : null;
|
||||
if (content) content.style.padding = '0';
|
||||
var modal = layerEl.querySelector('.soon-subscribe-modal');
|
||||
if (!modal) return;
|
||||
_gate.index = index;
|
||||
_gate.gateOpts = opts;
|
||||
bindModal(modal);
|
||||
},
|
||||
end: function () {
|
||||
var payIdx = _gate.payIndex;
|
||||
var closedOpts = _gate.gateOpts;
|
||||
if (payIdx != null) {
|
||||
try { layer.close(payIdx); } catch (e) { /* ignore */ }
|
||||
}
|
||||
_gate = emptyGate();
|
||||
if (closedOpts && closedOpts.onClose) closedOpts.onClose();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function soonShowSubscribeGate(opts) {
|
||||
if (!pay || !plansUi) {
|
||||
toast('订阅功能暂不可用,请刷新页面后重试', 'warn');
|
||||
return false;
|
||||
}
|
||||
ensureLayer(function () { openGate(opts || {}); });
|
||||
return false;
|
||||
}
|
||||
|
||||
window.soonShowSubscribeGate = soonShowSubscribeGate;
|
||||
|
||||
if (!window._soonSubscribeFocusBound) {
|
||||
window._soonSubscribeFocusBound = true;
|
||||
window.addEventListener('focus', function () {
|
||||
if (typeof window.soonLoadMembership !== 'function') return;
|
||||
window.soonLoadMembership(true).then(function () {
|
||||
if (typeof window.soonRefreshPortalIdentity === 'function') window.soonRefreshPortalIdentity();
|
||||
});
|
||||
});
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user