- 账单明细:7 个平台账单分支下推查询,按 active_generation 取当前版本(修正抖音重复计数),跨月大数据量由 73s 降到 1-2s - 筛选:平台/店铺/账单项目/财务类型多选下拉(打字模糊搜索+滚动),日期改日历选择 - 导出:全部筛选结果 CSV 流式下载并按显示列最小取数,带进度轮询;列设置按账号存本机 - 分页:每页 10/20/50,首页/上下页/末页,第 X-Y 条/共 N 条 - 科目映射表:字段改名财务备注/财务类型(导入兼容旧名)
325 lines
13 KiB
PHP
325 lines
13 KiB
PHP
{{--
|
||
开始日期 ─ 至 ─ 结束日期 的日历选择控件(与「小额打款」页同一套交互)。
|
||
用法:
|
||
<div class="upstream-data-field upstream-data-field--dates">
|
||
<span>发生时间</span>
|
||
<div class="upstream-data-date-range">
|
||
<span class="upstream-data-date" data-date-picker>
|
||
<input type="text" name="start" readonly data-date-input>
|
||
<button type="button" class="upstream-data-date__clear" data-date-clear hidden>…</button>
|
||
<button type="button" class="upstream-data-date__pick" data-date-toggle>…</button>
|
||
<div class="upstream-data-calendar" data-date-calendar hidden></div>
|
||
</span>
|
||
<b class="upstream-data-date-range__sep">至</b>
|
||
<span class="upstream-data-date" data-date-picker>…</span>
|
||
</div>
|
||
</div>
|
||
window.CaiWuDatePicker.setup(document.getElementById('pdFilterForm'));
|
||
说明:样式与脚本目前与退款 › 小额打款工单页保持一致(那页是内联实现),
|
||
后续如要合并,把那边也改成引用本 partial。
|
||
--}}
|
||
<style>
|
||
.upstream-data-field--dates { flex: 1 1 20rem; }
|
||
.upstream-data-date-range { display: flex; align-items: center; gap: 0.5rem; }
|
||
.upstream-data-date { position: relative; display: flex; flex: 1 1 8.5rem; min-width: 0; }
|
||
.upstream-data-date input {
|
||
flex: 1 1 auto;
|
||
width: auto;
|
||
min-width: 0;
|
||
padding-right: 1.75rem;
|
||
border-radius: 0.5rem 0 0 0.5rem;
|
||
cursor: pointer;
|
||
}
|
||
.upstream-data-date__clear {
|
||
position: absolute;
|
||
top: 0;
|
||
right: 2.25rem;
|
||
bottom: 0;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 1.5rem;
|
||
height: 1.5rem;
|
||
margin: auto 0;
|
||
padding: 0;
|
||
border: 0;
|
||
border-radius: 0.25rem;
|
||
background: transparent;
|
||
color: var(--vip-content-muted, #595959);
|
||
cursor: pointer;
|
||
}
|
||
.upstream-data-date__clear:hover {
|
||
background: rgba(133, 102, 50, 0.12);
|
||
color: var(--vip-content-accent-dark, #856632);
|
||
}
|
||
.upstream-data-date__clear[hidden] { display: none; }
|
||
.upstream-data-date__clear .material-icons-outlined { font-size: 0.9375rem; }
|
||
.upstream-data-date__pick {
|
||
display: inline-flex;
|
||
flex: 0 0 2.125rem;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 2.125rem;
|
||
height: 2.125rem;
|
||
padding: 0;
|
||
border: 1px solid var(--vip-border, #d8d2c7);
|
||
border-left: 0;
|
||
border-radius: 0 0.5rem 0.5rem 0;
|
||
background: var(--vip-surface, #ffffff);
|
||
color: var(--vip-content-accent-dark, #856632);
|
||
cursor: pointer;
|
||
}
|
||
.upstream-data-date__pick:hover,
|
||
.upstream-data-date__pick:focus-visible {
|
||
background: #f7f0de;
|
||
color: #6a5129;
|
||
outline: none;
|
||
}
|
||
.upstream-data-date__pick .material-icons-outlined { font-size: 1.125rem; }
|
||
.upstream-data-date-range__sep {
|
||
color: var(--vip-content-muted, #595959);
|
||
font-weight: 400;
|
||
}
|
||
.upstream-data-calendar {
|
||
position: absolute;
|
||
top: calc(100% + 0.35rem);
|
||
left: 0;
|
||
z-index: 30;
|
||
width: 16.5rem;
|
||
padding: 0.5rem;
|
||
border: 1px solid var(--vip-border, #d8d2c7);
|
||
border-radius: 0.5rem;
|
||
background: var(--vip-surface, #ffffff);
|
||
box-shadow: 0 4px 8px rgba(23, 23, 23, 0.1);
|
||
}
|
||
.upstream-data-calendar[hidden] { display: none; }
|
||
.upstream-data-calendar__head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 0.25rem;
|
||
margin-bottom: 0.25rem;
|
||
}
|
||
.upstream-data-calendar__title {
|
||
color: var(--vip-content-ink, #171717);
|
||
font-size: 0.8125rem;
|
||
font-weight: 600;
|
||
}
|
||
.upstream-data-calendar__nav {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 1.5rem;
|
||
height: 1.5rem;
|
||
padding: 0;
|
||
border: 0;
|
||
border-radius: 0.25rem;
|
||
background: transparent;
|
||
color: var(--vip-content-accent-dark, #856632);
|
||
cursor: pointer;
|
||
}
|
||
.upstream-data-calendar__nav:hover { background: #f7f0de; }
|
||
.upstream-data-calendar__nav .material-icons-outlined { font-size: 1.125rem; }
|
||
.upstream-data-calendar__week,
|
||
.upstream-data-calendar__grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(7, 1fr);
|
||
gap: 0.125rem;
|
||
}
|
||
.upstream-data-calendar__week span {
|
||
padding: 0.25rem 0;
|
||
color: var(--vip-content-muted, #595959);
|
||
font-size: 0.75rem;
|
||
text-align: center;
|
||
}
|
||
.upstream-data-calendar__day {
|
||
height: 1.75rem;
|
||
padding: 0;
|
||
border: 1px solid transparent;
|
||
border-radius: 0.25rem;
|
||
background: transparent;
|
||
color: var(--vip-content-ink, #171717);
|
||
cursor: pointer;
|
||
font: inherit;
|
||
font-size: 0.75rem;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.upstream-data-calendar__day:hover { background: #f7f0de; }
|
||
.upstream-data-calendar__day.is-outside { color: #b9b3a8; }
|
||
.upstream-data-calendar__day.is-today { border-color: var(--vip-content-accent-dark, #856632); }
|
||
.upstream-data-calendar__day.is-selected {
|
||
border-color: var(--vip-content-accent-dark, #856632);
|
||
background: var(--vip-content-accent-dark, #856632);
|
||
color: #ffffff;
|
||
}
|
||
/* 结束日期的日历向右展开会顶出工具栏,改成向左 */
|
||
.upstream-data-date:last-of-type .upstream-data-calendar {
|
||
right: 0;
|
||
left: auto;
|
||
}
|
||
</style>
|
||
<script>
|
||
(function () {
|
||
if (window.CaiWuDatePicker) { return; }
|
||
|
||
var closers = [];
|
||
var WEEK_LABELS = ['日', '一', '二', '三', '四', '五', '六'];
|
||
|
||
function pad2(number) {
|
||
return (number < 10 ? '0' : '') + number;
|
||
}
|
||
|
||
function isoDate(date) {
|
||
return date.getFullYear() + '-' + pad2(date.getMonth() + 1) + '-' + pad2(date.getDate());
|
||
}
|
||
|
||
function parseIsoDate(text) {
|
||
var match = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(text || '').trim());
|
||
if (!match) { return null; }
|
||
var date = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
|
||
|
||
return date.getFullYear() === Number(match[1])
|
||
&& date.getMonth() === Number(match[2]) - 1
|
||
&& date.getDate() === Number(match[3])
|
||
? date
|
||
: null;
|
||
}
|
||
|
||
function sameDay(left, right) {
|
||
return left.getFullYear() === right.getFullYear()
|
||
&& left.getMonth() === right.getMonth()
|
||
&& left.getDate() === right.getDate();
|
||
}
|
||
|
||
function setupDatePicker(picker) {
|
||
var input = picker.querySelector('[data-date-input]');
|
||
var panel = picker.querySelector('[data-date-calendar]');
|
||
var toggle = picker.querySelector('[data-date-toggle]');
|
||
var clear = picker.querySelector('[data-date-clear]');
|
||
if (!input || !panel || !toggle) { return; }
|
||
|
||
var today = new Date();
|
||
var selected = parseIsoDate(input.value);
|
||
var anchor = selected || today;
|
||
var viewYear = anchor.getFullYear();
|
||
var viewMonth = anchor.getMonth();
|
||
|
||
function isOpen() { return !panel.hidden; }
|
||
|
||
function syncClear() {
|
||
if (clear) { clear.hidden = input.value === ''; }
|
||
}
|
||
|
||
function render() {
|
||
var monthStart = new Date(viewYear, viewMonth, 1);
|
||
var gridStart = new Date(viewYear, viewMonth, 1 - monthStart.getDay());
|
||
var html = '<div class="upstream-data-calendar__head">'
|
||
+ '<button type="button" class="upstream-data-calendar__nav" data-calendar-nav="-1" aria-label="上个月">'
|
||
+ '<span class="material-icons-outlined" aria-hidden="true">chevron_left</span></button>'
|
||
+ '<span class="upstream-data-calendar__title">' + viewYear + ' 年 ' + (viewMonth + 1) + ' 月</span>'
|
||
+ '<button type="button" class="upstream-data-calendar__nav" data-calendar-nav="1" aria-label="下个月">'
|
||
+ '<span class="material-icons-outlined" aria-hidden="true">chevron_right</span></button>'
|
||
+ '</div>'
|
||
+ '<div class="upstream-data-calendar__week">'
|
||
+ WEEK_LABELS.map(function (label) { return '<span>' + label + '</span>'; }).join('')
|
||
+ '</div>'
|
||
+ '<div class="upstream-data-calendar__grid">';
|
||
|
||
for (var offset = 0; offset < 42; offset += 1) {
|
||
var day = new Date(gridStart.getFullYear(), gridStart.getMonth(), gridStart.getDate() + offset);
|
||
var classes = 'upstream-data-calendar__day';
|
||
if (day.getMonth() !== viewMonth) { classes += ' is-outside'; }
|
||
if (sameDay(day, today)) { classes += ' is-today'; }
|
||
if (selected && sameDay(day, selected)) { classes += ' is-selected'; }
|
||
html += '<button type="button" class="' + classes + '" data-calendar-day="'
|
||
+ isoDate(day) + '">' + day.getDate() + '</button>';
|
||
}
|
||
|
||
panel.innerHTML = html + '</div>';
|
||
}
|
||
|
||
function close() {
|
||
if (!isOpen()) { return; }
|
||
panel.hidden = true;
|
||
toggle.setAttribute('aria-expanded', 'false');
|
||
}
|
||
|
||
function open() {
|
||
closers.forEach(function (otherClose) { if (otherClose !== close) { otherClose(); } });
|
||
if (isOpen()) { return; }
|
||
selected = parseIsoDate(input.value);
|
||
anchor = selected || new Date();
|
||
viewYear = anchor.getFullYear();
|
||
viewMonth = anchor.getMonth();
|
||
render();
|
||
panel.hidden = false;
|
||
toggle.setAttribute('aria-expanded', 'true');
|
||
}
|
||
|
||
function shiftMonth(step) {
|
||
anchor = new Date(viewYear, viewMonth + step, 1);
|
||
viewYear = anchor.getFullYear();
|
||
viewMonth = anchor.getMonth();
|
||
render();
|
||
}
|
||
|
||
closers.push(close);
|
||
toggle.setAttribute('aria-expanded', 'false');
|
||
|
||
panel.addEventListener('click', function (event) {
|
||
// 必须拦住冒泡:翻月会重绘面板,被点的箭头随即脱离 DOM,
|
||
// 外层的「点空白处收起」监听会因此误判成点了外面,把日历关掉。
|
||
event.stopPropagation();
|
||
var nav = event.target.closest('[data-calendar-nav]');
|
||
if (nav && panel.contains(nav)) {
|
||
shiftMonth(Number(nav.getAttribute('data-calendar-nav')));
|
||
return;
|
||
}
|
||
var day = event.target.closest('[data-calendar-day]');
|
||
if (day && panel.contains(day)) {
|
||
input.value = day.getAttribute('data-calendar-day');
|
||
selected = parseIsoDate(input.value);
|
||
syncClear();
|
||
close();
|
||
}
|
||
});
|
||
|
||
input.addEventListener('click', open);
|
||
input.addEventListener('keydown', function (event) {
|
||
if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowDown') {
|
||
event.preventDefault();
|
||
open();
|
||
}
|
||
});
|
||
picker.addEventListener('keydown', function (event) {
|
||
if (event.key === 'Escape') { close(); }
|
||
});
|
||
toggle.addEventListener('click', function () {
|
||
if (isOpen()) { close(); } else { open(); }
|
||
});
|
||
if (clear) {
|
||
clear.addEventListener('click', function () {
|
||
input.value = '';
|
||
selected = null;
|
||
syncClear();
|
||
close();
|
||
});
|
||
}
|
||
document.addEventListener('click', function (event) {
|
||
var target = event.target;
|
||
// 目标已经不在文档里(点了会被重绘的日历按钮)时,别当成点了外面
|
||
if (target && target.isConnected === false) { return; }
|
||
if (!picker.contains(target)) { close(); }
|
||
});
|
||
|
||
syncClear();
|
||
}
|
||
|
||
window.CaiWuDatePicker = {
|
||
setup: function (scope) {
|
||
(scope || document).querySelectorAll('[data-date-picker]').forEach(setupDatePicker);
|
||
}
|
||
};
|
||
})();
|
||
</script>
|