前端配置迁至 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:
24kycj
2026-06-08 20:48:51 +08:00
parent 88c6ce8ccc
commit 152228d41f
47 changed files with 1448 additions and 486 deletions
+117 -26
View File
@@ -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
View File
@@ -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();
+3 -2
View File
@@ -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();