前端配置迁至 config/local.js,完善支付、模板库与部署脚本
- 页面直接引用 local.js 设置 SOON_DEPLOY_CONFIG,移除 deploy-config - Docker sync-config 生成 local.js;更新 README 与 agent-core 说明 - 模板库自动扫描 .soon;新增后端部署/种子/排查脚本 - 完善支付配置、订阅弹窗与后台支付管理页 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,6 +3,13 @@
|
||||
|
||||
var POLL_TIMEOUT_MS = 5 * 60 * 1000;
|
||||
var PAY_STEP_LABELS = ['选择支付', '扫码付款', '订阅生效'];
|
||||
var _displayChannels = ['alipay'];
|
||||
var _settingsPromise = null;
|
||||
|
||||
var CHANNEL_UI = {
|
||||
alipay: { name: '支付宝', hint: '网页支付', icon: '支', iconCls: 'alipay' },
|
||||
wechat: { name: '微信支付', hint: '扫码付款', icon: '微', iconCls: 'wechat' },
|
||||
};
|
||||
|
||||
function esc(s) {
|
||||
if (s == null) return '';
|
||||
@@ -92,6 +99,83 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
function parseDisplayChannels(raw) {
|
||||
if (Array.isArray(raw)) {
|
||||
var list = raw.filter(function (ch) { return ch === 'alipay' || ch === 'wechat'; });
|
||||
return list.length ? list : ['alipay'];
|
||||
}
|
||||
var parts = String(raw || 'alipay').split(/[\s,]+/).filter(Boolean);
|
||||
var out = [];
|
||||
parts.forEach(function (ch) {
|
||||
if ((ch === 'alipay' || ch === 'wechat') && out.indexOf(ch) < 0) out.push(ch);
|
||||
});
|
||||
return out.length ? out : ['alipay'];
|
||||
}
|
||||
|
||||
function loadDisplayChannels() {
|
||||
if (_settingsPromise) return _settingsPromise;
|
||||
_settingsPromise = apiGet('/settings').then(function (j) {
|
||||
if (j.ok && j.data) {
|
||||
if (j.data.payment_display_channels) {
|
||||
_displayChannels = parseDisplayChannels(j.data.payment_display_channels);
|
||||
} else if (j.data['payment.display_channels'] != null) {
|
||||
_displayChannels = parseDisplayChannels(j.data['payment.display_channels']);
|
||||
}
|
||||
}
|
||||
return _displayChannels;
|
||||
}).catch(function () {
|
||||
_displayChannels = ['alipay'];
|
||||
return _displayChannels;
|
||||
});
|
||||
return _settingsPromise;
|
||||
}
|
||||
|
||||
function displayChannels() {
|
||||
return _displayChannels.slice();
|
||||
}
|
||||
|
||||
function defaultChannel(channels) {
|
||||
var list = channels || _displayChannels;
|
||||
return list[0] || 'alipay';
|
||||
}
|
||||
|
||||
function resolveChannel(requested, channels) {
|
||||
var list = channels || _displayChannels;
|
||||
if (requested && list.indexOf(requested) >= 0) return requested;
|
||||
return defaultChannel(list);
|
||||
}
|
||||
|
||||
function channelCardHtml(ch, active, selectable) {
|
||||
var ui = CHANNEL_UI[ch];
|
||||
if (!ui) return '';
|
||||
var cls = 'soon-pay-channel' + (active ? ' is-active' : '') + (selectable ? '' : ' is-fixed');
|
||||
var inner = '<span class="soon-pay-channel__radio" aria-hidden="true"></span>' +
|
||||
'<span class="soon-pay-channel__icon soon-pay-channel__icon--' + ui.iconCls + '">' + ui.icon + '</span>' +
|
||||
'<span class="soon-pay-channel__text"><span class="soon-pay-channel__name">' + ui.name + '</span>' +
|
||||
'<span class="soon-pay-channel__hint">' + ui.hint + '</span></span>';
|
||||
if (selectable) {
|
||||
return '<button type="button" class="' + cls + '" data-ch="' + ch + '">' + inner + '</button>';
|
||||
}
|
||||
return '<div class="' + cls + '" data-ch="' + ch + '">' + inner + '</div>';
|
||||
}
|
||||
|
||||
function payChannelsHtml(activeChannel, channels, opts) {
|
||||
opts = opts || {};
|
||||
channels = channels || _displayChannels;
|
||||
var showChannel = activeChannel || defaultChannel(channels);
|
||||
var selectable = opts.selectable !== false && channels.length > 1;
|
||||
var list = selectable ? channels : [showChannel];
|
||||
var cards = list.map(function (ch) {
|
||||
return channelCardHtml(ch, ch === showChannel, selectable);
|
||||
}).join('');
|
||||
if (!cards) return '';
|
||||
return '<section class="soon-pay-section soon-pay-section--channels' +
|
||||
(selectable ? '' : ' soon-pay-section--channels-fixed') + '" id="payChannels">' +
|
||||
'<h4 class="soon-pay-section__title">支付方式</h4>' +
|
||||
'<div class="soon-pay-channels' + (list.length === 1 ? ' soon-pay-channels--single' : '') + '">' +
|
||||
cards + '</div></section>';
|
||||
}
|
||||
|
||||
function payStepLabel(step, channel) {
|
||||
if (step === 2 && channel === 'alipay') return '网页支付';
|
||||
if (step === 2 && channel === 'wechat') return '扫码付款';
|
||||
@@ -118,19 +202,20 @@
|
||||
|
||||
function updatePayStep(root, step, channel) {
|
||||
if (!root) return;
|
||||
var html = payStepperHtml(step, channel || 'wechat');
|
||||
var html = payStepperHtml(step, channel || defaultChannel());
|
||||
var old = root.querySelector('#payStepper');
|
||||
if (old) old.outerHTML = html;
|
||||
}
|
||||
|
||||
function payEmptyStateHtml() {
|
||||
function payEmptyStateHtml(channel) {
|
||||
var ui = CHANNEL_UI[channel] || CHANNEL_UI[defaultChannel()];
|
||||
return '<div class="soon-pay-checkout-empty">' +
|
||||
'<div class="soon-pay-checkout-empty__icon" aria-hidden="true">' +
|
||||
'<svg viewBox="0 0 48 48" fill="none"><rect x="8" y="12" width="32" height="24" rx="4" stroke="currentColor" stroke-width="1.5"/>' +
|
||||
'<path d="M8 20h32" stroke="currentColor" stroke-width="1.5"/>' +
|
||||
'<circle cx="32" cy="30" r="3" fill="currentColor" opacity="0.5"/></svg></div>' +
|
||||
'<p class="soon-pay-checkout-empty__title">等待生成付款码</p>' +
|
||||
'<p class="soon-pay-checkout-empty__desc">确认支付方式后,将在此显示二维码或跳转链接</p></div>';
|
||||
'<p class="soon-pay-checkout-empty__title">等待生成付款信息</p>' +
|
||||
'<p class="soon-pay-checkout-empty__desc">点击下方按钮,将使用' + esc(ui.name) + '(' + esc(ui.hint) + ')</p></div>';
|
||||
}
|
||||
|
||||
function renderPayPayload(data, qrEl, orderNoEl) {
|
||||
@@ -195,7 +280,10 @@
|
||||
function paySheetHtml(opts) {
|
||||
opts = opts || {};
|
||||
var resumeOrderNo = opts.orderNo || null;
|
||||
var channel = opts.channel || 'wechat';
|
||||
var channels = opts.displayChannels || _displayChannels;
|
||||
var channel = (resumeOrderNo && opts.channel)
|
||||
? opts.channel
|
||||
: resolveChannel(opts.channel, channels);
|
||||
var embedded = opts.variant === 'embedded';
|
||||
var backBtn = !embedded && opts.showBack
|
||||
? '<button type="button" class="soon-btn soon-btn--ghost soon-btn--sm soon-pay-sheet__back" id="payBack">返回选择方案</button>'
|
||||
@@ -222,21 +310,9 @@
|
||||
'<div class="soon-pay-sheet__body">' +
|
||||
orderInBody +
|
||||
payStepperHtml(1, channel) +
|
||||
'<section class="soon-pay-section soon-pay-section--channels" id="payChannels">' +
|
||||
'<h4 class="soon-pay-section__title">支付方式</h4>' +
|
||||
'<div class="soon-pay-channels">' +
|
||||
'<button type="button" class="soon-pay-channel' + (channel === 'wechat' ? ' is-active' : '') + '" data-ch="wechat">' +
|
||||
'<span class="soon-pay-channel__radio" aria-hidden="true"></span>' +
|
||||
'<span class="soon-pay-channel__icon soon-pay-channel__icon--wechat">微</span>' +
|
||||
'<span class="soon-pay-channel__text"><span class="soon-pay-channel__name">微信支付</span>' +
|
||||
'<span class="soon-pay-channel__hint">扫码付款</span></span></button>' +
|
||||
'<button type="button" class="soon-pay-channel' + (channel === 'alipay' ? ' is-active' : '') + '" data-ch="alipay">' +
|
||||
'<span class="soon-pay-channel__radio" aria-hidden="true"></span>' +
|
||||
'<span class="soon-pay-channel__icon soon-pay-channel__icon--alipay">支</span>' +
|
||||
'<span class="soon-pay-channel__text"><span class="soon-pay-channel__name">支付宝</span>' +
|
||||
'<span class="soon-pay-channel__hint">网页支付</span></span></button></div></section>' +
|
||||
payChannelsHtml(channel, channels, { selectable: !resumeOrderNo && channels.length > 1 }) +
|
||||
'<section class="soon-pay-section soon-pay-section--checkout">' +
|
||||
'<div class="soon-pay-qr-panel" id="payQr">' + payEmptyStateHtml() + '</div></section>' +
|
||||
'<div class="soon-pay-qr-panel" id="payQr">' + payEmptyStateHtml(channel) + '</div></section>' +
|
||||
'<div class="soon-pay-sheet__foot">' +
|
||||
'<p class="soon-pay-status" id="payStatus"></p>' +
|
||||
'<button type="button" class="soon-btn soon-pay-sheet__cta" id="payConfirm">' +
|
||||
@@ -249,7 +325,10 @@
|
||||
opts = opts || {};
|
||||
var planId = opts.planId;
|
||||
var resumeOrderNo = opts.orderNo || null;
|
||||
var channel = opts.channel || 'wechat';
|
||||
var channels = opts.displayChannels || _displayChannels;
|
||||
var channel = (resumeOrderNo && opts.channel)
|
||||
? opts.channel
|
||||
: resolveChannel(opts.channel, channels);
|
||||
var onPaid = typeof opts.onPaid === 'function' ? opts.onPaid : function () {};
|
||||
var pollTimer = null;
|
||||
var pollStartedAt = 0;
|
||||
@@ -321,7 +400,7 @@
|
||||
setStep(1);
|
||||
if (manualBtn) manualBtn.style.display = 'none';
|
||||
if (confirmBtn) confirmBtn.style.display = '';
|
||||
qrEl.innerHTML = payEmptyStateHtml();
|
||||
qrEl.innerHTML = payEmptyStateHtml(channel);
|
||||
if (orderNoEl && !resumeOrderNo) {
|
||||
orderNoEl.textContent = '';
|
||||
orderNoEl.style.display = 'none';
|
||||
@@ -360,18 +439,18 @@
|
||||
if (confirmBtn) confirmBtn.disabled = false;
|
||||
if (!j.ok) {
|
||||
setStatus(j.message || '下单失败', 'err');
|
||||
qrEl.innerHTML = payEmptyStateHtml();
|
||||
qrEl.innerHTML = payEmptyStateHtml(channel);
|
||||
return;
|
||||
}
|
||||
applyCheckout(j.data);
|
||||
}).catch(function () {
|
||||
if (confirmBtn) confirmBtn.disabled = false;
|
||||
setStatus('网络错误,请重试', 'err');
|
||||
qrEl.innerHTML = payEmptyStateHtml();
|
||||
qrEl.innerHTML = payEmptyStateHtml(channel);
|
||||
});
|
||||
}
|
||||
|
||||
sheetRoot.querySelectorAll('.soon-pay-channel').forEach(function (el) {
|
||||
sheetRoot.querySelectorAll('.soon-pay-channel:not(.is-fixed)').forEach(function (el) {
|
||||
el.onclick = function () {
|
||||
if (el.classList.contains('is-active')) return;
|
||||
sheetRoot.querySelectorAll('.soon-pay-channel').forEach(function (x) { x.classList.remove('is-active'); });
|
||||
@@ -399,11 +478,16 @@
|
||||
function openPayModal(opts) {
|
||||
opts = opts || {};
|
||||
if (typeof layer === 'undefined' || !layer.open) return null;
|
||||
var channels = _displayChannels;
|
||||
var channel = (opts.orderNo && opts.channel)
|
||||
? opts.channel
|
||||
: resolveChannel(opts.channel, channels);
|
||||
var html = paySheetHtml({
|
||||
planName: opts.planName,
|
||||
priceDisplay: opts.priceDisplay,
|
||||
orderNo: opts.orderNo,
|
||||
channel: opts.channel || 'wechat',
|
||||
channel: channel,
|
||||
displayChannels: channels,
|
||||
});
|
||||
var payCtrl = null;
|
||||
var idx = layer.open({
|
||||
@@ -430,7 +514,8 @@
|
||||
planName: opts.planName,
|
||||
priceDisplay: opts.priceDisplay,
|
||||
orderNo: opts.orderNo,
|
||||
channel: opts.channel || 'wechat',
|
||||
channel: channel,
|
||||
displayChannels: channels,
|
||||
onPaid: function () {
|
||||
layer.close(idx);
|
||||
if (typeof opts.onPaid === 'function') opts.onPaid();
|
||||
@@ -446,6 +531,8 @@
|
||||
return idx;
|
||||
}
|
||||
|
||||
loadDisplayChannels();
|
||||
|
||||
window.SoonMemberPay = {
|
||||
apiBase: apiBase,
|
||||
apiGet: apiGet,
|
||||
@@ -459,6 +546,10 @@
|
||||
payEmptyStateHtml: payEmptyStateHtml,
|
||||
renderPayPayload: renderPayPayload,
|
||||
submitAlipayForm: submitAlipayForm,
|
||||
loadDisplayChannels: loadDisplayChannels,
|
||||
displayChannels: displayChannels,
|
||||
defaultChannel: defaultChannel,
|
||||
resolveChannel: resolveChannel,
|
||||
POLL_TIMEOUT_MS: POLL_TIMEOUT_MS,
|
||||
};
|
||||
})();
|
||||
|
||||
+85
-13
@@ -425,24 +425,92 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
|
||||
}
|
||||
|
||||
function templateCardHtml(m, i) {
|
||||
var defaultThumb = soonAsset('bg_1.png');
|
||||
var thumbSrc = typeof soonSafeImageUrl === 'function'
|
||||
? soonSafeImageUrl(m.thumbnail_url, defaultThumb)
|
||||
: (m.thumbnail_url || defaultThumb);
|
||||
if (!thumbSrc) thumbSrc = defaultThumb;
|
||||
function templateFallbackThumb(type) {
|
||||
var t = Number(type) || 1;
|
||||
return soonAsset(t === 2 ? 'bg_2.png' : 'bg_1.png');
|
||||
}
|
||||
|
||||
async function resolveTemplateItem(m) {
|
||||
var type = Number(m.type) || 1;
|
||||
var fallbackThumb = templateFallbackThumb(type);
|
||||
var fileUrl = m.file_url || '';
|
||||
var out = {
|
||||
name: m.name || m.title || '模板',
|
||||
type: type,
|
||||
file_url: fileUrl,
|
||||
thumbSrc: fallbackThumb,
|
||||
_soonData: null
|
||||
};
|
||||
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) {
|
||||
var type = Number(m.type) || 1;
|
||||
function openWithJson(j) {
|
||||
if (!j) {
|
||||
openDesign(type, '');
|
||||
return;
|
||||
}
|
||||
var key = 'soondesign_session:tpl-' + Date.now();
|
||||
try {
|
||||
sessionStorage.setItem(key, JSON.stringify(j));
|
||||
openDesign(type, key);
|
||||
} catch (e) {
|
||||
openDesign(type, '');
|
||||
}
|
||||
}
|
||||
if (m._soonData) {
|
||||
openWithJson(m._soonData);
|
||||
return;
|
||||
}
|
||||
var fileUrl = m.file_url || '';
|
||||
if (!fileUrl) {
|
||||
openDesign(type, '');
|
||||
return;
|
||||
}
|
||||
fetch(fileUrl).then(function (r) {
|
||||
if (!r.ok) throw new Error('fetch');
|
||||
return r.json();
|
||||
}).then(openWithJson).catch(function () {
|
||||
openDesign(type, '');
|
||||
});
|
||||
}
|
||||
|
||||
function templateCardHtml(m, itemIndex) {
|
||||
var type = Number(m.type) || 1;
|
||||
var thumbSrc = m.thumbSrc || templateFallbackThumb(type);
|
||||
var esc = typeof soonEscapeHtml === 'function' ? soonEscapeHtml : escapeAttr;
|
||||
var name = esc(m.name || m.title || '模板');
|
||||
return '<div class="soon-template-card" style="animation-delay:' + (i * 0.05) + 's" data-type="' + (m.type || 1) + '">' +
|
||||
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="">' +
|
||||
'<span>' + name + '</span></div>';
|
||||
'<div class="tip">' + name + '</div>' +
|
||||
'</div></div>';
|
||||
}
|
||||
|
||||
function bindTemplateCards(grid) {
|
||||
if (!grid) return;
|
||||
grid.querySelectorAll('.soon-template-card').forEach(function (card) {
|
||||
grid.querySelectorAll('.card[data-index]').forEach(function (card) {
|
||||
card.onclick = function () {
|
||||
openDesign(Number(card.dataset.type) || 1, '');
|
||||
var idx = Number(card.dataset.index);
|
||||
var m = templateListState.items[idx];
|
||||
if (m) openTemplateItem(m);
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -482,7 +550,7 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
|
||||
var start = (templateListState.page - 1) * templateListState.size;
|
||||
var slice = items.slice(start, start + templateListState.size);
|
||||
grid.innerHTML = slice.map(function (m, i) { return templateCardHtml(m, i); }).join('');
|
||||
grid.innerHTML = slice.map(function (m, i) { return templateCardHtml(m, start + i); }).join('');
|
||||
bindTemplateCards(grid);
|
||||
renderTemplatePager();
|
||||
}
|
||||
@@ -524,8 +592,12 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
}
|
||||
|
||||
var items = (j.data && j.data.items) ? j.data.items : [];
|
||||
templateListState.items = items;
|
||||
templateListState.total = items.length;
|
||||
var resolved = [];
|
||||
for (var ti = 0; ti < items.length; ti++) {
|
||||
resolved.push(await resolveTemplateItem(items[ti]));
|
||||
}
|
||||
templateListState.items = resolved;
|
||||
templateListState.total = resolved.length;
|
||||
templateListState.page = 1;
|
||||
renderTemplatesPage();
|
||||
|
||||
|
||||
@@ -263,7 +263,8 @@
|
||||
if (act === 'pay') {
|
||||
openPayModal(null, btn.dataset.plan, btn.dataset.price, {
|
||||
orderNo: no,
|
||||
channel: btn.dataset.channel || 'wechat',
|
||||
channel: btn.dataset.channel || (window.SoonMemberPay && SoonMemberPay.defaultChannel
|
||||
? SoonMemberPay.defaultChannel() : 'alipay'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -403,7 +404,7 @@
|
||||
planName: planName,
|
||||
priceDisplay: priceDisplay,
|
||||
orderNo: opts.orderNo,
|
||||
channel: opts.channel || 'wechat',
|
||||
channel: opts.channel || (payCore.defaultChannel ? payCore.defaultChannel() : 'alipay'),
|
||||
onPaid: function () {
|
||||
showPayBanner();
|
||||
loadMembership();
|
||||
|
||||
Reference in New Issue
Block a user