永久会员与模板库后台化:预览门控、轻量首页与 Admin CRUD
- 会员改为永久激活方案;云端文件不再拦截非会员;预览弹窗内导出/打印才校验 - 首页最近文件支持本地记录,登录后与云端合并;移除独立会员页与订阅页 - 模板库入库管理:soon_templates 表、Admin 上传 CRUD、/templates 轻量列表与按需下载 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+212
-109
@@ -205,9 +205,111 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
: ('soondesign_file:' + id + ':v' + version);
|
||||
}
|
||||
|
||||
var fileListState = { page: 1, size: 12, total: 0 };
|
||||
var fileListState = { page: 1, size: 12, total: 0, items: [] };
|
||||
var templateListState = { page: 1, size: 8, total: 0, items: [] };
|
||||
|
||||
function displayNameFromPath(path) {
|
||||
if (!path) return 'design.soon';
|
||||
var sessionPrefix = 'soondesign_session:';
|
||||
if (path.indexOf(sessionPrefix) === 0) return path.substring(sessionPrefix.length);
|
||||
if (path.indexOf('soondesign_file:') === 0) {
|
||||
var meta = window._soonFileMeta;
|
||||
if (meta && meta.name) return meta.name;
|
||||
return path.replace(/^soondesign_file:(\d+).*/, '文件 #$1');
|
||||
}
|
||||
return get_filename(path) || 'design.soon';
|
||||
}
|
||||
|
||||
function parseItemTime(value) {
|
||||
if (!value) return 0;
|
||||
var d = new Date(String(value).replace(' ', 'T'));
|
||||
return isNaN(d.getTime()) ? 0 : d.getTime();
|
||||
}
|
||||
|
||||
function mergeRecentItems(localHistory, cloudItems) {
|
||||
var items = [];
|
||||
var cloudById = {};
|
||||
var seenCloudIds = {};
|
||||
(cloudItems || []).forEach(function (c) {
|
||||
if (c && c.id != null) cloudById[c.id] = c;
|
||||
});
|
||||
|
||||
(localHistory || []).forEach(function (h) {
|
||||
if (!h || !h.path) return;
|
||||
var cloudMatch = String(h.path).match(/^soondesign_file:(\d+)/);
|
||||
if (cloudMatch) {
|
||||
var id = parseInt(cloudMatch[1], 10);
|
||||
var cloud = cloudById[id];
|
||||
if (cloud) {
|
||||
seenCloudIds[id] = true;
|
||||
items.push({
|
||||
kind: 'cloud',
|
||||
filePath: makeFileKey(cloud.id, cloud.version),
|
||||
fileId: cloud.id,
|
||||
name: cloud.name || ('文件 #' + cloud.id),
|
||||
type: h.type,
|
||||
sortTime: parseItemTime(cloud.updated_at || cloud.created_at || h.time),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
items.push({
|
||||
kind: 'local',
|
||||
filePath: h.path,
|
||||
name: displayNameFromPath(h.path),
|
||||
type: h.type,
|
||||
sortTime: parseItemTime(h.time),
|
||||
});
|
||||
});
|
||||
|
||||
(cloudItems || []).forEach(function (c) {
|
||||
if (!c || c.id == null || seenCloudIds[c.id]) return;
|
||||
items.push({
|
||||
kind: 'cloud',
|
||||
filePath: makeFileKey(c.id, c.version),
|
||||
fileId: c.id,
|
||||
name: c.name || ('文件 #' + c.id),
|
||||
type: null,
|
||||
sortTime: parseItemTime(c.updated_at || c.created_at),
|
||||
});
|
||||
});
|
||||
|
||||
items.sort(function (a, b) { return (b.sortTime || 0) - (a.sortTime || 0); });
|
||||
return items;
|
||||
}
|
||||
|
||||
async function fetchLocalHistory() {
|
||||
if (!window.sysAPI || typeof window.sysAPI.readHistory !== 'function') return [];
|
||||
try {
|
||||
var j = await window.sysAPI.readHistory();
|
||||
return (j && Array.isArray(j.history)) ? j.history : [];
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCloudItems() {
|
||||
if (!hasCloudAuth() || !window.platformBridge || !window.platformBridge.listCloudFiles) return [];
|
||||
try {
|
||||
var data = await window.platformBridge.listCloudFiles({ page: 1, size: 50 });
|
||||
return (data && data.items) ? data.items : [];
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function removeLocalHistoryPath(path) {
|
||||
if (!path || !window.sysAPI || typeof window.sysAPI.readHistory !== 'function') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return window.sysAPI.readHistory().then(function (j) {
|
||||
var list = (j && Array.isArray(j.history)) ? j.history : [];
|
||||
var next = list.filter(function (item) { return item.path !== path; });
|
||||
if (next.length === list.length) return null;
|
||||
return window.sysAPI.writeHistory({ history: next });
|
||||
}).catch(function () { return null; });
|
||||
}
|
||||
|
||||
function renderFilePager() {
|
||||
var el = document.getElementById('filePager');
|
||||
if (!el) return;
|
||||
@@ -247,41 +349,17 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
try {
|
||||
|
||||
var recentCountEl = document.getElementById('recentCount');
|
||||
var filePagerEl = document.getElementById('filePager');
|
||||
|
||||
if (!hasCloudAuth()) {
|
||||
|
||||
if (recentCountEl) recentCountEl.textContent = '0';
|
||||
if (filePagerEl) filePagerEl.innerHTML = '';
|
||||
|
||||
$(".card-list").html(soonEmptyBlock('登录后查看文件', '请先登录以管理云端设计文件',
|
||||
|
||||
'<a href="login.web.html" class="soon-btn soon-btn--sm soon-empty__action">去登录</a>'));
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
if (!window.platformBridge || !window.platformBridge.listCloudFiles) {
|
||||
|
||||
$(".card-list").html("");
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
var data = await window.platformBridge.listCloudFiles({
|
||||
page: fileListState.page,
|
||||
size: fileListState.size,
|
||||
});
|
||||
|
||||
var items = (data && data.items) ? data.items : [];
|
||||
fileListState.total = (data && data.total != null) ? data.total : items.length;
|
||||
if (data && data.page) fileListState.page = data.page;
|
||||
var localHistory = await fetchLocalHistory();
|
||||
var cloudItems = await fetchCloudItems();
|
||||
var merged = mergeRecentItems(localHistory, cloudItems);
|
||||
fileListState.items = merged;
|
||||
fileListState.total = merged.length;
|
||||
var pages = Math.max(1, Math.ceil(fileListState.total / fileListState.size));
|
||||
if (fileListState.page > pages) fileListState.page = pages;
|
||||
|
||||
if (recentCountEl) recentCountEl.textContent = String(fileListState.total);
|
||||
|
||||
if (!items.length) {
|
||||
if (!merged.length) {
|
||||
|
||||
$(".card-list").html(soonEmptyBlock('暂无文件', '点击「打开文件」导入,或新建模板开始设计'));
|
||||
renderFilePager();
|
||||
@@ -289,20 +367,17 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
|
||||
}
|
||||
|
||||
var start = (fileListState.page - 1) * fileListState.size;
|
||||
var slice = merged.slice(start, start + fileListState.size);
|
||||
let h = "";
|
||||
|
||||
for (let item of items) {
|
||||
|
||||
let filePath = makeFileKey(item.id, item.version);
|
||||
for (let item of slice) {
|
||||
|
||||
let filePath = item.filePath;
|
||||
let src = "";
|
||||
|
||||
let fileExists = true;
|
||||
|
||||
let imgStyle = "";
|
||||
|
||||
let realType = 1;
|
||||
|
||||
let realType = item.type || 1;
|
||||
const soonData = await window.sysAPI.readJsonFile(filePath);
|
||||
|
||||
if (soonData) {
|
||||
@@ -323,13 +398,13 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
|
||||
fileExists = false;
|
||||
|
||||
src = soonAsset('bg_1.png');
|
||||
src = soonAsset((realType == 2 || realType == "2") ? 'bg_2.png' : 'bg_1.png');
|
||||
|
||||
imgStyle = "opacity: 0.6; filter: grayscale(100%);";
|
||||
|
||||
}
|
||||
|
||||
let displayName = item.name || ('文件 #' + item.id);
|
||||
let displayName = item.name || displayNameFromPath(filePath);
|
||||
|
||||
let cardTitle = displayName;
|
||||
|
||||
@@ -349,27 +424,29 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
|
||||
: '';
|
||||
|
||||
var dlBtn = '<button type="button" class="soon-file-card__action soon-file-card__download" title="下载 .soon" aria-label="下载">'
|
||||
var actions = '';
|
||||
if (item.kind === 'cloud' && item.fileId) {
|
||||
actions = '<button type="button" class="soon-file-card__action soon-file-card__download" title="下载 .soon" aria-label="下载">'
|
||||
+ '<svg class="soon-file-card__icon" viewBox="0 0 24 24" width="15" height="15" aria-hidden="true">'
|
||||
+ '<path d="M12 4v10m0 0l3.5-3.5M12 14l-3.5-3.5M5 20h14"/></svg></button>'
|
||||
+ '<button type="button" class="soon-file-card__action soon-file-card__delete" title="' + escapeAttr(language_str('deleteBtn')) + '" aria-label="' + escapeAttr(language_str('deleteBtn')) + '">'
|
||||
+ '<svg class="soon-file-card__icon" viewBox="0 0 24 24" width="15" height="15" aria-hidden="true">'
|
||||
+ '<path d="M4 7h16"/><path d="M7 7l1.1 11.1a.9.9 0 00.9.9h6a.9.9 0 00.9-.9L17 7"/>'
|
||||
+ '<path d="M9.5 7V5.6A1.6 1.6 0 0111.1 4h1.8a1.6 1.6 0 011.6 1.6V7"/>'
|
||||
+ '<path d="M10 11v5.5"/><path d="M14 11v5.5"/></svg></button>';
|
||||
} else {
|
||||
actions = '<button type="button" class="soon-file-card__action soon-file-card__delete" title="' + escapeAttr(language_str('deleteBtn')) + '" aria-label="' + escapeAttr(language_str('deleteBtn')) + '">'
|
||||
+ '<svg class="soon-file-card__icon" viewBox="0 0 24 24" width="15" height="15" aria-hidden="true">'
|
||||
+ '<path d="M4 7h16"/><path d="M7 7l1.1 11.1a.9.9 0 00.9.9h6a.9.9 0 00.9-.9L17 7"/>'
|
||||
+ '<path d="M9.5 7V5.6A1.6 1.6 0 0111.1 4h1.8a1.6 1.6 0 011.6 1.6V7"/>'
|
||||
+ '<path d="M10 11v5.5"/><path d="M14 11v5.5"/></svg></button>';
|
||||
}
|
||||
|
||||
+ '<svg class="soon-file-card__icon" viewBox="0 0 24 24" width="15" height="15" aria-hidden="true">'
|
||||
|
||||
+ '<path d="M12 4v10m0 0l3.5-3.5M12 14l-3.5-3.5M5 20h14"/></svg></button>';
|
||||
|
||||
var delBtn = '<button type="button" class="soon-file-card__action soon-file-card__delete" title="' + escapeAttr(language_str('deleteBtn')) + '" aria-label="' + escapeAttr(language_str('deleteBtn')) + '">'
|
||||
|
||||
+ '<svg class="soon-file-card__icon" viewBox="0 0 24 24" width="15" height="15" aria-hidden="true">'
|
||||
|
||||
+ '<path d="M4 7h16"/><path d="M7 7l1.1 11.1a.9.9 0 00.9.9h6a.9.9 0 00.9-.9L17 7"/>'
|
||||
|
||||
+ '<path d="M9.5 7V5.6A1.6 1.6 0 0111.1 4h1.8a1.6 1.6 0 011.6 1.6V7"/>'
|
||||
|
||||
+ '<path d="M10 11v5.5"/><path d="M14 11v5.5"/></svg></button>';
|
||||
|
||||
h += `<div class="${cardClass}" data-file="${escapeAttr(filePath)}" data-id="${item.id}" data-name="${escapeAttr(displayName)}" title="${escapeAttr(cardTitle)}">
|
||||
h += `<div class="${cardClass}" data-file="${escapeAttr(filePath)}" data-kind="${item.kind}"${item.fileId ? (' data-id="' + item.fileId + '"') : ''} data-name="${escapeAttr(displayName)}" title="${escapeAttr(cardTitle)}">
|
||||
|
||||
<div class="rect ${realType == 2 ? 'rect1' : ''}">
|
||||
|
||||
<div class="soon-file-card__actions">${dlBtn}${delBtn}</div>
|
||||
<div class="soon-file-card__actions">${actions}</div>
|
||||
|
||||
<img src="${escapeAttr(src)}" style="${imgStyle}" alt="">
|
||||
|
||||
@@ -398,6 +475,8 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
|
||||
}
|
||||
|
||||
window.soonReloadRecentFiles = loadHistory;
|
||||
|
||||
|
||||
|
||||
function renderTemplatesError(message) {
|
||||
@@ -430,33 +509,16 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
return soonAsset(t === 2 ? 'bg_2.png' : 'bg_1.png');
|
||||
}
|
||||
|
||||
async function resolveTemplateItem(m) {
|
||||
function resolveTemplateItem(m, base) {
|
||||
var type = Number(m.type) || 1;
|
||||
var fallbackThumb = templateFallbackThumb(type);
|
||||
var fileUrl = m.file_url || '';
|
||||
var out = {
|
||||
var id = m.id;
|
||||
return {
|
||||
id: id,
|
||||
name: m.name || m.title || '模板',
|
||||
type: type,
|
||||
file_url: fileUrl,
|
||||
thumbSrc: fallbackThumb,
|
||||
_soonData: null
|
||||
thumbSrc: id ? (base + '/templates/' + id + '/thumb') : templateFallbackThumb(type),
|
||||
file_url: id ? (base + '/templates/' + id + '/file') : (m.file_url || '')
|
||||
};
|
||||
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) {
|
||||
@@ -474,10 +536,6 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
openDesign(type, '');
|
||||
}
|
||||
}
|
||||
if (m._soonData) {
|
||||
openWithJson(m._soonData);
|
||||
return;
|
||||
}
|
||||
var fileUrl = m.file_url || '';
|
||||
if (!fileUrl) {
|
||||
openDesign(type, '');
|
||||
@@ -493,13 +551,14 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
|
||||
function templateCardHtml(m, itemIndex) {
|
||||
var type = Number(m.type) || 1;
|
||||
var thumbSrc = m.thumbSrc || templateFallbackThumb(type);
|
||||
var fallback = templateFallbackThumb(type);
|
||||
var thumbSrc = m.thumbSrc || fallback;
|
||||
var esc = typeof soonEscapeHtml === 'function' ? soonEscapeHtml : escapeAttr;
|
||||
var name = esc(m.name || m.title || '模板');
|
||||
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="">' +
|
||||
'<img src="' + esc(thumbSrc) + '" alt="" loading="lazy" onerror="this.onerror=null;this.src=\'' + esc(fallback) + '\'">' +
|
||||
'<div class="tip">' + name + '</div>' +
|
||||
'</div></div>';
|
||||
}
|
||||
@@ -579,7 +638,7 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
|
||||
try {
|
||||
|
||||
var r = await fetch(base + '/soon-models');
|
||||
var r = await fetch(base + '/templates');
|
||||
|
||||
var j = await r.json();
|
||||
|
||||
@@ -592,12 +651,8 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
}
|
||||
|
||||
var items = (j.data && j.data.items) ? j.data.items : [];
|
||||
var resolved = [];
|
||||
for (var ti = 0; ti < items.length; ti++) {
|
||||
resolved.push(await resolveTemplateItem(items[ti]));
|
||||
}
|
||||
templateListState.items = resolved;
|
||||
templateListState.total = resolved.length;
|
||||
templateListState.items = items.map(function (it) { return resolveTemplateItem(it, base); });
|
||||
templateListState.total = templateListState.items.length;
|
||||
templateListState.page = 1;
|
||||
renderTemplatesPage();
|
||||
|
||||
@@ -631,19 +686,11 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
$("#openfile").click(function () { OpenDialog(); });
|
||||
|
||||
$("#new1").click(function () {
|
||||
|
||||
if (typeof window.soonRequireLogin === 'function' && !window.soonRequireLogin('新建')) return;
|
||||
|
||||
openDesign(1, '');
|
||||
|
||||
});
|
||||
|
||||
$("#new2").click(function () {
|
||||
|
||||
if (typeof window.soonRequireLogin === 'function' && !window.soonRequireLogin('新建')) return;
|
||||
|
||||
openDesign(2, '');
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -683,8 +730,6 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
|
||||
var name = card.attr('data-name') || 'design.soon';
|
||||
|
||||
if (typeof window.soonRequireMember === 'function' && !window.soonRequireMember('下载')) return;
|
||||
|
||||
if (id && window.platformBridge && window.platformBridge.downloadCloudFile) {
|
||||
|
||||
window.platformBridge.downloadCloudFile(id, name).catch(function () {});
|
||||
@@ -699,9 +744,42 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
|
||||
var card = $(this).closest('.card');
|
||||
|
||||
var kind = card.attr('data-kind') || 'cloud';
|
||||
|
||||
var fileId = parseInt(card.attr('data-id'), 10);
|
||||
|
||||
if (!fileId) return;
|
||||
var filePath = card.attr('data-file') || '';
|
||||
|
||||
if (kind === 'local' || !fileId) {
|
||||
|
||||
layer.open({
|
||||
type: 1,
|
||||
skin: 'soon-layer',
|
||||
title: language_str("delTitle"),
|
||||
content: '<div class="soon-dialog-body">' + language_str('deleteFileConfirm') + '</div>',
|
||||
btn: [language_str("comfirm"), language_str("cancel")],
|
||||
btnAlign: 'r',
|
||||
area: ['320px', 'auto'],
|
||||
resize: false,
|
||||
shadeClose: true,
|
||||
yes: function (index) {
|
||||
removeLocalHistoryPath(filePath).then(function () {
|
||||
if (filePath.indexOf('soondesign_session:') === 0) {
|
||||
try {
|
||||
sessionStorage.removeItem(filePath);
|
||||
localStorage.removeItem(filePath);
|
||||
} catch (err) { /* ignore */ }
|
||||
}
|
||||
layer.msg(language_str("deleted"), { icon: 1, time: 1000 });
|
||||
loadHistory();
|
||||
layer.close(index);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
confirmDeleteCloudFile(fileId, language_str('deleteFileConfirm'), loadHistory);
|
||||
|
||||
@@ -725,8 +803,26 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
|
||||
} else {
|
||||
|
||||
var kind = $(this).attr('data-kind') || 'cloud';
|
||||
|
||||
var fileId = parseInt($(this).attr('data-id'), 10);
|
||||
|
||||
if (kind === 'local' || !fileId) {
|
||||
|
||||
removeLocalHistoryPath(filePath).then(function () {
|
||||
if (filePath.indexOf('soondesign_session:') === 0) {
|
||||
try {
|
||||
sessionStorage.removeItem(filePath);
|
||||
localStorage.removeItem(filePath);
|
||||
} catch (err) { /* ignore */ }
|
||||
}
|
||||
loadHistory();
|
||||
});
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
confirmDeleteCloudFile(fileId, language_str("delContent"), loadHistory);
|
||||
|
||||
}
|
||||
@@ -737,8 +833,6 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
|
||||
function OpenDialog() {
|
||||
|
||||
if (typeof window.soonRequireLogin === 'function' && !window.soonRequireLogin('打开文件')) return;
|
||||
|
||||
var dialogApi = window.platformBridge && window.platformBridge.showOpenDialog
|
||||
|
||||
? { showOpenDialog: function (opts) { return window.platformBridge.showOpenDialog(opts); } }
|
||||
@@ -785,6 +879,15 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
|
||||
}
|
||||
|
||||
var tok = typeof window.soonGetAccessToken === 'function'
|
||||
? window.soonGetAccessToken()
|
||||
: (localStorage.getItem('soon_access') || '');
|
||||
|
||||
if (!tok && typeof window.soonOpenSoonJsonLocally === 'function') {
|
||||
window.soonOpenSoonJsonLocally(soonData, fileName);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.platformBridge || !window.platformBridge.importSoonFile) return;
|
||||
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user