重构 monorepo 并完善网页端订阅与首页体验

- 迁移为 frontend-web、frontend-electron、backend-web 与 docker 部署结构
- 网页端:订阅门禁二次弹窗、套餐/支付组件化、顶栏分组对齐
- 首页:最近文件与模板库布局优化,缩略图对齐,下载与删除操作
- 新增管理后台、支付与云端文件 API,更新 README 与项目规范

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
24kycj
2026-06-08 18:17:39 +08:00
parent 5814b7bc0e
commit 88c6ce8ccc
511 changed files with 189528 additions and 22804 deletions
+41
View File
@@ -0,0 +1,41 @@
(function () {
'use strict';
function computeAssetBase() {
if (typeof window === 'undefined' || !window.location) {
return 'assets/images/';
}
var p = window.location.pathname || '/';
if (p.indexOf('/pages/') !== -1 || /\.web\.html$/i.test(p) || /\/pages\/[^/]+\.html$/i.test(p)) {
return '../assets/images/';
}
if (p.endsWith('/pages/') || p.endsWith('/pages')) {
return '../assets/images/';
}
return 'assets/images/';
}
function computeJsBase() {
if (typeof window === 'undefined' || !window.location) {
return 'js/';
}
var p = window.location.pathname || '/';
if (p.indexOf('/pages/') !== -1 || /\.web\.html$/i.test(p) || /\/pages\/[^/]+\.html$/i.test(p)) {
return '../js/';
}
return 'js/';
}
window.SOON_ASSET_BASE = computeAssetBase();
window.SOON_JS_BASE = computeJsBase();
window.soonAsset = function (name) {
var n = String(name || '').replace(/^\/+/, '');
return window.SOON_ASSET_BASE + n;
};
window.soonJs = function (rel) {
var n = String(rel || '').replace(/^\/+/, '');
return window.SOON_JS_BASE + n;
};
})();
+73
View File
@@ -0,0 +1,73 @@
(function () {
'use strict';
var ALLOWED = /^(index\.web\.html|member\.web\.html|login\.web\.html|register\.web\.html|admin\/(?:index|login)\.html|index\.html|login\.html|design[12]\.web\.html)(\?.*)?$/i;
function soonEscapeHtml(s) {
if (s == null) return '';
return String(s)
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
function soonSafeRedirect(urlParam, fallback) {
fallback = fallback || 'index.web.html';
if (!urlParam || typeof urlParam !== 'string') return fallback;
var raw = urlParam.trim();
if (!raw) return fallback;
try {
raw = decodeURIComponent(raw);
} catch (e) {
return fallback;
}
var lower = raw.toLowerCase();
if (lower.indexOf('//') === 0 || lower.indexOf('http:') === 0 || lower.indexOf('https:') === 0 ||
lower.indexOf('javascript:') === 0 || raw.indexOf('..') !== -1 || raw.indexOf('\\') !== -1) {
return fallback;
}
var pathOnly = raw.split(/[#?]/)[0];
if (ALLOWED.test(pathOnly)) return raw;
try {
var resolved = new URL(raw, window.location.href);
if (resolved.origin !== window.location.origin) return fallback;
var path = resolved.pathname.replace(/^.*\/pages\//, '').replace(/^\//, '');
if (!ALLOWED.test(path.split(/[#?]/)[0])) return fallback;
return path + resolved.search + resolved.hash;
} catch (e) {
return fallback;
}
}
function soonSafeImageUrl(url, fallback) {
if (!url || typeof url !== 'string') return fallback || '';
var raw = url.trim();
if (!raw) return fallback || '';
var lower = raw.toLowerCase();
if (lower.indexOf('javascript:') === 0 || lower.indexOf('data:text/html') === 0 ||
lower.indexOf('vbscript:') === 0 || lower.indexOf('//') === 0) {
return fallback || '';
}
if (lower.indexOf('data:image/') === 0) return raw;
if (lower.indexOf('blob:') === 0) return raw;
if (lower.indexOf('https:') === 0) return raw;
if (lower.indexOf('http:') === 0) {
try {
var apiBase = (typeof window !== 'undefined' && window.SOON_DEPLOY_CONFIG && window.SOON_DEPLOY_CONFIG.api_v1_base);
if (apiBase && new URL(raw).origin === new URL(apiBase).origin) return raw;
} catch (e) { /* fall through */ }
}
if (raw.indexOf('../') === 0 || raw.indexOf('./') === 0 || raw.indexOf('/') === 0) {
try {
var resolved = new URL(raw, window.location.href);
if (resolved.origin === window.location.origin) return raw;
} catch (e) { /* fall through */ }
}
return fallback || '';
}
window.soonEscapeHtml = soonEscapeHtml;
window.soonSafeRedirect = soonSafeRedirect;
window.soonSafeImageUrl = soonSafeImageUrl;
})();
+107
View File
@@ -0,0 +1,107 @@
(function () {
'use strict';
var refreshPromise = null;
function getAccessToken() {
try { return localStorage.getItem('soon_access') || ''; } catch (e) { return ''; }
}
function getRefreshToken() {
try { return localStorage.getItem('soon_refresh') || ''; } catch (e) { return ''; }
}
function clearSession() {
try {
localStorage.removeItem('soon_access');
localStorage.removeItem('soon_refresh');
} catch (e) { /* ignore */ }
}
function getApiBase() {
var cfg = (typeof window !== 'undefined' && window.SOON_DEPLOY_CONFIG && window.SOON_DEPLOY_CONFIG.api_v1_base);
if (cfg) return String(cfg).replace(/\/+$/, '');
if (typeof window !== 'undefined' && window.location) {
return window.location.origin + '/api/v1';
}
return '/api/v1';
}
function redirectToLogin() {
var path = 'login.web.html';
try {
var current = window.location.pathname.replace(/^.*\/pages\//, '').replace(/^\//, '');
if (current && current.indexOf('login.web.html') !== 0) {
var redirect = typeof soonSafeRedirect === 'function'
? soonSafeRedirect(current + window.location.search, 'index.web.html')
: 'index.web.html';
path = 'login.web.html?redirect=' + encodeURIComponent(redirect);
}
} catch (e) { /* use default */ }
window.location.href = path;
}
function refreshAccessToken() {
if (refreshPromise) return refreshPromise;
var refresh = getRefreshToken();
if (!refresh) {
return Promise.reject(new Error('no_refresh_token'));
}
refreshPromise = fetch(getApiBase() + '/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: refresh }),
}).then(function (r) {
return r.json().then(function (j) {
if (!r.ok || !j.ok || !j.data || !j.data.access_token) {
throw new Error(j.message || 'refresh_failed');
}
localStorage.setItem('soon_access', j.data.access_token);
if (j.data.refresh_token) {
localStorage.setItem('soon_refresh', j.data.refresh_token);
}
return j.data.access_token;
});
}).finally(function () {
refreshPromise = null;
});
return refreshPromise;
}
function soonAuthedFetch(url, opts) {
opts = opts || {};
var headers = Object.assign({}, opts.headers || {});
var token = getAccessToken();
if (token) {
headers['Authorization'] = 'Bearer ' + token;
} else {
delete headers['Authorization'];
delete headers['authorization'];
}
opts.headers = headers;
var fullUrl = url;
if (url.indexOf('http') !== 0) {
fullUrl = getApiBase() + '/' + String(url).replace(/^\/+/, '');
}
return fetch(fullUrl, opts).then(function (response) {
if (response.status !== 401) return response;
return refreshAccessToken().then(function (newToken) {
var retryHeaders = Object.assign({}, opts.headers || {});
retryHeaders['Authorization'] = 'Bearer ' + newToken;
var retryOpts = Object.assign({}, opts, { headers: retryHeaders });
return fetch(fullUrl, retryOpts);
}).catch(function () {
clearSession();
redirectToLogin();
return response;
});
});
}
window.soonAuthedFetch = soonAuthedFetch;
window.soonClearAuthSession = clearSession;
window.soonGetAccessToken = getAccessToken;
window.soonRedirectToLogin = redirectToLogin;
})();
+312
View File
@@ -0,0 +1,312 @@
(function () {
'use strict';
var FILE_PREFIX = 'soondesign_file:';
function soonIsWebPortal() {
return typeof window !== 'undefined' && window.platformBridge && !window.fs;
}
function soonGetAccessToken() {
if (typeof window.soonGetAccessToken === 'function') {
return window.soonGetAccessToken();
}
try { return localStorage.getItem('soon_access') || ''; } catch (e) { return ''; }
}
function soonParseFileKey(key) {
if (!key || typeof key !== 'string') return null;
var m = key.match(/^soondesign_file:(\d+)(?::v(\d+))?$/);
if (!m) return null;
return { id: parseInt(m[1], 10), version: m[2] ? parseInt(m[2], 10) : null };
}
function soonMakeFileKey(id, version) {
return FILE_PREFIX + id + ':v' + version;
}
function soonNormalizeSoonName(pathOrName) {
var n = String(pathOrName || 'design.soon');
if (n.indexOf(FILE_PREFIX) === 0) {
var meta = window._soonFileMeta;
return (meta && meta.name) ? meta.name : 'design.soon';
}
if (n.indexOf('soondesign_session:') === 0) {
n = n.replace(/^soondesign_session:/, '');
}
n = n.split(/[/\\]/).pop();
if (!/\.soon$/i.test(n)) n = (n.replace(/\.soon$/i, '') || 'design') + '.soon';
return n;
}
function soonApplyCloudMeta(data) {
if (!data || !data.id) return null;
window._soonFileMeta = { id: data.id, version: data.version, name: data.name };
return soonMakeFileKey(data.id, data.version);
}
function soonBindFileMeta(fileKey, opts) {
if (!fileKey || fileKey.indexOf(FILE_PREFIX) !== 0) return;
var parsed = soonParseFileKey(fileKey);
if (!parsed) return;
var prev = window._soonFileMeta || {};
window._soonFileMeta = {
id: parsed.id,
version: parsed.version != null ? parsed.version : prev.version,
name: (opts && opts.name) || (prev.id === parsed.id ? prev.name : '') || ''
};
}
function soonFormatOpenTitle(name) {
if (!name) return '';
if (typeof window.soonDisplayFileName === 'function') return window.soonDisplayFileName(name);
return String(name).split(/[/\\]/).pop();
}
var _membership = {
loaded: false,
isMember: false,
name: '免费版',
tier: 'free',
expiresAt: null,
loading: null,
};
function soonApiBase() {
var cfg = (typeof window !== 'undefined' && window.SOON_DEPLOY_CONFIG && window.SOON_DEPLOY_CONFIG.api_v1_base);
if (cfg) return String(cfg).replace(/\/+$/, '');
if (typeof window !== 'undefined' && window.location) {
return window.location.origin + '/api/v1';
}
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 || {};
var isMember = m.is_member === true
|| m.tier === 'member'
|| m.tier === 'pro'
|| (sub.status && sub.status !== 'free' && sub.status !== 'expired');
_membership = {
loaded: true,
isMember: !!isMember,
name: m.name || (isMember ? '订阅版' : '免费版'),
tier: m.tier || (isMember ? 'member' : 'free'),
expiresAt: sub.expires_at || null,
loading: null,
};
if (typeof window.soonRefreshPortalIdentity === 'function') {
window.soonRefreshPortalIdentity();
}
return _membership;
}
function soonRefreshPortalIdentity() {
var el = document.getElementById('auth_identity');
if (!el) return;
if (!soonGetAccessToken()) {
el.style.display = 'none';
el.textContent = '';
return;
}
if (!_membership.loaded) {
soonLoadMembership(false).then(function () {
soonRefreshPortalIdentity();
});
return;
}
var text;
var cls = 'soon-portal-identity';
if (_membership.isMember) {
cls += ' soon-portal-identity--member';
text = _membership.name || '订阅版';
if (_membership.expiresAt) {
text += ' · 至 ' + formatExpireDate(_membership.expiresAt);
}
} else {
cls += ' soon-portal-identity--free';
text = _membership.name || '免费版';
}
el.className = cls;
el.textContent = text;
el.style.display = 'inline-flex';
var account = document.getElementById('auth_account');
if (account) account.style.display = 'inline-flex';
}
function soonLoadMembership(force) {
if (_membership.loaded && !force) return Promise.resolve(_membership);
if (_membership.loading && !force) return _membership.loading;
if (!soonGetAccessToken()) {
soonApplyMembership(null);
return Promise.resolve(_membership);
}
var url = soonApiBase() + '/plans/me';
var fetchFn = typeof window.soonAuthedFetch === 'function'
? window.soonAuthedFetch(url, { headers: { Accept: 'application/json' } })
: fetch(url, { headers: { Authorization: 'Bearer ' + soonGetAccessToken(), Accept: 'application/json' } });
_membership.loading = fetchFn
.then(function (r) { return r.json(); })
.then(function (j) {
if (j && j.ok && j.data) {
soonApplyMembership(j.data.membership || j.data);
} else {
soonApplyMembership(null);
}
return _membership;
})
.catch(function () {
soonApplyMembership(null);
return _membership;
})
.finally(function () {
_membership.loading = null;
});
return _membership.loading;
}
function soonIsMember() {
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');
}
return false;
}
function soonGuardCloudSave() {
if (!soonIsWebPortal()) return true;
return soonRequireMember('保存');
}
function soonGuardMemberPreview() {
if (!soonIsWebPortal()) return true;
return soonRequireMember('预览效果');
}
function soonGuardMemberExport() {
if (!soonIsWebPortal()) return true;
return soonRequireMember('导出');
}
function soonToast(message, type) {
if (typeof window.soonToast === 'function') window.soonToast(message, type);
}
function soonRequireLogin(actionLabel) {
if (soonGetAccessToken()) return true;
var label = actionLabel || '继续';
soonToast('请先登录后再' + label, 'warn');
if (typeof window.soonRedirectToLogin === 'function') {
setTimeout(function () { window.soonRedirectToLogin(); }, 600);
} else if (typeof window.redirectToLogin === 'function') {
setTimeout(function () { window.redirectToLogin(); }, 600);
} else {
window.location.href = 'login.web.html';
}
return false;
}
function soonParseApiError(response, bodyText) {
var msg = '操作失败,请稍后重试';
var code = '';
try {
var j = bodyText ? JSON.parse(bodyText) : null;
if (j && j.message) msg = j.message;
if (j && j.error) code = j.error;
} catch (e) { /* ignore */ }
if (response && response.status === 401) {
return { status: 401, message: '请先登录', code: code || 'unauthorized' };
}
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 || 'quota_exceeded' };
}
if (response && response.status === 404) {
return { status: 404, message: '文件不存在', code: code || 'not_found' };
}
return { status: response ? response.status : 0, message: msg, code: code };
}
function soonShowApiError(info) {
if (!info) return;
var kind = 'error';
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: '使用' });
}
}
}
function soonDisplayFileName(pathOrKey) {
if (!pathOrKey) return '';
if (pathOrKey.indexOf(FILE_PREFIX) === 0) {
var meta = window._soonFileMeta;
if (meta && meta.name) return meta.name;
var parsed = soonParseFileKey(pathOrKey);
return parsed ? ('文件 #' + parsed.id) : pathOrKey;
}
if (pathOrKey.indexOf('soondesign_session:') === 0) {
return pathOrKey.replace(/^soondesign_session:/, '');
}
return String(pathOrKey).split(/[/\\]/).pop();
}
window.soonIsWebPortal = soonIsWebPortal;
window.soonParseFileKey = soonParseFileKey;
window.soonMakeFileKey = soonMakeFileKey;
window.soonNormalizeSoonName = soonNormalizeSoonName;
window.soonApplyCloudMeta = soonApplyCloudMeta;
window.soonBindFileMeta = soonBindFileMeta;
window.soonFormatOpenTitle = soonFormatOpenTitle;
window.soonGuardCloudSave = soonGuardCloudSave;
window.soonGuardMemberPreview = soonGuardMemberPreview;
window.soonGuardMemberExport = soonGuardMemberExport;
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;
})();
+164
View File
@@ -0,0 +1,164 @@
// Fabric.js 扩展类
(function (fabric) {
fabric.CurvedText = fabric.util.createClass(fabric.Object, {
type: 'curved-text',
diameter: 0,
kerning: 0,
text: '',
flipped: false,
fill: '#000',
fontFamily: 'Times New Roman',
fontSize: 24, // in px
fontWeight: 'normal',
fontStyle: '', // "normal", "italic" or "oblique".
cacheProperties: fabric.Object.prototype.cacheProperties.concat('diameter', 'textBackgroundColor', 'kerning', 'flipped', 'fill', 'fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'strokeStyle', 'strokeWidth'),
strokeStyle: null,
strokeWidth: 0,
initialize: function (text, options) {
options || (options = {});
if (typeof text === 'object' && text) {
for (let key in text) {
this[key] = text[key]
}
} else {
this.text = text;
}
this.callSuper('initialize', options);
this.set('lockUniScaling', true);
var canvas = this.getCircularText();
this.cropCanvas(canvas);
this.set('width', canvas.width);
this.set('height', canvas.height);
},
_getFontDeclaration: function () {
return [
(fabric.isLikelyNode ? this.fontWeight : this.fontStyle),
(fabric.isLikelyNode ? this.fontStyle : this.fontWeight),
this.fontSize + 'px',
(fabric.isLikelyNode ? ('"' + this.fontFamily + '"') : this.fontFamily)
].join(' ');
},
cropCanvas: function (canvas) {
var ctx = canvas.getContext('2d'),
w = canvas.width,
h = canvas.height,
pix = { x: [], y: [] }, n,
imageData = ctx.getImageData(0, 0, w, h),
fn = function (a, b) { return a - b };
for (var y = 0; y < h; y++) {
for (var x = 0; x < w; x++) {
if (imageData.data[((y * w + x) * 4) + 3] > 0) {
pix.x.push(x);
pix.y.push(y);
}
}
}
// 如果没有找到任何非透明像素,直接返回,不进行裁剪
if (pix.x.length === 0 || pix.y.length === 0) {
return;
}
pix.x.sort(fn);
pix.y.sort(fn);
n = pix.x.length - 1;
w = pix.x[n] - pix.x[0];
h = pix.y[n] - pix.y[0];
// 确保宽度和高度是有效的正数
if (w <= 0 || h <= 0 || isNaN(w) || isNaN(h)) {
return;
}
var cut = ctx.getImageData(pix.x[0], pix.y[0], w, h);
canvas.width = w;
canvas.height = h;
ctx.putImageData(cut, 0, 0);
},
getCircularText: function () {
var text = this.text,
diameter = this.diameter,
flipped = this.flipped,
kerning = this.kerning,
fill = this.fill,
inwardFacing = true,
startAngle = 0,
canvas = fabric.util.createCanvasElement(),
ctx = canvas.getContext('2d'),
cw, // character-width
x, // iterator
clockwise = -1; // draw clockwise for aligned right. Else Anticlockwise
if (flipped) {
// startAngle = 180;
inwardFacing = false;
}
startAngle *= Math.PI / 180; // convert to radians
var d = document.createElement('div');
d.style.fontFamily = this.fontFamily;
d.style.fontSize = this.fontSize + 'px';
d.style.fontWeight = this.fontWeight;
d.style.fontStyle = this.fontStyle;
d.textContent = text;
document.body.appendChild(d);
var textHeight = d.offsetHeight;
document.body.removeChild(d);
canvas.width = canvas.height = diameter;
ctx.font = this._getFontDeclaration();
if (inwardFacing) { text = text.split('').reverse().join('') };
ctx.translate(diameter / 2, diameter / 2); // Move to center
startAngle += (Math.PI * !inwardFacing); // Rotate 180 if outward
ctx.textBaseline = 'middle'; // Ensure we draw in exact center
ctx.textAlign = 'center'; // Ensure we draw in exact center
for (x = 0; x < text.length; x++) {
cw = ctx.measureText(text[x]).width;
startAngle += ((cw + (x == text.length - 1 ? 0 : kerning)) / (diameter / 2 - textHeight)) / 2 * -clockwise;
}
ctx.rotate(startAngle);
for (x = 0; x < text.length; x++) {
cw = ctx.measureText(text[x]).width; // half letter
ctx.rotate((cw / 2) / (diameter / 2 - textHeight) * clockwise);
if (this.strokeStyle && this.strokeWidth) {
ctx.strokeStyle = this.strokeStyle;
ctx.lineWidth = this.strokeWidth;
ctx.miterLimit = 2;
ctx.strokeText(text[x], 0, (inwardFacing ? 1 : -1) * (0 - diameter / 2 + textHeight / 2));
}
ctx.fillStyle = fill;
ctx.fillText(text[x], 0, (inwardFacing ? 1 : -1) * (0 - diameter / 2 + textHeight / 2));
ctx.rotate((cw / 2 + kerning) / (diameter / 2 - textHeight) * clockwise); // rotate half letter
}
return canvas;
},
_set: function (key, value) {
switch (key) {
case 'scaleX':
this.fontSize *= value;
this.diameter *= value;
this.width *= value;
this.scaleX = 1;
if (this.width < 1) { this.width = 1; }
break;
case 'scaleY':
this.height *= value;
this.scaleY = 1;
if (this.height < 1) { this.height = 1; }
break;
default:
this.callSuper('_set', key, value);
break;
}
},
_render: function (ctx) {
var canvas = this.getCircularText();
this.cropCanvas(canvas);
this.set('width', canvas.width);
this.set('height', canvas.height);
ctx.drawImage(canvas, -this.width / 2, -this.height / 2, this.width, this.height);
this.setCoords();
},
toObject: function (propertiesToInclude) {
return this.callSuper('toObject', ['text', 'diameter', 'textBackgroundColor', 'kerning', 'flipped', 'fill', 'fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'strokeStyle', 'strokeWidth'].concat(propertiesToInclude));
}
});
fabric.CurvedText.fromObject = function (object, callback, forceAsync) {
return fabric.Object._fromObject('CurvedText', object, callback, forceAsync, 'curved-text');
};
})(typeof fabric !== 'undefined' ? fabric : require('fabric').fabric);
+56
View File
@@ -0,0 +1,56 @@
// 多语言支持模块
/**
* 多语言切换
*/
function langua_ge(lan = 'zh') {
$("[language='m']").each(function (i) {
$(this).html($(this).attr(lan));
});
$("[language='t']").each(function (i) {
$(this).attr("title", $(this).attr(lan));
});
}
/**
* 多语言字符串获取
* @param {string} str - 字符串键
* @param {string} s_lan - 当前语言 ('zh', 'ozh', 'en')
*/
function language_str(str, s_lan) {
let t = {
"whetherSave": { zh: "是否保存当前文件?", ozh: "是否保存當前文件?", en: "Do you want to save the current file?" },
"save": { zh: "保存", ozh: "保存", en: "Save" },
"noSave": { zh: "不保存", ozh: "不保存", en: "No saving" },
"cancel": { zh: "取消", ozh: "取消", en: "Cancel" },
"saveSucc": { zh: "保存成功至", ozh: "保存成功至", en: "Save successfully to" },
"saveFile": { zh: "保存文件", ozh: "保存文件", en: "Save file" },
"display": { zh: "预览", ozh: "預覽", en: "Display" },
"output": { zh: "导出", ozh: "導出", en: "Export" },
"bg": { zh: "背景", ozh: "背景", en: "Background" },
"selectPic": { zh: "请选择图片", ozh: "請選擇圖片", en: "Please select a picture" },
"img": { zh: "圖片", ozh: "圖片", en: "Image" },
"simg": { zh: "合成圖片", ozh: "合成圖片", en: "Synthesized Image" },
"text": { zh: "文本", ozh: "文本", en: "Text" },
"ctext": { zh: "圆形文本", ozh: "圆形文本", en: "Circle Text" },
"stext": { zh: "合成文本", ozh: "合成文本", en: "Synthesized Text" },
"rect": { zh: "矩形", ozh: "矩形", en: "Rectangle" },
"circ": { zh: "圓形", ozh: "圓形", en: "Circle" },
"line": { zh: "直線", ozh: "直線", en: "Line" },
"qrc": { zh: "二維碼", ozh: "二維碼", en: "QR code" },
"barc": { zh: "條形碼", ozh: "條形碼", en: "Barcode" },
"counter": { zh: "計數器", ozh: "计数器", en: "Counter" },
"about": { zh: "关于Soon Design", ozh: "關於Soon Design", en: "About Soon Design" },
"vers": { zh: "版本", ozh: "版本", en: "Version" },
"comf": { zh: "确认", ozh: "確認", en: "Select" },
"copyright": { zh: "© 2022 cardsoon Co., Ltd. All rights reserved.", ozh: "© 2022 cardsoon Co., Ltd. All rights reserved.", en: "© 2022 cardsoon Co., Ltd. All rights reserved." }
};
return t[str] ? t[str][s_lan] : str;
}
module.exports = {
langua_ge,
language_str
};
+464
View File
@@ -0,0 +1,464 @@
(function () {
'use strict';
var POLL_TIMEOUT_MS = 5 * 60 * 1000;
var PAY_STEP_LABELS = ['选择支付', '扫码付款', '订阅生效'];
function esc(s) {
if (s == null) return '';
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function apiBase() {
var cfg = (typeof window !== 'undefined' && window.SOON_DEPLOY_CONFIG && window.SOON_DEPLOY_CONFIG.api_v1_base);
if (cfg) return String(cfg).replace(/\/+$/, '');
if (typeof window !== 'undefined' && window.location) {
return window.location.origin + '/api/v1';
}
return '/api/v1';
}
function apiFetch(url, opts) {
if (typeof window.soonAuthedFetch === 'function') return window.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(apiBase() + p).then(function (r) {
if (!r.ok && r.status === 401) return { ok: false, message: '未登录或会话已过期' };
return parseApiJson(r);
});
}
function apiPost(p, body) {
return apiFetch(apiBase() + 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 isSafeAlipayGateway(url) {
if (!url || typeof url !== 'string') return false;
try {
var u = new URL(url);
return u.protocol === 'https:'
&& (u.hostname === 'openapi.alipay.com' || u.hostname === 'openapi.alipaydev.com');
} catch (e) {
return false;
}
}
function submitAlipayForm(payForm) {
if (!payForm || !payForm.gateway || !payForm.params) return false;
if (!isSafeAlipayGateway(payForm.gateway)) return false;
var form = document.createElement('form');
form.method = 'POST';
form.action = payForm.gateway;
form.acceptCharset = 'UTF-8';
form.style.display = 'none';
Object.keys(payForm.params).forEach(function (key) {
var val = payForm.params[key];
if (val == null || val === '') return;
var input = document.createElement('input');
input.type = 'hidden';
input.name = key;
input.value = String(val);
form.appendChild(input);
});
document.body.appendChild(form);
form.submit();
return true;
}
function payStepLabel(step, channel) {
if (step === 2 && channel === 'alipay') return '网页支付';
if (step === 2 && channel === 'wechat') return '扫码付款';
return PAY_STEP_LABELS[step - 1];
}
function payStepperHtml(activeStep, channel) {
var parts = [];
for (var i = 1; i <= 3; i++) {
if (i > 1) {
var bridgeCls = 'soon-pay-stepper__bridge';
if (i - 1 < activeStep) bridgeCls += ' is-done';
parts.push('<li class="' + bridgeCls + '" aria-hidden="true"></li>');
}
var cls = 'soon-pay-stepper__item';
if (i < activeStep) cls += ' is-done';
else if (i === activeStep) cls += ' is-active';
parts.push('<li class="' + cls + '">' +
'<span class="soon-pay-stepper__dot">' + (i < activeStep ? '✓' : String(i)) + '</span>' +
'<span class="soon-pay-stepper__label">' + payStepLabel(i, channel) + '</span></li>');
}
return '<ol class="soon-pay-stepper" id="payStepper">' + parts.join('') + '</ol>';
}
function updatePayStep(root, step, channel) {
if (!root) return;
var html = payStepperHtml(step, channel || 'wechat');
var old = root.querySelector('#payStepper');
if (old) old.outerHTML = html;
}
function payEmptyStateHtml() {
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>';
}
function renderPayPayload(data, qrEl, orderNoEl) {
if (orderNoEl && data.order_no) {
orderNoEl.textContent = '订单号 ' + data.order_no;
orderNoEl.style.display = '';
}
if (data.pay_form && data.pay_form.gateway) {
if (!isSafeAlipayGateway(data.pay_form.gateway)) return '支付网关无效';
sessionStorage.setItem('soon_pay_return', '1');
var payForm = data.pay_form;
qrEl.innerHTML =
'<div class="soon-pay-checkout-ready soon-pay-checkout-ready--alipay">' +
'<div class="soon-pay-checkout-ready__brand soon-pay-checkout-ready__brand--alipay">支</div>' +
'<p class="soon-pay-checkout-ready__title">支付宝电脑网站支付</p>' +
'<p class="soon-pay-checkout-ready__desc">点击下方按钮跳转至支付宝收银台,支付完成后将自动返回</p>' +
'<button type="button" class="soon-btn soon-pay-checkout-ready__btn" id="goAlipay">前往支付宝付款</button></div>';
var alipayBtn = qrEl.querySelector('#goAlipay');
if (alipayBtn) {
alipayBtn.onclick = function () {
if (!submitAlipayForm(payForm)) {
if (typeof window.soonToast === 'function') {
window.soonToast('无法跳转支付宝,请检查支付配置', 'warn');
}
}
};
}
return '';
}
if (data.qrcode) {
qrEl.innerHTML =
'<div class="soon-pay-checkout-ready">' +
'<div class="soon-pay-checkout-ready__brand soon-pay-checkout-ready__brand--wechat">微</div>' +
'<p class="soon-pay-checkout-ready__title">微信扫码支付</p>' +
'<div class="soon-pay-checkout-ready__qr" id="payQrCanvas"></div>' +
'<p class="soon-pay-checkout-ready__desc">请使用微信扫一扫,有效期约 2 小时</p></div>';
if (typeof QRCode !== 'undefined') {
new QRCode(qrEl.querySelector('#payQrCanvas'), { text: data.qrcode, width: 188, height: 188 });
}
return '';
}
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) {
opts = opts || {};
var resumeOrderNo = opts.orderNo || null;
var channel = opts.channel || 'wechat';
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>'
: '';
var headBlock = embedded ? '' :
'<header class="soon-pay-sheet__head">' +
'<div class="soon-pay-sheet__head-main">' +
'<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"') + '>' +
(resumeOrderNo ? '订单号 ' + esc(resumeOrderNo) : '') + '</p></div>' +
'<div class="soon-pay-sheet__head-price">' +
'<span class="soon-pay-sheet__currency">¥</span>' +
'<span class="soon-pay-sheet__amount">' + esc(opts.priceDisplay || '') + '</span></div></header>';
var orderInBody = embedded
? '<p class="soon-pay-sheet__order soon-pay-sheet__order--inline" id="payOrderNo"' +
(resumeOrderNo ? '' : ' style="display:none"') + '>' +
(resumeOrderNo ? '订单号 ' + esc(resumeOrderNo) : '') + '</p>'
: '';
return '<div class="soon-pay-sheet' + (embedded ? ' soon-pay-sheet--embedded' : '') + '">' +
(backBtn ? '<div class="soon-pay-sheet__toolbar">' + backBtn + '</div>' : '') +
headBlock +
'<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>' +
'<section class="soon-pay-section soon-pay-section--checkout">' +
'<div class="soon-pay-qr-panel" id="payQr">' + payEmptyStateHtml() + '</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">' +
(resumeOrderNo ? '继续支付' : '确认并支付') + '</button>' +
'<button type="button" class="soon-btn soon-btn--ghost soon-btn--sm soon-pay-sheet__cta soon-pay-sheet__cta--secondary" id="payManualRefresh" style="display:none">已完成支付,刷新状态</button>' +
'</div></div></div>';
}
function bindPaySheet(sheetRoot, opts) {
opts = opts || {};
var planId = opts.planId;
var resumeOrderNo = opts.orderNo || null;
var channel = opts.channel || 'wechat';
var onPaid = typeof opts.onPaid === 'function' ? opts.onPaid : function () {};
var pollTimer = null;
var pollStartedAt = 0;
var orderNo = resumeOrderNo;
var paymentReady = false;
if (!sheetRoot) return { destroy: function () {} };
var manualBtn = sheetRoot.querySelector('#payManualRefresh');
var statusEl = sheetRoot.querySelector('#payStatus');
var confirmBtn = sheetRoot.querySelector('#payConfirm');
var qrEl = sheetRoot.querySelector('#payQr');
var orderNoEl = sheetRoot.querySelector('#payOrderNo');
var backBtn = sheetRoot.querySelector('#payBack');
function setStatus(text, type) {
if (!statusEl) return;
statusEl.textContent = text || '';
statusEl.className = 'soon-pay-status' + (type ? ' soon-pay-status--' + type : '');
statusEl.style.display = text ? 'block' : 'none';
}
function setStep(step) {
updatePayStep(sheetRoot, step, channel);
sheetRoot.classList.toggle('soon-pay-sheet--checkout', step >= 2);
var chSec = sheetRoot.querySelector('#payChannels');
if (chSec) chSec.style.display = step >= 2 ? 'none' : '';
}
function stopPoll() {
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
}
function startPoll() {
stopPoll();
pollStartedAt = Date.now();
pollTimer = setInterval(function () {
if (Date.now() - pollStartedAt > POLL_TIMEOUT_MS) {
stopPoll();
setStatus('自动检测超时,如已支付请点击下方按钮', 'err');
return;
}
checkOrderStatus();
}, 3000);
}
function handlePaid() {
stopPoll();
setStep(3);
setStatus('支付成功,正在更新订阅…', 'ok');
setTimeout(onPaid, 600);
}
function checkOrderStatus() {
if (!orderNo) return;
apiGet('/pay/orders/' + encodeURIComponent(orderNo)).then(function (st) {
if (st.ok && st.data && st.data.status === 'paid') handlePaid();
});
}
if (manualBtn) manualBtn.onclick = checkOrderStatus;
function resetPaymentView() {
paymentReady = false;
stopPoll();
setStep(1);
if (manualBtn) manualBtn.style.display = 'none';
if (confirmBtn) confirmBtn.style.display = '';
qrEl.innerHTML = payEmptyStateHtml();
if (orderNoEl && !resumeOrderNo) {
orderNoEl.textContent = '';
orderNoEl.style.display = 'none';
}
setStatus('');
}
function applyCheckout(data) {
orderNo = data.order_no;
var err = renderPayPayload(data, qrEl, orderNoEl);
if (err) {
setStatus(err, 'err');
return;
}
paymentReady = true;
setStep(2);
if (confirmBtn) confirmBtn.style.display = 'none';
if (manualBtn) manualBtn.style.display = '';
setStatus('等待支付结果,完成后将自动刷新', 'ok');
startPoll();
}
function submitCheckout() {
if (confirmBtn) confirmBtn.disabled = true;
setStatus('正在生成付款信息…');
qrEl.innerHTML = '<div class="soon-pay-checkout-loading"><div class="soon-skeleton soon-skeleton--h200"></div>' +
'<p>正在连接支付渠道…</p></div>';
var req;
if (resumeOrderNo || orderNo) {
var existingNo = orderNo || resumeOrderNo;
req = apiPost('/pay/orders/' + encodeURIComponent(existingNo) + '/checkout', { channel: channel });
} else {
req = apiPost('/pay/orders', { plan_id: planId, channel: channel });
}
req.then(function (j) {
if (confirmBtn) confirmBtn.disabled = false;
if (!j.ok) {
setStatus(j.message || '下单失败', 'err');
qrEl.innerHTML = payEmptyStateHtml();
return;
}
applyCheckout(j.data);
}).catch(function () {
if (confirmBtn) confirmBtn.disabled = false;
setStatus('网络错误,请重试', 'err');
qrEl.innerHTML = payEmptyStateHtml();
});
}
sheetRoot.querySelectorAll('.soon-pay-channel').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'); });
el.classList.add('is-active');
channel = el.dataset.ch;
if (paymentReady) resetPaymentView();
};
});
if (confirmBtn) confirmBtn.onclick = submitCheckout;
if (backBtn && typeof opts.onBack === 'function') backBtn.onclick = opts.onBack;
function destroy() {
stopPoll();
paymentReady = false;
orderNo = resumeOrderNo;
}
return {
destroy: destroy,
reset: resetPaymentView,
};
}
function openPayModal(opts) {
opts = opts || {};
if (typeof layer === 'undefined' || !layer.open) return null;
var html = paySheetHtml({
planName: opts.planName,
priceDisplay: opts.priceDisplay,
orderNo: opts.orderNo,
channel: opts.channel || 'wechat',
});
var payCtrl = null;
var idx = layer.open({
type: 1,
skin: 'soon-layer',
title: false,
closeBtn: 1,
shadeClose: !!opts.shadeClose,
area: ['480px'],
content: html,
success: function (layero) {
var layerEl = layero && layero[0] ? layero[0] : layero;
if (layerEl && layerEl.classList) {
layerEl.classList.add('soon-layer--pay');
var layerContent = layerEl.querySelector('.layui-layer-content');
if (layerContent) {
layerContent.style.overflow = 'visible';
layerContent.style.maxHeight = 'none';
}
}
var sheet = layerEl.querySelector('.soon-pay-sheet');
payCtrl = bindPaySheet(sheet, {
planId: opts.planId,
planName: opts.planName,
priceDisplay: opts.priceDisplay,
orderNo: opts.orderNo,
channel: opts.channel || 'wechat',
onPaid: function () {
layer.close(idx);
if (typeof opts.onPaid === 'function') opts.onPaid();
},
onBack: opts.onBack,
});
},
end: function () {
if (payCtrl) payCtrl.destroy();
if (typeof opts.onClose === 'function') opts.onClose();
},
});
return idx;
}
window.SoonMemberPay = {
apiBase: apiBase,
apiGet: apiGet,
apiPost: apiPost,
esc: esc,
monthlyPlan: monthlyPlan,
billingSavePercent: billingSavePercent,
paySheetHtml: paySheetHtml,
bindPaySheet: bindPaySheet,
openPayModal: openPayModal,
payEmptyStateHtml: payEmptyStateHtml,
renderPayPayload: renderPayPayload,
submitAlipayForm: submitAlipayForm,
POLL_TIMEOUT_MS: POLL_TIMEOUT_MS,
};
})();
+104
View File
@@ -0,0 +1,104 @@
(function () {
'use strict';
var CHECK_SVG = '<svg viewBox="0 0 12 12" fill="none" stroke-width="2" stroke="currentColor">' +
'<path d="M2 6l3 3 5-6"/></svg>';
function esc(s) {
if (window.SoonMemberPay && window.SoonMemberPay.esc) return window.SoonMemberPay.esc(s);
if (s == null) return '';
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function isPlanFeatureLine(line) {
if (!line) return false;
return line.indexOf('到期') < 0
&& line.indexOf('订阅有效期') < 0
&& line.indexOf('订阅周期') < 0
&& line.indexOf('权益一致') < 0;
}
function featureItem(text) {
return '<li><span class="soon-plan-card__check">' + CHECK_SVG + '</span><span>' + esc(text) + '</span></li>';
}
function billingSavePercent(items, plan) {
if (window.SoonMemberPay && window.SoonMemberPay.billingSavePercent) {
return window.SoonMemberPay.billingSavePercent(items, plan);
}
return null;
}
function planCardHtml(plan, ctx) {
ctx = ctx || {};
var items = ctx.allPlans || [];
var mode = ctx.mode || 'page';
var isMember = !!ctx.isMember;
var currentCode = ctx.currentCode || '';
var selectedId = ctx.selectedId;
var feat = plan.is_recommended ? ' soon-plan-card--featured' : '';
var cur = plan.code === currentCode ? ' soon-plan-card--current' : '';
var isGate = mode === 'gate';
var isPicker = mode === 'picker' || isGate;
var picker = isPicker ? ' soon-plan-card--picker' : '';
if (isGate) picker += ' soon-plan-card--gate';
var selected = isPicker && plan.id === selectedId ? ' soon-plan-card--selected' : '';
var ribbon = plan.is_recommended
? '<span class="soon-plan-card__ribbon soon-plan-card__ribbon--rec">推荐</span>'
: (plan.code === currentCode ? '<span class="soon-plan-card__ribbon soon-plan-card__ribbon--current">当前</span>' : '');
var savePct = billingSavePercent(items, plan);
if (savePct) ribbon += '<span class="soon-plan-card__save">省 ' + savePct + '%</span>';
var badgeCls = ribbon ? ' soon-plan-card--badged' : '';
if (savePct) badgeCls += ' soon-plan-card--save';
var period = plan.duration_days > 0
? '<span class="soon-plan-card__period">/' + esc(plan.period_label || plan.duration_days + '天') + '</span>'
: '';
var features = (plan.features || []).filter(isPlanFeatureLine).map(featureItem).join('');
var desc = plan.description || '';
var price = esc(plan.price_display || (plan.price_cents / 100).toFixed(2));
var inner =
ribbon +
'<div class="soon-plan-card__head">' +
'<div class="soon-plan-card__name">' + esc(plan.name) + '</div>' +
(desc ? '<p class="soon-plan-card__desc">' + esc(desc) + '</p>' : '') +
'<div class="soon-plan-card__price-row">' +
'<span class="soon-plan-card__currency">¥</span>' +
'<span class="soon-plan-card__price">' + price + '</span>' + period + '</div></div>';
if (!isGate) {
inner += '<div class="soon-plan-card__body">' +
'<ul class="soon-plan-card__features">' + features + '</ul></div>';
}
if (isPicker) {
return '<button type="button" class="soon-plan-card' + feat + cur + badgeCls + picker + selected + '" ' +
'data-action="pick-plan" data-id="' + plan.id + '" aria-pressed="' + (plan.id === selectedId ? 'true' : 'false') + '">' +
inner + '</button>';
}
var cta = '<button type="button" class="soon-btn soon-plan-card__cta" data-id="' + plan.id +
'" data-name="' + esc(plan.name) + '" data-price="' + price + '">' +
(isMember ? '续订' : '立即订阅') + '</button>';
return '<article class="soon-plan-card' + feat + cur + badgeCls + '">' + inner +
'<div class="soon-plan-card__foot">' + cta + '</div></article>';
}
function plansGridHtml(plans, ctx) {
return (plans || []).map(function (p) {
return planCardHtml(p, ctx);
}).join('');
}
window.SoonMemberPlan = {
isPlanFeatureLine: isPlanFeatureLine,
planCardHtml: planCardHtml,
plansGridHtml: plansGridHtml,
};
})();
+306
View File
@@ -0,0 +1,306 @@
(function () {
'use strict';
var GATE_FEATURES = ['高清预览', '成品打印', '云端保存', '导出设计文件'];
var _gate = { index: null, payIndex: null, plans: [], selectedId: null, gateOpts: null };
var pay = window.SoonMemberPay;
var plansUi = window.SoonMemberPlan;
function esc(s) {
if (pay && pay.esc) return pay.esc(s);
if (s == null) return '';
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function toast(msg, type) {
if (typeof window.soonToast === 'function') window.soonToast(msg, type);
}
function emptyGate() {
return { index: null, payIndex: null, plans: [], selectedId: null, gateOpts: null };
}
function ensureLayer(cb) {
if (typeof layer !== 'undefined' && layer.open) {
if (layer.config) layer.config({ skin: 'soon-layer' });
cb();
return;
}
if (typeof layui !== 'undefined') {
layui.use(['layer'], function () {
window.layer = layui.layer;
layer.config({ skin: 'soon-layer' });
cb();
});
return;
}
toast('订阅功能暂不可用,请刷新页面后重试', 'warn');
}
function actionCopy(actionLabel) {
var label = (actionLabel || '使用').trim();
if (label.indexOf('预览') >= 0 || label.indexOf('导出') >= 0) return '预览、打印与导出成品';
if (label.indexOf('保存') >= 0 || label.indexOf('另存') >= 0) return '保存或另存到云端';
if (label.indexOf('下载') >= 0) return '下载设计文件';
return label;
}
function shellHtml(actionLabel) {
var feat = GATE_FEATURES.map(function (t) {
return '<li>' + esc(t) + '</li>';
}).join('');
return '<div class="soon-subscribe-modal">' +
'<header class="soon-subscribe-modal__head">' +
'<span class="soon-subscribe-modal__eyebrow">订阅功能</span>' +
'<h3 class="soon-subscribe-modal__title">开通订阅,解锁完整能力</h3>' +
'<p class="soon-subscribe-modal__desc">免费版可使用设计工具;订阅后可' + esc(actionCopy(actionLabel)) + '。</p>' +
'<ul class="soon-subscribe-modal__features">' + feat + '</ul>' +
'</header>' +
'<div class="soon-subscribe-modal__body">' +
'<div class="soon-subscribe-modal__section-head">' +
'<h4 class="soon-subscribe-modal__section-title"><span class="soon-subscribe-modal__section-mark"></span>订阅方案</h4>' +
'<p class="soon-subscribe-modal__section-sub">选择订阅周期,功能相同,随时续订</p></div>' +
'<section class="soon-plans-grid soon-subscribe-plans-grid" data-role="plan-picker">' +
'<p class="soon-subscribe-plans__loading">正在加载订阅方案…</p></section>' +
'</div>' +
'<footer class="soon-subscribe-modal__foot">' +
'<button type="button" class="soon-subscribe-modal__stay" data-action="stay">继续设计</button>' +
'<button type="button" class="soon-btn soon-subscribe-modal__pay" data-action="go-pay" disabled>立即支付</button>' +
'</footer></div>';
}
function findPlan(id) {
var pid = Number(id);
for (var i = 0; i < _gate.plans.length; i++) {
if (_gate.plans[i].id === pid) return _gate.plans[i];
}
return null;
}
function planPriceDisplay(plan) {
if (!plan) return '';
return plan.price_display || (plan.price_cents / 100).toFixed(2);
}
function renderPlanPicker(modal) {
var picker = modal.querySelector('[data-role="plan-picker"]');
if (!picker || !plansUi) return;
if (!_gate.plans.length) {
picker.innerHTML = '<p class="soon-subscribe-plans__empty">暂无可用订阅方案</p>';
return;
}
picker.innerHTML = plansUi.plansGridHtml(_gate.plans, {
allPlans: _gate.plans,
selectedId: _gate.selectedId,
mode: 'gate',
});
}
function updatePayButton(modal) {
var btn = modal.querySelector('[data-action="go-pay"]');
var plan = findPlan(_gate.selectedId);
if (!btn) return;
if (!plan) {
btn.disabled = true;
btn.textContent = '立即支付';
return;
}
btn.disabled = false;
btn.textContent = '立即支付 ¥' + planPriceDisplay(plan);
}
function selectPlan(modal, planId) {
var plan = findPlan(planId);
if (!plan) return;
_gate.selectedId = plan.id;
renderPlanPicker(modal);
updatePayButton(modal);
}
function closeGate(fireStay) {
var idx = _gate.index;
var payIdx = _gate.payIndex;
var opts = _gate.gateOpts;
_gate = emptyGate();
if (typeof layer !== 'undefined') {
if (idx != null) layer.close(idx);
if (payIdx != null) layer.close(payIdx);
}
if (fireStay && opts && typeof opts.onStay === 'function') opts.onStay();
}
function onPaySuccess() {
var gateIdx = _gate.index;
var opts = _gate.gateOpts;
_gate = emptyGate();
if (typeof layer !== 'undefined') {
if (gateIdx != null) layer.close(gateIdx);
}
toast('恭喜您,订阅已成功生效!', 'success');
if (typeof window.soonLoadMembership === 'function') {
window.soonLoadMembership(true).then(function () {
if (typeof window.soonRefreshPortalIdentity === 'function') window.soonRefreshPortalIdentity();
});
}
if (opts && typeof opts.onSuccess === 'function') opts.onSuccess();
}
function openPayStep(modal) {
var plan = findPlan(_gate.selectedId);
if (!plan || !pay || !pay.openPayModal) {
toast('请先选择订阅方案', 'warn');
return;
}
if (_gate.payIndex != null) {
try { layer.close(_gate.payIndex); } catch (e) { /* ignore */ }
_gate.payIndex = null;
}
var priceDisplay = planPriceDisplay(plan);
_gate.payIndex = pay.openPayModal({
planId: plan.id,
planName: plan.name,
priceDisplay: priceDisplay,
shadeClose: true,
onPaid: onPaySuccess,
onClose: function () {
_gate.payIndex = null;
updatePayButton(modal);
},
});
}
function renderPlansError(modal, message) {
var picker = modal.querySelector('[data-role="plan-picker"]');
if (!picker) return;
picker.innerHTML = '<p class="soon-subscribe-plans__empty">' + esc(message) +
'<br><button type="button" class="soon-btn soon-btn--sm" data-action="retry-plans">重试</button></p>';
updatePayButton(modal);
}
function loadPlans(modal) {
var picker = modal.querySelector('[data-role="plan-picker"]');
if (!picker) return;
if (!pay || !plansUi) {
renderPlansError(modal, '订阅模块未加载,请刷新页面');
return;
}
picker.innerHTML = '<p class="soon-subscribe-plans__loading">正在加载订阅方案…</p>';
updatePayButton(modal);
pay.apiGet('/plans').then(function (ps) {
if (!ps.ok) {
renderPlansError(modal, ps.message || '方案加载失败');
return;
}
var paid = ((ps.data && ps.data.items) || []).filter(function (p) {
return p.code !== 'free' && p.price_cents > 0;
});
if (!paid.length) {
renderPlansError(modal, '暂无可用订阅方案');
return;
}
_gate.plans = paid;
var def = paid[0];
for (var i = 0; i < paid.length; i++) {
if (paid[i].is_recommended) { def = paid[i]; break; }
}
selectPlan(modal, def.id);
}).catch(function () {
renderPlansError(modal, '网络错误,请稍后重试');
});
}
function bindModal(modal) {
modal.addEventListener('click', function (e) {
var stay = e.target.closest('[data-action="stay"]');
if (stay) {
e.preventDefault();
closeGate(true);
return;
}
var retry = e.target.closest('[data-action="retry-plans"]');
if (retry) {
e.preventDefault();
loadPlans(modal);
return;
}
var goPay = e.target.closest('[data-action="go-pay"]');
if (goPay) {
e.preventDefault();
if (!goPay.disabled) openPayStep(modal);
return;
}
var pick = e.target.closest('[data-action="pick-plan"]');
if (pick) {
e.preventDefault();
var id = Number(pick.dataset.id);
if (id) selectPlan(modal, id);
}
});
loadPlans(modal);
}
function openGate(opts) {
opts = opts || {};
if (_gate.index != null) {
try { layer.close(_gate.index); } catch (e) { /* ignore */ }
}
if (_gate.payIndex != null) {
try { layer.close(_gate.payIndex); } catch (e) { /* ignore */ }
}
_gate = emptyGate();
_gate.gateOpts = opts;
var width = Math.min(1000, window.innerWidth - 24);
layer.open({
type: 1,
skin: 'soon-layer',
title: false,
closeBtn: 1,
shadeClose: true,
area: [width + 'px', 'auto'],
content: shellHtml(opts.action),
success: function (layero, index) {
var layerEl = layero && layero[0] ? layero[0] : layero;
if (layerEl && layerEl.classList) layerEl.classList.add('soon-layer--subscribe');
var content = layerEl && layerEl.querySelector ? layerEl.querySelector('.layui-layer-content') : null;
if (content) content.style.padding = '0';
var modal = layerEl.querySelector('.soon-subscribe-modal');
if (!modal) return;
_gate.index = index;
_gate.gateOpts = opts;
bindModal(modal);
},
end: function () {
var payIdx = _gate.payIndex;
var closedOpts = _gate.gateOpts;
if (payIdx != null) {
try { layer.close(payIdx); } catch (e) { /* ignore */ }
}
_gate = emptyGate();
if (closedOpts && closedOpts.onClose) closedOpts.onClose();
},
});
}
function soonShowSubscribeGate(opts) {
if (!pay || !plansUi) {
toast('订阅功能暂不可用,请刷新页面后重试', 'warn');
return false;
}
ensureLayer(function () { openGate(opts || {}); });
return false;
}
window.soonShowSubscribeGate = soonShowSubscribeGate;
if (!window._soonSubscribeFocusBound) {
window._soonSubscribeFocusBound = true;
window.addEventListener('focus', function () {
if (typeof window.soonLoadMembership !== 'function') return;
window.soonLoadMembership(true).then(function () {
if (typeof window.soonRefreshPortalIdentity === 'function') window.soonRefreshPortalIdentity();
});
});
}
})();
+81
View File
@@ -0,0 +1,81 @@
(function () {
'use strict';
function showAuthedNav() {
var guest = document.getElementById('auth_guest');
var account = document.getElementById('auth_account');
var session = document.getElementById('auth_session');
if (guest) guest.style.display = 'none';
if (account) account.style.display = 'inline-flex';
if (session) session.style.display = 'inline-flex';
}
function clearSession() {
if (typeof window.soonClearAuthSession === 'function') {
window.soonClearAuthSession();
} else {
localStorage.removeItem('soon_access');
localStorage.removeItem('soon_refresh');
}
}
function initTopbar(options) {
options = options || {};
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');
if (!tok) return;
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) || '';
if (!base) return;
var meFetch = typeof window.soonAuthedFetch === 'function'
? window.soonAuthedFetch(base + '/auth/me', { headers: { Authorization: 'Bearer ' + tok } })
: fetch(base + '/auth/me', { headers: { Authorization: 'Bearer ' + tok } });
meFetch
.then(function (r) { return r.json(); })
.then(function (j) {
if (!j.ok || !j.data) {
clearSession();
location.reload();
return;
}
if (j.data.role === 'admin' && admin) admin.style.display = 'inline-flex';
if (avatar && j.data.email) {
avatar.style.display = 'inline-flex';
avatar.textContent = j.data.email.charAt(0).toUpperCase();
avatar.title = j.data.email;
}
if (typeof window.soonLoadMembership === 'function') {
window.soonLoadMembership().then(function () {
if (typeof window.soonRefreshPortalIdentity === 'function') {
window.soonRefreshPortalIdentity();
}
});
}
})
.catch(function () { /* keep authed shell */ });
if (logout) {
logout.addEventListener('click', function (e) {
e.preventDefault();
clearSession();
if (options.logoutReload !== false) location.reload();
else location.href = 'index.web.html';
});
}
}
window.soonPortalAuth = { init: initTopbar };
})();
+106
View File
@@ -0,0 +1,106 @@
(function () {
'use strict';
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' },
];
function esc(s) {
if (s == null) return '';
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/"/g, '&quot;');
}
function authBlock() {
return '<div class="soon-portal-topbar__guest" id="auth_guest">' +
'<a href="login.web.html" id="auth_login" class="soon-btn soon-btn--sm">登录</a>' +
'<a href="register.web.html" id="auth_register" class="soon-btn soon-btn--sm soon-btn--ghost">注册</a>' +
'</div>' +
'<div class="soon-portal-topbar__account" id="auth_account" style="display:none">' +
'<span id="auth_avatar" class="soon-portal-avatar" style="display:none"></span>' +
'<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>';
}
function renderLinks(links, active) {
return links.map(function (lnk) {
if (lnk.id === 'member' || 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('');
}
function toolsBlock(lang, linksHtml) {
return '<div class="soon-portal-topbar__tools">' +
lang +
(linksHtml ? '<nav class="soon-portal-topbar__links" aria-label="导航">' + linksHtml + '</nav>' : '') +
'</div>';
}
/**
* @param {object} opts
* @param {string} [opts.variant] standard | auth | design
* @param {string} [opts.active] home | member
* @param {string} [opts.extraClass]
*/
function html(opts) {
opts = opts || {};
var base = opts.assetBase || '../';
var variant = opts.variant || 'standard';
var extraCls = opts.extraClass ? ' ' + opts.extraClass : '';
if (variant === 'auth') {
return '<header class="soon-portal-topbar soon-auth-topbar' + extraCls + '">' +
'<div class="soon-portal-topbar__brand"><a href="index.web.html">' +
'<img src="' + esc(base) + 'assets/images/logo.png" alt=""><span>SoonDesign</span></a></div>' +
'<div class="soon-portal-topbar__actions">' +
toolsBlock('', '<a href="index.web.html" class="soon-portal-topbar__link">返回首页</a>') +
'</div></header>';
}
if (variant === 'design') {
return '<header class="soon-portal-topbar soon-design-portal-bar' + extraCls + '">' +
'<div class="soon-portal-topbar__brand"><a href="index.web.html">' +
'<img src="' + esc(base) + 'assets/images/logo.png" alt=""><span>SoonDesign</span></a></div>' +
'<div class="soon-portal-topbar__actions">' +
toolsBlock('', '<a href="index.web.html" class="soon-portal-topbar__link">返回首页</a>') +
authBlock() + '</div></header>';
}
var active = opts.active || '';
var showLang = opts.showLanguage !== false;
var links = opts.links || STANDARD_LINKS;
var lang = showLang
? '<select id="language_select" class="soon-portal-topbar__lang" aria-label="语言">' +
'<option value="zh">中文-简体</option><option value="ozh">中文-繁體</option><option value="en">English</option></select>'
: '';
return '<header class="soon-portal-topbar' + extraCls + '">' +
'<div class="soon-portal-topbar__brand"><a href="' + esc(opts.brandHref || 'index.web.html') + '">' +
'<img src="' + esc(base) + 'assets/images/logo.png" alt=""><span>SoonDesign</span></a></div>' +
'<div class="soon-portal-topbar__actions">' +
toolsBlock(lang, renderLinks(links, active)) +
authBlock() + '</div></header>';
}
function mount(selector, opts) {
var el = typeof selector === 'string' ? document.querySelector(selector) : selector;
if (!el) return;
el.innerHTML = html(opts);
if (opts && opts.initLang !== false) {
var sel = document.getElementById('language_select');
if (sel) {
sel.value = localStorage.getItem('lang') || 'zh';
sel.onchange = function () { localStorage.setItem('lang', sel.value); };
}
}
}
window.SoonPortalTopbar = { html: html, mount: mount };
})();
+20
View File
@@ -0,0 +1,20 @@
(function () {
'use strict';
function soonToast(message, type) {
if (typeof layer === 'undefined' || !layer.msg) return;
var skin = 'soon-toast';
if (type === 'success') skin += ' soon-toast--success';
else if (type === 'error') skin += ' soon-toast--error';
else if (type === 'warn') skin += ' soon-toast--warn';
layer.msg(message, {
time: 2400,
shade: 0,
offset: '72px',
icon: -1,
skin: skin
});
}
window.soonToast = soonToast;
})();
+212
View File
@@ -0,0 +1,212 @@
// 公共工具函数模块
/**
* DES加密函数
*/
function desEncrypt(str, key = "df6a551ca43181fc485f3043bcdd2fbc") {
var APIFMS;
try {
var keyHex_encrypt = CryptoJS.enc.Utf8.parse(key);
var encrypted = CryptoJS.DES.encrypt(str, keyHex_encrypt, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
APIFMS = CryptoJS.enc.Base64.stringify(encrypted.ciphertext);
} catch (err) {
console.log('des 加密 -------------------------');
console.log(err);
}
return APIFMS;
}
/**
* 获取URL参数
*/
function GetFile() {
const params = new URLSearchParams(window.location.search);
return params;
}
/**
* 日期格式化
*/
function getDate() {
let date = new Date();
return date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate();
}
/**
* 获取对象的绝对坐标
*/
function getAbsoluteXY(_obj) {
let coord = _obj.get("lineCoords");
return [_obj.get("left"), _obj.get("top")];
}
/**
* 计算两点之间的距离
*/
function getDisdance(x1, y1, x2, y2) {
var dx = Math.abs(x2 - x1); // 计算x轴上的距离差,并取绝对值
var dy = Math.abs(y2 - y1); // 计算y轴上的距离差,并取绝对值
var distance = Math.sqrt(dx * dx + dy * dy); // 应用勾股定理计算距离
return distance;
}
/**
* 计算角度(相对于画布中心)
*/
function getDeg(pointer, canvas) {
// 计算点击位置与画布中心的坐标差
var centerX = canvas.width / 2;
var centerY = canvas.height / 2;
var mouseX = pointer.x;
var mouseY = pointer.y;
var dx = mouseX - centerX;
var dy = mouseY - centerY;
// 计算旋转角度(弧度)
var angleRad = Math.atan2(dy, dx);
// 将角度转换为度数
var angleDeg = angleRad * (180 / Math.PI);
if (angleDeg < 0) {
angleDeg += 360;
}
angleDeg += 90;
return angleDeg;
}
/**
* 检查字段名是否重复
*/
function checkName(name, objs1, objs2) {
//检查字段名是否重复了
for (let item of objs1) {
if (item.name == name) {
return false;
}
}
for (let item of objs2) {
if (item.name == name) {
return false;
}
}
return true;
}
/**
* 返回不重复的字段名
*/
function resName(pre_name, objs1, objs2) {
//返回的是不重复的字段名
let num = (objs1.length + objs2.length) - 1;
do {
num++;
}
while (!checkName(pre_name + num, objs1, objs2));//重复了就再加1
return pre_name + num;
}
/**
* 获取对象在画布中的索引
*/
function getIndex(target, canvas, _canvas = null) {
let temp_objs;
if (_canvas == null) {
temp_objs = canvas.getObjects();
} else {
temp_objs = _canvas.getObjects();
}
let i = 0;
for (let res of temp_objs) {
if (target == res) {
return i;
}
i++;
}
return 0;
}
/**
* 获取对象类型
*/
function getType(target, canvas, objs) {
let temp_objs = canvas.getObjects();
let i = 0;
for (let res of temp_objs) {
if (target == res) {
return objs[i].type;
}
i++;
}
}
/**
* 获取坐标的最小X值
*/
function getCoordsMinX(acoords) {
let x = acoords[0].x;
for (let item of acoords) {
if (item.x < x) {
x = item.x;
}
}
return x;
}
/**
* 获取坐标的最大X值
*/
function getCoordsMaxX(acoords) {
let x = acoords[0].x;
for (let item of acoords) {
if (item.x > x) {
x = item.x;
}
}
return x;
}
/**
* 获取坐标的最小Y值
*/
function getCoordsMinY(acoords) {
let y = acoords[0].y;
for (let item of acoords) {
if (item.y < y) {
y = item.y;
}
}
return y;
}
/**
* 获取坐标的最大Y值
*/
function getCoordsMaxY(acoords) {
let y = acoords[0].y;
for (let item of acoords) {
if (item.y > y) {
y = item.y;
}
}
return y;
}
module.exports = {
desEncrypt,
GetFile,
getDate,
getAbsoluteXY,
getDisdance,
getDeg,
checkName,
resName,
getIndex,
getType,
getCoordsMinX,
getCoordsMaxX,
getCoordsMinY,
getCoordsMaxY
};