永久会员与模板库后台化:预览门控、轻量首页与 Admin CRUD

- 会员改为永久激活方案;云端文件不再拦截非会员;预览弹窗内导出/打印才校验
- 首页最近文件支持本地记录,登录后与云端合并;移除独立会员页与订阅页
- 模板库入库管理:soon_templates 表、Admin 上传 CRUD、/templates 轻量列表与按需下载

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
24kycj
2026-06-08 23:52:56 +08:00
parent 152228d41f
commit ebe191b06d
45 changed files with 1616 additions and 1259 deletions
+99 -54
View File
@@ -66,9 +66,8 @@
var _membership = {
loaded: false,
isMember: false,
name: '免费版',
name: '普通用户',
tier: 'free',
expiresAt: null,
loading: null,
};
@@ -81,18 +80,6 @@
return '/api/v1';
}
function formatExpireDate(iso) {
if (!iso) return '';
var d = new Date(String(iso).replace(' ', 'T'));
if (isNaN(d.getTime())) return '';
var y = d.getFullYear();
var mo = String(d.getMonth() + 1);
var day = String(d.getDate());
if (mo.length < 2) mo = '0' + mo;
if (day.length < 2) day = '0' + day;
return y + '-' + mo + '-' + day;
}
function soonApplyMembership(data) {
var m = data || {};
var sub = m.subscription || {};
@@ -103,9 +90,8 @@
_membership = {
loaded: true,
isMember: !!isMember,
name: m.name || (isMember ? '订阅版' : '免费版'),
name: isMember ? (m.name || '会员') : '普通用户',
tier: m.tier || (isMember ? 'member' : 'free'),
expiresAt: sub.expires_at || null,
loading: null,
};
if (typeof window.soonRefreshPortalIdentity === 'function') {
@@ -132,13 +118,10 @@
var cls = 'soon-portal-identity';
if (_membership.isMember) {
cls += ' soon-portal-identity--member';
text = _membership.name || '订阅版';
if (_membership.expiresAt) {
text += ' · 至 ' + formatExpireDate(_membership.expiresAt);
}
text = _membership.name || '会员';
} else {
cls += ' soon-portal-identity--free';
text = _membership.name || '免费版';
text = _membership.name || '普通用户';
}
el.className = cls;
el.textContent = text;
@@ -182,34 +165,70 @@
return !!_membership.isMember;
}
function soonMemberPageUrl() {
return 'member.web.html';
}
function soonRequireMember(actionLabel) {
if (!soonGetAccessToken()) return soonRequireLogin(actionLabel);
if (soonIsMember()) return true;
if (typeof window.soonShowSubscribeGate === 'function') {
window.soonShowSubscribeGate({ action: actionLabel || '使用' });
} else {
soonToast('订阅后可' + (actionLabel || '使用'), 'warn');
function soonGuardPreviewDeliver(actionLabel, onAllowed) {
if (typeof onAllowed !== 'function') return;
if (!soonIsWebPortal()) {
onAllowed();
return;
}
if (!soonGetAccessToken()) {
if (typeof window.soonShowLoginGate === 'function') {
window.soonShowLoginGate({
reason: actionLabel || '导出或打印',
onSuccess: function () {
soonGuardPreviewDeliver(actionLabel, onAllowed);
},
});
} else {
soonRequireLogin(actionLabel);
}
return;
}
if (soonIsMember()) {
onAllowed();
return;
}
if (typeof window.soonShowActivateGate === 'function') {
window.soonShowActivateGate({
reason: actionLabel || '导出或打印',
onSuccess: function () {
onAllowed();
},
});
} else {
soonToast('请先激活会员后再' + (actionLabel || '操作'), 'warn');
}
return false;
}
function soonGuardCloudSave() {
if (!soonIsWebPortal()) return true;
return soonRequireMember('保存');
return true;
}
function soonGuardMemberPreview() {
if (!soonIsWebPortal()) return true;
return soonRequireMember('预览效果');
function soonSoonTypeFromJson(j) {
return j && j.soonType ? j.soonType : (j && j.backBlackPic ? 2 : 1);
}
function soonGuardMemberExport() {
if (!soonIsWebPortal()) return true;
return soonRequireMember('导出');
function soonPutSoonSession(j, fileName) {
if (!j) return '';
var hint = (fileName || 'design').replace(/\.soon$/i, '');
var key = 'soondesign_session:' + hint + '-' + Date.now();
try {
sessionStorage.setItem(key, JSON.stringify(j));
try { localStorage.setItem(key, JSON.stringify(j)); } catch (e2) { /* ignore quota */ }
return key;
} catch (e) {
return '';
}
}
function soonOpenSoonJsonLocally(j, fileName) {
var key = soonPutSoonSession(j, fileName);
if (!key) return '';
var type = soonSoonTypeFromJson(j);
if (window.platformBridge && window.platformBridge.openDesignPage) {
window.platformBridge.openDesignPage(key, type);
}
return key;
}
function soonToast(message, type) {
@@ -244,14 +263,11 @@
if (response && response.status === 409) {
return { status: 409, message: '文件已被其他端修改,请刷新后重试', code: code || 'conflict' };
}
if (response && response.status === 403 && (code === 'membership_required' || msg.indexOf('订阅') >= 0 || msg.indexOf('会员') >= 0)) {
return { status: 403, message: msg || '此功能需要订阅后使用', code: code || 'membership_required' };
}
if (response && response.status === 413) {
if (code === 'file_limit_exceeded') {
return { status: 413, message: msg || '文件数量已达上限,请清理文件或续订', code: code };
return { status: 413, message: msg || '文件数量已达上限,请清理文件', code: code };
}
return { status: 413, message: msg || '存储空间不足,请清理文件或续订', code: code || 'quota_exceeded' };
return { status: 413, message: msg || '存储空间不足,请清理文件', code: code || 'quota_exceeded' };
}
if (response && response.status === 404) {
return { status: 404, message: '文件不存在', code: code || 'not_found' };
@@ -265,14 +281,9 @@
if (info.status === 401) kind = 'warn';
else if (info.status === 409) kind = 'warn';
else if (info.status === 413) kind = 'warn';
else if (info.status === 403 && info.code === 'membership_required') kind = 'warn';
soonToast(info.message, kind);
if (info.status === 401) {
soonRequireLogin('操作');
} else if (info.status === 403 && info.code === 'membership_required') {
if (typeof window.soonShowSubscribeGate === 'function') {
window.soonShowSubscribeGate({ action: '使用' });
}
}
}
@@ -290,6 +301,32 @@
return String(pathOrKey).split(/[/\\]/).pop();
}
function soonHandlePayReturn() {
var paid = null;
var flag = false;
try {
paid = new URLSearchParams(location.search).get('paid');
} catch (e) { /* ignore */ }
try {
flag = !!sessionStorage.getItem('soon_pay_return');
} catch (e) { /* ignore */ }
if (!paid && !flag) return;
try {
sessionStorage.removeItem('soon_pay_return');
} catch (e) { /* ignore */ }
if (paid) {
try {
var u = new URL(location.href);
u.searchParams.delete('paid');
history.replaceState(null, '', u.pathname + (u.search || '') + (u.hash || ''));
} catch (e) { /* ignore */ }
}
soonLoadMembership(true).then(function () {
soonRefreshPortalIdentity();
soonToast('支付成功,会员已激活', 'success');
});
}
window.soonIsWebPortal = soonIsWebPortal;
window.soonParseFileKey = soonParseFileKey;
window.soonMakeFileKey = soonMakeFileKey;
@@ -298,15 +335,23 @@
window.soonBindFileMeta = soonBindFileMeta;
window.soonFormatOpenTitle = soonFormatOpenTitle;
window.soonGuardCloudSave = soonGuardCloudSave;
window.soonGuardMemberPreview = soonGuardMemberPreview;
window.soonGuardMemberExport = soonGuardMemberExport;
window.soonGuardPreviewDeliver = soonGuardPreviewDeliver;
window.soonHandlePayReturn = soonHandlePayReturn;
window.soonOpenSoonJsonLocally = soonOpenSoonJsonLocally;
window.soonPutSoonSession = soonPutSoonSession;
window.soonSoonTypeFromJson = soonSoonTypeFromJson;
window.soonLoadMembership = soonLoadMembership;
window.soonApplyMembership = soonApplyMembership;
window.soonRefreshPortalIdentity = soonRefreshPortalIdentity;
window.soonIsMember = soonIsMember;
window.soonRequireMember = soonRequireMember;
window.soonRequireLogin = soonRequireLogin;
window.soonParseApiError = soonParseApiError;
window.soonShowApiError = soonShowApiError;
window.soonDisplayFileName = soonDisplayFileName;
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', soonHandlePayReturn);
} else {
soonHandlePayReturn();
}
})();
+249
View File
@@ -0,0 +1,249 @@
(function () {
'use strict';
var GATE_FEATURES = ['预览内导出成品', '预览内打印'];
var _gate = { index: null, payIndex: null, plan: null, gateOpts: null };
var pay = window.SoonMemberPay;
function esc(s) {
if (pay && pay.esc) return pay.esc(s);
if (s == null) return '';
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function toast(msg, type) {
if (typeof window.soonToast === 'function') window.soonToast(msg, type);
}
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 planPriceDisplay(plan) {
if (!plan) return '';
return plan.price_display || (plan.price_cents / 100).toFixed(2);
}
function shellHtml(actionLabel, plan) {
var feat = GATE_FEATURES.map(function (t) {
return '<li>' + esc(t) + '</li>';
}).join('');
var price = plan ? planPriceDisplay(plan) : '—';
var channels = (pay && pay.displayChannels) ? pay.displayChannels() : ['alipay'];
var chHtml = channels.map(function (ch, i) {
var label = ch === 'wechat' ? '微信支付' : '支付宝';
return '<label class="soon-activate-channel' + (i === 0 ? ' is-active' : '') + '">' +
'<input type="radio" name="activate_channel" value="' + esc(ch) + '"' + (i === 0 ? ' checked' : '') + '>' +
esc(label) + '</label>';
}).join('');
return '<div class="soon-subscribe-modal soon-activate-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(actionLabel || '导出或打印') + '需激活会员。</p>' +
'<ul class="soon-subscribe-modal__features">' + feat + '</ul>' +
'</header>' +
'<div class="soon-subscribe-modal__body">' +
'<div class="soon-activate-price"><span class="soon-activate-price__label">激活价格</span>' +
'<span class="soon-activate-price__value">¥' + esc(price) + '</span>' +
'<span class="soon-activate-price__hint">一次激活,永久有效</span></div>' +
'<div class="soon-activate-channels" data-role="channels">' + chHtml + '</div>' +
'<p class="soon-activate-load" data-role="plan-status"></p>' +
'</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 closeGate(fireStay) {
var idx = _gate.index;
var payIdx = _gate.payIndex;
var opts = _gate.gateOpts;
_gate = { index: null, payIndex: null, plan: null, gateOpts: null };
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 selectedChannel(modal) {
var checked = modal.querySelector('input[name="activate_channel"]:checked');
return checked ? checked.value : 'alipay';
}
function onPaySuccess() {
var gateIdx = _gate.index;
var opts = _gate.gateOpts;
_gate.payIndex = null;
if (typeof layer !== 'undefined' && gateIdx != null) layer.close(gateIdx);
_gate = { index: null, payIndex: null, plan: null, gateOpts: null };
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();
});
return;
}
if (opts && typeof opts.onSuccess === 'function') opts.onSuccess();
}
function updatePayButton(modal) {
var btn = modal.querySelector('[data-action="go-pay"]');
var plan = _gate.plan;
if (!btn) return;
if (!plan) {
btn.disabled = true;
btn.textContent = '立即激活';
return;
}
btn.disabled = false;
btn.textContent = '立即激活 ¥' + planPriceDisplay(plan);
}
function loadPlan(modal) {
var statusEl = modal.querySelector('[data-role="plan-status"]');
if (!pay || !pay.apiGet) {
if (statusEl) statusEl.textContent = '激活模块未加载,请刷新页面';
updatePayButton(modal);
return;
}
if (statusEl) statusEl.textContent = '正在加载激活方案…';
updatePayButton(modal);
pay.apiGet('/plans').then(function (ps) {
if (!ps.ok) {
if (statusEl) statusEl.textContent = ps.message || '方案加载失败';
updatePayButton(modal);
return;
}
var items = (ps.data && ps.data.items) || [];
var plan = items[0] || null;
if (!plan) {
if (statusEl) statusEl.textContent = '暂无可用激活方案';
updatePayButton(modal);
return;
}
_gate.plan = plan;
if (statusEl) statusEl.textContent = '';
updatePayButton(modal);
}).catch(function () {
if (statusEl) statusEl.textContent = '网络错误,请稍后重试';
updatePayButton(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 goPay = e.target.closest('[data-action="go-pay"]');
if (goPay) {
e.preventDefault();
if (goPay.disabled || !_gate.plan || !pay || !pay.openPayModal) return;
if (_gate.payIndex != null) {
try { layer.close(_gate.payIndex); } catch (err) { /* ignore */ }
_gate.payIndex = null;
}
_gate.payIndex = pay.openPayModal({
planId: _gate.plan.id,
planName: _gate.plan.name,
priceDisplay: planPriceDisplay(_gate.plan),
channel: selectedChannel(modal),
shadeClose: true,
onPaid: onPaySuccess,
onClose: function () {
_gate.payIndex = null;
updatePayButton(modal);
},
});
}
var ch = e.target.closest('.soon-activate-channel');
if (ch) {
modal.querySelectorAll('.soon-activate-channel').forEach(function (el) {
el.classList.remove('is-active');
});
ch.classList.add('is-active');
var input = ch.querySelector('input[type="radio"]');
if (input) input.checked = true;
}
});
loadPlan(modal);
}
function soonShowActivateGate(opts) {
opts = opts || {};
if (!pay) {
toast('激活功能暂不可用,请刷新页面后重试', 'warn');
return;
}
if (pay.loadDisplayChannels) pay.loadDisplayChannels();
ensureLayer(function () {
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 = { index: null, payIndex: null, plan: null, gateOpts: opts };
var width = Math.min(520, window.innerWidth - 24);
layer.open({
type: 1,
skin: 'soon-layer',
title: false,
closeBtn: 1,
shadeClose: true,
area: [width + 'px', 'auto'],
content: shellHtml(opts.reason, null),
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-activate-modal');
_gate.index = index;
_gate.gateOpts = opts;
if (modal) bindModal(modal);
},
end: function () {
var payIdx = _gate.payIndex;
if (payIdx != null) {
try { layer.close(payIdx); } catch (e) { /* ignore */ }
}
_gate = { index: null, payIndex: null, plan: null, gateOpts: null };
},
});
});
}
window.soonShowActivateGate = soonShowActivateGate;
if (!window._soonActivateFocusBound) {
window._soonActivateFocusBound = true;
window.addEventListener('focus', function () {
if (typeof window.soonLoadMembership !== 'function') return;
window.soonLoadMembership(true).then(function () {
if (typeof window.soonRefreshPortalIdentity === 'function') window.soonRefreshPortalIdentity();
});
});
}
})();
+152
View File
@@ -0,0 +1,152 @@
(function () {
'use strict';
var _gate = { index: null, opts: null };
function esc(s) {
if (s == null) return '';
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function toast(msg, type) {
if (typeof window.soonToast === 'function') window.soonToast(msg, type);
}
function apiBase() {
var cfg = window.SOON_DEPLOY_CONFIG && window.SOON_DEPLOY_CONFIG.api_v1_base;
if (cfg) return String(cfg).replace(/\/+$/, '');
if (window.location) return window.location.origin + '/api/v1';
return '/api/v1';
}
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 shellHtml(reason) {
return '<div class="soon-login-gate">' +
'<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(reason || '导出或打印') + '需先登录账号。</p>' +
'</header>' +
'<form class="soon-login-gate__form" data-role="login-form">' +
'<div class="soon-input-wrap"><input type="email" name="email" required class="soon-input" placeholder="邮箱" autocomplete="email"></div>' +
'<div class="soon-input-wrap"><input type="password" name="password" required class="soon-input" placeholder="密码" autocomplete="current-password"></div>' +
'<p class="soon-login-gate__error" data-role="login-error" style="display:none"></p>' +
'<button type="submit" class="soon-btn soon-btn--block">登录</button>' +
'</form>' +
'<p class="soon-login-gate__links">还没有账号?<a href="register.web.html" target="_blank" rel="noopener">立即注册</a></p>' +
'<footer class="soon-subscribe-modal__foot">' +
'<button type="button" class="soon-subscribe-modal__stay" data-action="stay">继续设计</button>' +
'</footer></div>';
}
function closeGate() {
var idx = _gate.index;
_gate = { index: null, opts: null };
if (idx != null && typeof layer !== 'undefined') {
try { layer.close(idx); } catch (e) { /* ignore */ }
}
}
function bindModal(modal) {
var form = modal.querySelector('[data-role="login-form"]');
var errEl = modal.querySelector('[data-role="login-error"]');
modal.addEventListener('click', function (e) {
var stay = e.target.closest('[data-action="stay"]');
if (stay) {
e.preventDefault();
closeGate();
}
});
if (!form) return;
form.addEventListener('submit', function (e) {
e.preventDefault();
if (errEl) {
errEl.style.display = 'none';
errEl.textContent = '';
}
var email = (form.email && form.email.value || '').trim();
var password = form.password ? form.password.value : '';
fetch(apiBase() + '/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: email, password: password }),
}).then(function (r) { return r.json(); }).then(function (j) {
if (!j.ok || !j.data || !j.data.access_token) {
if (errEl) {
errEl.textContent = (j && j.message) || '登录失败';
errEl.style.display = 'block';
}
return;
}
localStorage.setItem('soon_access', j.data.access_token);
if (j.data.refresh_token) localStorage.setItem('soon_refresh', j.data.refresh_token);
var opts = _gate.opts;
closeGate();
var load = typeof window.soonLoadMembership === 'function'
? window.soonLoadMembership(true)
: Promise.resolve();
load.then(function () {
if (typeof window.soonRefreshPortalIdentity === 'function') window.soonRefreshPortalIdentity();
if (typeof window.soonReloadRecentFiles === 'function') window.soonReloadRecentFiles();
if (opts && typeof opts.onSuccess === 'function') opts.onSuccess();
});
}).catch(function () {
if (errEl) {
errEl.textContent = '网络错误,请稍后重试';
errEl.style.display = 'block';
}
});
});
}
function soonShowLoginGate(opts) {
opts = opts || {};
ensureLayer(function () {
if (_gate.index != null) {
try { layer.close(_gate.index); } catch (e) { /* ignore */ }
}
_gate.opts = opts;
var width = Math.min(420, window.innerWidth - 24);
layer.open({
type: 1,
skin: 'soon-layer',
title: false,
closeBtn: 1,
shadeClose: true,
area: [width + 'px', 'auto'],
content: shellHtml(opts.reason),
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-login-gate');
_gate.index = index;
_gate.opts = opts;
if (modal) bindModal(modal);
},
end: function () {
_gate = { index: null, opts: null };
},
});
});
}
window.soonShowLoginGate = soonShowLoginGate;
})();
+4 -23
View File
@@ -2,7 +2,7 @@
'use strict';
var POLL_TIMEOUT_MS = 5 * 60 * 1000;
var PAY_STEP_LABELS = ['选择支付', '扫码付款', '订阅生效'];
var PAY_STEP_LABELS = ['选择支付', '扫码付款', '激活生效'];
var _displayChannels = ['alipay'];
var _settingsPromise = null;
@@ -260,24 +260,7 @@
return '未获取到支付信息';
}
function monthlyPlan(items) {
var found = null;
(items || []).forEach(function (p) {
if (p.code === 'member_monthly' || p.code === 'pro_monthly') found = p;
});
return found;
}
function billingSavePercent(items, plan) {
var monthly = monthlyPlan(items);
if (!monthly || monthly.price_cents <= 0 || !plan || plan.duration_days <= 30) return null;
var months = plan.duration_days / 30;
var full = monthly.price_cents * months;
if (full <= plan.price_cents) return null;
return Math.round((1 - plan.price_cents / full) * 100);
}
function paySheetHtml(opts) {
function payEmptyStateHtml(channel) {
opts = opts || {};
var resumeOrderNo = opts.orderNo || null;
var channels = opts.displayChannels || _displayChannels;
@@ -291,7 +274,7 @@
var headBlock = embedded ? '' :
'<header class="soon-pay-sheet__head">' +
'<div class="soon-pay-sheet__head-main">' +
'<span class="soon-pay-sheet__eyebrow">' + (resumeOrderNo ? '继续支付' : '确认订阅') + '</span>' +
'<span class="soon-pay-sheet__eyebrow">' + (resumeOrderNo ? '继续支付' : '确认激活') + '</span>' +
'<h3 class="soon-pay-sheet__title">' + esc(opts.planName || '') + '</h3>' +
'<p class="soon-pay-sheet__order" id="payOrderNo"' +
(resumeOrderNo ? '' : ' style="display:none"') + '>' +
@@ -381,7 +364,7 @@
function handlePaid() {
stopPoll();
setStep(3);
setStatus('支付成功,正在更新订阅…', 'ok');
setStatus('支付成功,正在更新会员状态…', 'ok');
setTimeout(onPaid, 600);
}
@@ -538,8 +521,6 @@
apiGet: apiGet,
apiPost: apiPost,
esc: esc,
monthlyPlan: monthlyPlan,
billingSavePercent: billingSavePercent,
paySheetHtml: paySheetHtml,
bindPaySheet: bindPaySheet,
openPayModal: openPayModal,
-104
View File
@@ -1,104 +0,0 @@
(function () {
'use strict';
var CHECK_SVG = '<svg viewBox="0 0 12 12" fill="none" stroke-width="2" stroke="currentColor">' +
'<path d="M2 6l3 3 5-6"/></svg>';
function esc(s) {
if (window.SoonMemberPay && window.SoonMemberPay.esc) return window.SoonMemberPay.esc(s);
if (s == null) return '';
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function isPlanFeatureLine(line) {
if (!line) return false;
return line.indexOf('到期') < 0
&& line.indexOf('订阅有效期') < 0
&& line.indexOf('订阅周期') < 0
&& line.indexOf('权益一致') < 0;
}
function featureItem(text) {
return '<li><span class="soon-plan-card__check">' + CHECK_SVG + '</span><span>' + esc(text) + '</span></li>';
}
function billingSavePercent(items, plan) {
if (window.SoonMemberPay && window.SoonMemberPay.billingSavePercent) {
return window.SoonMemberPay.billingSavePercent(items, plan);
}
return null;
}
function planCardHtml(plan, ctx) {
ctx = ctx || {};
var items = ctx.allPlans || [];
var mode = ctx.mode || 'page';
var isMember = !!ctx.isMember;
var currentCode = ctx.currentCode || '';
var selectedId = ctx.selectedId;
var feat = plan.is_recommended ? ' soon-plan-card--featured' : '';
var cur = plan.code === currentCode ? ' soon-plan-card--current' : '';
var isGate = mode === 'gate';
var isPicker = mode === 'picker' || isGate;
var picker = isPicker ? ' soon-plan-card--picker' : '';
if (isGate) picker += ' soon-plan-card--gate';
var selected = isPicker && plan.id === selectedId ? ' soon-plan-card--selected' : '';
var ribbon = plan.is_recommended
? '<span class="soon-plan-card__ribbon soon-plan-card__ribbon--rec">推荐</span>'
: (plan.code === currentCode ? '<span class="soon-plan-card__ribbon soon-plan-card__ribbon--current">当前</span>' : '');
var savePct = billingSavePercent(items, plan);
if (savePct) ribbon += '<span class="soon-plan-card__save">省 ' + savePct + '%</span>';
var badgeCls = ribbon ? ' soon-plan-card--badged' : '';
if (savePct) badgeCls += ' soon-plan-card--save';
var period = plan.duration_days > 0
? '<span class="soon-plan-card__period">/' + esc(plan.period_label || plan.duration_days + '天') + '</span>'
: '';
var features = (plan.features || []).filter(isPlanFeatureLine).map(featureItem).join('');
var desc = plan.description || '';
var price = esc(plan.price_display || (plan.price_cents / 100).toFixed(2));
var inner =
ribbon +
'<div class="soon-plan-card__head">' +
'<div class="soon-plan-card__name">' + esc(plan.name) + '</div>' +
(desc ? '<p class="soon-plan-card__desc">' + esc(desc) + '</p>' : '') +
'<div class="soon-plan-card__price-row">' +
'<span class="soon-plan-card__currency">¥</span>' +
'<span class="soon-plan-card__price">' + price + '</span>' + period + '</div></div>';
if (!isGate) {
inner += '<div class="soon-plan-card__body">' +
'<ul class="soon-plan-card__features">' + features + '</ul></div>';
}
if (isPicker) {
return '<button type="button" class="soon-plan-card' + feat + cur + badgeCls + picker + selected + '" ' +
'data-action="pick-plan" data-id="' + plan.id + '" aria-pressed="' + (plan.id === selectedId ? 'true' : 'false') + '">' +
inner + '</button>';
}
var cta = '<button type="button" class="soon-btn soon-plan-card__cta" data-id="' + plan.id +
'" data-name="' + esc(plan.name) + '" data-price="' + price + '">' +
(isMember ? '续订' : '立即订阅') + '</button>';
return '<article class="soon-plan-card' + feat + cur + badgeCls + '">' + inner +
'<div class="soon-plan-card__foot">' + cta + '</div></article>';
}
function plansGridHtml(plans, ctx) {
return (plans || []).map(function (p) {
return planCardHtml(p, ctx);
}).join('');
}
window.SoonMemberPlan = {
isPlanFeatureLine: isPlanFeatureLine,
planCardHtml: planCardHtml,
plansGridHtml: plansGridHtml,
};
})();
-306
View File
@@ -1,306 +0,0 @@
(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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
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();
});
});
}
})();
-2
View File
@@ -24,7 +24,6 @@
var tok = localStorage.getItem('soon_access') || '';
var login = document.getElementById('auth_login');
var reg = document.getElementById('auth_register');
var member = document.getElementById('auth_member');
var admin = document.getElementById('auth_admin');
var logout = document.getElementById('auth_logout');
var avatar = document.getElementById('auth_avatar');
@@ -34,7 +33,6 @@
showAuthedNav();
if (login) login.style.display = 'none';
if (reg) reg.style.display = 'none';
if (member) member.style.display = 'inline-flex';
if (logout) logout.style.display = 'inline-flex';
var base = (window.SOON_DEPLOY_CONFIG && window.SOON_DEPLOY_CONFIG.api_v1_base) || '';
+2 -4
View File
@@ -3,7 +3,6 @@
var STANDARD_LINKS = [
{ id: 'home', href: 'index.web.html', label: '设计首页', show: 'always' },
{ id: 'member', href: 'member.web.html', label: '订阅', show: 'authed', elId: 'auth_member' },
{ id: 'admin', href: 'admin/index.html', label: '管理', show: 'admin', elId: 'auth_admin' },
];
@@ -22,7 +21,6 @@
'<span id="auth_identity" class="soon-portal-identity" style="display:none"></span>' +
'</div>' +
'<nav class="soon-portal-topbar__session" id="auth_session" style="display:none" aria-label="账户">' +
'<a href="member.web.html" id="auth_member" class="soon-portal-topbar__link" style="display:none">订阅</a>' +
'<a href="admin/index.html" id="auth_admin" class="soon-portal-topbar__link" style="display:none">管理</a>' +
'<a href="#" id="auth_logout" class="soon-portal-topbar__link" style="display:none">退出</a>' +
'</nav>';
@@ -30,7 +28,7 @@
function renderLinks(links, active) {
return links.map(function (lnk) {
if (lnk.id === 'member' || lnk.id === 'admin') return '';
if (lnk.id === 'admin') return '';
var cls = 'soon-portal-topbar__link' + (active === lnk.id ? ' is-active' : '');
return '<a href="' + esc(lnk.href) + '" class="' + cls + '">' + esc(lnk.label) + '</a>';
}).join('');
@@ -46,7 +44,7 @@
/**
* @param {object} opts
* @param {string} [opts.variant] standard | auth | design
* @param {string} [opts.active] home | member
* @param {string} [opts.active] home
* @param {string} [opts.extraClass]
*/
function html(opts) {