Improve dashboard filter controls
This commit is contained in:
+160
-48
@@ -46,17 +46,33 @@
|
|||||||
.alert[hidden] { display: none; }
|
.alert[hidden] { display: none; }
|
||||||
.filters { display: grid; gap: 12px; margin: 0 0 20px; padding: 16px 18px; border: 1px solid var(--border); border-radius: 14px; background: var(--surface); box-shadow: var(--shadow); }
|
.filters { display: grid; gap: 12px; margin: 0 0 20px; padding: 16px 18px; border: 1px solid var(--border); border-radius: 14px; background: var(--surface); box-shadow: var(--shadow); }
|
||||||
.filter-row { display: flex; flex-wrap: wrap; align-items: end; gap: 14px 20px; }
|
.filter-row { display: flex; flex-wrap: wrap; align-items: end; gap: 14px 20px; }
|
||||||
.filter-group { display: grid; gap: 5px; }
|
.filter-group { display: grid; gap: 5px; min-width: 160px; }
|
||||||
.filter-group > span, .filter-group > label { color: var(--muted); font-size: 13px; }
|
.filter-group > span, .filter-group > label { color: var(--muted); font-size: 13px; }
|
||||||
.filter-group input, .filter-group select { min-height: 36px; padding: 6px 9px; border: 1px solid var(--border); border-radius: 8px; background: #fff; color: var(--text); font: inherit; }
|
.filter-group input, .filter-group select { min-height: 36px; padding: 6px 9px; border: 1px solid var(--border); border-radius: 8px; background: #fff; color: var(--text); font: inherit; }
|
||||||
.filter-group select[multiple] { min-width: 220px; min-height: 96px; }
|
|
||||||
.filter-group select:disabled { background: #f1f4f8; color: var(--muted); }
|
.filter-group select:disabled { background: #f1f4f8; color: var(--muted); }
|
||||||
.filter-help { color: var(--muted); font-size: 12px; }
|
.currency-group { min-width: 150px; }
|
||||||
.range-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
.currency-help, .filter-help, .rate-info { color: var(--muted); font-size: 12px; }
|
||||||
.range-button { min-height: 36px; padding: 6px 11px; border: 1px solid var(--border); border-radius: 8px; background: #fff; color: var(--text); cursor: pointer; font: inherit; }
|
.currency-help { line-height: 1.35; }
|
||||||
.range-button:hover, .range-button:focus, .range-button[data-active="true"] { border-color: var(--primary); color: var(--primary); }
|
.date-fields { display: flex; flex-wrap: wrap; gap: 12px 14px; align-items: end; }
|
||||||
.currency-help, .rate-info { color: var(--muted); font-size: 12px; }
|
.date-fields[hidden] { display: none; }
|
||||||
.rate-info { display: flex; flex-wrap: wrap; gap: 5px 10px; align-items: center; }
|
.multi-select { position: relative; min-width: 250px; }
|
||||||
|
.multi-select-trigger { display: flex; width: 100%; min-height: 36px; align-items: center; justify-content: space-between; gap: 12px; padding: 6px 10px; border: 1px solid var(--border); border-radius: 8px; background: #fff; color: var(--text); cursor: pointer; font: inherit; text-align: left; }
|
||||||
|
.multi-select-trigger::after { content: '⌄'; color: var(--muted); }
|
||||||
|
.multi-select-trigger:hover, .multi-select-trigger:focus, .multi-select-trigger[aria-expanded="true"] { border-color: var(--primary); outline: 2px solid rgba(37, 99, 235, 0.12); }
|
||||||
|
.multi-select-trigger:disabled { background: #f1f4f8; color: var(--muted); cursor: not-allowed; }
|
||||||
|
.multi-select-panel { position: absolute; z-index: 10; top: calc(100% + 6px); left: 0; width: min(320px, calc(100vw - 40px)); padding: 10px; border: 1px solid var(--border); border-radius: 10px; background: var(--surface); box-shadow: var(--shadow); }
|
||||||
|
.multi-select-panel[hidden] { display: none; }
|
||||||
|
.multi-select-search { width: 100%; min-height: 34px; margin-bottom: 8px; }
|
||||||
|
.multi-select-list { display: grid; max-height: 220px; gap: 2px; overflow-y: auto; padding: 2px; }
|
||||||
|
.multi-select-option { display: flex; min-height: 32px; align-items: center; gap: 8px; padding: 5px 6px; border-radius: 6px; cursor: pointer; }
|
||||||
|
.multi-select-option:hover, .multi-select-option:focus-within { background: #f1f5ff; }
|
||||||
|
.multi-select-option input { width: 16px; height: 16px; min-height: 0; margin: 0; }
|
||||||
|
.multi-select-empty { padding: 8px 6px; color: var(--muted); font-size: 13px; }
|
||||||
|
.multi-select-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border); }
|
||||||
|
.multi-select-action { min-height: 32px; padding: 5px 10px; border: 1px solid var(--border); border-radius: 7px; background: #fff; color: var(--text); cursor: pointer; font: inherit; }
|
||||||
|
.multi-select-action:hover, .multi-select-action:focus { border-color: var(--primary); color: var(--primary); }
|
||||||
|
.multi-select-action[data-action="confirm"] { border-color: var(--primary); background: var(--primary); color: #fff; }
|
||||||
|
.rate-info { display: flex; flex-wrap: wrap; gap: 4px 10px; align-items: center; }
|
||||||
.rate-info a { color: var(--primary); }
|
.rate-info a { color: var(--primary); }
|
||||||
.metrics { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; margin-bottom: 20px; }
|
.metrics { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; margin-bottom: 20px; }
|
||||||
.metric, .chart-card { border: 1px solid var(--border); border-radius: 14px; background: var(--surface); box-shadow: var(--shadow); }
|
.metric, .chart-card { border: 1px solid var(--border); border-radius: 14px; background: var(--surface); box-shadow: var(--shadow); }
|
||||||
@@ -83,6 +99,8 @@
|
|||||||
.chart { height: 360px; }
|
.chart { height: 360px; }
|
||||||
.filter-row { align-items: stretch; }
|
.filter-row { align-items: stretch; }
|
||||||
.filter-group { flex: 1 1 145px; }
|
.filter-group { flex: 1 1 145px; }
|
||||||
|
.multi-select { min-width: 0; }
|
||||||
|
.multi-select-panel { left: 50%; transform: translateX(-50%); max-width: calc(100vw - 32px); }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@@ -103,20 +121,21 @@
|
|||||||
|
|
||||||
<section class="filters" id="filters" aria-label="筛选与换算" hidden>
|
<section class="filters" id="filters" aria-label="筛选与换算" hidden>
|
||||||
<div class="filter-row">
|
<div class="filter-row">
|
||||||
<div class="filter-group">
|
<div class="filter-group currency-group">
|
||||||
<label for="currency-select">显示币种</label>
|
<label for="currency-select">显示币种</label>
|
||||||
<select id="currency-select"></select>
|
<select id="currency-select"></select>
|
||||||
|
<span class="currency-help" id="currency-help"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
<span>日期范围</span>
|
<label for="range-select">日期范围</label>
|
||||||
<div class="range-actions">
|
<select id="range-select">
|
||||||
<button class="range-button" type="button" data-range="month">当月(按数据最新月份)</button>
|
<option value="month">当月(按数据最新月份)</option>
|
||||||
<button class="range-button" type="button" data-range="recent">最近30天(按数据最新日期)</button>
|
<option value="recent">最近30天</option>
|
||||||
<button class="range-button" type="button" data-range="all">全部</button>
|
<option value="all">全部</option>
|
||||||
|
<option value="custom">自定义</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="date-fields" id="date-fields" hidden>
|
||||||
</div>
|
|
||||||
<div class="filter-row">
|
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
<label for="start-date">开始日期</label>
|
<label for="start-date">开始日期</label>
|
||||||
<input id="start-date" type="date">
|
<input id="start-date" type="date">
|
||||||
@@ -125,12 +144,23 @@
|
|||||||
<label for="end-date">结束日期</label>
|
<label for="end-date">结束日期</label>
|
||||||
<input id="end-date" type="date">
|
<input id="end-date" type="date">
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
<label for="link-name-select">LinkName</label>
|
<label for="link-name-trigger">LinkName</label>
|
||||||
<select id="link-name-select" multiple size="4" disabled aria-describedby="link-name-summary"></select>
|
<div class="multi-select" id="link-name-picker">
|
||||||
|
<button class="multi-select-trigger" id="link-name-trigger" type="button" aria-haspopup="dialog" aria-expanded="false" aria-controls="link-name-panel" disabled>无可选 LinkName</button>
|
||||||
|
<div class="multi-select-panel" id="link-name-panel" role="dialog" aria-label="选择 LinkName" hidden>
|
||||||
|
<input class="multi-select-search" id="link-name-search" type="search" placeholder="搜索 LinkName" aria-label="搜索 LinkName">
|
||||||
|
<div class="multi-select-list" id="link-name-list" role="group"></div>
|
||||||
|
<div class="multi-select-actions">
|
||||||
|
<button class="multi-select-action" type="button" data-action="select-all">全选</button>
|
||||||
|
<button class="multi-select-action" type="button" data-action="clear">清空</button>
|
||||||
|
<button class="multi-select-action" type="button" data-action="confirm">确定</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<span class="filter-help" id="link-name-summary" aria-live="polite">无可选 LinkName</span>
|
<span class="filter-help" id="link-name-summary" aria-live="polite">无可选 LinkName</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="currency-help" id="currency-help"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="rate-info" id="rate-info">
|
<div class="rate-info" id="rate-info">
|
||||||
<span id="rate-status">尚未需要汇率换算</span>
|
<span id="rate-status">尚未需要汇率换算</span>
|
||||||
@@ -193,11 +223,17 @@
|
|||||||
filters: document.getElementById('filters'),
|
filters: document.getElementById('filters'),
|
||||||
currency: document.getElementById('currency-select'),
|
currency: document.getElementById('currency-select'),
|
||||||
currencyHelp: document.getElementById('currency-help'),
|
currencyHelp: document.getElementById('currency-help'),
|
||||||
|
rangeSelect: document.getElementById('range-select'),
|
||||||
|
dateFields: document.getElementById('date-fields'),
|
||||||
startDate: document.getElementById('start-date'),
|
startDate: document.getElementById('start-date'),
|
||||||
endDate: document.getElementById('end-date'),
|
endDate: document.getElementById('end-date'),
|
||||||
linkName: document.getElementById('link-name-select'),
|
linkNamePicker: document.getElementById('link-name-picker'),
|
||||||
|
linkNameTrigger: document.getElementById('link-name-trigger'),
|
||||||
|
linkNamePanel: document.getElementById('link-name-panel'),
|
||||||
|
linkNameSearch: document.getElementById('link-name-search'),
|
||||||
|
linkNameList: document.getElementById('link-name-list'),
|
||||||
|
linkNameActions: Array.from(document.querySelectorAll('[data-action]')),
|
||||||
linkNameSummary: document.getElementById('link-name-summary'),
|
linkNameSummary: document.getElementById('link-name-summary'),
|
||||||
rangeButtons: Array.from(document.querySelectorAll('[data-range]')),
|
|
||||||
rateStatus: document.getElementById('rate-status'),
|
rateStatus: document.getElementById('rate-status'),
|
||||||
metrics: document.getElementById('metrics'),
|
metrics: document.getElementById('metrics'),
|
||||||
recordCount: document.getElementById('record-count'),
|
recordCount: document.getElementById('record-count'),
|
||||||
@@ -223,7 +259,9 @@
|
|||||||
invalidCurrencyRows: 0,
|
invalidCurrencyRows: 0,
|
||||||
syncLog: null,
|
syncLog: null,
|
||||||
syncLogUnavailable: false,
|
syncLogUnavailable: false,
|
||||||
selectedLinkNames: new Set()
|
availableLinkNames: [],
|
||||||
|
selectedLinkNames: new Set(),
|
||||||
|
draftLinkNames: new Set()
|
||||||
};
|
};
|
||||||
|
|
||||||
function setStatus(message, status) {
|
function setStatus(message, status) {
|
||||||
@@ -408,13 +446,19 @@
|
|||||||
var dates = state.rawRecords.map(function (record) { return record.date; }).sort(function (a, b) { return a.time - b.time; });
|
var dates = state.rawRecords.map(function (record) { return record.date; }).sort(function (a, b) { return a.time - b.time; });
|
||||||
var first = dates[0];
|
var first = dates[0];
|
||||||
var latest = dates[dates.length - 1];
|
var latest = dates[dates.length - 1];
|
||||||
|
elements.rangeSelect.value = rangeName;
|
||||||
|
elements.dateFields.hidden = rangeName !== 'custom';
|
||||||
|
if (rangeName === 'custom') {
|
||||||
|
if (!elements.startDate.value) elements.startDate.value = first.key;
|
||||||
|
if (!elements.endDate.value) elements.endDate.value = latest.key;
|
||||||
|
return;
|
||||||
|
}
|
||||||
var start = first;
|
var start = first;
|
||||||
if (rangeName === 'month') start = parseDate(latest.key.slice(0, 7) + '-01');
|
if (rangeName === 'month') start = parseDate(latest.key.slice(0, 7) + '-01');
|
||||||
if (rangeName === 'recent') start = new Date(latest.time - 29 * 24 * 60 * 60 * 1000);
|
if (rangeName === 'recent') start = new Date(latest.time - 29 * 24 * 60 * 60 * 1000);
|
||||||
var startKey = rangeName === 'recent' ? new Date(Date.UTC(start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate())).toISOString().slice(0, 10) : start.key;
|
var startKey = rangeName === 'recent' ? new Date(Date.UTC(start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate())).toISOString().slice(0, 10) : start.key;
|
||||||
elements.startDate.value = startKey < first.key ? first.key : startKey;
|
elements.startDate.value = rangeName === 'all' ? first.key : (startKey < first.key ? first.key : startKey);
|
||||||
elements.endDate.value = latest.key;
|
elements.endDate.value = latest.key;
|
||||||
elements.rangeButtons.forEach(function (button) { button.dataset.active = button.dataset.range === rangeName ? 'true' : 'false'; });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function currentRange() {
|
function currentRange() {
|
||||||
@@ -425,15 +469,65 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateLinkNameSummary() {
|
function updateLinkNameSummary() {
|
||||||
var total = elements.linkName.options.length;
|
var total = state.availableLinkNames.length;
|
||||||
var selected = elements.linkName.selectedOptions.length;
|
var selected = state.selectedLinkNames.size;
|
||||||
|
var label;
|
||||||
if (!total) {
|
if (!total) {
|
||||||
elements.linkNameSummary.textContent = '无可选 LinkName';
|
label = '无可选 LinkName';
|
||||||
} else if (selected === total) {
|
} else if (selected === total) {
|
||||||
elements.linkNameSummary.textContent = '全部 LinkName(' + formatNumber(total) + ')';
|
label = '全部 LinkName';
|
||||||
} else {
|
} else {
|
||||||
elements.linkNameSummary.textContent = '已选 ' + formatNumber(selected) + ' / ' + formatNumber(total) + ' 个 LinkName';
|
label = '已选 ' + formatNumber(selected) + ' 项';
|
||||||
}
|
}
|
||||||
|
elements.linkNameTrigger.textContent = label;
|
||||||
|
elements.linkNameSummary.textContent = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLinkNameOptions() {
|
||||||
|
elements.linkNameList.replaceChildren();
|
||||||
|
var query = elements.linkNameSearch.value.trim().toLocaleLowerCase();
|
||||||
|
var visibleCount = 0;
|
||||||
|
state.availableLinkNames.forEach(function (linkName) {
|
||||||
|
if (query && linkName.toLocaleLowerCase().indexOf(query) === -1) return;
|
||||||
|
visibleCount += 1;
|
||||||
|
var label = document.createElement('label');
|
||||||
|
label.className = 'multi-select-option';
|
||||||
|
var checkbox = document.createElement('input');
|
||||||
|
checkbox.type = 'checkbox';
|
||||||
|
checkbox.value = linkName;
|
||||||
|
checkbox.checked = state.draftLinkNames.has(linkName);
|
||||||
|
checkbox.setAttribute('aria-label', linkName);
|
||||||
|
checkbox.addEventListener('change', function () {
|
||||||
|
if (checkbox.checked) state.draftLinkNames.add(linkName);
|
||||||
|
else state.draftLinkNames.delete(linkName);
|
||||||
|
});
|
||||||
|
var text = document.createElement('span');
|
||||||
|
text.textContent = linkName;
|
||||||
|
label.appendChild(checkbox);
|
||||||
|
label.appendChild(text);
|
||||||
|
elements.linkNameList.appendChild(label);
|
||||||
|
});
|
||||||
|
if (!visibleCount) {
|
||||||
|
var empty = document.createElement('div');
|
||||||
|
empty.className = 'multi-select-empty';
|
||||||
|
empty.textContent = '没有匹配的 LinkName';
|
||||||
|
elements.linkNameList.appendChild(empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeLinkNamePicker() {
|
||||||
|
elements.linkNamePanel.hidden = true;
|
||||||
|
elements.linkNameTrigger.setAttribute('aria-expanded', 'false');
|
||||||
|
}
|
||||||
|
|
||||||
|
function openLinkNamePicker() {
|
||||||
|
if (elements.linkNameTrigger.disabled) return;
|
||||||
|
state.draftLinkNames = new Set(state.selectedLinkNames);
|
||||||
|
elements.linkNameSearch.value = '';
|
||||||
|
renderLinkNameOptions();
|
||||||
|
elements.linkNamePanel.hidden = false;
|
||||||
|
elements.linkNameTrigger.setAttribute('aria-expanded', 'true');
|
||||||
|
elements.linkNameSearch.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateLinkNameOptions() {
|
function updateLinkNameOptions() {
|
||||||
@@ -448,16 +542,12 @@
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
available = [];
|
available = [];
|
||||||
}
|
}
|
||||||
elements.linkName.replaceChildren();
|
state.availableLinkNames = available;
|
||||||
available.forEach(function (linkName) {
|
|
||||||
var option = document.createElement('option');
|
|
||||||
option.value = linkName;
|
|
||||||
option.textContent = linkName;
|
|
||||||
option.selected = true;
|
|
||||||
elements.linkName.appendChild(option);
|
|
||||||
});
|
|
||||||
state.selectedLinkNames = new Set(available);
|
state.selectedLinkNames = new Set(available);
|
||||||
elements.linkName.disabled = available.length === 0;
|
state.draftLinkNames = new Set(available);
|
||||||
|
elements.linkNameTrigger.disabled = available.length === 0;
|
||||||
|
closeLinkNamePicker();
|
||||||
|
renderLinkNameOptions();
|
||||||
updateLinkNameSummary();
|
updateLinkNameSummary();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -696,25 +786,47 @@
|
|||||||
state.selectedCurrency = elements.currency.value;
|
state.selectedCurrency = elements.currency.value;
|
||||||
renderCurrentView();
|
renderCurrentView();
|
||||||
});
|
});
|
||||||
elements.linkName.addEventListener('change', function () {
|
elements.rangeSelect.addEventListener('change', function () {
|
||||||
state.selectedLinkNames = new Set(Array.from(elements.linkName.selectedOptions).map(function (option) {
|
setRange(elements.rangeSelect.value);
|
||||||
return option.value;
|
updateLinkNameOptions();
|
||||||
}));
|
|
||||||
updateLinkNameSummary();
|
|
||||||
renderCurrentView();
|
renderCurrentView();
|
||||||
});
|
});
|
||||||
elements.startDate.addEventListener('change', function () {
|
elements.startDate.addEventListener('change', function () {
|
||||||
elements.rangeButtons.forEach(function (button) { button.dataset.active = 'false'; });
|
|
||||||
updateLinkNameOptions();
|
updateLinkNameOptions();
|
||||||
renderCurrentView();
|
renderCurrentView();
|
||||||
});
|
});
|
||||||
elements.endDate.addEventListener('change', function () {
|
elements.endDate.addEventListener('change', function () {
|
||||||
elements.rangeButtons.forEach(function (button) { button.dataset.active = 'false'; });
|
|
||||||
updateLinkNameOptions();
|
updateLinkNameOptions();
|
||||||
renderCurrentView();
|
renderCurrentView();
|
||||||
});
|
});
|
||||||
elements.rangeButtons.forEach(function (button) {
|
elements.linkNameTrigger.addEventListener('click', function () {
|
||||||
button.addEventListener('click', function () { setRange(button.dataset.range); updateLinkNameOptions(); renderCurrentView(); });
|
if (elements.linkNamePanel.hidden) openLinkNamePicker();
|
||||||
|
else closeLinkNamePicker();
|
||||||
|
});
|
||||||
|
elements.linkNameSearch.addEventListener('input', renderLinkNameOptions);
|
||||||
|
elements.linkNameActions.forEach(function (button) {
|
||||||
|
button.addEventListener('click', function () {
|
||||||
|
var action = button.dataset.action;
|
||||||
|
if (action === 'select-all') state.draftLinkNames = new Set(state.availableLinkNames);
|
||||||
|
if (action === 'clear') state.draftLinkNames = new Set();
|
||||||
|
if (action === 'confirm') {
|
||||||
|
state.selectedLinkNames = new Set(state.draftLinkNames);
|
||||||
|
updateLinkNameSummary();
|
||||||
|
closeLinkNamePicker();
|
||||||
|
renderCurrentView();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
renderLinkNameOptions();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
document.addEventListener('click', function (event) {
|
||||||
|
if (!elements.linkNamePicker.contains(event.target)) closeLinkNamePicker();
|
||||||
|
});
|
||||||
|
document.addEventListener('keydown', function (event) {
|
||||||
|
if (event.key === 'Escape' && !elements.linkNamePanel.hidden) {
|
||||||
|
closeLinkNamePicker();
|
||||||
|
elements.linkNameTrigger.focus();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
window.addEventListener('resize', function () { charts.forEach(function (chart) { chart.resize(); }); });
|
window.addEventListener('resize', function () { charts.forEach(function (chart) { chart.resize(); }); });
|
||||||
loadData();
|
loadData();
|
||||||
|
|||||||
Reference in New Issue
Block a user