Files
SoonDesign/frontend-web/js/index.js
T
24kycj 877fd278c2 Web 端数据交互与 design2 布局修复
统一门户 JSON 契约:新增 GET /templates/{id},模板/云文件/保存/打开走虚拟 key;
layer 保存弹层替代 prompt,本地 .soon 已登录后台 POST 登记;修复保存静默失败与 design2 全宽布局。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 01:20:34 +08:00

890 lines
25 KiB
JavaScript

function get_filename(filePath) {
if (!filePath) return "";
return filePath.split(/[/\\]/).pop();
}
function escapeAttr(s) {
if (s == null) return "";
return String(s).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
var SOON_EMPTY_IMG = '../assets/images/web/empty-state.png';
function soonEmptyBlock(title, hint, extraHtml) {
var html = '<div class="soon-empty soon-empty--inline">' +
'<img src="' + SOON_EMPTY_IMG + '" alt="" class="soon-empty__icon">' +
'<p class="soon-empty__title">' + escapeAttr(title) + '</p>';
if (hint) {
html += '<p class="soon-empty__hint">' + escapeAttr(hint) + '</p>';
}
if (extraHtml) html += extraHtml;
html += '</div>';
return html;
}
let s_lan = "zh";
function language_str(str) {
let t = {
"noFile": { zh: "当前文件不存在!", ozh: "This file does not exist!", en: "File not found!" },
"saveTime": { zh: "保存时间:", ozh: "保存時間:", en: "Saved on:" },
"selectFile": { zh: "请选择文件", ozh: "請選擇文件", en: "Please select a file" },
"comfirm": { zh: "确认", ozh: "確認", en: "Confirm" },
"cancel": { zh: "取消", ozh: "取消", en: "Cancel" },
"SDFile": { zh: "SoonDesign模板文件", ozh: "SoonDesign模闆文件", en: "SoonDesign Template File" },
"lost": { zh: "(文件已丢失)", ozh: "(文件已丟失)", en: "(File Lost)" },
"clickToDelete": { zh: "文件已丢失,点击删除此记录", ozh: "文件已丟失,點擊刪除此記錄", en: "File lost, click to remove" },
"deleted": { zh: "已删除", ozh: "已刪除", en: "Deleted" },
"whetherSave": { zh: "是否保存当前文件?", ozh: "是否保存當前文件?", en: "Save current file?" },
"delTitle": { zh: "警告", ozh: "警告", en: "Warning" },
"delContent": { zh: "文件已丢失,确认是否删除?", ozh: "文件已丟失,確認是否刪除?", en: "File lost, confirm delete?" },
"deleteFileConfirm": { zh: "确认删除该文件?删除后无法恢复。", ozh: "確認刪除該文件?刪除後無法恢復。", en: "Delete this file? This cannot be undone." },
"deleteBtn": { zh: "删除", ozh: "刪除", en: "Delete" }
};
return t[str][s_lan] || t[str]['zh'];
}
function openDesign(type, filePath) {
if (window.platformBridge && window.platformBridge.openDesignPage) {
window.platformBridge.openDesignPage(filePath || '', type);
}
}
layui.use(['layer', 'form', 'jquery'], function () {
var $ = layui.$;
var layer = layui.layer;
layer.config({ skin: 'soon-layer' });
if (window.sysAPI && window.sysAPI.getAppVersion) {
window.sysAPI.getAppVersion().then(function (ver) {
$("#app-version").text("v" + ver);
}).catch(function () {});
}
function langua_ge(lan) {
lan = lan || 'zh';
$("[language='m']").each(function () {
$(this).html($(this).attr(lan));
});
$("[language='t']").each(function () {
$(this).attr("title", $(this).attr(lan));
});
}
var lang = localStorage.getItem("lang");
if (lang) {
s_lan = lang;
langua_ge(s_lan);
$("#language_select").val(s_lan);
} else if (window.platformBridge && window.platformBridge.getLocale) {
window.platformBridge.getLocale().then(function (loc) {
if (loc && loc.indexOf('zh') === 0) {
s_lan = loc.indexOf('TW') >= 0 ? 'ozh' : 'zh';
} else {
s_lan = 'en';
}
langua_ge(s_lan);
$("#language_select").val(s_lan);
localStorage.setItem("lang", s_lan);
});
}
$('#language_select').on('change', function () {
langua_ge($(this).find('option:selected').val());
s_lan = $(this).find('option:selected').val();
localStorage.setItem("lang", s_lan);
loadHistory();
});
function hasCloudAuth() {
if (typeof window.soonGetAccessToken === 'function') return !!window.soonGetAccessToken();
try { return !!localStorage.getItem('soon_access'); } catch (e) { return false; }
}
function makeFileKey(id, version) {
return typeof window.soonMakeFileKey === 'function'
? window.soonMakeFileKey(id, version)
: ('soondesign_file:' + id + ':v' + version);
}
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;
var page = fileListState.page;
var size = fileListState.size;
var total = fileListState.total;
var pages = Math.max(1, Math.ceil(total / size));
if (total === 0) {
el.innerHTML = '';
return;
}
el.innerHTML =
'<div class="soon-file-pager">' +
'<button type="button" class="soon-btn soon-btn--sm soon-btn--ghost" id="filePrev"' +
(page <= 1 ? ' disabled' : '') + '>上一页</button>' +
'<span class="soon-file-pager__info">第 ' + page + ' / ' + pages + ' 页,共 ' + total + ' 个</span>' +
'<button type="button" class="soon-btn soon-btn--sm soon-btn--ghost" id="fileNext"' +
(page >= pages ? ' disabled' : '') + '>下一页</button></div>';
var prev = document.getElementById('filePrev');
var next = document.getElementById('fileNext');
if (prev && !prev.disabled) {
prev.onclick = function () {
fileListState.page = Math.max(1, fileListState.page - 1);
loadHistory();
};
}
if (next && !next.disabled) {
next.onclick = function () {
fileListState.page = Math.min(pages, fileListState.page + 1);
loadHistory();
};
}
}
async function loadHistory() {
try {
var recentCountEl = document.getElementById('recentCount');
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 (!merged.length) {
$(".card-list").html(soonEmptyBlock('暂无文件', '点击「打开文件」导入,或新建模板开始设计'));
renderFilePager();
return;
}
var start = (fileListState.page - 1) * fileListState.size;
var slice = merged.slice(start, start + fileListState.size);
let h = "";
for (let item of slice) {
let filePath = item.filePath;
let src = "";
let fileExists = true;
let imgStyle = "";
let realType = item.type || 1;
const soonData = await window.sysAPI.readJsonFile(filePath);
if (soonData) {
realType = soonData.soonType ? soonData.soonType : (soonData.backBlackPic ? 2 : 1);
var fallbackThumb = (realType == 2 || realType == "2") ? soonAsset('bg_2.png') : soonAsset('bg_1.png');
src = typeof soonSafeImageUrl === 'function'
? soonSafeImageUrl(soonData.frontDisplayPic, fallbackThumb)
: (soonData.frontDisplayPic || fallbackThumb);
if (!src) src = fallbackThumb;
} else {
fileExists = false;
src = soonAsset((realType == 2 || realType == "2") ? 'bg_2.png' : 'bg_1.png');
imgStyle = "opacity: 0.6; filter: grayscale(100%);";
}
let displayName = item.name || displayNameFromPath(filePath);
let cardTitle = displayName;
var cardClass = 'card';
if (!fileExists) {
cardTitle = language_str("clickToDelete");
cardClass += ' soon-card--lost';
}
var lostBadge = !fileExists
? ' <span class="soon-badge soon-badge--disabled">' + language_str("lost") + '</span>'
: '';
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>';
}
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">${actions}</div>
<img src="${escapeAttr(src)}" style="${imgStyle}" alt="">
<div class="tip">${escapeAttr(displayName)}${lostBadge}</div>
</div>
</div>`;
}
$(".card-list").html(h);
renderFilePager();
} catch (e) {
console.error("加载文件列表出错", e);
$(".card-list").html(soonEmptyBlock('加载失败', '请刷新页面重试',
'<button type="button" class="soon-btn soon-btn--sm soon-empty__action" id="fileListRetry">重试</button>'));
var retry = document.getElementById('fileListRetry');
if (retry) retry.onclick = function () { loadHistory(); };
if (document.getElementById('filePager')) document.getElementById('filePager').innerHTML = '';
}
}
window.soonReloadRecentFiles = loadHistory;
function renderTemplatesError(message) {
var grid = document.getElementById('templatesGrid');
var countEl = document.getElementById('templateCount');
if (!grid) return;
templateListState.items = [];
templateListState.total = 0;
templateListState.page = 1;
grid.innerHTML = soonEmptyBlock(message, null,
'<button type="button" class="soon-btn soon-btn--sm soon-empty__action" id="templatesRetry">重试</button>');
if (countEl) countEl.textContent = '!';
renderTemplatePager();
var btn = document.getElementById('templatesRetry');
if (btn) btn.onclick = loadTemplates;
}
function templateFallbackThumb(type) {
var t = Number(type) || 1;
return soonAsset(t === 2 ? 'bg_2.png' : 'bg_1.png');
}
function resolveTemplateItem(m, base) {
var type = Number(m.type) || 1;
var id = m.id;
var v = m.updated_at ? ('?v=' + encodeURIComponent(m.updated_at)) : '';
return {
id: id,
name: m.name || m.title || '模板',
type: type,
thumbSrc: id ? (base + '/templates/' + id + '/thumb' + v) : templateFallbackThumb(type)
};
}
function openTemplateItem(m) {
var type = Number(m.type) || 1;
var id = m && m.id;
if (!id) {
if (typeof window.soonToast === 'function') window.soonToast('模板不可用', 'error');
return;
}
var templateKey = typeof window.soonMakeTemplateKey === 'function'
? window.soonMakeTemplateKey(id)
: ('soondesign_template:' + id);
openDesign(type, templateKey);
}
function templateCardHtml(m, itemIndex) {
var type = Number(m.type) || 1;
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="" loading="lazy" onerror="this.onerror=null;this.src=\'' + esc(fallback) + '\'">' +
'<div class="tip">' + name + '</div>' +
'</div></div>';
}
function bindTemplateCards(grid) {
if (!grid) return;
grid.querySelectorAll('.card[data-index]').forEach(function (card) {
card.onclick = function () {
var idx = Number(card.dataset.index);
var m = templateListState.items[idx];
if (m) openTemplateItem(m);
};
});
}
function renderTemplatePager() {
var prevNav = document.getElementById('templatePrev');
var nextNav = document.getElementById('templateNext');
var page = templateListState.page;
var size = templateListState.size;
var total = templateListState.total;
var pages = Math.max(1, Math.ceil(total / size));
if (prevNav) prevNav.disabled = total === 0 || page <= 1;
if (nextNav) nextNav.disabled = total === 0 || page >= pages;
}
function goTemplatePage(page) {
var pages = Math.max(1, Math.ceil(templateListState.total / templateListState.size));
templateListState.page = Math.max(1, Math.min(pages, page));
renderTemplatesPage();
}
function renderTemplatesPage() {
var grid = document.getElementById('templatesGrid');
var countEl = document.getElementById('templateCount');
if (!grid) return;
var items = templateListState.items || [];
templateListState.total = items.length;
if (countEl) countEl.textContent = String(items.length);
if (!items.length) {
grid.innerHTML = soonEmptyBlock('暂无模板');
renderTemplatePager();
return;
}
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, start + i); }).join('');
bindTemplateCards(grid);
renderTemplatePager();
}
var _templatesLoadedAt = 0;
async function loadTemplates(force) {
var grid = document.getElementById('templatesGrid');
var countEl = document.getElementById('templateCount');
if (!grid) return;
if (!force && _templatesLoadedAt && (Date.now() - _templatesLoadedAt) < 60000) {
return;
}
var base = (window.SOON_DEPLOY_CONFIG && window.SOON_DEPLOY_CONFIG.api_v1_base) || '';
if (!base) {
templateListState.items = [];
templateListState.total = 0;
templateListState.page = 1;
grid.innerHTML = soonEmptyBlock('暂不可用', '请稍后重试');
if (countEl) countEl.textContent = '0';
renderTemplatePager();
return;
}
try {
var r = await fetch(base + '/templates');
var j = await r.json();
if (!j.ok) {
renderTemplatesError(j.message || '模板加载失败');
return;
}
var items = (j.data && j.data.items) ? j.data.items : [];
templateListState.items = items.map(function (it) { return resolveTemplateItem(it, base); });
templateListState.total = templateListState.items.length;
templateListState.page = 1;
renderTemplatesPage();
_templatesLoadedAt = Date.now();
} catch (e) {
renderTemplatesError('网络错误,请稍后重试');
}
}
loadHistory();
loadTemplates();
window.soonReloadTemplates = function () { loadTemplates(true); };
document.addEventListener('visibilitychange', function () {
if (document.visibilityState === 'visible') loadTemplates(false);
});
window.addEventListener('pageshow', function (e) {
if (e.persisted) loadTemplates(false);
});
var templatePrevNav = document.getElementById('templatePrev');
var templateNextNav = document.getElementById('templateNext');
if (templatePrevNav) {
templatePrevNav.onclick = function () {
if (!templatePrevNav.disabled) goTemplatePage(templateListState.page - 1);
};
}
if (templateNextNav) {
templateNextNav.onclick = function () {
if (!templateNextNav.disabled) goTemplatePage(templateListState.page + 1);
};
}
$("#openfile").click(function () { OpenDialog(); });
$("#new1").click(function () {
openDesign(1, '');
});
$("#new2").click(function () {
openDesign(2, '');
});
function confirmDeleteCloudFile(fileId, message, onDone) {
if (!fileId) return;
layer.open({
type: 1,
skin: 'soon-layer',
title: language_str("delTitle"),
content: '<div class="soon-dialog-body">' + message + '</div>',
btn: [language_str("comfirm"), language_str("cancel")],
btnAlign: 'r',
area: ['320px', 'auto'],
resize: false,
shadeClose: true,
yes: async function (index) {
if (window.platformBridge && window.platformBridge.deleteCloudFile) {
try {
await window.platformBridge.deleteCloudFile(fileId);
} catch (e) { /* ignore */ }
}
layer.msg(language_str("deleted"), { icon: 1, time: 1000 });
if (typeof onDone === 'function') onDone();
layer.close(index);
}
});
}
$(".card-list").on("click", '.soon-file-card__download', function (e) {
e.stopPropagation();
var card = $(this).closest('.card');
var id = parseInt(card.attr('data-id'), 10);
var name = card.attr('data-name') || 'design.soon';
if (id && window.platformBridge && window.platformBridge.downloadCloudFile) {
window.platformBridge.downloadCloudFile(id, name).catch(function () {});
}
});
$(".card-list").on("click", '.soon-file-card__delete', function (e) {
e.stopPropagation();
var card = $(this).closest('.card');
var kind = card.attr('data-kind') || 'cloud';
var fileId = parseInt(card.attr('data-id'), 10);
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);
});
$(".card-list").on("click", '.card', async function () {
let filePath = $(this).attr("data-file") || $(this).attr("data");
const soonData = await window.sysAPI.readJsonFile(filePath);
if (soonData) {
let type = soonData.soonType ? soonData.soonType : (soonData.backBlackPic ? 2 : 1);
if (typeof window.soonBindFileMeta === 'function') {
window.soonBindFileMeta(filePath, { name: $(this).attr('data-name') || '' });
}
openDesign(type, filePath);
} 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);
}
});
function OpenDialog() {
var dialogApi = window.platformBridge && window.platformBridge.showOpenDialog
? { showOpenDialog: function (opts) { return window.platformBridge.showOpenDialog(opts); } }
: null;
if (!dialogApi) return;
dialogApi.showOpenDialog({
title: language_str("selectFile"),
buttonLabel: language_str("comfirm"),
filters: [{ name: language_str("SDFile"), extensions: ['soon'] }]
}).then(async function (result) {
if (result.canceled) return;
var file = (result.files && result.files[0]) || result.file;
if (!file || !file.text) {
layer.msg(language_str("noFile"));
return;
}
var fileName = file.name || 'design.soon';
var soonData;
try {
soonData = JSON.parse(await file.text());
} catch (e) {
layer.msg(language_str("noFile"));
return;
}
window.soonOpenSoonJsonLocally(soonData, fileName);
}).catch(function (err) { console.log(err); });
}
});