重构 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
+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;
})();