fix(web): 本地优先保存与 design 页体验修复

- 保存先写本地缓存,登录后可选云端同步;退出登录不再因文件操作跳转登录

- 修复 Layui 遮罩残留、design2 保存后线条、toast 被 finally 提前关闭

- 保存过程恢复 loading spinner;成功提示 2.5 秒

- 云端 payload 瘦身与体积超限提示;后端 schema 迁移与 FileService 容错

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
24kycj
2026-06-24 19:24:34 +08:00
parent 4d19d7e814
commit 5fb269cc0a
36 changed files with 894 additions and 358 deletions
@@ -486,12 +486,45 @@ body.soon-admin-page .soon-admin-date-wrap input[type="date"].soon-input::-webki
border-radius: 4px;
}
.soon-admin-form { padding: var(--soon-space-md); }
.soon-admin-form--modal-shell {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
width: 100%;
padding: 0;
box-sizing: border-box;
}
.soon-admin-form__scroll {
flex: 1;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
padding: var(--soon-space-md);
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.18) transparent;
}
.soon-admin-form__scroll::-webkit-scrollbar { width: 8px; }
.soon-admin-form__scroll::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.16);
border-radius: 4px;
}
.soon-layer-content--form-fixed .soon-admin-form--modal-shell {
height: 100%;
}
.soon-admin-form--modal-shell.soon-admin-form--plan .soon-admin-form__scroll,
.soon-admin-tpl-modal.soon-admin-form--modal-shell .soon-admin-form__scroll {
padding: var(--soon-space-lg);
}
.soon-admin-form--plan {
padding: var(--soon-space-lg);
}
.soon-admin-form--plan .soon-admin-form-panel:last-of-type {
margin-bottom: 0;
}
.soon-admin-form--modal-shell .soon-admin-form-panel:last-of-type {
margin-bottom: 0;
}
.soon-admin-form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
@@ -677,6 +710,22 @@ body.soon-admin-page .soon-admin-date-wrap input[type="date"].soon-input::-webki
padding-top: var(--soon-space-md);
border-top: 1px solid var(--soon-border);
}
.soon-admin-form__foot {
display: flex;
justify-content: flex-end;
gap: var(--soon-space-sm);
flex-shrink: 0;
margin-top: 0;
padding: var(--soon-space-md) var(--soon-space-lg);
border-top: 1px solid var(--soon-border);
background: var(--soon-bg-elevated);
}
.soon-admin-tpl-modal.soon-admin-form--modal-shell {
padding: 0;
}
.soon-admin-tpl-modal__actions.soon-admin-form__foot {
padding-top: var(--soon-space-md);
}
.soon-admin-filter-bar {
display: flex;
+54 -10
View File
@@ -141,10 +141,62 @@
setBtnLoading(btn, true, opts.loadingLabel);
ret.finally(function () {
setBtnLoading(btn, false);
if (typeof window.soonCleanupLayerMask === 'function') window.soonCleanupLayerMask();
});
}
}
function layerContentEl(layero) {
if (layero && typeof layero.find === 'function') {
return layero.find('.layui-layer-content')[0] || null;
}
if (layero && layero.querySelector) {
return layero.querySelector('.layui-layer-content');
}
return null;
}
/** 弹窗表单:内容区滚动,保存按钮固定底栏 */
function layoutAdminFormModal(contentEl, opts) {
if (!contentEl || contentEl.dataset.soonFormLayout) return;
opts = opts || {};
var root = contentEl.querySelector('.soon-admin-form, .soon-admin-tpl-modal');
if (!root) return;
var foot = root.querySelector('.soon-admin-form__actions, .soon-admin-tpl-modal__actions');
if (!foot) {
var saveBtn = root.querySelector('#adminModalSave');
if (!saveBtn) return;
foot = document.createElement('div');
foot.className = 'soon-admin-form__actions';
saveBtn.parentNode.insertBefore(foot, saveBtn);
foot.appendChild(saveBtn);
}
var scroll = document.createElement('div');
scroll.className = 'soon-admin-form__scroll';
while (root.firstChild && root.firstChild !== foot) {
scroll.appendChild(root.firstChild);
}
if (!scroll.childNodes.length) return;
root.insertBefore(scroll, foot);
root.classList.add('soon-admin-form--modal-shell');
foot.classList.add('soon-admin-form__foot');
contentEl.classList.add('soon-layer-content--form');
var area = opts.area || ['420px', 'auto'];
if (area[1] && area[1] !== 'auto') {
contentEl.classList.add('soon-layer-content--form-fixed');
contentEl.style.height = area[1];
contentEl.style.maxHeight = area[1];
} else if (opts.maxHeight) {
contentEl.classList.add('soon-layer-content--form-fixed');
contentEl.style.maxHeight = opts.maxHeight;
}
contentEl.dataset.soonFormLayout = '1';
}
function openFormModal(title, formHtml, onSave, opts) {
if (typeof layer === 'undefined') return;
opts = opts || {};
@@ -156,16 +208,8 @@
maxWidth: opts.maxWidth || '96vw',
content: formHtml,
success: function (layero) {
var area = opts.area || ['420px', 'auto'];
if (area[1] && area[1] !== 'auto') {
var contentEl = null;
if (layero && typeof layero.find === 'function') {
contentEl = layero.find('.layui-layer-content')[0];
} else if (layero && layero.querySelector) {
contentEl = layero.querySelector('.layui-layer-content');
}
if (contentEl) contentEl.classList.add('soon-layer-content--scroll');
}
var contentEl = layerContentEl(layero);
layoutAdminFormModal(contentEl, opts);
var btn = document.getElementById('adminModalSave');
if (btn) {
btn.onclick = function () {
@@ -4,6 +4,7 @@
var Api = function () { return window.AdminApi; };
var Ui = function () { return window.AdminUi; };
var I = function () { return window.AdminI18n; };
var Form = function () { return window.AdminForm; };
var State = function () { return window.AdminState; };
var Filter = function () { return window.AdminFilter; };
var Menu = function () { return window.AdminMenu; };
@@ -162,7 +163,7 @@
'<div class="soon-form-field"><label class="soon-form-label">备注</label>' +
'<textarea id="orderNote" class="soon-input soon-input--sm" rows="3" placeholder="' + I().esc(placeholder) +
'"></textarea></div>' +
'<button type="button" class="soon-btn soon-btn--sm" id="adminModalSave">确认</button></div>';
Form().actions('确认', 'adminModalSave') + '</div>';
Ui().openFormModal(title, form, function () {
return onSave(Ui().el('orderNote').value.trim());
});
@@ -246,7 +247,7 @@
openNoteModal(title, '可选:填写审核备注', function (note) {
return Api().postJson('orders/' + btn.dataset.id + '/refund/approve', { note: note }).then(function (r) {
if (r) {
layer.closeAll();
soonLayerDismiss();
var gw = r.data && r.data.gateway;
var msg = '退款已完成';
if (gw && gw.indexOf('skipped') === 0) msg += '(未配置支付密钥,仅标记退款并撤销会员)';
@@ -268,7 +269,7 @@
}
return Api().postJson('orders/' + btn.dataset.id + '/refund/reject', { note: note }).then(function (r) {
if (r) {
layer.closeAll();
soonLayerDismiss();
soonToast('已拒绝退款申请', 'success');
Ui().closeDrawer();
render();
@@ -192,7 +192,7 @@
};
return Api().postJson('plans', body).then(function (r) {
if (r) {
layer.closeAll();
soonLayerDismiss();
soonToast('已保存', 'success');
render();
}
@@ -87,19 +87,20 @@
var form = '<div class="soon-admin-form">' +
Form().field('配置键', '<input class="soon-input soon-input--sm" value="' + I().esc(key) + '" disabled>') +
Form().field('配置值', Form().input('settingVal', { value: val, type: 'textarea', rows: 5 })) +
'<button type="button" class="soon-btn soon-btn--sm" id="adminModalSave">保存</button></div>';
Form().actions('保存', 'adminModalSave') + '</div>';
Ui().openFormModal('修改 ' + key, form, function () {
Ui().confirm('确认保存该配置?', function () {
var saveBtn = Ui().el('adminModalSave');
Ui().setBtnLoading(saveBtn, true);
Api().postJson('settings', { key: key, value: Ui().el('settingVal').value }).then(function (r) {
if (r) {
layer.closeAll();
soonLayerDismiss();
soonToast('配置已保存', 'success');
render();
}
}).finally(function () {
Ui().setBtnLoading(saveBtn, false);
if (typeof window.soonCleanupLayerMask === 'function') window.soonCleanupLayerMask();
});
});
});
@@ -284,7 +284,7 @@
}).then(function (r) {
if (r) {
revokeBlob(modalState);
layer.closeAll();
soonLayerDismiss();
soonToast('已上传', 'success');
render();
}
@@ -307,7 +307,7 @@
return Api().putJson('templates/' + plan.id, body).then(function (r) {
if (r) {
revokeBlob(modalState);
layer.closeAll();
soonLayerDismiss();
soonToast('已保存', 'success');
render();
}
@@ -8,6 +8,7 @@
var Filter = function () { return window.AdminFilter; };
var Menu = function () { return window.AdminMenu; };
var Router = function () { return window.AdminRouter; };
var Form = function () { return window.AdminForm; };
function buildQuery() {
var st = State().users;
@@ -57,7 +58,7 @@
? formField('管理级别', '<div id="adminLevelField" style="display:none"><select id="ucAdminLevel" class="soon-input soon-input--sm">' +
adminLevelOptions('ops') + '</select></div>', '运营管理员不可访问设置、审计与支付密钥')
: '') +
'<button type="button" class="soon-btn soon-btn--sm" id="adminModalSave">创建</button></div>';
Form().actions('创建', 'adminModalSave') + '</div>';
Ui().openFormModal('新增用户', form, function () {
var email = Ui().el('ucEmail').value.trim();
var pwd = Ui().el('ucPwd').value;
@@ -76,7 +77,7 @@
}
return Api().postJson('users', body).then(function (r) {
if (r) {
layer.closeAll();
soonLayerDismiss();
soonToast('用户已创建', 'success');
render();
}
@@ -95,7 +96,7 @@
? formField('管理级别', '<div id="adminLevelField"><select id="ueAdminLevel" class="soon-input soon-input--sm">' +
adminLevelOptions(level) + '</select></div>', '仅对管理员账号生效')
: '') +
'<button type="button" class="soon-btn soon-btn--sm" id="adminModalSave">保存</button></div>';
Form().actions('保存', 'adminModalSave') + '</div>';
Ui().openFormModal('编辑用户 #' + user.id, form, function () {
var body = {
email: Ui().el('ueEmail').value.trim(),
@@ -107,7 +108,7 @@
}
return Api().putJson('users/' + user.id, body).then(function (r) {
if (r) {
layer.closeAll();
soonLayerDismiss();
soonToast('用户信息已更新', 'success');
render();
}
@@ -121,7 +122,7 @@
var form = '<div class="soon-admin-form">' +
formField('新密码', '<input id="upPwd" type="password" class="soon-input soon-input--sm" autocomplete="new-password" placeholder="至少 8 位">') +
formField('确认密码', '<input id="upPwd2" type="password" class="soon-input soon-input--sm" autocomplete="new-password">') +
'<button type="button" class="soon-btn soon-btn--sm" id="adminModalSave">重置密码</button></div>';
Form().actions('重置密码', 'adminModalSave') + '</div>';
Ui().openFormModal('重置密码', form, function () {
var p1 = Ui().el('upPwd').value;
var p2 = Ui().el('upPwd2').value;
@@ -135,7 +136,7 @@
}
return Api().postJson('users/' + userId + '/password', { password: p1 }).then(function (r) {
if (r) {
layer.closeAll();
soonLayerDismiss();
soonToast('密码已重置', 'success');
}
});
@@ -153,7 +154,7 @@
formField('套餐', '<select id="usPlan" class="soon-input soon-input--sm">' + opts + '</select>') +
formField('有效天数', '<input id="usDays" type="number" min="0" class="soon-input soon-input--sm" placeholder="留空则用套餐默认天数">') +
'<p class="soon-admin-form-hint">选择免费版将取消当前有效会员。</p>' +
'<button type="button" class="soon-btn soon-btn--sm" id="adminModalSave">保存</button></div>';
Form().actions('保存', 'adminModalSave') + '</div>';
Ui().openFormModal('设置会员', form, function () {
var planSel = Ui().el('usPlan');
var daysVal = Ui().el('usDays').value.trim();
@@ -161,7 +162,7 @@
if (daysVal) body.days = parseInt(daysVal, 10);
return Api().postJson('users/' + userId + '/subscription', body).then(function (r) {
if (r) {
layer.closeAll();
soonLayerDismiss();
soonToast('会员已更新', 'success');
render();
}
+1
View File
@@ -139,6 +139,7 @@
<script src="../js/common/auth-redirect.js"></script>
<script src="../js/common/auth-session.js"></script>
<script src="../js/common/soon-toast.js"></script>
<script src="../js/common/cloud-files.js"></script>
<script src="../js/common/soon-local-store.js"></script>
<script src="../js/common/soon-recent.js"></script>