重构 monorepo 并完善网页端订阅与首页体验
- 迁移为 frontend-web、frontend-electron、backend-web 与 docker 部署结构 - 网页端:订阅门禁二次弹窗、套餐/支付组件化、顶栏分组对齐 - 首页:最近文件与模板库布局优化,缩略图对齐,下载与删除操作 - 新增管理后台、支付与云端文件 API,更新 README 与项目规范 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
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 Tabs = function () { return window.AdminTabs; };
|
||||
var Filter = function () { return window.AdminFilter; };
|
||||
var State = function () { return window.AdminState; };
|
||||
var Router = function () { return window.AdminRouter; };
|
||||
|
||||
var SETTING_LABELS = {
|
||||
'site.base_url': 'API 站点外网地址',
|
||||
'site.front_base_url': '前端站点地址',
|
||||
};
|
||||
|
||||
var valuesByKey = {};
|
||||
var allItems = [];
|
||||
|
||||
function siteFields() {
|
||||
var cfg = window.SOON_DEPLOY_CONFIG || {};
|
||||
var apiBase = String(cfg.api_v1_base || '').replace(/\/api\/v1\/?$/, '');
|
||||
var frontBase = cfg.front_base_url || cfg.web_base || '';
|
||||
return [
|
||||
{
|
||||
key: 'site.base_url',
|
||||
label: 'API 站点外网地址',
|
||||
hint: '须 HTTPS 外网可达;支付宝/微信 notify 回调依赖此地址',
|
||||
placeholder: apiBase,
|
||||
},
|
||||
{
|
||||
key: 'site.front_base_url',
|
||||
label: '前端站点地址',
|
||||
hint: '支付完成 return_url 跳转至会员页',
|
||||
placeholder: frontBase,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function settingLabel(key) {
|
||||
if (SETTING_LABELS[key]) {
|
||||
return I().esc(key) + '<br><span class="soon-admin-setting-hint">' + I().esc(SETTING_LABELS[key]) + '</span>';
|
||||
}
|
||||
return I().esc(key);
|
||||
}
|
||||
|
||||
function maskValue(key, val) {
|
||||
if (/secret|password|key/i.test(key) && val) return '******';
|
||||
return I().esc(val);
|
||||
}
|
||||
|
||||
function siteFormHtml() {
|
||||
var fields = siteFields().map(function (f) {
|
||||
var val = valuesByKey[f.key] != null ? valuesByKey[f.key] : '';
|
||||
return Form().field(f.label,
|
||||
Form().input('cfg_' + f.key.replace(/\./g, '_'), { value: val, placeholder: f.placeholder }),
|
||||
f.hint);
|
||||
}).join('');
|
||||
return '<div class="soon-admin-form">' + Form().panel('站点与回调地址', fields) +
|
||||
Form().actions('保存站点配置', 'settingsSiteSave') + '</div>';
|
||||
}
|
||||
|
||||
function allSettingsHtml(filterQ) {
|
||||
var q = (filterQ || '').toLowerCase();
|
||||
var items = allItems.filter(function (s) {
|
||||
if (!q) return true;
|
||||
return String(s.key).toLowerCase().indexOf(q) >= 0 ||
|
||||
String(s.value || '').toLowerCase().indexOf(q) >= 0;
|
||||
});
|
||||
var rows = items.map(function (s) {
|
||||
return '<tr><td>' + settingLabel(s.key) + '</td><td>' + maskValue(s.key, s.value) + '</td><td>' +
|
||||
I().formatDateTime(s.updated_at) + '</td><td>' +
|
||||
'<button type="button" class="soon-btn soon-btn--sm soon-btn--ghost setting-edit" data-key="' +
|
||||
I().esc(s.key) + '">编辑</button></td></tr>';
|
||||
}).join('');
|
||||
var filter = Filter().bar(
|
||||
Filter().field('搜索', Filter().searchInput('settingsSearchQ', filterQ, '按 key 或 value 搜索')),
|
||||
'<button type="button" class="soon-btn soon-btn--sm" id="settingsSearchBtn">搜索</button>' +
|
||||
'<button type="button" class="soon-btn soon-btn--sm soon-btn--ghost" id="settingsSearchReset">重置</button>'
|
||||
);
|
||||
return filter + Ui().dataTable(['配置项', '当前值', '更新时间', '操作'], rows);
|
||||
}
|
||||
|
||||
function bindSiteForm() {
|
||||
Ui().bindFormSubmit('settingsSiteSave', function () {
|
||||
var items = siteFields().map(function (f) {
|
||||
var el = Ui().el('cfg_' + f.key.replace(/\./g, '_'));
|
||||
return { key: f.key, value: el ? el.value.trim() : '' };
|
||||
});
|
||||
return Api().postJson('settings/batch', { items: items }).then(function (r) {
|
||||
if (r) {
|
||||
soonToast('站点配置已保存', 'success');
|
||||
render();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function bindAllSettings() {
|
||||
var st = State().settings;
|
||||
var searchBtn = Ui().el('settingsSearchBtn');
|
||||
var resetBtn = Ui().el('settingsSearchReset');
|
||||
if (searchBtn) {
|
||||
searchBtn.onclick = function () {
|
||||
st.q = Ui().el('settingsSearchQ').value.trim();
|
||||
st.tab = 'all';
|
||||
Router().syncQuery();
|
||||
render();
|
||||
};
|
||||
}
|
||||
if (resetBtn) {
|
||||
resetBtn.onclick = function () {
|
||||
st.q = '';
|
||||
st.tab = 'all';
|
||||
Router().syncQuery();
|
||||
render();
|
||||
};
|
||||
}
|
||||
var searchInput = Ui().el('settingsSearchQ');
|
||||
if (searchInput) {
|
||||
searchInput.onkeydown = function (e) {
|
||||
if (e.key === 'Enter' && searchBtn) searchBtn.click();
|
||||
};
|
||||
}
|
||||
document.querySelectorAll('#panel-settings .setting-edit').forEach(function (btn) {
|
||||
btn.onclick = function () {
|
||||
var key = btn.dataset.key;
|
||||
var val = valuesByKey[key] != null ? valuesByKey[key] : '';
|
||||
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>';
|
||||
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();
|
||||
soonToast('配置已保存', 'success');
|
||||
render();
|
||||
}
|
||||
}).finally(function () {
|
||||
Ui().setBtnLoading(saveBtn, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function render() {
|
||||
var panel = Ui().el('panel-settings');
|
||||
if (!panel) return;
|
||||
var st = State().settings;
|
||||
var header = Ui().pageHeader('系统设置', '站点地址与运行时参数;修改后即时生效');
|
||||
panel.innerHTML = '<div class="soon-admin-page">' + header + Ui().skeletonTable() + '</div>';
|
||||
|
||||
Api().get('settings').then(function (j) {
|
||||
if (!j) {
|
||||
panel.innerHTML = '<div class="soon-admin-page">' + header +
|
||||
Ui().listError('设置加载失败', 'settingsRetry') + '</div>';
|
||||
Ui().el('settingsRetry').onclick = render;
|
||||
return;
|
||||
}
|
||||
valuesByKey = {};
|
||||
allItems = j.data.items || [];
|
||||
allItems.forEach(function (s) { valuesByKey[s.key] = s.value; });
|
||||
|
||||
var tabsHtml = Tabs().html('settingsTabs', [
|
||||
{ id: 'site', label: '站点配置' },
|
||||
{ id: 'all', label: '全部配置' },
|
||||
], {
|
||||
site: siteFormHtml(),
|
||||
all: allSettingsHtml(st.q),
|
||||
}, st.tab);
|
||||
|
||||
panel.innerHTML = '<div class="soon-admin-page">' + header + tabsHtml + '</div>';
|
||||
Tabs().bind(Ui().el('settingsTabs'), function (tabId) {
|
||||
st.tab = tabId;
|
||||
Router().syncQuery();
|
||||
});
|
||||
bindSiteForm();
|
||||
bindAllSettings();
|
||||
});
|
||||
}
|
||||
|
||||
window.AdminViews = window.AdminViews || {};
|
||||
window.AdminViews.settings = { render: render };
|
||||
})();
|
||||
Reference in New Issue
Block a user