|
|
@@ -69,7 +69,8 @@
|
|
|
<span class="ops-tilde">~</span>
|
|
|
<input type="date" class="ops-input" id="rpTo">
|
|
|
</div>
|
|
|
- <div class="rp-filter-chip" id="rpFilterChip" hidden></div>
|
|
|
+ <div class="ops-multi" id="rpSchool"></div>
|
|
|
+ <div class="ops-multi" id="rpShop"></div>
|
|
|
<div class="rp-tabs" id="rpTabs">
|
|
|
<button type="button" data-mode="text" class="on">图文报告</button>
|
|
|
<button type="button" data-mode="interactive">交互报告</button>
|
|
|
@@ -486,9 +487,10 @@
|
|
|
// ---------- 数据 ----------
|
|
|
|
|
|
async function load(force) {
|
|
|
- if (state.loading) return;
|
|
|
+ if (state.loading) { state.pending = true; return; }
|
|
|
state.loading = true;
|
|
|
const page = $('rpPage');
|
|
|
+ if (page) page.classList.add('rp-stale');
|
|
|
const saveBtn = $('rpSave');
|
|
|
if (saveBtn) saveBtn.disabled = true;
|
|
|
if (page && !state.report) page.innerHTML = '<div class="rp-loading">正在生成报告…</div>';
|
|
|
@@ -511,58 +513,85 @@
|
|
|
page.innerHTML = `<div class="rp-loading">报告生成失败:${esc(e.message || e)}</div>`;
|
|
|
} finally {
|
|
|
state.loading = false;
|
|
|
+ if (page) page.classList.remove('rp-stale');
|
|
|
+ if (state.pending) {
|
|
|
+ state.pending = false;
|
|
|
+ load(true);
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- function renderFilterChip() {
|
|
|
- const chip = $('rpFilterChip');
|
|
|
- if (!chip) return;
|
|
|
- const parts = [];
|
|
|
- if (state.schools.length) parts.push(state.schools.length === 1 ? state.schools[0] : `${state.schools.length} 个学校`);
|
|
|
- if (state.shops.length) parts.push(state.shops.length === 1 ? `1 家门店` : `${state.shops.length} 家门店`);
|
|
|
- if (!parts.length) {
|
|
|
- chip.hidden = true;
|
|
|
- chip.innerHTML = '';
|
|
|
- return;
|
|
|
- }
|
|
|
- chip.hidden = false;
|
|
|
- chip.innerHTML = `已按 ${parts.join(' · ')} 筛选 <button type="button" id="rpFilterClear">清除</button>`;
|
|
|
- $('rpFilterClear').addEventListener('click', () => {
|
|
|
- state.schools = [];
|
|
|
- state.shops = [];
|
|
|
- renderFilterChip();
|
|
|
- load(true);
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
async function initDates() {
|
|
|
const search = new URLSearchParams(location.search);
|
|
|
const qFrom = String(search.get('from') || '').replace(/\D/g, '');
|
|
|
const qTo = String(search.get('to') || '').replace(/\D/g, '');
|
|
|
- const qShops = String(search.get('shops') || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
|
- const qSchools = String(search.get('schools') || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
|
- state.shops = qShops;
|
|
|
- state.schools = qSchools;
|
|
|
+ state.shops = String(search.get('shops') || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
|
+ state.schools = String(search.get('schools') || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
|
+
|
|
|
+ const { body } = await api('/api/ops/summary');
|
|
|
+ const summary = body || { schools: [], shops: [] };
|
|
|
+ const coverage = summary.coverage;
|
|
|
const toInput = (ymd) => `${ymd.slice(0, 4)}-${ymd.slice(4, 6)}-${ymd.slice(6, 8)}`;
|
|
|
+
|
|
|
if (/^\d{8}$/.test(qFrom) && /^\d{8}$/.test(qTo)) {
|
|
|
state.from = qFrom;
|
|
|
state.to = qTo;
|
|
|
- $('rpFrom').value = toInput(qFrom);
|
|
|
- $('rpTo').value = toInput(qTo);
|
|
|
- renderFilterChip();
|
|
|
- return;
|
|
|
+ } else if (coverage) {
|
|
|
+ const to = coverage.to;
|
|
|
+ const d = new Date(Date.UTC(Number(to.slice(0, 4)), Number(to.slice(4, 6)) - 1, Number(to.slice(6, 8)) - 6));
|
|
|
+ const from = `${d.getUTCFullYear()}${String(d.getUTCMonth() + 1).padStart(2, '0')}${String(d.getUTCDate()).padStart(2, '0')}`;
|
|
|
+ state.to = to;
|
|
|
+ state.from = from < coverage.from ? coverage.from : from;
|
|
|
}
|
|
|
- const { body } = await api('/api/ops/summary');
|
|
|
- const coverage = body && body.coverage;
|
|
|
- if (!coverage) return;
|
|
|
- const to = coverage.to;
|
|
|
- const d = new Date(Date.UTC(Number(to.slice(0, 4)), Number(to.slice(4, 6)) - 1, Number(to.slice(6, 8)) - 6));
|
|
|
- const from = `${d.getUTCFullYear()}${String(d.getUTCMonth() + 1).padStart(2, '0')}${String(d.getUTCDate()).padStart(2, '0')}`;
|
|
|
- state.to = to;
|
|
|
- state.from = from < coverage.from ? coverage.from : from;
|
|
|
- $('rpFrom').value = toInput(state.from);
|
|
|
- $('rpTo').value = toInput(state.to);
|
|
|
- renderFilterChip();
|
|
|
+ if (state.from && state.to) {
|
|
|
+ $('rpFrom').value = toInput(state.from);
|
|
|
+ $('rpTo').value = toInput(state.to);
|
|
|
+ }
|
|
|
+ mountFilters(summary);
|
|
|
+ syncUrl();
|
|
|
+ }
|
|
|
+
|
|
|
+ function mountFilters(summary) {
|
|
|
+ const schools = (summary.schools || []).map((s) => ({ key: s.name, label: s.name, hint: `${s.shopCount || 0} 家` }));
|
|
|
+ window.createMultiSelect($('rpSchool'), {
|
|
|
+ label: '学校',
|
|
|
+ items: schools,
|
|
|
+ selected: state.schools,
|
|
|
+ allowEmpty: true,
|
|
|
+ emptyLabel: '全部',
|
|
|
+ onChange: (keys) => {
|
|
|
+ state.schools = keys;
|
|
|
+ syncUrl();
|
|
|
+ load(true);
|
|
|
+ },
|
|
|
+ });
|
|
|
+ const shops = (summary.shops || []).map((s) => ({
|
|
|
+ key: String(s.key || s.shopId || ''),
|
|
|
+ label: s.shopName || s.key,
|
|
|
+ hint: s.school || '',
|
|
|
+ })).filter((item) => item.key);
|
|
|
+ window.createMultiSelect($('rpShop'), {
|
|
|
+ label: '店铺',
|
|
|
+ items: shops,
|
|
|
+ selected: state.shops,
|
|
|
+ allowEmpty: true,
|
|
|
+ emptyLabel: '全部',
|
|
|
+ onChange: (keys) => {
|
|
|
+ state.shops = keys;
|
|
|
+ syncUrl();
|
|
|
+ load(true);
|
|
|
+ },
|
|
|
+ });
|
|
|
+ window.bindMultiSelectDismiss();
|
|
|
+ }
|
|
|
+
|
|
|
+ function syncUrl() {
|
|
|
+ const params = new URLSearchParams({ view: 'report' });
|
|
|
+ if (state.from) params.set('from', state.from);
|
|
|
+ if (state.to) params.set('to', state.to);
|
|
|
+ if (state.schools.length) params.set('schools', state.schools.join(','));
|
|
|
+ if (state.shops.length) params.set('shops', state.shops.join(','));
|
|
|
+ history.replaceState(null, '', '/?' + params.toString());
|
|
|
}
|
|
|
|
|
|
window.shanReportInit = function () {
|