前端配置迁至 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
+28
View File
@@ -966,6 +966,34 @@
gap: 10px;
}
.soon-pay-channels--single {
grid-template-columns: 1fr;
}
.soon-pay-channel.is-fixed {
cursor: default;
}
.soon-pay-channel.is-fixed:hover {
border-color: rgba(255, 255, 255, 0.08);
background: rgba(0, 0, 0, 0.18);
}
.soon-pay-channel.is-fixed.is-active:hover {
border-color: var(--soon-accent);
background: rgba(0, 150, 136, 0.07);
}
.soon-pay-channel.is-fixed .soon-pay-channel__radio {
border-radius: 50%;
background: currentColor;
opacity: 0.85;
}
.soon-pay-channel.is-fixed.is-active .soon-pay-channel__radio::after {
display: none;
}
.soon-pay-channel {
display: flex;
align-items: center;
+28 -45
View File
@@ -271,44 +271,12 @@
animation: soon-fadeInUp 0.4s;
}
.soon-member-wrap { max-width: 960px; margin: 0 auto; }
.soon-template-card {
flex: 1 1 0;
min-width: 0;
max-width: 148px;
background: var(--soon-bg-panel);
border: 1px solid var(--soon-border-subtle);
border-radius: var(--soon-radius-sm);
overflow: hidden;
cursor: pointer;
transition: transform 0.2s var(--soon-ease), border-color 0.2s, box-shadow 0.2s;
animation: soon-fadeInUp 0.4s var(--soon-ease) backwards;
}
.soon-templates-strip .soon-empty--inline {
flex: 1 1 100%;
width: 100%;
min-width: 100%;
}
.soon-template-card:hover {
border-color: var(--soon-accent);
background: var(--soon-bg-elevated);
}
.soon-template-card img {
width: 100%;
height: 100px;
object-fit: cover;
background: var(--soon-bg-deep);
}
.soon-template-card span {
display: block;
padding: var(--soon-space-sm);
font-size: 12px;
color: var(--soon-text);
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.soon-member-header {
@@ -506,9 +474,12 @@
}
.soon-home .card-list > .card,
.soon-home-recent-grid > .card {
.soon-home-recent-grid > .card,
.soon-home .soon-templates-strip > .card {
width: 168px;
max-width: 168px;
flex-shrink: 0;
margin: 0;
}
.soon-section--recent #filePager {
@@ -518,7 +489,8 @@
border-top: 1px solid var(--soon-border-subtle);
}
.soon-home .card-list .rect {
.soon-home .card-list .rect,
.soon-home .soon-templates-strip .rect {
width: 168px;
height: 160px;
background: var(--soon-bg-panel);
@@ -535,11 +507,13 @@
overflow: hidden;
box-sizing: border-box;
}
.soon-home .card-list .rect:hover {
.soon-home .card-list .rect:hover,
.soon-home .soon-templates-strip .rect:hover {
border-color: var(--soon-accent);
background: var(--soon-bg-elevated);
}
.soon-home .card-list .rect img {
.soon-home .card-list .rect img,
.soon-home .soon-templates-strip .rect img {
display: block;
width: 100%;
height: 135px;
@@ -553,9 +527,12 @@
border-radius: 4px;
transition: transform 0.3s;
}
.soon-home .card-list .rect1 img { transform: scale(0.8); }
.soon-home .card-list .rect1:hover img { transform: scale(0.85); }
.soon-home .card-list .tip {
.soon-home .card-list .rect1 img,
.soon-home .soon-templates-strip .rect1 img { transform: scale(0.8); }
.soon-home .card-list .rect1:hover img,
.soon-home .soon-templates-strip .rect1:hover img { transform: scale(0.85); }
.soon-home .card-list .tip,
.soon-home .soon-templates-strip .tip {
flex-shrink: 0;
width: 100%;
height: 25px;
@@ -571,7 +548,8 @@
border-top: 1px solid var(--soon-border-subtle);
background: rgba(0, 0, 0, 0.15);
}
.soon-home .card-list .rect:hover .tip { color: var(--soon-text-strong); }
.soon-home .card-list .rect:hover .tip,
.soon-home .soon-templates-strip .rect:hover .tip { color: var(--soon-text-strong); }
.soon-home .card-list .soon-file-card__actions {
position: absolute;
top: 6px;
@@ -636,7 +614,8 @@
max-width: none;
}
.soon-home .card-list .rect:not(.rect1)::after {
.soon-home .card-list .rect:not(.rect1)::after,
.soon-home .soon-templates-strip .rect:not(.rect1)::after {
content: '';
position: absolute;
left: 50%;
@@ -736,18 +715,22 @@
min-height: 300px;
}
.soon-home .card-list > .card,
.soon-home-recent-grid > .card {
.soon-home-recent-grid > .card,
.soon-home .soon-templates-strip > .card {
width: 148px;
max-width: 148px;
}
.soon-home .card-list .rect {
.soon-home .card-list .rect,
.soon-home .soon-templates-strip .rect {
width: 148px;
height: 150px;
}
.soon-home .card-list .rect img {
.soon-home .card-list .rect img,
.soon-home .soon-templates-strip .rect img {
height: 125px;
}
.soon-home .card-list .rect:not(.rect1)::after {
.soon-home .card-list .rect:not(.rect1)::after,
.soon-home .soon-templates-strip .rect:not(.rect1)::after {
transform: translate(-50%, calc(-50% - 12px));
width: 20px;
height: 20px;
-7
View File
@@ -1,7 +0,0 @@
window.SOON_DEPLOY_CONFIG = {
api_v1_base: 'http://localhost:8102/api/v1',
api_admin_base: 'http://localhost:8102/api/admin',
front_base_url: 'http://localhost:8100',
web_base: 'http://localhost:8100',
version: 'local-dev'
};
-7
View File
@@ -1,7 +0,0 @@
{
"api_v1_base": "https://designadmin.cardsoon.com/api/v1",
"api_admin_base": "https://designadmin.cardsoon.com/api/admin",
"front_base_url": "https://design.cardsoon.com",
"web_base": "https://design.cardsoon.com",
"version": "1.0.0"
}
+8
View File
@@ -0,0 +1,8 @@
// 复制为同目录 local.js(全量更新 frontend 时不要覆盖 local.js
window.SOON_DEPLOY_CONFIG = {
api_v1_base: 'https://designadmin.cardsoon.com/api/v1',
api_admin_base: 'https://designadmin.cardsoon.com/api/admin',
+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();
+107 -16
View File
@@ -719,26 +719,117 @@ body.soon-admin-page .soon-admin-date-wrap input[type="date"].soon-input::-webki
white-space: nowrap;
}
.soon-admin-key-form-row {
display: grid;
grid-template-columns: 140px 1fr auto;
gap: var(--soon-space-sm);
align-items: start;
padding: var(--soon-space-md) 0;
border-bottom: 1px solid var(--soon-border);
.soon-admin-form--pay { padding: 0; }
.soon-admin-form-panel--pay {
padding: var(--soon-space-md) var(--soon-space-lg);
}
.soon-admin-key-form-row:last-child { border-bottom: none; }
.soon-admin-key-form-row__label {
font-size: var(--soon-font-sm);
color: var(--soon-text);
padding-top: 8px;
.soon-admin-form--pay .soon-admin-tabs {
margin-top: var(--soon-space-md);
padding-top: var(--soon-space-md);
border-top: 1px solid var(--soon-border);
}
.soon-admin-key-form-row__status {
.soon-admin-form--pay .soon-admin-form__actions {
margin-top: var(--soon-space-sm);
padding: var(--soon-space-sm) 0 0;
background: none;
border: none;
border-radius: 0;
}
.soon-admin-pay-status {
display: flex;
gap: 8px;
margin-bottom: var(--soon-space-md);
}
.soon-admin-pay-status__item {
padding: 2px 10px;
border-radius: 999px;
font-size: var(--soon-font-xs);
margin-top: 4px;
border: 1px solid var(--soon-border);
color: var(--soon-text-muted);
}
.soon-admin-pay-status__item.is-ok {
color: var(--soon-accent);
border-color: rgba(0, 150, 136, 0.35);
background: rgba(0, 150, 136, 0.08);
}
.soon-admin-pay-status__item.is-miss {
color: var(--soon-danger);
border-color: rgba(244, 67, 54, 0.35);
background: rgba(244, 67, 54, 0.08);
}
.soon-admin-pay-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px 20px;
}
@media (max-width: 768px) {
.soon-admin-pay-grid { grid-template-columns: 1fr; }
}
.soon-admin-pay-field { min-width: 0; }
.soon-admin-pay-field--wide { grid-column: 1 / -1; }
.soon-admin-pay-field__label {
display: block;
margin-bottom: 4px;
font-size: var(--soon-font-xs);
color: var(--soon-text-muted);
}
.soon-admin-pay-field__control { min-width: 0; }
.soon-admin-pay-field__control > .soon-input { width: 100%; }
.soon-admin-pay-checks {
display: flex;
flex-wrap: wrap;
gap: 12px 20px;
min-height: 32px;
align-items: center;
}
.soon-input--readonly {
opacity: 0.8;
cursor: default;
background: rgba(255, 255, 255, 0.03);
font-size: var(--soon-font-xs);
}
.soon-admin-pay-key {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
}
.soon-admin-pay-key__badge {
min-width: 48px;
padding: 1px 6px;
border-radius: 4px;
font-size: 11px;
text-align: center;
}
.soon-admin-pay-key__badge.is-ok {
color: var(--soon-accent);
background: rgba(0, 150, 136, 0.12);
}
.soon-admin-pay-key__badge.is-miss {
color: var(--soon-danger);
background: rgba(244, 67, 54, 0.1);
}
.soon-admin-pay-key__text { flex: 1; min-width: 0; }
.soon-admin-pay-key__file { cursor: pointer; margin: 0; }
.soon-admin-pay-key__paste {
display: none;
width: 100%;
margin-top: 6px;
}
.soon-admin-pay-key__paste.is-open { display: block; }
.soon-admin-pay-key__paste .soon-input { width: 100%; margin-bottom: 4px; }
.soon-admin-form-checks--inline {
margin-top: 0;
padding-top: 0;
border-top: none;
}
.soon-admin-key-form-row__status--ok { color: var(--soon-accent); }
.soon-admin-key-form-row__status--miss { color: var(--soon-danger); }
.soon-admin-btn-group {
display: flex;
@@ -1,290 +1,272 @@
(function () {
'use strict';
var Api = function () { return window.AdminApi; };
var Ui = function () { return window.AdminUi; };
var I = function () { return window.AdminI18n; };
var Form = function () { return window.AdminForm; };
var Tabs = function () { return window.AdminTabs; };
var PARAM_KEYS = [
'site.base_url',
'site.front_base_url',
'alipay.app_id',
'wechat.app_id',
'wechat.mch_id',
'wechat.mch_serial_no',
];
var CHANNEL_CONFIG = {
alipay: {
label: '支付宝',
kinds: [
{ kind: 'private_key', file: 'alipay_private_key.pem', label: '应用私钥 private_key' },
{ kind: 'public_key', file: 'alipay_public_key.pem', label: '支付宝公钥 public_key' },
],
},
wechat: {
label: '微信支付',
kinds: [
{ kind: 'mch_private_key', file: 'wechat_mch_private_key.pem', label: '商户私钥 mch_private_key' },
{ kind: 'api_v3_key', file: 'wechat_api_v3_key.pem', label: 'APIv3 密钥 api_v3_key' },
{ kind: 'platform_cert', file: 'wechat_platform_cert.pem', label: '平台证书 platform_cert' },
],
},
var KEY_FIELDS = {
alipay: [
{ kind: 'private_key', file: 'alipay_private_key.pem', label: '应用私钥' },
{ kind: 'public_key', file: 'alipay_public_key.pem', label: '支付宝公钥' },
],
wechat: [
{ kind: 'mch_private_key', file: 'wechat_mch_private_key.pem', label: '商户私钥' },
{ kind: 'api_v3_key', file: 'wechat_api_v3_key.pem', label: 'APIv3 密钥', text: true },
{ kind: 'platform_cert', file: 'wechat_platform_cert.pem', label: '平台证书' },
],
};
function cfgId(key) {
return 'pay_' + key.replace(/\./g, '_');
}
function fileSet(items) {
var set = {};
(items || []).forEach(function (p) { set[p.name] = p; });
return set;
}
function channelFormHtml(channel, files) {
var cfg = CHANNEL_CONFIG[channel];
var rows = cfg.kinds.map(function (k) {
var ok = !!files[k.file];
var statusCls = ok ? 'soon-admin-key-form-row__status--ok' : 'soon-admin-key-form-row__status--miss';
var statusText = ok ? '已就绪' : '缺失';
var id = channel + '_' + k.kind;
return '<div class="soon-admin-key-form-row">' +
'<div><div class="soon-admin-key-form-row__label">' + I().esc(k.label) + '</div>' +
'<div class="soon-admin-key-form-row__status ' + statusCls + '">' + statusText + '</div></div>' +
'<textarea id="' + id + '" class="soon-input soon-input--sm soon-input--textarea" rows="4" ' +
'placeholder="粘贴 PEM 内容或拖入 .pem 文件"></textarea>' +
'<button type="button" class="soon-btn soon-btn--sm pay-upload-btn" data-channel="' + channel +
'" data-kind="' + k.kind + '" data-target="' + id + '">上传</button></div>';
}).join('');
return '<div class="soon-admin-form">' + Form().panel(cfg.label + ' 密钥', rows) +
'<details class="soon-admin-hint"><summary>回调说明</summary>' +
'<p>notify<code>/api/v1/pay/' + channel + '/notify</code>(须配置 site.base_url 为 HTTPS 外网地址)</p></details>' +
'<div class="soon-admin-upload" id="payDrop_' + channel + '">拖拽 .pem 到此处自动填入对应字段</div></div>';
}
function filesTabHtml(items) {
var rows = (items || []).map(function (p) {
return '<tr><td>' + I().esc(p.name) + '</td><td>' + p.bytes + ' B</td><td>' +
I().formatDateTime(p.mtime) + '</td></tr>';
}).join('');
return Ui().dataTable(['文件名', '大小', '修改时间'], rows);
}
function uploadPem(channel, kind, content, onDone) {
return Api().fetch('payment/upload', {
method: 'POST',
body: JSON.stringify({ channel: channel, kind: kind, content: content }),
}).then(function (j) {
if (j) {
soonToast('密钥已上传', 'success');
if (onDone) onDone();
}
return j;
});
}
function bindChannel(channel, panel) {
panel.querySelectorAll('.pay-upload-btn').forEach(function (btn) {
btn.onclick = function () {
var target = Ui().el(btn.dataset.target);
var content = target ? target.value.trim() : '';
if (!content) {
soonToast('请先粘贴或拖入 PEM 内容', 'warn');
return;
}
Ui().runFormSubmit(btn, function () {
return uploadPem(btn.dataset.channel, btn.dataset.kind, content, render);
});
};
});
var drop = Ui().el('payDrop_' + channel);
if (!drop) return;
drop.ondragover = function (e) { e.preventDefault(); drop.classList.add('is-dragover'); };
drop.ondragleave = function () { drop.classList.remove('is-dragover'); };
drop.ondrop = function (e) {
e.preventDefault();
drop.classList.remove('is-dragover');
var file = e.dataTransfer.files[0];
if (!file) return;
file.text().then(function (text) {
var name = file.name.toLowerCase();
var cfg = CHANNEL_CONFIG[channel];
var matched = cfg.kinds.find(function (k) { return name.indexOf(k.kind.replace(/_/g, '')) >= 0 || name === k.file; });
var targetId = matched ? channel + '_' + matched.kind : channel + '_' + cfg.kinds[0].kind;
var ta = Ui().el(targetId);
if (ta) ta.value = text;
soonToast('已填入 ' + (matched ? matched.label : '首个字段') + ',请确认后点击上传', 'success');
});
function deployDefaults() {
var cfg = window.SOON_DEPLOY_CONFIG || {};
return {
apiBase: String(cfg.api_v1_base || '').replace(/\/api\/v1\/?$/, ''),
frontBase: cfg.front_base_url || cfg.web_base || '',
};
}
function parseDisplayChannels(values) {
var raw = values && values['payment.display_channels'] != null
? values['payment.display_channels'] : 'alipay';
return String(raw).split(/[\s,]+/).filter(function (v) { return v === 'alipay' || v === 'wechat'; });
}
function statusLine(readiness) {
readiness = readiness || {};
var a = readiness.alipay || {};
var w = readiness.wechat || {};
return '<div class="soon-admin-pay-status">' +
'<span class="soon-admin-pay-status__item' + (a.ready ? ' is-ok' : ' is-miss') + '">支付宝</span>' +
'<span class="soon-admin-pay-status__item' + (w.ready ? ' is-ok' : ' is-miss') + '">微信支付</span></div>';
}
function field(label, control, wide) {
return '<div class="soon-admin-pay-field' + (wide ? ' soon-admin-pay-field--wide' : '') + '">' +
'<label class="soon-admin-pay-field__label">' + I().esc(label) + '</label>' +
'<div class="soon-admin-pay-field__control">' + control + '</div></div>';
}
function grid(cells) {
return '<div class="soon-admin-pay-grid">' + cells.join('') + '</div>';
}
function textInput(key, values, placeholder) {
var val = values && values[key] != null ? values[key] : '';
return Form().input(cfgId(key), { value: val, placeholder: placeholder || '' });
}
function readonlyInput(value) {
return '<input class="soon-input soon-input--sm soon-input--readonly" readonly value="' +
I().esc(value || '') + '">';
}
function displayChannelsField(selected) {
if (!selected.length) selected = ['alipay'];
var mk = function (ch, label) {
var on = selected.indexOf(ch) >= 0;
return '<label class="soon-admin-check"><input type="checkbox" class="pay-disp-ch" value="' + ch + '"' +
(on ? ' checked' : '') + '> ' + label + '</label>';
};
return field('用户端展示',
'<div class="soon-admin-pay-checks">' + mk('alipay', '支付宝') + mk('wechat', '微信') + '</div>', true);
}
function siteGrid(values, notifyUrls) {
var defs = deployDefaults();
return grid([
displayChannelsField(parseDisplayChannels(values)),
field('API 外网', textInput('site.base_url', values, defs.apiBase)),
field('前端站点', textInput('site.front_base_url', values, defs.frontBase)),
field('支付宝回调', readonlyInput(notifyUrls.alipay)),
field('微信回调', readonlyInput(notifyUrls.wechat)),
]);
}
function keyField(channel, keyDef, files) {
var ok = !!files[keyDef.file];
var id = channel + '_' + keyDef.kind;
var pasteId = id + '_paste';
var badge = '<span class="soon-admin-pay-key__badge ' + (ok ? 'is-ok' : 'is-miss') + '">' +
(ok ? '已配置' : '未配置') + '</span>';
var inner;
if (keyDef.text) {
inner = badge +
Form().input(id + '_text', { placeholder: 'APIv3 密钥', className: 'soon-admin-pay-key__text' }) +
'<button type="button" class="soon-btn soon-btn--xs soon-btn--ghost pay-key-upload" data-channel="' +
channel + '" data-kind="' + keyDef.kind + '" data-source="' + id + '_text">写入</button>';
} else {
inner = badge +
'<label class="soon-btn soon-btn--xs soon-btn--ghost soon-admin-pay-key__file">上传' +
'<input type="file" class="pay-key-file" accept=".pem,.txt,.key" data-channel="' +
channel + '" data-kind="' + keyDef.kind + '" hidden></label>' +
'<button type="button" class="soon-btn soon-btn--xs soon-btn--ghost pay-key-paste" data-target="' +
pasteId + '">粘贴</button>' +
'<div id="' + pasteId + '_box" class="soon-admin-pay-key__paste">' +
'<textarea id="' + pasteId + '" class="soon-input soon-input--sm" rows="2" placeholder="PEM"></textarea>' +
'<button type="button" class="soon-btn soon-btn--xs pay-key-commit" data-channel="' + channel +
'" data-kind="' + keyDef.kind + '" data-target="' + pasteId + '">确认</button></div>';
}
return field(keyDef.label, '<div class="soon-admin-pay-key">' + inner + '</div>');
}
function channelGrid(channel, files, values) {
var cells = [];
if (channel === 'alipay') {
cells.push(field('APPID', textInput('alipay.app_id', values, '开放平台 APPID')));
KEY_FIELDS.alipay.forEach(function (f) { cells.push(keyField('alipay', f, files)); });
} else {
cells.push(field('AppID', textInput('wechat.app_id', values, 'AppID')));
cells.push(field('商户号', textInput('wechat.mch_id', values, 'mch_id')));
cells.push(field('证书序列号', textInput('wechat.mch_serial_no', values, 'serial_no')));
KEY_FIELDS.wechat.forEach(function (f) { cells.push(keyField('wechat', f, files)); });
}
return grid(cells);
}
function formHtml(values, files, notifyUrls, readiness) {
var tabs = Tabs().html('paymentTabs', [
{ id: 'alipay', label: '支付宝' },
{ id: 'wechat', label: '微信' },
], {
alipay: channelGrid('alipay', files, values),
wechat: channelGrid('wechat', files, values),
}, 'alipay');
return '<div class="soon-admin-form soon-admin-form--pay">' +
'<div class="soon-admin-form-panel soon-admin-form-panel--pay">' +
statusLine(readiness) + siteGrid(values, notifyUrls) + tabs +
'</div>' + Form().actions('保存', 'payFormSave') + '</div>';
}
function collectParams() {
var items = PARAM_KEYS.map(function (key) {
var el = Ui().el(cfgId(key));
return { key: key, value: el ? el.value.trim() : '' };
});
var disp = [];
document.querySelectorAll('.pay-disp-ch:checked').forEach(function (el) { disp.push(el.value); });
items.push({ key: 'payment.display_channels', value: disp.join(',') });
return items;
}
function uploadKey(channel, kind, content) {
return Api().fetch('payment/upload', {
method: 'POST',
body: JSON.stringify({ channel: channel, kind: kind, content: content }),
});
}
function bindKeys(panel) {
panel.querySelectorAll('.pay-key-file').forEach(function (input) {
input.onchange = function () {
var file = input.files && input.files[0];
if (!file) return;
file.text().then(function (text) {
var content = text.trim();
if (!content) {
soonToast('文件为空', 'warn');
return;
}
uploadKey(input.dataset.channel, input.dataset.kind, content).then(function (j) {
if (j) {
soonToast('已保存', 'success');
render();
}
});
}).finally(function () { input.value = ''; });
};
});
panel.querySelectorAll('.pay-key-paste').forEach(function (btn) {
btn.onclick = function () {
var box = Ui().el(btn.dataset.target + '_box');
if (!box) return;
var open = box.classList.toggle('is-open');
var wrap = box.closest('.soon-admin-pay-field');
if (wrap) wrap.classList.toggle('soon-admin-pay-field--wide', open);
if (open) {
var ta = Ui().el(btn.dataset.target);
if (ta) ta.focus();
}
};
});
panel.querySelectorAll('.pay-key-upload, .pay-key-commit').forEach(function (btn) {
btn.onclick = function () {
var content = '';
if (btn.classList.contains('pay-key-upload')) {
var src = Ui().el(btn.dataset.source);
content = src ? src.value.trim() : '';
} else {
var ta = Ui().el(btn.dataset.target);
content = ta ? ta.value.trim() : '';
}
if (!content) {
soonToast('内容为空', 'warn');
return;
}
Ui().runFormSubmit(btn, function () {
return uploadKey(btn.dataset.channel, btn.dataset.kind, content).then(function (j) {
if (j) {
soonToast('已保存', 'success');
render();
}
});
});
};
});
}
function bindSave() {
Ui().bindFormSubmit('payFormSave', function () {
return Api().postJson('payment/config', { items: collectParams() }).then(function (j) {
if (j) {
soonToast('已保存', 'success');
render();
}
});
});
}
function render() {
var panel = Ui().el('panel-payment');
if (!panel) return;
panel.innerHTML = '<div class="soon-admin-page">' + Ui().pageHeader('支付配置', '按渠道管理密钥文件,上传后立即热加载') +
Ui().skeletonTable() + '</div>';
var header = Ui().pageHeader('支付配置');
panel.innerHTML = '<div class="soon-admin-page">' + header + Ui().skeletonTable() + '</div>';
Api().get('payment/status').then(function (j) {
if (!j) return;
var items = j.data.items || [];
var files = fileSet(items);
var tabsHtml = Tabs().html('paymentTabs', [
{ id: 'alipay', label: '支付宝' },
{ id: 'wechat', label: '微信支付' },
{ id: 'files', label: '文件清单' },
], {
alipay: channelFormHtml('alipay', files),
wechat: channelFormHtml('wechat', files),
files: filesTabHtml(items),
}, 'alipay');
panel.innerHTML = '<div class="soon-admin-page">' +
Ui().pageHeader('支付配置', '按渠道管理密钥文件,上传后立即热加载') +
tabsHtml + '</div>';
var tabsRoot = Ui().el('paymentTabs');
Tabs().bind(tabsRoot);
bindChannel('alipay', panel);
bindChannel('wechat', panel);
var data = j.data || {};
panel.innerHTML = '<div class="soon-admin-page">' + header +
formHtml(data.params || {}, fileSet(data.items), data.notify_urls || {}, data.readiness) +
'</div>';
Tabs().bind(Ui().el('paymentTabs'));
bindSave();
bindKeys(panel);
});
}
window.AdminViews = window.AdminViews || {};
window.AdminViews.payment = { render: render };
})();
@@ -5,39 +5,22 @@
var Ui = function () { return window.AdminUi; };
var I = function () { return window.AdminI18n; };
var Form = function () { return window.AdminForm; };
var Tabs = function () { return window.AdminTabs; };
var Filter = function () { return window.AdminFilter; };
var State = function () { return window.AdminState; };
var Router = function () { return window.AdminRouter; };
var SETTING_LABELS = {
'site.base_url': 'API 站点外网地址',
'site.front_base_url': '前端站点地址',
'site.base_url': 'API 站点外网地址(见支付配置)',
'payment.display_channels': '前端展示渠道(见支付配置)',
'alipay.app_id': '支付宝 APPID(见支付配置)',
'wechat.app_id': '微信 AppID(见支付配置)',
'wechat.mch_id': '微信商户号(见支付配置)',
'wechat.mch_serial_no': '商户证书序列号(见支付配置)',
};
var valuesByKey = {};
var allItems = [];
function siteFields() {
var cfg = window.SOON_DEPLOY_CONFIG || {};
var apiBase = String(cfg.api_v1_base || '').replace(/\/api\/v1\/?$/, '');
var frontBase = cfg.front_base_url || cfg.web_base || '';
return [
{
key: 'site.base_url',
label: 'API 站点外网地址',
hint: '须 HTTPS 外网可达;支付宝/微信 notify 回调依赖此地址',
placeholder: apiBase,
},
{
key: 'site.front_base_url',
label: '前端站点地址',
hint: '支付完成 return_url 跳转至会员页',
placeholder: frontBase,
},
];
}
function settingLabel(key) {
if (SETTING_LABELS[key]) {
return I().esc(key) + '<br><span class="soon-admin-setting-hint">' + I().esc(SETTING_LABELS[key]) + '</span>';
@@ -50,17 +33,6 @@
return I().esc(val);
}
function siteFormHtml() {
var fields = siteFields().map(function (f) {
var val = valuesByKey[f.key] != null ? valuesByKey[f.key] : '';
return Form().field(f.label,
Form().input('cfg_' + f.key.replace(/\./g, '_'), { value: val, placeholder: f.placeholder }),
f.hint);
}).join('');
return '<div class="soon-admin-form">' + Form().panel('站点与回调地址', fields) +
Form().actions('保存站点配置', 'settingsSiteSave') + '</div>';
}
function allSettingsHtml(filterQ) {
var q = (filterQ || '').toLowerCase();
var items = allItems.filter(function (s) {
@@ -82,21 +54,6 @@
return filter + Ui().dataTable(['配置项', '当前值', '更新时间', '操作'], rows);
}
function bindSiteForm() {
Ui().bindFormSubmit('settingsSiteSave', function () {
var items = siteFields().map(function (f) {
var el = Ui().el('cfg_' + f.key.replace(/\./g, '_'));
return { key: f.key, value: el ? el.value.trim() : '' };
});
return Api().postJson('settings/batch', { items: items }).then(function (r) {
if (r) {
soonToast('站点配置已保存', 'success');
render();
}
});
});
}
function bindAllSettings() {
var st = State().settings;
var searchBtn = Ui().el('settingsSearchBtn');
@@ -154,7 +111,7 @@
var panel = Ui().el('panel-settings');
if (!panel) return;
var st = State().settings;
var header = Ui().pageHeader('系统设置', '站点地址与运行时参数;修改后即时生效');
var header = Ui().pageHeader('系统设置', '运行时参数;支付相关请在「支付配置」维护');
panel.innerHTML = '<div class="soon-admin-page">' + header + Ui().skeletonTable() + '</div>';
Api().get('settings').then(function (j) {
@@ -168,20 +125,7 @@
allItems = j.data.items || [];
allItems.forEach(function (s) { valuesByKey[s.key] = s.value; });
var tabsHtml = Tabs().html('settingsTabs', [
{ id: 'site', label: '站点配置' },
{ id: 'all', label: '全部配置' },
], {
site: siteFormHtml(),
all: allSettingsHtml(st.q),
}, st.tab);
panel.innerHTML = '<div class="soon-admin-page">' + header + tabsHtml + '</div>';
Tabs().bind(Ui().el('settingsTabs'), function (tabId) {
st.tab = tabId;
Router().syncQuery();
});
bindSiteForm();
panel.innerHTML = '<div class="soon-admin-page">' + header + allSettingsHtml(st.q) + '</div>';
bindAllSettings();
});
}
+1 -1
View File
@@ -35,7 +35,7 @@
</div>
</div>
<script src="../../assets/deploy-config.js"></script>
<script src="../../config/local.js"></script>
<script src="../../js/common/auth-redirect.js"></script>
<script src="../../vendor/layui/layui.js"></script>
<script src="../../js/common/soon-toast.js"></script>
+1 -1
View File
@@ -41,7 +41,7 @@
</div>
</div>
<script src="../../assets/deploy-config.js"></script>
<script src="../../config/local.js"></script>
<script src="../../js/common/auth-redirect.js"></script>
<script src="../../vendor/layui/layui.js"></script>
<script src="../../js/common/soon-toast.js"></script>
+1 -1
View File
@@ -875,7 +875,7 @@
<script src="../vendor/js/print.min.js"></script>
<script src="../vendor/js/crypto-js.min.js"></script>
<script src="../vendor/js/jspdf.umd.min.js"></script>
<script src="../assets/deploy-config.js"></script>
<script src="../config/local.js"></script>
<script src="../js/common/auth-redirect.js"></script>
<script src="../js/common/auth-session.js"></script>
<script src="../js/common/member-pay-core.js"></script>
+1 -1
View File
@@ -665,7 +665,7 @@
<script src="../vendor/js/print.min.js"></script>
<script src="../vendor/js/crypto-js.min.js"></script>
<script src="../vendor/js/jspdf.umd.min.js"></script>
<script src="../assets/deploy-config.js"></script>
<script src="../config/local.js"></script>
<script src="../js/common/auth-redirect.js"></script>
<script src="../js/common/auth-session.js"></script>
<script src="../js/common/member-pay-core.js"></script>
+1 -1
View File
@@ -136,7 +136,7 @@
<script src="../assets/deploy-config.js"></script>
<script src="../config/local.js"></script>
<script src="../js/common/auth-redirect.js"></script>
+1 -1
View File
@@ -39,7 +39,7 @@
</div>
</div>
<script src="../assets/deploy-config.js"></script>
<script src="../config/local.js"></script>
<script src="../js/common/portal-topbar.js"></script>
<script>SoonPortalTopbar.mount('#portal-topbar', { variant: 'auth' });</script>
<script src="../js/common/auth-redirect.js"></script>
+1 -1
View File
@@ -50,7 +50,7 @@
</section>
</div>
<script src="../assets/deploy-config.js"></script>
<script src="../config/local.js"></script>
<script src="../js/common/auth-redirect.js"></script>
<script src="../js/common/auth-session.js"></script>
<script src="../js/common/member-pay-core.js"></script>
+1 -1
View File
@@ -39,7 +39,7 @@
</div>
</div>
<script src="../assets/deploy-config.js"></script>
<script src="../config/local.js"></script>
<script src="../js/common/portal-topbar.js"></script>
<script>SoonPortalTopbar.mount('#portal-topbar', { variant: 'auth' });</script>
<script src="../js/common/auth-redirect.js"></script>