永久会员与模板库后台化:预览门控、轻量首页与 Admin CRUD
- 会员改为永久激活方案;云端文件不再拦截非会员;预览弹窗内导出/打印才校验 - 首页最近文件支持本地记录,登录后与云端合并;移除独立会员页与订阅页 - 模板库入库管理:soon_templates 表、Admin 上传 CRUD、/templates 轻量列表与按需下载 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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();
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
}
|
||||
})();
|
||||
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
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;
|
||||
})();
|
||||
@@ -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,
|
||||
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
})();
|
||||
@@ -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, '&').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();
|
||||
});
|
||||
});
|
||||
}
|
||||
})();
|
||||
@@ -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) || '';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -52,9 +52,7 @@ async function saveImageAsPNG(buffer) {
|
||||
}
|
||||
}
|
||||
|
||||
// 将 display_func 附加到 window 对象,确保全局可访问
|
||||
window.display_func = function display_func(img1, img2, img3) {
|
||||
if (typeof window.soonGuardMemberPreview === 'function' && !window.soonGuardMemberPreview()) return;
|
||||
$("#base_control").hide();
|
||||
$("#line_control").hide();
|
||||
$("#pic_control").hide();
|
||||
@@ -418,16 +416,33 @@ window.display_func = function display_func(img1, img2, img3) {
|
||||
</div>
|
||||
</div>`,
|
||||
btn: [language_str("output"), "打印"],//'导出'
|
||||
btn1: function (index, layero) {
|
||||
saveImageAsPNG(buffer);
|
||||
btn1: function () {
|
||||
if (typeof window.soonGuardPreviewDeliver === 'function') {
|
||||
window.soonGuardPreviewDeliver(language_str('output') || '导出', function () {
|
||||
saveImageAsPNG(buffer);
|
||||
});
|
||||
} else {
|
||||
saveImageAsPNG(buffer);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
btn2: function () {
|
||||
if (window.platformBridge && window.platformBridge.printPdf) {
|
||||
if (typeof window.soonGuardPreviewDeliver === 'function') {
|
||||
window.soonGuardPreviewDeliver('打印', function () {
|
||||
if (window.platformBridge && window.platformBridge.printPdf) {
|
||||
var blob = buffer instanceof Uint8Array ? new Blob([buffer], { type: 'image/png' }) : new Blob([buffer], { type: 'image/png' });
|
||||
window.platformBridge.printPdf(blob);
|
||||
} else if (typeof printJS === 'function') {
|
||||
printJS({ printable: url3, type: 'image', style: 'img { width: 100%; height: auto; }' });
|
||||
}
|
||||
});
|
||||
} else if (window.platformBridge && window.platformBridge.printPdf) {
|
||||
var blob = buffer instanceof Uint8Array ? new Blob([buffer], { type: 'image/png' }) : new Blob([buffer], { type: 'image/png' });
|
||||
window.platformBridge.printPdf(blob);
|
||||
} else if (typeof printJS === 'function') {
|
||||
printJS({ printable: url3, type: 'image', style: 'img { width: 100%; height: auto; }' });
|
||||
}
|
||||
return false;
|
||||
},
|
||||
end: function () {
|
||||
if (printBlobUrl) try { URL.revokeObjectURL(printBlobUrl); } catch (e) {}
|
||||
@@ -456,7 +471,6 @@ window.display_func = function display_func(img1, img2, img3) {
|
||||
|
||||
// 将 output 附加到 window 对象,确保全局可访问
|
||||
window.output = function output(callback = null, _save = save) {
|
||||
if (typeof window.soonGuardMemberExport === 'function' && !window.soonGuardMemberExport()) return;
|
||||
// 类型改变
|
||||
fabric.Image.fromURL(soonAsset('op_1.png'), function (i1) {
|
||||
i1.left = background_image.left;
|
||||
@@ -1395,7 +1409,6 @@ function save(op1, callback) {
|
||||
}
|
||||
|
||||
window.saveHistory = function saveHistory() {
|
||||
if (typeof window.soonIsWebPortal === 'function' && window.soonIsWebPortal()) return;
|
||||
function doWrite(j) {
|
||||
var currentPath = openAs.name;
|
||||
if (!currentPath) return;
|
||||
|
||||
@@ -1355,7 +1355,6 @@ $("#open").click(function () {
|
||||
OpenDialog();
|
||||
});
|
||||
function OpenDialog() {
|
||||
if (typeof window.soonRequireLogin === 'function' && !window.soonRequireLogin('打开文件')) return;
|
||||
var dialogApi = (typeof dialog !== 'undefined' && dialog) ? dialog : (window.platformBridge && window.platformBridge.showOpenDialog ? { showOpenDialog: function(opts) { return window.platformBridge.showOpenDialog(opts); } } : null);
|
||||
if (!dialogApi) return;
|
||||
dialogApi.showOpenDialog({
|
||||
@@ -1376,6 +1375,14 @@ $("#open").click(function () {
|
||||
}
|
||||
}
|
||||
function importAndOpen(j, fileName) {
|
||||
var tok = typeof window.soonGetAccessToken === 'function'
|
||||
? window.soonGetAccessToken()
|
||||
: (localStorage.getItem('soon_access') || '');
|
||||
if (!tok && typeof window.soonPutSoonSession === 'function') {
|
||||
var localKey = window.soonPutSoonSession(j, fileName || 'design.soon');
|
||||
if (localKey) openWithKey(localKey, j);
|
||||
return;
|
||||
}
|
||||
if (!window.platformBridge || !window.platformBridge.importSoonFile) return;
|
||||
window.platformBridge.importSoonFile(fileName || 'design.soon', j).then(function(res) {
|
||||
if (res && res.fileKey) openWithKey(res.fileKey, j);
|
||||
@@ -2365,7 +2372,6 @@ $("#help").click(function() {
|
||||
|
||||
// ===========================================================
|
||||
$('#display').on('click', function () {
|
||||
if (typeof window.soonGuardMemberPreview === 'function' && !window.soonGuardMemberPreview()) return;
|
||||
// 类型改变
|
||||
fabric.Image.fromURL(soonAsset('front_bg') + bg_version + '_1.png', function (i1) {
|
||||
i1.left = background_image.left;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
// 将 display_func 附加到 window 对象,确保全局可访问
|
||||
window.display_func = function display_func(img1, img2, img3) {
|
||||
if (typeof window.soonGuardMemberPreview === 'function' && !window.soonGuardMemberPreview()) return;
|
||||
$('#base_control').hide()
|
||||
$('#line_control').hide()
|
||||
$('#pic_control').hide()
|
||||
@@ -356,29 +354,55 @@ window.display_func = function display_func(img1, img2, img3) {
|
||||
</div>
|
||||
</div>`,
|
||||
btn: btns, //'导出'
|
||||
btn1: function (index, layero) {
|
||||
if (typeof window.savePdf === 'function') {
|
||||
btn1: function () {
|
||||
if (typeof window.soonGuardPreviewDeliver === 'function') {
|
||||
window.soonGuardPreviewDeliver('导出', function () {
|
||||
if (typeof window.savePdf === 'function') window.savePdf(pdfBlob);
|
||||
});
|
||||
} else if (typeof window.savePdf === 'function') {
|
||||
window.savePdf(pdfBlob);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
btn2: function () {
|
||||
if (window.platformBridge && window.platformBridge.printPdf && (printPath3.indexOf('data:') === 0 || printPath4.indexOf('data:') === 0)) {
|
||||
if (typeof window.soonGuardPreviewDeliver === 'function') {
|
||||
window.soonGuardPreviewDeliver('打印', function () {
|
||||
if (window.platformBridge && window.platformBridge.printPdf && (printPath3.indexOf('data:') === 0 || printPath4.indexOf('data:') === 0)) {
|
||||
if (btns[1] === '打印正面') window.platformBridge.printPdf(pdfBlob);
|
||||
else if (btns[1] === '打印背面') window.platformBridge.printPdf(pdfBlob);
|
||||
return;
|
||||
}
|
||||
if (btns[1] === '打印正面') {
|
||||
printJS({ printable: printPath3, type: 'image', style: 'img { width: 100%; height: auto; }' });
|
||||
} else if (btns[1] === '打印背面') {
|
||||
printJS({ printable: printPath4, type: 'image', style: 'img { width: 100%; height: auto; }' });
|
||||
}
|
||||
});
|
||||
} else if (window.platformBridge && window.platformBridge.printPdf && (printPath3.indexOf('data:') === 0 || printPath4.indexOf('data:') === 0)) {
|
||||
if (btns[1] === '打印正面') window.platformBridge.printPdf(pdfBlob);
|
||||
else if (btns[1] === '打印背面') window.platformBridge.printPdf(pdfBlob);
|
||||
return;
|
||||
}
|
||||
if (btns[1] === '打印正面') {
|
||||
} else if (btns[1] === '打印正面') {
|
||||
printJS({ printable: printPath3, type: 'image', style: 'img { width: 100%; height: auto; }' });
|
||||
} else if (btns[1] === '打印背面') {
|
||||
printJS({ printable: printPath4, type: 'image', style: 'img { width: 100%; height: auto; }' });
|
||||
}
|
||||
return false;
|
||||
},
|
||||
btn3: function () {
|
||||
if (window.platformBridge && window.platformBridge.printPdf) {
|
||||
if (typeof window.soonGuardPreviewDeliver === 'function') {
|
||||
window.soonGuardPreviewDeliver('打印', function () {
|
||||
if (window.platformBridge && window.platformBridge.printPdf) {
|
||||
window.platformBridge.printPdf(pdfBlob);
|
||||
return;
|
||||
}
|
||||
printJS({ printable: printPath4, type: 'image', style: 'img { width: 100%; height: auto; }' });
|
||||
});
|
||||
} else if (window.platformBridge && window.platformBridge.printPdf) {
|
||||
window.platformBridge.printPdf(pdfBlob);
|
||||
return;
|
||||
} else {
|
||||
printJS({ printable: printPath4, type: 'image', style: 'img { width: 100%; height: auto; }' });
|
||||
}
|
||||
printJS({ printable: printPath4, type: 'image', style: 'img { width: 100%; height: auto; }' });
|
||||
return false;
|
||||
},
|
||||
end: function () {
|
||||
// 预览窗口关闭后,恢复所有对象的 selectable 和 evented 状态
|
||||
@@ -406,7 +430,6 @@ window.display_func = function display_func(img1, img2, img3) {
|
||||
|
||||
// 将 output 附加到 window 对象,确保全局可访问
|
||||
window.output = function output(callback = null, _save = save) {
|
||||
if (typeof window.soonGuardMemberExport === 'function' && !window.soonGuardMemberExport()) return;
|
||||
// 类型改变
|
||||
fabric.Image.fromURL(soonAsset('op_2.png'), function (i1) {
|
||||
i1.left = background_image.left
|
||||
@@ -1215,7 +1238,6 @@ function save(op1, callback) {
|
||||
}
|
||||
|
||||
window.saveHistory = function saveHistory() {
|
||||
if (typeof window.soonIsWebPortal === 'function' && window.soonIsWebPortal()) return;
|
||||
function doWrite(j) {
|
||||
var currentPath = openAs.name;
|
||||
if (!currentPath) return;
|
||||
|
||||
@@ -1311,7 +1311,6 @@ $('#open').click(function () {
|
||||
}
|
||||
)
|
||||
function OpenDialog() {
|
||||
if (typeof window.soonRequireLogin === 'function' && !window.soonRequireLogin('打开文件')) return;
|
||||
var dialogApi = (typeof dialog !== 'undefined' && dialog) ? dialog : (window.platformBridge && window.platformBridge.showOpenDialog ? { showOpenDialog: function(opts) { return window.platformBridge.showOpenDialog(opts); } } : null);
|
||||
if (!dialogApi) return;
|
||||
dialogApi.showOpenDialog({ title: '请选择文件', buttonLabel: language_str('comf'), filters: [{ name: 'Soon File Type', extensions: ['soon'] }] })
|
||||
@@ -1327,6 +1326,14 @@ $('#open').click(function () {
|
||||
if (typeof window.openFile === 'function') window.openFile(fileKey, j);
|
||||
}
|
||||
function importAndOpen(j, fileName) {
|
||||
var tok = typeof window.soonGetAccessToken === 'function'
|
||||
? window.soonGetAccessToken()
|
||||
: (localStorage.getItem('soon_access') || '');
|
||||
if (!tok && typeof window.soonPutSoonSession === 'function') {
|
||||
var localKey = window.soonPutSoonSession(j, fileName || 'design.soon');
|
||||
if (localKey) openWithKey(localKey, j);
|
||||
return;
|
||||
}
|
||||
if (!window.platformBridge || !window.platformBridge.importSoonFile) return;
|
||||
window.platformBridge.importSoonFile(fileName || 'design.soon', j).then(function(res) {
|
||||
if (res && res.fileKey) openWithKey(res.fileKey, j);
|
||||
@@ -2694,7 +2701,6 @@ $("#help").click(function() {
|
||||
|
||||
// ===========================================================
|
||||
$('#display').off('click').on('click', function () {
|
||||
if (typeof window.soonGuardMemberPreview === 'function' && !window.soonGuardMemberPreview()) return;
|
||||
// 类型改变
|
||||
fabric.Image.fromURL(soonAsset('front_bg') + bg_version + '_2.png', function (i1) {
|
||||
i1.left = background_image.left
|
||||
|
||||
+212
-109
@@ -205,9 +205,111 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
: ('soondesign_file:' + id + ':v' + version);
|
||||
}
|
||||
|
||||
var fileListState = { page: 1, size: 12, total: 0 };
|
||||
var fileListState = { page: 1, size: 12, total: 0, items: [] };
|
||||
var templateListState = { page: 1, size: 8, total: 0, items: [] };
|
||||
|
||||
function displayNameFromPath(path) {
|
||||
if (!path) return 'design.soon';
|
||||
var sessionPrefix = 'soondesign_session:';
|
||||
if (path.indexOf(sessionPrefix) === 0) return path.substring(sessionPrefix.length);
|
||||
if (path.indexOf('soondesign_file:') === 0) {
|
||||
var meta = window._soonFileMeta;
|
||||
if (meta && meta.name) return meta.name;
|
||||
return path.replace(/^soondesign_file:(\d+).*/, '文件 #$1');
|
||||
}
|
||||
return get_filename(path) || 'design.soon';
|
||||
}
|
||||
|
||||
function parseItemTime(value) {
|
||||
if (!value) return 0;
|
||||
var d = new Date(String(value).replace(' ', 'T'));
|
||||
return isNaN(d.getTime()) ? 0 : d.getTime();
|
||||
}
|
||||
|
||||
function mergeRecentItems(localHistory, cloudItems) {
|
||||
var items = [];
|
||||
var cloudById = {};
|
||||
var seenCloudIds = {};
|
||||
(cloudItems || []).forEach(function (c) {
|
||||
if (c && c.id != null) cloudById[c.id] = c;
|
||||
});
|
||||
|
||||
(localHistory || []).forEach(function (h) {
|
||||
if (!h || !h.path) return;
|
||||
var cloudMatch = String(h.path).match(/^soondesign_file:(\d+)/);
|
||||
if (cloudMatch) {
|
||||
var id = parseInt(cloudMatch[1], 10);
|
||||
var cloud = cloudById[id];
|
||||
if (cloud) {
|
||||
seenCloudIds[id] = true;
|
||||
items.push({
|
||||
kind: 'cloud',
|
||||
filePath: makeFileKey(cloud.id, cloud.version),
|
||||
fileId: cloud.id,
|
||||
name: cloud.name || ('文件 #' + cloud.id),
|
||||
type: h.type,
|
||||
sortTime: parseItemTime(cloud.updated_at || cloud.created_at || h.time),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
items.push({
|
||||
kind: 'local',
|
||||
filePath: h.path,
|
||||
name: displayNameFromPath(h.path),
|
||||
type: h.type,
|
||||
sortTime: parseItemTime(h.time),
|
||||
});
|
||||
});
|
||||
|
||||
(cloudItems || []).forEach(function (c) {
|
||||
if (!c || c.id == null || seenCloudIds[c.id]) return;
|
||||
items.push({
|
||||
kind: 'cloud',
|
||||
filePath: makeFileKey(c.id, c.version),
|
||||
fileId: c.id,
|
||||
name: c.name || ('文件 #' + c.id),
|
||||
type: null,
|
||||
sortTime: parseItemTime(c.updated_at || c.created_at),
|
||||
});
|
||||
});
|
||||
|
||||
items.sort(function (a, b) { return (b.sortTime || 0) - (a.sortTime || 0); });
|
||||
return items;
|
||||
}
|
||||
|
||||
async function fetchLocalHistory() {
|
||||
if (!window.sysAPI || typeof window.sysAPI.readHistory !== 'function') return [];
|
||||
try {
|
||||
var j = await window.sysAPI.readHistory();
|
||||
return (j && Array.isArray(j.history)) ? j.history : [];
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCloudItems() {
|
||||
if (!hasCloudAuth() || !window.platformBridge || !window.platformBridge.listCloudFiles) return [];
|
||||
try {
|
||||
var data = await window.platformBridge.listCloudFiles({ page: 1, size: 50 });
|
||||
return (data && data.items) ? data.items : [];
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function removeLocalHistoryPath(path) {
|
||||
if (!path || !window.sysAPI || typeof window.sysAPI.readHistory !== 'function') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return window.sysAPI.readHistory().then(function (j) {
|
||||
var list = (j && Array.isArray(j.history)) ? j.history : [];
|
||||
var next = list.filter(function (item) { return item.path !== path; });
|
||||
if (next.length === list.length) return null;
|
||||
return window.sysAPI.writeHistory({ history: next });
|
||||
}).catch(function () { return null; });
|
||||
}
|
||||
|
||||
function renderFilePager() {
|
||||
var el = document.getElementById('filePager');
|
||||
if (!el) return;
|
||||
@@ -247,41 +349,17 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
try {
|
||||
|
||||
var recentCountEl = document.getElementById('recentCount');
|
||||
var filePagerEl = document.getElementById('filePager');
|
||||
|
||||
if (!hasCloudAuth()) {
|
||||
|
||||
if (recentCountEl) recentCountEl.textContent = '0';
|
||||
if (filePagerEl) filePagerEl.innerHTML = '';
|
||||
|
||||
$(".card-list").html(soonEmptyBlock('登录后查看文件', '请先登录以管理云端设计文件',
|
||||
|
||||
'<a href="login.web.html" class="soon-btn soon-btn--sm soon-empty__action">去登录</a>'));
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
if (!window.platformBridge || !window.platformBridge.listCloudFiles) {
|
||||
|
||||
$(".card-list").html("");
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
var data = await window.platformBridge.listCloudFiles({
|
||||
page: fileListState.page,
|
||||
size: fileListState.size,
|
||||
});
|
||||
|
||||
var items = (data && data.items) ? data.items : [];
|
||||
fileListState.total = (data && data.total != null) ? data.total : items.length;
|
||||
if (data && data.page) fileListState.page = data.page;
|
||||
var localHistory = await fetchLocalHistory();
|
||||
var cloudItems = await fetchCloudItems();
|
||||
var merged = mergeRecentItems(localHistory, cloudItems);
|
||||
fileListState.items = merged;
|
||||
fileListState.total = merged.length;
|
||||
var pages = Math.max(1, Math.ceil(fileListState.total / fileListState.size));
|
||||
if (fileListState.page > pages) fileListState.page = pages;
|
||||
|
||||
if (recentCountEl) recentCountEl.textContent = String(fileListState.total);
|
||||
|
||||
if (!items.length) {
|
||||
if (!merged.length) {
|
||||
|
||||
$(".card-list").html(soonEmptyBlock('暂无文件', '点击「打开文件」导入,或新建模板开始设计'));
|
||||
renderFilePager();
|
||||
@@ -289,20 +367,17 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
|
||||
}
|
||||
|
||||
var start = (fileListState.page - 1) * fileListState.size;
|
||||
var slice = merged.slice(start, start + fileListState.size);
|
||||
let h = "";
|
||||
|
||||
for (let item of items) {
|
||||
|
||||
let filePath = makeFileKey(item.id, item.version);
|
||||
for (let item of slice) {
|
||||
|
||||
let filePath = item.filePath;
|
||||
let src = "";
|
||||
|
||||
let fileExists = true;
|
||||
|
||||
let imgStyle = "";
|
||||
|
||||
let realType = 1;
|
||||
|
||||
let realType = item.type || 1;
|
||||
const soonData = await window.sysAPI.readJsonFile(filePath);
|
||||
|
||||
if (soonData) {
|
||||
@@ -323,13 +398,13 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
|
||||
fileExists = false;
|
||||
|
||||
src = soonAsset('bg_1.png');
|
||||
src = soonAsset((realType == 2 || realType == "2") ? 'bg_2.png' : 'bg_1.png');
|
||||
|
||||
imgStyle = "opacity: 0.6; filter: grayscale(100%);";
|
||||
|
||||
}
|
||||
|
||||
let displayName = item.name || ('文件 #' + item.id);
|
||||
let displayName = item.name || displayNameFromPath(filePath);
|
||||
|
||||
let cardTitle = displayName;
|
||||
|
||||
@@ -349,27 +424,29 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
|
||||
: '';
|
||||
|
||||
var dlBtn = '<button type="button" class="soon-file-card__action soon-file-card__download" title="下载 .soon" aria-label="下载">'
|
||||
var actions = '';
|
||||
if (item.kind === 'cloud' && item.fileId) {
|
||||
actions = '<button type="button" class="soon-file-card__action soon-file-card__download" title="下载 .soon" aria-label="下载">'
|
||||
+ '<svg class="soon-file-card__icon" viewBox="0 0 24 24" width="15" height="15" aria-hidden="true">'
|
||||
+ '<path d="M12 4v10m0 0l3.5-3.5M12 14l-3.5-3.5M5 20h14"/></svg></button>'
|
||||
+ '<button type="button" class="soon-file-card__action soon-file-card__delete" title="' + escapeAttr(language_str('deleteBtn')) + '" aria-label="' + escapeAttr(language_str('deleteBtn')) + '">'
|
||||
+ '<svg class="soon-file-card__icon" viewBox="0 0 24 24" width="15" height="15" aria-hidden="true">'
|
||||
+ '<path d="M4 7h16"/><path d="M7 7l1.1 11.1a.9.9 0 00.9.9h6a.9.9 0 00.9-.9L17 7"/>'
|
||||
+ '<path d="M9.5 7V5.6A1.6 1.6 0 0111.1 4h1.8a1.6 1.6 0 011.6 1.6V7"/>'
|
||||
+ '<path d="M10 11v5.5"/><path d="M14 11v5.5"/></svg></button>';
|
||||
} else {
|
||||
actions = '<button type="button" class="soon-file-card__action soon-file-card__delete" title="' + escapeAttr(language_str('deleteBtn')) + '" aria-label="' + escapeAttr(language_str('deleteBtn')) + '">'
|
||||
+ '<svg class="soon-file-card__icon" viewBox="0 0 24 24" width="15" height="15" aria-hidden="true">'
|
||||
+ '<path d="M4 7h16"/><path d="M7 7l1.1 11.1a.9.9 0 00.9.9h6a.9.9 0 00.9-.9L17 7"/>'
|
||||
+ '<path d="M9.5 7V5.6A1.6 1.6 0 0111.1 4h1.8a1.6 1.6 0 011.6 1.6V7"/>'
|
||||
+ '<path d="M10 11v5.5"/><path d="M14 11v5.5"/></svg></button>';
|
||||
}
|
||||
|
||||
+ '<svg class="soon-file-card__icon" viewBox="0 0 24 24" width="15" height="15" aria-hidden="true">'
|
||||
|
||||
+ '<path d="M12 4v10m0 0l3.5-3.5M12 14l-3.5-3.5M5 20h14"/></svg></button>';
|
||||
|
||||
var delBtn = '<button type="button" class="soon-file-card__action soon-file-card__delete" title="' + escapeAttr(language_str('deleteBtn')) + '" aria-label="' + escapeAttr(language_str('deleteBtn')) + '">'
|
||||
|
||||
+ '<svg class="soon-file-card__icon" viewBox="0 0 24 24" width="15" height="15" aria-hidden="true">'
|
||||
|
||||
+ '<path d="M4 7h16"/><path d="M7 7l1.1 11.1a.9.9 0 00.9.9h6a.9.9 0 00.9-.9L17 7"/>'
|
||||
|
||||
+ '<path d="M9.5 7V5.6A1.6 1.6 0 0111.1 4h1.8a1.6 1.6 0 011.6 1.6V7"/>'
|
||||
|
||||
+ '<path d="M10 11v5.5"/><path d="M14 11v5.5"/></svg></button>';
|
||||
|
||||
h += `<div class="${cardClass}" data-file="${escapeAttr(filePath)}" data-id="${item.id}" data-name="${escapeAttr(displayName)}" title="${escapeAttr(cardTitle)}">
|
||||
h += `<div class="${cardClass}" data-file="${escapeAttr(filePath)}" data-kind="${item.kind}"${item.fileId ? (' data-id="' + item.fileId + '"') : ''} data-name="${escapeAttr(displayName)}" title="${escapeAttr(cardTitle)}">
|
||||
|
||||
<div class="rect ${realType == 2 ? 'rect1' : ''}">
|
||||
|
||||
<div class="soon-file-card__actions">${dlBtn}${delBtn}</div>
|
||||
<div class="soon-file-card__actions">${actions}</div>
|
||||
|
||||
<img src="${escapeAttr(src)}" style="${imgStyle}" alt="">
|
||||
|
||||
@@ -398,6 +475,8 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
|
||||
}
|
||||
|
||||
window.soonReloadRecentFiles = loadHistory;
|
||||
|
||||
|
||||
|
||||
function renderTemplatesError(message) {
|
||||
@@ -430,33 +509,16 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
return soonAsset(t === 2 ? 'bg_2.png' : 'bg_1.png');
|
||||
}
|
||||
|
||||
async function resolveTemplateItem(m) {
|
||||
function resolveTemplateItem(m, base) {
|
||||
var type = Number(m.type) || 1;
|
||||
var fallbackThumb = templateFallbackThumb(type);
|
||||
var fileUrl = m.file_url || '';
|
||||
var out = {
|
||||
var id = m.id;
|
||||
return {
|
||||
id: id,
|
||||
name: m.name || m.title || '模板',
|
||||
type: type,
|
||||
file_url: fileUrl,
|
||||
thumbSrc: fallbackThumb,
|
||||
_soonData: null
|
||||
thumbSrc: id ? (base + '/templates/' + id + '/thumb') : templateFallbackThumb(type),
|
||||
file_url: id ? (base + '/templates/' + id + '/file') : (m.file_url || '')
|
||||
};
|
||||
if (!fileUrl) return out;
|
||||
try {
|
||||
var r = await fetch(fileUrl);
|
||||
if (!r.ok) throw new Error('fetch');
|
||||
var soonData = await r.json();
|
||||
if (!soonData) return out;
|
||||
out._soonData = soonData;
|
||||
type = soonData.soonType ? soonData.soonType : (soonData.backBlackPic ? 2 : 1);
|
||||
out.type = type;
|
||||
fallbackThumb = templateFallbackThumb(type);
|
||||
var src = typeof soonSafeImageUrl === 'function'
|
||||
? soonSafeImageUrl(soonData.frontDisplayPic, fallbackThumb)
|
||||
: (soonData.frontDisplayPic || fallbackThumb);
|
||||
out.thumbSrc = src || fallbackThumb;
|
||||
} catch (e) { /* 与最近文件一致:失败时用默认图 */ }
|
||||
return out;
|
||||
}
|
||||
|
||||
function openTemplateItem(m) {
|
||||
@@ -474,10 +536,6 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
openDesign(type, '');
|
||||
}
|
||||
}
|
||||
if (m._soonData) {
|
||||
openWithJson(m._soonData);
|
||||
return;
|
||||
}
|
||||
var fileUrl = m.file_url || '';
|
||||
if (!fileUrl) {
|
||||
openDesign(type, '');
|
||||
@@ -493,13 +551,14 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
|
||||
function templateCardHtml(m, itemIndex) {
|
||||
var type = Number(m.type) || 1;
|
||||
var thumbSrc = m.thumbSrc || templateFallbackThumb(type);
|
||||
var fallback = templateFallbackThumb(type);
|
||||
var thumbSrc = m.thumbSrc || fallback;
|
||||
var esc = typeof soonEscapeHtml === 'function' ? soonEscapeHtml : escapeAttr;
|
||||
var name = esc(m.name || m.title || '模板');
|
||||
var rectClass = 'rect' + (type == 2 ? ' rect1' : '');
|
||||
return '<div class="card" data-type="' + type + '" data-index="' + itemIndex + '" title="' + name + '">' +
|
||||
'<div class="' + rectClass + '">' +
|
||||
'<img src="' + esc(thumbSrc) + '" alt="">' +
|
||||
'<img src="' + esc(thumbSrc) + '" alt="" loading="lazy" onerror="this.onerror=null;this.src=\'' + esc(fallback) + '\'">' +
|
||||
'<div class="tip">' + name + '</div>' +
|
||||
'</div></div>';
|
||||
}
|
||||
@@ -579,7 +638,7 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
|
||||
try {
|
||||
|
||||
var r = await fetch(base + '/soon-models');
|
||||
var r = await fetch(base + '/templates');
|
||||
|
||||
var j = await r.json();
|
||||
|
||||
@@ -592,12 +651,8 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
}
|
||||
|
||||
var items = (j.data && j.data.items) ? j.data.items : [];
|
||||
var resolved = [];
|
||||
for (var ti = 0; ti < items.length; ti++) {
|
||||
resolved.push(await resolveTemplateItem(items[ti]));
|
||||
}
|
||||
templateListState.items = resolved;
|
||||
templateListState.total = resolved.length;
|
||||
templateListState.items = items.map(function (it) { return resolveTemplateItem(it, base); });
|
||||
templateListState.total = templateListState.items.length;
|
||||
templateListState.page = 1;
|
||||
renderTemplatesPage();
|
||||
|
||||
@@ -631,19 +686,11 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
$("#openfile").click(function () { OpenDialog(); });
|
||||
|
||||
$("#new1").click(function () {
|
||||
|
||||
if (typeof window.soonRequireLogin === 'function' && !window.soonRequireLogin('新建')) return;
|
||||
|
||||
openDesign(1, '');
|
||||
|
||||
});
|
||||
|
||||
$("#new2").click(function () {
|
||||
|
||||
if (typeof window.soonRequireLogin === 'function' && !window.soonRequireLogin('新建')) return;
|
||||
|
||||
openDesign(2, '');
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -683,8 +730,6 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
|
||||
var name = card.attr('data-name') || 'design.soon';
|
||||
|
||||
if (typeof window.soonRequireMember === 'function' && !window.soonRequireMember('下载')) return;
|
||||
|
||||
if (id && window.platformBridge && window.platformBridge.downloadCloudFile) {
|
||||
|
||||
window.platformBridge.downloadCloudFile(id, name).catch(function () {});
|
||||
@@ -699,9 +744,42 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
|
||||
var card = $(this).closest('.card');
|
||||
|
||||
var kind = card.attr('data-kind') || 'cloud';
|
||||
|
||||
var fileId = parseInt(card.attr('data-id'), 10);
|
||||
|
||||
if (!fileId) return;
|
||||
var filePath = card.attr('data-file') || '';
|
||||
|
||||
if (kind === 'local' || !fileId) {
|
||||
|
||||
layer.open({
|
||||
type: 1,
|
||||
skin: 'soon-layer',
|
||||
title: language_str("delTitle"),
|
||||
content: '<div class="soon-dialog-body">' + language_str('deleteFileConfirm') + '</div>',
|
||||
btn: [language_str("comfirm"), language_str("cancel")],
|
||||
btnAlign: 'r',
|
||||
area: ['320px', 'auto'],
|
||||
resize: false,
|
||||
shadeClose: true,
|
||||
yes: function (index) {
|
||||
removeLocalHistoryPath(filePath).then(function () {
|
||||
if (filePath.indexOf('soondesign_session:') === 0) {
|
||||
try {
|
||||
sessionStorage.removeItem(filePath);
|
||||
localStorage.removeItem(filePath);
|
||||
} catch (err) { /* ignore */ }
|
||||
}
|
||||
layer.msg(language_str("deleted"), { icon: 1, time: 1000 });
|
||||
loadHistory();
|
||||
layer.close(index);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
confirmDeleteCloudFile(fileId, language_str('deleteFileConfirm'), loadHistory);
|
||||
|
||||
@@ -725,8 +803,26 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
|
||||
} else {
|
||||
|
||||
var kind = $(this).attr('data-kind') || 'cloud';
|
||||
|
||||
var fileId = parseInt($(this).attr('data-id'), 10);
|
||||
|
||||
if (kind === 'local' || !fileId) {
|
||||
|
||||
removeLocalHistoryPath(filePath).then(function () {
|
||||
if (filePath.indexOf('soondesign_session:') === 0) {
|
||||
try {
|
||||
sessionStorage.removeItem(filePath);
|
||||
localStorage.removeItem(filePath);
|
||||
} catch (err) { /* ignore */ }
|
||||
}
|
||||
loadHistory();
|
||||
});
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
confirmDeleteCloudFile(fileId, language_str("delContent"), loadHistory);
|
||||
|
||||
}
|
||||
@@ -737,8 +833,6 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
|
||||
function OpenDialog() {
|
||||
|
||||
if (typeof window.soonRequireLogin === 'function' && !window.soonRequireLogin('打开文件')) return;
|
||||
|
||||
var dialogApi = window.platformBridge && window.platformBridge.showOpenDialog
|
||||
|
||||
? { showOpenDialog: function (opts) { return window.platformBridge.showOpenDialog(opts); } }
|
||||
@@ -785,6 +879,15 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
|
||||
}
|
||||
|
||||
var tok = typeof window.soonGetAccessToken === 'function'
|
||||
? window.soonGetAccessToken()
|
||||
: (localStorage.getItem('soon_access') || '');
|
||||
|
||||
if (!tok && typeof window.soonOpenSoonJsonLocally === 'function') {
|
||||
window.soonOpenSoonJsonLocally(soonData, fileName);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.platformBridge || !window.platformBridge.importSoonFile) return;
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,472 +0,0 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var base = (window.SOON_DEPLOY_CONFIG && window.SOON_DEPLOY_CONFIG.api_v1_base) || '/api/v1';
|
||||
var currentMembership = null;
|
||||
var orderState = { page: 1, size: 8, total: 0 };
|
||||
|
||||
var esc = typeof soonEscapeHtml === 'function' ? soonEscapeHtml : function (s) {
|
||||
if (s == null) return '';
|
||||
return String(s);
|
||||
};
|
||||
|
||||
var STATUS_LABEL = {
|
||||
pending: '待支付',
|
||||
paid: '已支付',
|
||||
cancelled: '已取消',
|
||||
refunded: '已退款',
|
||||
};
|
||||
|
||||
var CHANNEL_LABEL = { alipay: '支付宝', wechat: '微信' };
|
||||
|
||||
function apiFetch(url, opts) {
|
||||
if (typeof soonAuthedFetch === 'function') return soonAuthedFetch(url, opts || {});
|
||||
opts = opts || {};
|
||||
var token = localStorage.getItem('soon_access') || '';
|
||||
opts.headers = Object.assign({}, opts.headers || {});
|
||||
if (token) opts.headers.Authorization = 'Bearer ' + token;
|
||||
return fetch(url, opts);
|
||||
}
|
||||
|
||||
function parseApiJson(r) {
|
||||
return r.text().then(function (text) {
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch (e) {
|
||||
return { ok: false, message: '服务暂时不可用,请稍后重试' };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function apiGet(p) {
|
||||
return apiFetch(base + p).then(function (r) {
|
||||
if (!r.ok && r.status === 401) return { ok: false, message: '未登录或会话已过期' };
|
||||
return parseApiJson(r);
|
||||
});
|
||||
}
|
||||
|
||||
function apiPost(p, body) {
|
||||
return apiFetch(base + p, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body || {}),
|
||||
}).then(function (r) {
|
||||
if (!r.ok && r.status === 401) return { ok: false, message: '未登录或会话已过期' };
|
||||
return parseApiJson(r);
|
||||
});
|
||||
}
|
||||
|
||||
function usageMetrics(usage, quotaMb) {
|
||||
var bytes = (usage && usage.storage_bytes) || 0;
|
||||
var totalBytes = (quotaMb || 0) * 1024 * 1024;
|
||||
var pctNum = totalBytes > 0 ? (bytes / totalBytes) * 100 : 0;
|
||||
var pctLabel = pctNum > 0 && pctNum < 1 ? '<1' : String(Math.min(100, Math.round(pctNum)));
|
||||
var barWidth = bytes > 0 ? Math.max(pctNum < 1 ? 0.8 : pctNum, 0.8) : 0;
|
||||
return {
|
||||
pctNum: pctNum,
|
||||
pctLabel: pctLabel,
|
||||
barWidth: Math.min(100, barWidth),
|
||||
usedDisplay: (usage && usage.used_display) || '0 MB',
|
||||
};
|
||||
}
|
||||
|
||||
function showPayBanner() {
|
||||
var b = document.getElementById('payBanner');
|
||||
if (!b) return;
|
||||
b.classList.add('is-visible');
|
||||
setTimeout(function () { b.classList.remove('is-visible'); }, 5000);
|
||||
}
|
||||
|
||||
function initLanguageSelect() {
|
||||
var sel = document.getElementById('language_select');
|
||||
if (!sel) return;
|
||||
sel.value = localStorage.getItem('lang') || 'zh';
|
||||
sel.onchange = function () { localStorage.setItem('lang', sel.value); };
|
||||
}
|
||||
|
||||
function isPaidMember(m) {
|
||||
if (!m) return false;
|
||||
if (m.is_member === true) return true;
|
||||
return m.tier === 'member' || m.tier === 'pro';
|
||||
}
|
||||
|
||||
function statusPill(sub, isMember) {
|
||||
if (!isMember) {
|
||||
return '<span class="soon-member-status-pill soon-member-status-pill--free">免费版</span>';
|
||||
}
|
||||
var st = (sub && sub.status) || 'active';
|
||||
var cls = 'soon-member-status-pill--active';
|
||||
var text = '生效中';
|
||||
if (st === 'expiring') { cls = 'soon-member-status-pill--expiring'; text = '即将到期'; }
|
||||
return '<span class="soon-member-status-pill ' + cls + '">' + text + '</span>';
|
||||
}
|
||||
|
||||
function storageRing(metrics, warn) {
|
||||
var r = 52;
|
||||
var c = 2 * Math.PI * r;
|
||||
var offset = c - (c * Math.min(metrics.pctNum, 100) / 100);
|
||||
var fillCls = warn ? ' soon-member-storage-ring__fill--warn' : '';
|
||||
return '<div class="soon-member-storage-ring">' +
|
||||
'<svg viewBox="0 0 120 120" aria-hidden="true">' +
|
||||
'<defs>' +
|
||||
'<linearGradient id="memberRingGrad" x1="0%" y1="0%" x2="100%" y2="0%">' +
|
||||
'<stop offset="0%" stop-color="#00897b"/><stop offset="100%" stop-color="#4db6ac"/>' +
|
||||
'</linearGradient>' +
|
||||
'<linearGradient id="memberRingWarn" x1="0%" y1="0%" x2="100%" y2="0%">' +
|
||||
'<stop offset="0%" stop-color="#f57c00"/><stop offset="100%" stop-color="#ffb74d"/>' +
|
||||
'</linearGradient></defs>' +
|
||||
'<circle class="soon-member-storage-ring__track" cx="60" cy="60" r="' + r + '"/>' +
|
||||
'<circle class="soon-member-storage-ring__fill' + fillCls + '" cx="60" cy="60" r="' + r + '" ' +
|
||||
'stroke-dasharray="' + c + '" stroke-dashoffset="' + offset + '"/></svg>' +
|
||||
'<div class="soon-member-storage-ring__center">' +
|
||||
'<span class="soon-member-storage-ring__pct">' + metrics.pctLabel + '%</span>' +
|
||||
'<span class="soon-member-storage-ring__label">已使用</span></div></div>';
|
||||
}
|
||||
|
||||
function renderMyPlan(m) {
|
||||
var el = document.getElementById('myPlan');
|
||||
if (!el) return;
|
||||
if (!m) {
|
||||
el.innerHTML = '<div class="soon-member-dash"><div class="soon-member-dash__body">' +
|
||||
'<p class="soon-member-dash__desc">加载失败,请重试</p>' +
|
||||
'<button type="button" class="soon-btn soon-btn--sm" id="myPlanRetry">重试</button></div></div>';
|
||||
var retry = document.getElementById('myPlanRetry');
|
||||
if (retry) retry.onclick = loadMembership;
|
||||
return;
|
||||
}
|
||||
currentMembership = m;
|
||||
var isMember = isPaidMember(m);
|
||||
if (typeof window.soonApplyMembership === 'function') {
|
||||
window.soonApplyMembership(m);
|
||||
} else if (typeof window.soonLoadMembership === 'function') {
|
||||
window.soonLoadMembership(true);
|
||||
}
|
||||
var usage = m.usage || {};
|
||||
var sub = m.subscription || {};
|
||||
|
||||
if (!isMember) {
|
||||
el.innerHTML =
|
||||
'<div class="soon-member-dash">' +
|
||||
'<div class="soon-member-dash__body soon-member-dash__body--guest">' +
|
||||
'<div class="soon-member-dash__badge-row">' +
|
||||
'<span class="soon-member-tier soon-member-tier--free">免费版</span>' + statusPill(sub, false) + '</div>' +
|
||||
'<h2 class="soon-member-dash__title">' + esc(m.name || '免费版') + '</h2>' +
|
||||
'<p class="soon-member-dash__desc">' + esc(m.description || '免费体验设计与编辑') + '</p>' +
|
||||
'<p class="soon-member-dash__hint">订阅后可预览、打印、保存并导出作品</p>' +
|
||||
'<div class="soon-member-dash__actions">' +
|
||||
'<button type="button" class="soon-btn soon-btn--sm" id="myPlanGoPlans">查看订阅方案</button>' +
|
||||
'</div></div></div>';
|
||||
var goPlans = document.getElementById('myPlanGoPlans');
|
||||
if (goPlans) {
|
||||
goPlans.onclick = function () {
|
||||
var sec = document.getElementById('plans');
|
||||
if (sec) sec.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
};
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var quota = m.quota_mb || 2048;
|
||||
var metrics = usageMetrics(usage, quota);
|
||||
var warn = metrics.pctNum >= 90;
|
||||
var barCls = warn ? 'soon-member-progress__bar soon-member-progress__bar--warn' : 'soon-member-progress__bar';
|
||||
var expires = sub.expires_at
|
||||
? esc(String(sub.expires_at).slice(0, 16).replace('T', ' '))
|
||||
: '—';
|
||||
var days = sub.days_remaining != null ? sub.days_remaining + ' 天' : '—';
|
||||
|
||||
el.innerHTML =
|
||||
'<div class="soon-member-dash">' +
|
||||
'<div class="soon-member-dash__body">' +
|
||||
'<div class="soon-member-dash__top">' +
|
||||
'<div><div class="soon-member-dash__badge-row">' +
|
||||
'<span class="soon-member-tier">订阅版</span>' + statusPill(sub, true) + '</div>' +
|
||||
'<h2 class="soon-member-dash__title">' + esc(m.name) + '</h2>' +
|
||||
'<p class="soon-member-dash__desc">' + esc(m.description || '已解锁完整交付能力') + '</p>' +
|
||||
'<p class="soon-member-dash__expire">有效期至 <strong>' + expires + '</strong> · 剩余 ' + esc(days) + '</p></div>' +
|
||||
storageRing(metrics, warn) + '</div>' +
|
||||
'<div class="soon-member-dash__usage">' +
|
||||
'<div class="soon-member-dash__usage-head"><span>云端存储占用</span><strong>' +
|
||||
esc(metrics.usedDisplay) + ' / ' + esc(m.quota_display || quota + ' MB') +
|
||||
'(' + metrics.pctLabel + '%)</strong></div>' +
|
||||
'<div class="soon-member-progress"><div class="' + barCls + '" style="width:' + metrics.barWidth + '%"></div></div>' +
|
||||
'</div></div>' +
|
||||
'<div class="soon-member-dash__stats">' +
|
||||
'<div class="soon-member-dash__stat"><div class="soon-member-dash__stat-value">' + (usage.files_count || 0) +
|
||||
'</div><div class="soon-member-dash__stat-label">云端文件</div></div>' +
|
||||
'<div class="soon-member-dash__stat"><div class="soon-member-dash__stat-value">' + esc(days) +
|
||||
'</div><div class="soon-member-dash__stat-label">剩余天数</div></div>' +
|
||||
'</div></div>';
|
||||
}
|
||||
|
||||
function orderBadge(status, refundStatus) {
|
||||
if (refundStatus === 'pending') {
|
||||
return '<span class="soon-member-order-badge soon-member-order-badge--pending">退款审核</span>';
|
||||
}
|
||||
var cls = 'soon-member-order-badge--' + (status || 'pending');
|
||||
return '<span class="soon-member-order-badge ' + cls + '">' + esc(STATUS_LABEL[status] || status) + '</span>';
|
||||
}
|
||||
|
||||
function orderPagerHtml() {
|
||||
var page = orderState.page;
|
||||
var size = orderState.size;
|
||||
var total = orderState.total;
|
||||
var pages = Math.max(1, Math.ceil(total / size));
|
||||
if (total === 0) return '';
|
||||
return '<div class="soon-member-pager">' +
|
||||
'<button type="button" class="soon-btn soon-btn--sm soon-btn--ghost" id="orderPrev"' +
|
||||
(page <= 1 ? ' disabled' : '') + '>上一页</button>' +
|
||||
'<span>第 ' + page + ' / ' + pages + ' 页(共 ' + total + ' 条)</span>' +
|
||||
'<button type="button" class="soon-btn soon-btn--sm soon-btn--ghost" id="orderNext"' +
|
||||
(page >= pages ? ' disabled' : '') + '>下一页</button></div>';
|
||||
}
|
||||
|
||||
function bindOrderPager() {
|
||||
var prev = document.getElementById('orderPrev');
|
||||
var next = document.getElementById('orderNext');
|
||||
if (prev && !prev.disabled) {
|
||||
prev.onclick = function () {
|
||||
orderState.page = Math.max(1, orderState.page - 1);
|
||||
loadOrders();
|
||||
};
|
||||
}
|
||||
if (next && !next.disabled) {
|
||||
next.onclick = function () {
|
||||
var pages = Math.max(1, Math.ceil(orderState.total / orderState.size));
|
||||
orderState.page = Math.min(pages, orderState.page + 1);
|
||||
loadOrders();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function orderActionCell(o) {
|
||||
var actions = [];
|
||||
if (o.status === 'pending') {
|
||||
actions.push('<button type="button" class="soon-btn soon-btn--xs soon-btn--ghost soon-member-order-act" ' +
|
||||
'data-act="pay" data-no="' + esc(o.order_no) + '" data-plan="' + esc(o.plan_name) + '" data-price="' +
|
||||
(o.amount_cents / 100).toFixed(2) + '" data-channel="' + esc(o.channel) + '">继续支付</button>');
|
||||
actions.push('<button type="button" class="soon-btn soon-btn--xs soon-btn--ghost soon-member-order-act soon-member-order-act--muted" ' +
|
||||
'data-act="cancel" data-no="' + esc(o.order_no) + '">取消</button>');
|
||||
} else if (o.status === 'paid' && (!o.refund_status || o.refund_status === 'none')) {
|
||||
actions.push('<button type="button" class="soon-btn soon-btn--xs soon-btn--ghost soon-member-order-act" ' +
|
||||
'data-act="refund" data-no="' + esc(o.order_no) + '">申请退款</button>');
|
||||
}
|
||||
if (!actions.length) return '<span class="soon-member-order-act--empty">—</span>';
|
||||
return '<div class="soon-member-order-actions">' + actions.join('') + '</div>';
|
||||
}
|
||||
|
||||
function bindOrderActions() {
|
||||
document.querySelectorAll('.soon-member-order-act[data-act]').forEach(function (btn) {
|
||||
btn.onclick = function () {
|
||||
var act = btn.dataset.act;
|
||||
var no = btn.dataset.no;
|
||||
if (act === 'pay') {
|
||||
openPayModal(null, btn.dataset.plan, btn.dataset.price, {
|
||||
orderNo: no,
|
||||
channel: btn.dataset.channel || (window.SoonMemberPay && SoonMemberPay.defaultChannel
|
||||
? SoonMemberPay.defaultChannel() : 'alipay'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (act === 'cancel') {
|
||||
layer.confirm('确定取消该待支付订单?', { skin: 'soon-layer', title: '取消订单' }, function (idx) {
|
||||
apiPost('/pay/orders/' + encodeURIComponent(no) + '/cancel', {}).then(function (r) {
|
||||
layer.close(idx);
|
||||
if (r.ok) {
|
||||
soonToast('订单已取消', 'success');
|
||||
loadOrders();
|
||||
} else {
|
||||
soonToast(r.message || '取消失败', 'warn');
|
||||
}
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (act === 'refund') {
|
||||
layer.prompt({
|
||||
skin: 'soon-layer',
|
||||
title: '申请退款',
|
||||
formType: 2,
|
||||
value: '',
|
||||
maxlength: 200,
|
||||
}, function (reason, idx) {
|
||||
reason = (reason || '').trim();
|
||||
if (!reason) {
|
||||
soonToast('请填写退款原因', 'warn');
|
||||
return;
|
||||
}
|
||||
apiPost('/pay/orders/' + encodeURIComponent(no) + '/refund-request', { reason: reason }).then(function (r) {
|
||||
layer.close(idx);
|
||||
if (r.ok) {
|
||||
soonToast('退款申请已提交', 'success');
|
||||
loadOrders();
|
||||
} else {
|
||||
soonToast(r.message || '提交失败', 'warn');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function renderOrders(orders) {
|
||||
var el = document.getElementById('orderHistory');
|
||||
if (!el) return;
|
||||
if (!orders || !orders.length) {
|
||||
el.innerHTML = '<div class="soon-member-orders-empty">暂无订单,订阅后将显示在这里</div>' +
|
||||
orderPagerHtml();
|
||||
bindOrderPager();
|
||||
return;
|
||||
}
|
||||
var rows = orders.map(function (o) {
|
||||
return '<tr><td>' + esc(o.order_no) + '</td><td>' + esc(o.plan_name) + '</td><td class="soon-member-orders__amount">¥' +
|
||||
(o.amount_cents / 100).toFixed(2) + '</td><td>' + esc(CHANNEL_LABEL[o.channel] || o.channel) +
|
||||
'</td><td>' + orderBadge(o.status, o.refund_status) + '</td><td>' +
|
||||
esc(String(o.paid_at || o.created_at || '').slice(0, 16).replace('T', ' ')) + '</td><td>' +
|
||||
orderActionCell(o) + '</td></tr>';
|
||||
}).join('');
|
||||
el.innerHTML = '<table><thead><tr><th>订单号</th><th>方案</th><th>金额</th><th>渠道</th><th>状态</th><th>时间</th><th>操作</th></tr></thead><tbody>' +
|
||||
rows + '</tbody></table>' + orderPagerHtml();
|
||||
bindOrderPager();
|
||||
bindOrderActions();
|
||||
}
|
||||
|
||||
function loadOrders() {
|
||||
apiGet('/pay/orders?page=' + orderState.page + '&size=' + orderState.size).then(function (res) {
|
||||
if (!res.ok) {
|
||||
renderOrders([]);
|
||||
return;
|
||||
}
|
||||
var data = res.data || {};
|
||||
orderState.total = data.total || 0;
|
||||
orderState.page = data.page || orderState.page;
|
||||
orderState.size = data.size || orderState.size;
|
||||
renderOrders(data.items || []);
|
||||
}).catch(function () {
|
||||
renderOrders([]);
|
||||
});
|
||||
}
|
||||
|
||||
function renderPlans(items) {
|
||||
var grid = document.getElementById('plans');
|
||||
if (!grid || !window.SoonMemberPlan) return;
|
||||
var paid = items.filter(function (p) { return p.code !== 'free' && p.price_cents > 0; });
|
||||
var currentCode = currentMembership && isPaidMember(currentMembership) ? currentMembership.code : '';
|
||||
if (!paid.length) {
|
||||
grid.innerHTML = '<div class="soon-member-orders-empty" style="grid-column:1/-1">暂无可用订阅方案</div>';
|
||||
return;
|
||||
}
|
||||
grid.innerHTML = window.SoonMemberPlan.plansGridHtml(paid, {
|
||||
allPlans: paid,
|
||||
currentCode: currentCode,
|
||||
isMember: isPaidMember(currentMembership),
|
||||
mode: 'page',
|
||||
});
|
||||
grid.querySelectorAll('.soon-plan-card__cta[data-id]').forEach(function (btn) {
|
||||
btn.onclick = function () {
|
||||
openPayModal(Number(btn.dataset.id), btn.dataset.name, btn.dataset.price);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function renderPlansError(message, onRetry) {
|
||||
var grid = document.getElementById('plans');
|
||||
if (!grid) return;
|
||||
grid.innerHTML = '<div class="soon-member-orders-empty" style="grid-column:1/-1">' +
|
||||
esc(message || '方案加载失败') +
|
||||
'<br><button type="button" class="soon-btn soon-btn--sm" style="margin-top:14px" id="plansRetry">重试</button></div>';
|
||||
var btn = document.getElementById('plansRetry');
|
||||
if (btn && onRetry) btn.onclick = onRetry;
|
||||
}
|
||||
|
||||
function loadPlans() {
|
||||
apiGet('/plans').then(function (ps) {
|
||||
if (!ps.ok) {
|
||||
renderPlansError(ps.message || '方案加载失败', loadPlans);
|
||||
return;
|
||||
}
|
||||
renderPlans((ps.data && ps.data.items) || []);
|
||||
}).catch(function () {
|
||||
renderPlansError('网络错误,请稍后重试', loadPlans);
|
||||
});
|
||||
}
|
||||
|
||||
function openPayModal(planId, planName, priceDisplay, opts) {
|
||||
opts = opts || {};
|
||||
var payCore = window.SoonMemberPay;
|
||||
if (!payCore || !payCore.openPayModal) {
|
||||
soonToast('支付模块未加载,请刷新页面', 'warn');
|
||||
return;
|
||||
}
|
||||
payCore.openPayModal({
|
||||
planId: planId,
|
||||
planName: planName,
|
||||
priceDisplay: priceDisplay,
|
||||
orderNo: opts.orderNo,
|
||||
channel: opts.channel || (payCore.defaultChannel ? payCore.defaultChannel() : 'alipay'),
|
||||
onPaid: function () {
|
||||
showPayBanner();
|
||||
loadMembership();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function loadMembership() {
|
||||
apiGet('/plans/me').then(function (my) {
|
||||
if (!my.ok || !my.data) {
|
||||
renderMyPlan(null);
|
||||
return;
|
||||
}
|
||||
renderMyPlan(my.data.membership || my.data);
|
||||
loadOrders();
|
||||
loadPlans();
|
||||
}).catch(function () {
|
||||
renderMyPlan(null);
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (!localStorage.getItem('soon_access')) {
|
||||
location.href = 'login.web.html?redirect=' + encodeURIComponent('member.web.html');
|
||||
return;
|
||||
}
|
||||
if (window.SoonPortalTopbar) {
|
||||
SoonPortalTopbar.mount('#portal-topbar', { active: 'member' });
|
||||
} else {
|
||||
initLanguageSelect();
|
||||
}
|
||||
soonPortalAuth.init({ logoutReload: false });
|
||||
apiGet('/auth/me').then(function (me) {
|
||||
if (!me.ok) {
|
||||
location.href = 'login.web.html?redirect=' + encodeURIComponent('member.web.html');
|
||||
return;
|
||||
}
|
||||
if (me.data && me.data.role === 'admin') {
|
||||
var adminEl = document.getElementById('auth_admin');
|
||||
if (adminEl) adminEl.style.display = 'inline-flex';
|
||||
}
|
||||
loadMembership();
|
||||
if (new URLSearchParams(location.search).get('paid') || sessionStorage.getItem('soon_pay_return')) {
|
||||
sessionStorage.removeItem('soon_pay_return');
|
||||
showPayBanner();
|
||||
loadMembership();
|
||||
}
|
||||
}).catch(function () {
|
||||
renderMyPlan(null);
|
||||
renderPlansError('网络错误,请稍后重试', function () { location.reload(); });
|
||||
var ordersEl = document.getElementById('orderHistory');
|
||||
if (ordersEl) {
|
||||
ordersEl.innerHTML = '<div class="soon-member-orders-empty">加载失败,<button type="button" class="soon-btn soon-btn--sm" id="ordersRetry">重试</button></div>';
|
||||
var ordersRetry = document.getElementById('ordersRetry');
|
||||
if (ordersRetry) ordersRetry.onclick = function () { location.reload(); };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
@@ -124,7 +124,14 @@
|
||||
}
|
||||
|
||||
function listCloudFiles(pageOrLimit, sizeOrOffset) {
|
||||
if (!requireCloudAuth('查看文件')) return Promise.reject(new Error('unauthorized'));
|
||||
if (!getAccessToken()) {
|
||||
var empty = { items: [], total: 0 };
|
||||
if (typeof pageOrLimit === 'object' && pageOrLimit) {
|
||||
empty.page = pageOrLimit.page || 1;
|
||||
empty.size = pageOrLimit.size || 12;
|
||||
}
|
||||
return Promise.resolve(empty);
|
||||
}
|
||||
var url;
|
||||
var fallback = { items: [], total: 0 };
|
||||
if (typeof pageOrLimit === 'object' && pageOrLimit) {
|
||||
@@ -145,9 +152,6 @@
|
||||
|
||||
function createCloudFile(name, jsonStr) {
|
||||
if (!requireCloudAuth('保存')) return Promise.reject(new Error('unauthorized'));
|
||||
if (typeof window.soonRequireMember === 'function' && !window.soonRequireMember('保存')) {
|
||||
return Promise.reject(new Error('membership_required'));
|
||||
}
|
||||
return authedFetch('files', {
|
||||
method: 'POST',
|
||||
headers: JSON_HEADERS,
|
||||
@@ -159,9 +163,6 @@
|
||||
|
||||
function updateCloudFile(id, name, jsonStr, version) {
|
||||
if (!requireCloudAuth('保存')) return Promise.reject(new Error('unauthorized'));
|
||||
if (typeof window.soonRequireMember === 'function' && !window.soonRequireMember('保存')) {
|
||||
return Promise.reject(new Error('membership_required'));
|
||||
}
|
||||
var body = { name: normalizeSoonName(name), json: jsonStr };
|
||||
if (version != null) body.version = version;
|
||||
return authedFetch('files/' + id, {
|
||||
@@ -186,9 +187,6 @@
|
||||
|
||||
function downloadCloudFile(id, fileName) {
|
||||
if (!requireCloudAuth('下载')) return Promise.reject(new Error('unauthorized'));
|
||||
if (typeof window.soonRequireMember === 'function' && !window.soonRequireMember('下载')) {
|
||||
return Promise.reject(new Error('membership_required'));
|
||||
}
|
||||
return authedFetch('files/' + id + '/download', { headers: { Accept: 'application/octet-stream' } })
|
||||
.then(function (response) {
|
||||
if (!response.ok) {
|
||||
@@ -328,14 +326,31 @@
|
||||
pathOrHandle = typeof pathOrHandle.name === 'string' ? pathOrHandle.name : 'design.soon';
|
||||
}
|
||||
|
||||
if (!requireCloudAuth('保存')) {
|
||||
return Promise.reject(new Error('unauthorized'));
|
||||
}
|
||||
|
||||
var name = typeof pathOrHandle === 'string' ? pathOrHandle : 'design.soon';
|
||||
var cloudRef = parseCloudRef(name);
|
||||
var fileName = normalizeSoonName(name);
|
||||
|
||||
if (!getAccessToken()) {
|
||||
if (cloudRef && cloudRef.id) {
|
||||
requireCloudAuth('保存');
|
||||
return Promise.reject(new Error('unauthorized'));
|
||||
}
|
||||
var sessionKey = name.indexOf('soondesign_session:') === 0
|
||||
? name
|
||||
: 'soondesign_session:' + fileName.replace(/\.soon$/i, '') + '-' + Date.now();
|
||||
try {
|
||||
sessionStorage.setItem(sessionKey, str);
|
||||
try { localStorage.setItem(sessionKey, str); } catch (e2) { /* ignore quota */ }
|
||||
return Promise.resolve({ fileKey: sessionKey, name: fileName, version: 0 });
|
||||
} catch (e) {
|
||||
return Promise.reject(new Error('save_failed'));
|
||||
}
|
||||
}
|
||||
|
||||
if (!requireCloudAuth('保存')) {
|
||||
return Promise.reject(new Error('unauthorized'));
|
||||
}
|
||||
|
||||
if (cloudRef && cloudRef.id) {
|
||||
var ver = cloudRef.version;
|
||||
if (ver == null && window._soonFileMeta && window._soonFileMeta.id === cloudRef.id) {
|
||||
|
||||
Reference in New Issue
Block a user