Commit bf18719d authored by 黄同智's avatar 黄同智

交易明细页面万层

Co-authored-by: 's avatarCopilot <copilot@github.com>
parent f813e614
......@@ -4,6 +4,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="calendar-plugin/calendar.css">
<title>交易明细查询</title>
<style>
* {
......@@ -61,10 +62,6 @@
/* gap: 10px; */
}
.form-row .dateimg {
width: 27px;
height: 27px;
}
.form-label {
width: 120px;
......@@ -103,10 +100,6 @@
border-top: 8px solid #b0b0b0;
}
.form-select option:first-child {
color: #eee;
}
.form-select.hide {
appearance: none;
}
......@@ -218,6 +211,27 @@
.mt-20 {
margin-top: 20px;
}
.date-picker {
width: 27px;
height: 27px;
position: relative;
}
.date-picker .date-input {
position: absolute;
top: 0;
left: 0;
width: 27px;
height: 27px;
opacity: 0;
z-index: 1;
}
.date-picker .dateimg {
width: 27px;
height: 27px;
}
</style>
</head>
......@@ -243,97 +257,104 @@
<div class="form-container">
<div class="form-title">交易明细查询</div>
<div class="form-row">
<label class="form-label"><span class="required">*</span>查询类型:</label>
<select class="form-select">
<option>明细</option>
</select>
</div>
<div class="form-row">
<label class="form-label"><span class="required">*</span>起止日期:</label>
<input type="text" class="form-input small x-small" value="20260428">
<img class="dateimg" src="/imagse/date.png" alt="" />
<span>-</span>
<input type="text" class="form-input small x-small" value="20260428">
<img class="dateimg" src="/imagse/date.png" alt="" />
<div class="radio-group">
<label><input type="radio" name="dateRange" /> 最近7天</label>
<label><input type="radio" name="dateRange" /> 最近14天</label>
<label><input type="radio" name="dateRange" /> 最近30天</label>
<form id="myForm">
<div class="form-row">
<label class="form-label"><span class="required">*</span>查询类型:</label>
<select class="form-select" name="voucherType">
<option value="1">明细</option>
<option value="2">明细汇总</option>
</select>
</div>
<div class="form-row">
<label class="form-label"><span class="required">*</span>起止日期:</label>
<input type="text" id="startDate" class="form-input small x-small" name="dateFrom">
<div class="date-picker" id="sDate">
<img class="dateimg" src="/imagse/date.png" alt="" />
</div>
<span>-</span>
<input type="text" id="endDate" class="form-input small x-small" name="dateTo">
<div class="date-picker" id="eDate">
<img class="dateimg" src="/imagse/date.png" alt="" />
<!-- <input class="date-input endDate" type="date" /> -->
</div>
<div class="radio-group">
<label><input type="radio" name="quickDate" value="7" /> 最近7天</label>
<label><input type="radio" name="quickDate" value="14" /> 最近14天</label>
<label><input type="radio" name="quickDate" value="30" /> 最近30天</label>
</div>
<span class="form-question question1"></span>
</div>
<div class="form-row">
<label class="form-label">交易金额:</label>
<input type="text" class="form-input small" name="amtFrom">
<span class="px-2">-</span>
<input type="text" class="form-input small" name="amtTo">
</div>
<span class="form-question question1"></span>
</div>
<div class="form-row">
<label class="form-label">交易金额:</label>
<input type="text" class="form-input small">
<span class="px-2">-</span>
<input type="text" class="form-input small">
</div>
<div class="form-row">
<label class="form-label">交易方向:</label>
<div class="radio-group">
<label><input type="radio" name="direction" checked> 全部</label>
<label><input type="radio" name="direction"> 转入</label>
<label><input type="radio" name="direction"> 转出</label>
<div class="form-row">
<label class="form-label">交易方向:</label>
<div class="radio-group">
<label><input type="radio" name="direction" checked value="all"> 全部</label>
<label><input type="radio" name="direction" value="in"> 转入</label>
<label><input type="radio" name="direction" value="out"> 转出</label>
</div>
</div>
<div class="form-row mt-24">
<label class="form-label">冲正交易:</label>
<div class="radio-group">
<label><input type="radio" name="reverse" checked> 显示</label>
<label><input type="radio" name="reverse"> 不显示</label>
</div>
<span class="form-question question2"></span>
</div>
<div class="form-row mt-20">
<label class="form-label">对方户名:</label>
<input type="text" class="form-input wide" name="counterName">
<span class="form-tip">录入生僻字</span>
</div>
<div class="form-row">
<label class="form-label">对方账号:</label>
<input type="text" class="form-input wide" name="counterAccount">
</div>
</div>
<div class="form-row mt-24">
<label class="form-label">冲正交易:</label>
<div class="radio-group">
<label><input type="radio" name="reversal" checked> 显示</label>
<label><input type="radio" name="reversal"> 不显示</label>
<div class="form-row">
<label class="form-label">交易备注:</label>
<input type="text" class="form-input wide" name="remark">
<span class="form-tip">录入生僻字</span>
</div>
<div class="form-row">
<label class="form-label">交易摘要:</label>
<select class="form-select hide" style="width: 200px;" name="summary">
<option>请选择</option>
</select>
<div class="select-down-icon">
<div class="down-arrow"></div>
</div>
</div>
<div class="form-row">
<label class="form-label">起止明细编号:</label>
<input type="text" class="form-input small" name="detailFrom">
<span class="px-2">-</span>
<input type="text" class="form-input small" name="detailTo">
<span class="form-question"></span>
</div>
<span class="form-question question2"></span>
</div>
<div class="form-row mt-20">
<label class="form-label">对方户名:</label>
<input type="text" class="form-input wide">
<span class="form-tip">录入生僻字</span>
</div>
<div class="form-row">
<label class="form-label">对方账号:</label>
<input type="text" class="form-input wide">
</div>
<div class="form-row">
<label class="form-label">交易备注:</label>
<input type="text" class="form-input wide">
<span class="form-tip">录入生僻字</span>
</div>
<input type="date" id="dateInput" />
<div class="form-row">
<label class="form-label">交易摘要:</label>
<select class="form-select hide" style="width: 200px;">
<option>请选择</option>
<option>请选择</option>
<option>请选择</option>
</select>
<div class="select-down-icon">
<div class="down-arrow"></div>
<!-- 按钮组 -->
<div class="btn-group">
<button onclick="onAccount(1)">确定</button>
<button onclick="onAccount(2)">返回</button>
<button onclick="onAccount(3)" type="submit">查询长期</button>
</div>
</div>
<div class="form-row">
<label class="form-label">起止明细编号:</label>
<input type="text" class="form-input small">
<span class="px-2">-</span>
<input type="text" class="form-input small">
<span class="form-question"></span>
</div>
<!-- 按钮组 -->
<div class="btn-group">
<button onclick="onAccount(1)">确定</button>
<button onclick="onAccount(2)">返回</button>
<button onclick="onAccount(3)">查询长期</button>
</div>
</form>
</div>
<input type="date" />
<!-- 温馨提示 -->
<div class="tips-section">
......@@ -341,18 +362,64 @@
<p>1、签约汇总记账账户仅提供历史明细查询。</p>
<p>2、当前交易明细查询页面仅支持查询最近一年半的账户明细,“查询长期”支持查询最近十年的账户明细,如需查询十年前的账户明细请联系您的开户行。</p>
</div>
<script src="calendar-plugin/calendar.js"></script>
</body>
</html>
<script>
const pickerStart = new CalendarPlugin({
trigger: '#sDate',
onSelect: function (date) {
const result = date.replaceAll('-', '');
document.getElementById('startDate').value = result || '未选择';
}
});
const pickerEnd = new CalendarPlugin({
trigger: '#eDate',
onSelect: function (date) {
const result = date.replaceAll('-', '');
document.getElementById('endDate').value = result || '未选择';
}
});
// const dateStartInput = document.querySelector('.startDate');
// const dateEndInput = document.querySelector('.endDate');
// const startDate = document.querySelector('#startDate');
// const endDate = document.querySelector('#endDate');
// dateStartInput.addEventListener('change', function() {
// const result = this.value.replaceAll('-', '');
// startDate.value = result; // 格式:YYYYMMDD
// });
// dateEndInput.addEventListener('change', function() {
// const result = this.value.replaceAll('-', '');
// endDate.value = result; // 格式:YYYYMMDD
// });
const form = document.getElementById('myForm');
function onAccount(number) {
if (number == 2) {
window.history.back();
}
if (number == 3) {
window.location.href = './account_list.html'
form.addEventListener('submit', async (e) => {
e.preventDefault(); // 阻止页面刷新
// 方式1:使用 FormData 自动搜集
const formData = new FormData(form);
console.log(4444, Object.fromEntries(formData.entries()));
localStorage.setItem('accountQueryParams', JSON.stringify(Object.fromEntries(formData.entries())));
setTimeout(() => {
window.location.href = './account_list.html'
}, 500);
});
}
}
</script>
\ No newline at end of file
......@@ -396,7 +396,7 @@
</body>
</html>
<script>
console.log(2222, localStorage.getItem('accountQueryParams'))
function onBack(){
window.history.back();
}
......
/* calendar.css - 日历插件样式 */
.calendar-plugin-container {
position: relative;
display: inline-block;
}
.calendar-plugin-trigger {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background-color: #f0f2f5;
border: 1px solid #d1d9e6;
border-radius: 8px;
font-size: 14px;
color: #1f2a3e;
cursor: pointer;
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
transition: background 0.2s;
}
.calendar-plugin-trigger:hover {
background-color: #e6e9f0;
}
.calendar-plugin-popup {
position: absolute;
z-index: 9999;
background: #ffffff;
border: 1px solid #c5c5c5;
width: 230px;
font-family: system-ui, sans-serif;
opacity: 0;
visibility: hidden;
transition: opacity 0.15s ease, visibility 0.15s;
pointer-events: none;
}
.calendar-plugin-popup.open {
opacity: 1;
visibility: visible;
pointer-events: auto;
}
.calendar-plugin-inner {
padding: 4px 10px 2px 10px;
position: relative;
}
.calendar-nav-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 13px;
}
.calendar-nav-header .date-num {
padding-top: 6px;
color: #1f93d5;
}
.calendar-nav-header .date-text {
padding-top: 6px;
}
.year-month-area {
font-size: 18px;
font-weight: 560;
color: #1e2f3e;
background: #f8fafc;
padding: 4px 12px;
border-radius: 24px;
}
.nav-group {
display: flex;
justify-content: space-between;
gap: 6px;
padding: 4px 8px;
}
.nav-icon-btn {
background: transparent;
border: none;
font-size: 12px;
cursor: pointer;
/* width: 28px; */
border-radius: 20px;
}
.lashen {
transform: scaleY(1.8);
}
.nav-icon-btn.today {
font-size: 14px;
width: auto;
background: #1f93d5;
color: #fff;
border: none;
border-radius: 0;
padding: 3px 14px;
}
/* .nav-icon-btn:hover {
background: #e2e8f0;
} */
.flex {
display: flex;
align-items: center;
gap: 20px;
}
.action-buttons {
display: flex;
gap: 12px;
margin-bottom: 18px;
}
.action-btn {
flex: 1;
background: #FFFFF5;
border: none;
padding: 3px 30px;
cursor: pointer;
color: #626262;
position: absolute;
bottom: 40px;
right: 14px
}
.action-btn:hover {
background: #e2e8f0;
}
.week-row {
display: grid;
grid-template-columns: repeat(7, 1fr);
text-align: center;
margin-bottom: 4px;
font-size: 14px;
color: #fff;
background-color: #1f93d5;
line-height: 28px;
}
.days-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 2px;
margin-bottom: 6px;
}
.day-cell {
text-align: center;
padding: 4px 0;
font-size: 14px;
cursor: pointer;
/* border-radius: 24px; */
color: #4b5563;
}
.day-cell:hover {
background: #eef2ff;
}
.day-cell.other-month {
color: #b9c8e8;
}
.day-cell.selected {
background: #1f93d5;
color: white;
}
.footer-tip {
font-size: 10px;
color: #9aaebe;
text-align: center;
border-top: 1px solid #ecf3fa;
padding-top: 8px;
}
/* .calendar-plugin-popup::before {
content: '';
position: absolute;
width: 10px;
height: 10px;
background: white;
border-left: 1px solid #c5c5c5;
border-top: 1px solid #c5c5c5;
transform: rotate(45deg);
top: -6px;
left: 20px;
} */
.popup-arrow-bottom::before {
top: auto;
bottom: -6px;
transform: rotate(225deg);
}
\ No newline at end of file
/* calendar.js - 日历插件核心 */
(function(global) {
class CalendarPlugin {
constructor(options) {
this.options = Object.assign({
trigger: null, // 触发按钮(必填)
onSelect: null, // 选中回调
initialDate: null, // 初始日期
zIndex: 9999
}, options);
this.triggerEl = null;
this.popupEl = null;
this.isOpen = false;
this.currentYear = 2026;
this.currentMonth = 3;
this.selectedDate = null;
this.init();
}
init() {
if (!this.options.trigger) {
console.error('[CalendarPlugin] 缺少 trigger 参数');
return;
}
this.triggerEl = typeof this.options.trigger === 'string'
? document.querySelector(this.options.trigger)
: this.options.trigger;
if (!this.triggerEl) return;
this.createPopup();
this.bindEvents();
this.handleInitialDate();
// 窗口变化重新定位
window.addEventListener('resize', () => this.isOpen && this.positionPopup());
window.addEventListener('scroll', () => this.isOpen && this.positionPopup(), true);
}
handleInitialDate() {
if (this.options.initialDate) {
const parsed = this.parseDate(this.options.initialDate);
if (parsed) {
this.selectedDate = parsed;
this.currentYear = parsed.year;
this.currentMonth = parsed.month;
}
}
this.renderCalendar();
}
createPopup() {
this.popupEl = document.createElement('div');
this.popupEl.className = 'calendar-plugin-popup';
this.popupEl.style.position = 'absolute';
this.popupEl.style.zIndex = this.options.zIndex;
this.popupEl.innerHTML = `
<div class="calendar-plugin-inner">
<div class="calendar-nav-header">
<div class="nav-icon-btn lashen" data-action="prevMonth"><</div>
<span class="date-num" id="calYear">2026</span>
<span class="date-text">年</span>
<span class="date-num" id="calMonth">四</span>
<span class="date-text">月</span>
<div class="nav-icon-btn lashen" data-action="nextMonth">></div>
</div>
<div class="week-row">
<div>日</div><div>一</div><div>二</div><div>三</div><div>四</div><div>五</div><div>六</div>
</div>
<div class="days-grid" id="calDaysGrid"></div>
<div class="nav-group">
<div class="flex">
<div type="button" class="nav-icon-btn lashen" data-action="prevYear" title="上一年"><<</div>
<div type="button" class="nav-icon-btn lashen" data-action="prevMonth" title="上一月"><</div>
</div>
<button type="button" class="nav-icon-btn today" data-action="today" title="今天">今天</button>
<div class="flex">
<div type="button" class="nav-icon-btn lashen" data-action="nextMonth" title="下一月">></div>
<div type="button" class="nav-icon-btn lashen" data-action="nextYear" title="下一年">>></div>
</div>
</div>
<button class="action-btn" data-action="clear">清空</button>
</div>
`;
document.body.appendChild(this.popupEl);
}
bindEvents() {
// 触发按钮点击
this.triggerEl.addEventListener('click', (e) => {
e.stopPropagation();
this.toggle();
});
// 全局点击关闭
document.addEventListener('click', (e) => {
if (this.isOpen && this.popupEl &&
!this.popupEl.contains(e.target) &&
e.target !== this.triggerEl) {
this.close();
}
});
// 按钮事件
this.popupEl.querySelectorAll('[data-action]').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
this.handleAction(btn.dataset.action);
});
});
// 日期点击委托
this.popupEl.querySelector('#calDaysGrid').addEventListener('click', (e) => {
const cell = e.target.closest('.day-cell');
if (cell && cell.dataset.year) {
this.selectDate(
parseInt(cell.dataset.year),
parseInt(cell.dataset.month),
parseInt(cell.dataset.day)
);
}
});
}
handleAction(action) {
switch(action) {
case 'prevYear': this.currentYear--; break;
case 'nextYear': this.currentYear++; break;
case 'prevMonth':
this.currentMonth--;
if (this.currentMonth < 0) {
this.currentMonth = 11;
this.currentYear--;
}
break;
case 'nextMonth':
this.currentMonth++;
if (this.currentMonth > 11) {
this.currentMonth = 0;
this.currentYear++;
}
break;
case 'today':
const today = new Date();
this.selectDate(today.getFullYear(), today.getMonth(), today.getDate());
return;
case 'clear':
this.selectedDate = null;
this.renderCalendar();
if (this.options.onSelect) this.options.onSelect('');
this.close();
return;
}
this.renderCalendar();
}
selectDate(year, month, day) {
this.selectedDate = { year, month, day };
this.renderCalendar();
const dateStr = `${year}-${String(month+1).padStart(2,'0')}-${String(day).padStart(2,'0')}`;
if (this.options.onSelect) this.options.onSelect(dateStr);
this.close();
}
renderCalendar() {
const year = this.currentYear;
const month = this.currentMonth;
const monthNames = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二'];
// 更新标题
const titleEl = this.popupEl.querySelector('#calYearMonth');
const calYear = this.popupEl.querySelector('#calYear');
const calMonth = this.popupEl.querySelector('#calMonth');
if (titleEl) titleEl.innerText = `${year}${monthNames[month]}月`;
if (calYear) calYear.innerText = year;
if (calMonth) calMonth.innerText = monthNames[month];
// 计算日期数组
const firstDay = new Date(year, month, 1);
const startWeekday = firstDay.getDay();
const lastDate = new Date(year, month + 1, 0).getDate();
const prevLastDate = new Date(year, month, 0).getDate();
let days = [];
// 上月
for (let i = 0; i < startWeekday; i++) {
let prevY = month === 0 ? year - 1 : year;
let prevM = month === 0 ? 11 : month - 1;
days.push({ year: prevY, month: prevM, day: prevLastDate - startWeekday + i + 1, isCurrent: false });
}
// 当月
for (let i = 1; i <= lastDate; i++) {
days.push({ year, month, day: i, isCurrent: true });
}
// 下月
const remaining = 42 - days.length;
for (let i = 1; i <= remaining; i++) {
let nextY = month === 11 ? year + 1 : year;
let nextM = month === 11 ? 0 : month + 1;
days.push({ year: nextY, month: nextM, day: i, isCurrent: false });
}
// 渲染网格
const grid = this.popupEl.querySelector('#calDaysGrid');
grid.innerHTML = '';
const today = new Date();
days.forEach(d => {
const cell = document.createElement('div');
cell.className = 'day-cell';
if (!d.isCurrent) cell.classList.add('other-month');
if (this.selectedDate && this.selectedDate.year === d.year &&
this.selectedDate.month === d.month && this.selectedDate.day === d.day) {
cell.classList.add('selected');
}
cell.innerText = d.day;
cell.dataset.year = d.year;
cell.dataset.month = d.month;
cell.dataset.day = d.day;
grid.appendChild(cell);
});
}
positionPopup() {
const trigRect = this.triggerEl.getBoundingClientRect();
const popRect = this.popupEl.getBoundingClientRect();
let top = trigRect.bottom + window.scrollY + 6;
let left = trigRect.left + window.scrollX;
if (top + popRect.height > window.scrollY + window.innerHeight - 10) {
top = trigRect.top + window.scrollY - popRect.height - 6;
this.popupEl.classList.add('popup-arrow-bottom');
} else {
this.popupEl.classList.remove('popup-arrow-bottom');
}
if (left + popRect.width > window.scrollX + window.innerWidth - 8) {
left = trigRect.right + window.scrollX - popRect.width;
}
this.popupEl.style.top = `${top}px`;
this.popupEl.style.left = `${Math.max(left, window.scrollX + 4)}px`;
}
parseDate(dateStr) {
const parts = dateStr.split('-');
if (parts.length === 3) {
return {
year: parseInt(parts[0]),
month: parseInt(parts[1]) - 1,
day: parseInt(parts[2])
};
}
return null;
}
open() {
if (this.isOpen) return;
this.renderCalendar();
this.positionPopup();
this.popupEl.classList.add('open');
this.isOpen = true;
}
close() {
if (!this.isOpen) return;
this.popupEl.classList.remove('open');
this.isOpen = false;
}
toggle() {
this.isOpen ? this.close() : this.open();
}
destroy() {
if (this.popupEl) this.popupEl.remove();
}
}
// 暴露到全局
global.CalendarPlugin = CalendarPlugin;
})(window);
\ No newline at end of file
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>原生风格日历插件 - 按钮定位弹窗</title>
<style>
/* calendar.css - 日历插件样式 */
.calendar-plugin-container {
position: relative;
display: inline-block;
}
.calendar-plugin-trigger {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background-color: #f0f2f5;
border: 1px solid #d1d9e6;
border-radius: 8px;
font-size: 14px;
color: #1f2a3e;
cursor: pointer;
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
transition: background 0.2s;
}
.calendar-plugin-trigger:hover {
background-color: #e6e9f0;
}
.calendar-plugin-popup {
position: absolute;
z-index: 9999;
background: #ffffff;
border: 1px solid #e2e8f0;
width: 230px;
font-family: system-ui, sans-serif;
opacity: 0;
visibility: hidden;
transition: opacity 0.15s ease, visibility 0.15s;
pointer-events: none;
}
.calendar-plugin-popup.open {
opacity: 1;
visibility: visible;
pointer-events: auto;
}
.calendar-plugin-inner {
padding: 10px;
}
.calendar-nav-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 18px;
}
.calendar-nav-header .date-num {
padding-top: 2px;
color: #1f93d5;
}
.calendar-nav-header .date-text {
padding-top: 2px;
}
.year-month-area {
font-size: 18px;
font-weight: 560;
color: #1e2f3e;
background: #f8fafc;
padding: 4px 12px;
border-radius: 24px;
}
.nav-group {
display: flex;
justify-content: space-between;
gap: 6px;
padding: 4px 8px;
}
.nav-icon-btn {
background: transparent;
border: none;
font-size: 12px;
cursor: pointer;
/* width: 28px; */
border-radius: 20px;
}
.lashen {
transform: scaleY(1.8);
}
.nav-icon-btn.today {
font-size: 14px;
width: auto;
background: #1f93d5;
color: #fff;
border: none;
border-radius: 0;
padding: 3px 14px;
}
/* .nav-icon-btn:hover {
background: #e2e8f0;
} */
.flex {
display: flex;
align-items: center;
gap: 20px;
}
.action-buttons {
display: flex;
gap: 12px;
margin-bottom: 18px;
}
.action-btn {
flex: 1;
background: #f1f5f9;
border: none;
padding: 6px 0;
border-radius: 24px;
font-size: 13px;
cursor: pointer;
}
.action-btn:hover {
background: #e2e8f0;
}
.week-row {
display: grid;
grid-template-columns: repeat(7, 1fr);
text-align: center;
margin-bottom: 10px;
font-size: 14px;
color: #fff;
background-color: #1f93d5;
line-height: 28px;
}
.days-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 2px;
margin-bottom: 12px;
}
.day-cell {
text-align: center;
padding: 4px 0;
font-size: 14px;
cursor: pointer;
/* border-radius: 24px; */
color: #4b5563;
}
.day-cell:hover {
background: #eef2ff;
}
.day-cell.other-month {
color: #b9c8e8;
}
.day-cell.selected {
background: #2c6bff;
color: white;
}
.footer-tip {
font-size: 10px;
color: #9aaebe;
text-align: center;
border-top: 1px solid #ecf3fa;
padding-top: 8px;
}
.calendar-plugin-popup::before {
content: '';
position: absolute;
width: 10px;
height: 10px;
background: white;
border-left: 1px solid #e2e8f0;
border-top: 1px solid #e2e8f0;
transform: rotate(45deg);
top: -6px;
left: 20px;
}
.popup-arrow-bottom::before {
top: auto;
bottom: -6px;
transform: rotate(225deg);
}
</style>
</head>
<body style="padding: 60px; background: #f2f4f8;">
<!-- 演示区域: 多个按钮展示插件独立性和位置自适应 -->
<div style="display: flex; flex-wrap: wrap; gap: 40px; align-items: flex-start;">
<!-- 按钮1 常规位置 -->
<div id="calendar-demo-1"></div>
<!-- 按钮2 靠右位置测试 -->
<div id="calendar-demo-2" style="margin-left: auto;"></div>
<div style="width: 100%; margin-top: 50px;">
<div id="calendar-demo-3"></div>
<p style="margin-top: 24px; font-size: 14px; color: #4b5563; background: #eef2ff; padding: 12px; border-radius: 12px;">
✅ 点击按钮 → 日历弹窗基于按钮位置弹出,无遮罩层。<br>
✅ 功能: 「上一月/下一月」「上一年/下一年」「今天」「清空」。<br>
✅ 样式与截图高度一致:直角感+灰色背景选中态,日期排布完全按照2026年4月样式(29 30 31 1 2... 包含尾部3 4 5 6)。
</p>
</div>
</div>
<script>
/**
* 日历插件 (无依赖,纯原生JS)
* 功能:基于按钮弹出浮层,定位在按钮附近(类似原生日历组件),无遮罩。
* 支持: 上一年/下一年,上一月/下一月,今天,清空,以及日期选中回调。
*
* 用法:
* const picker = new CalendarPopup({
* triggerElement: '#myButton', // 按钮或容器
* onSelect: (dateStr) => { ... } // dateStr格式 YYYY-MM-DD
* });
*/
class CalendarPopup {
constructor(options) {
this.options = Object.assign({
triggerElement: null, // css选择器 或 DOM元素
onSelect: null, // 选中日期回调
initialDate: null, // 初始高亮日期 (yyyy-mm-dd)
zIndex: 9999,
popupClassName: '',
}, options);
this.triggerEl = null;
this.popupEl = null;
this.isOpen = false;
this.currentYear = 2026; // 默认与截图风格展示2026年4月
this.currentMonth = 3; // 3->四月
this.selectedDate = null; // 存储最终选中的日期 {year, month, day}
this.tempSelectedDate = null; // 临时选中(未确认?此处设计选中即确认,符合轻量直接)
// 但是为了交互流畅:点击日期即选中并关闭弹窗。 同时“今天”会直接选中今天并关闭,“清空”清空选择。
// 完全符合按钮弹出快速选择的需求。
this.init();
}
init() {
// 解析触发元素
if (!this.options.triggerElement) {
console.error('[CalendarPopup] 需要提供 triggerElement');
return;
}
this.triggerEl = typeof this.options.triggerElement === 'string'
? document.querySelector(this.options.triggerElement)
: this.options.triggerElement;
if (!this.triggerEl) return;
// 构建弹窗DOM
this.createPopup();
// 绑定触发按钮事件
this.triggerEl.addEventListener('click', (e) => {
e.stopPropagation();
this.togglePopup();
});
// 点击其他地方关闭弹窗(无遮罩层但监听全局点击,合理)
document.addEventListener('click', (e) => {
if (this.isOpen && this.popupEl && !this.popupEl.contains(e.target) && e.target !== this.triggerEl) {
this.closePopup();
}
});
// 可选:初始化日期(如果有 initialDate)
if (this.options.initialDate) {
const parsed = this.parseDateStr(this.options.initialDate);
if (parsed) {
this.selectedDate = parsed;
this.currentYear = parsed.year;
this.currentMonth = parsed.month;
if (this.options.onSelect) this.options.onSelect(this.options.initialDate);
}
}
// 当没有选中日期,默认展示2026年4月界面(截图风格)
if (!this.selectedDate && this.currentYear === 2026 && this.currentMonth === 3) {
// 保持展示四月
} else if (this.selectedDate) {
this.currentYear = this.selectedDate.year;
this.currentMonth = this.selectedDate.month;
}
// 窗口resize时重新调整位置(关闭或重新计算,简洁处理:关闭)
window.addEventListener('resize', () => {
if (this.isOpen) this.positionPopup();
});
window.addEventListener('scroll', () => {
if (this.isOpen) this.positionPopup();
}, true);
}
parseDateStr(dateStr) {
if (!dateStr) return null;
const parts = dateStr.split('-');
if (parts.length === 3) {
const y = parseInt(parts[0], 10);
const m = parseInt(parts[1], 10) - 1;
const d = parseInt(parts[2], 10);
if (!isNaN(y) && !isNaN(m) && !isNaN(d)) return { year: y, month: m, day: d };
}
return null;
}
createPopup() {
if (this.popupEl) return;
this.popupEl = document.createElement('div');
this.popupEl.className = `calendar-plugin-popup ${this.options.popupClassName}`;
this.popupEl.style.position = 'absolute';
this.popupEl.style.zIndex = this.options.zIndex;
document.body.appendChild(this.popupEl);
//
// <div class="action-buttons">
// <button type="button" class="action-btn" data-cal-action="today">今天</button>
// <button type="button" class="action-btn" data-cal-action="clear">清空</button>
// </div>
// <div class="year-month-area" id="cal_year_month_display">2026 四月</div>
// 内部结构
// <div class="action-buttons">
// <button class="action-btn" data-action="today">今天</button>
// <button class="action-btn" data-action="clear">清空</button>
// </div>
this.popupEl.innerHTML = `
<div class="calendar-plugin-inner">
<div class="calendar-nav-header">
<div class="lashen"><</div>
<span class="date-num">2029</span>
<span class="date-text">年</span>
<span class="date-num">四</span>
<span class="month-text">月</span>
<div class="lashen">></div>
</div>
<div class="week-row">
<div>日</div><div>一</div><div>二</div><div>三</div><div>四</div><div>五</div><div>六</div>
</div>
<div class="days-grid" id="cal_days_grid"></div>
<div class="nav-group">
<div class="flex">
<div type="button" class="nav-icon-btn lashen" data-cal-action="prevYear" title="上一年"><<</div>
<div type="button" class="nav-icon-btn lashen" data-cal-action="prevMonth" title="上一月"><</div>
</div>
<button type="button" class="nav-icon-btn today" data-cal-action="prevMonth" title="今天">今天</button>
<div class="flex">
<div type="button" class="nav-icon-btn lashen" data-cal-action="nextMonth" title="下一月">></div>
<div type="button" class="nav-icon-btn lashen" data-cal-action="nextYear" title="下一年">>></div>
</div>
</div>
</div>
`;
// 绑定内部按钮事件
this.bindEvents();
// 初始渲染一次日历内容
this.renderCalendarGrid();
}
bindEvents() {
if (!this.popupEl) return;
// 通过代理绑定
this.popupEl.querySelectorAll('[data-cal-action]').forEach(btn => {
btn.removeEventListener('click', this.handleAction);
btn.addEventListener('click', this.handleAction.bind(this));
});
// 日期网格使用事件委托
const daysGrid = this.popupEl.querySelector('#cal_days_grid');
if (daysGrid) {
daysGrid.removeEventListener('click', this.handleDayClick);
daysGrid.addEventListener('click', this.handleDayClick.bind(this));
}
}
handleAction(e) {
const action = e.currentTarget.getAttribute('data-cal-action');
e.stopPropagation();
switch(action) {
case 'prevYear':
this.currentYear -= 1;
this.renderCalendarGrid();
break;
case 'nextYear':
this.currentYear += 1;
this.renderCalendarGrid();
break;
case 'prevMonth':
let newMonth = this.currentMonth - 1;
let newYear = this.currentYear;
if (newMonth < 0) {
newMonth = 11;
newYear--;
}
this.currentYear = newYear;
this.currentMonth = newMonth;
this.renderCalendarGrid();
break;
case 'nextMonth':
let nMonth = this.currentMonth + 1;
let nYear = this.currentYear;
if (nMonth > 11) {
nMonth = 0;
nYear++;
}
this.currentYear = nYear;
this.currentMonth = nMonth;
this.renderCalendarGrid();
break;
case 'today':
const today = new Date();
const todayYear = today.getFullYear();
const todayMonth = today.getMonth();
const todayDay = today.getDate();
this.selectedDate = { year: todayYear, month: todayMonth, day: todayDay };
this.currentYear = todayYear;
this.currentMonth = todayMonth;
this.renderCalendarGrid();
if (this.options.onSelect) {
const dateStr = `${todayYear}-${String(todayMonth+1).padStart(2,'0')}-${String(todayDay).padStart(2,'0')}`;
this.options.onSelect(dateStr);
}
this.closePopup();
break;
case 'clear':
this.selectedDate = null;
this.renderCalendarGrid();
if (this.options.onSelect) {
this.options.onSelect('');
}
this.closePopup();
break;
default: break;
}
}
handleDayClick(e) {
let target = e.target.closest('.day-cell');
if (!target) return;
const year = parseInt(target.getAttribute('data-year'));
const month = parseInt(target.getAttribute('data-month'));
const day = parseInt(target.getAttribute('data-day'));
if (isNaN(year) || isNaN(month) || isNaN(day)) return;
// 选中日期
this.selectedDate = { year, month, day };
// 重新渲染让选中高亮
this.renderCalendarGrid();
const dateStr = `${year}-${String(month+1).padStart(2,'0')}-${String(day).padStart(2,'0')}`;
if (this.options.onSelect) {
this.options.onSelect(dateStr);
}
// 关闭弹窗
this.closePopup();
}
renderCalendarGrid() {
if (!this.popupEl) return;
const year = this.currentYear;
const month = this.currentMonth;
// 更新头部年月文本
const monthNames = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二'];
const yearMonthSpan = this.popupEl.querySelector('#cal_year_month_display');
if (yearMonthSpan) {
yearMonthSpan.innerText = `${year}${monthNames[month]}月`;
}
// 构建当月日历网格(完全参考截图排布)
const firstDayOfMonth = new Date(year, month, 1);
const startWeekday = firstDayOfMonth.getDay(); // 0周日
const lastDateOfMonth = new Date(year, month + 1, 0).getDate();
const prevMonthDate = new Date(year, month, 0);
const prevMonthTotalDays = prevMonthDate.getDate();
// 准备42格数据 (6行)
let daysArray = [];
// 上月填充
for (let i = 0; i < startWeekday; i++) {
let prevDateNum = prevMonthTotalDays - startWeekday + i + 1;
let prevYear = (month === 0) ? year - 1 : year;
let prevMonth = (month === 0) ? 11 : month - 1;
daysArray.push({
year: prevYear,
month: prevMonth,
day: prevDateNum,
isCurrentMonth: false
});
}
// 当月
for (let i = 1; i <= lastDateOfMonth; i++) {
daysArray.push({
year: year,
month: month,
day: i,
isCurrentMonth: true
});
}
// 补全下月 直到42
const remaining = 42 - daysArray.length;
for (let i = 1; i <= remaining; i++) {
let nextYear = (month === 11) ? year + 1 : year;
let nextMonth = (month === 11) ? 0 : month + 1;
daysArray.push({
year: nextYear,
month: nextMonth,
day: i,
isCurrentMonth: false
});
}
const gridContainer = this.popupEl.querySelector('#cal_days_grid');
if (!gridContainer) return;
gridContainer.innerHTML = '';
// 获取今天用于“今天”样式标记 (但不需要特殊圆点,只作参考)
const todayObj = new Date();
const todayYear = todayObj.getFullYear();
const todayMonth = todayObj.getMonth();
const todayDay = todayObj.getDate();
daysArray.forEach(item => {
const { year: y, month: m, day: d, isCurrentMonth } = item;
const cell = document.createElement('div');
cell.className = 'day-cell';
if (!isCurrentMonth) cell.classList.add('other-month');
// 判定选中样式
if (this.selectedDate && this.selectedDate.year === y && this.selectedDate.month === m && this.selectedDate.day === d) {
cell.classList.add('selected');
}
// 标记今天日期(仅用于视觉,不加额外背景但可以增加淡边,根据需求无关紧要)
if (y === todayYear && m === todayMonth && d === todayDay && !cell.classList.contains('selected')) {
cell.style.fontWeight = '500';
cell.style.textUnderlineOffset = '3px';
cell.style.background = 'red';
cell.style.color = '#fff';
}
cell.innerText = d;
cell.setAttribute('data-year', y);
cell.setAttribute('data-month', m);
cell.setAttribute('data-day', d);
gridContainer.appendChild(cell);
});
}
positionPopup() {
if (!this.popupEl || !this.triggerEl) return;
const triggerRect = this.triggerEl.getBoundingClientRect();
const popupRect = this.popupEl.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
let top = triggerRect.bottom + window.scrollY + 6;
let left = triggerRect.left + window.scrollX;
// 自适应:如果底部空间不足,显示在按钮上方
if (top + popupRect.height > window.scrollY + viewportHeight - 10) {
top = triggerRect.top + window.scrollY - popupRect.height - 6;
}
// 水平边界自适应
if (left + popupRect.width > viewportWidth + window.scrollX - 8) {
left = triggerRect.right + window.scrollX - popupRect.width;
}
if (left < window.scrollX + 4) left = window.scrollX + 4;
this.popupEl.style.top = `${top}px`;
this.popupEl.style.left = `${left}px`;
// 可选修正小箭头方向(简单移除旧类再判断)
this.popupEl.classList.remove('popup-arrow-bottom');
if (top > triggerRect.top + window.scrollY) {
// 在下方,默认箭头朝上,无需额外class
} else {
this.popupEl.classList.add('popup-arrow-bottom');
}
}
openPopup() {
if (this.isOpen) return;
this.renderCalendarGrid();
this.positionPopup();
this.popupEl.classList.add('open');
this.isOpen = true;
// 额外防止父级滚动影响无遮罩体验无影响
}
closePopup() {
if (!this.isOpen) return;
this.popupEl.classList.remove('open');
this.isOpen = false;
}
togglePopup() {
if (this.isOpen) {
this.closePopup();
} else {
// 打开前重新定位并刷新日历视图
this.renderCalendarGrid();
this.positionPopup();
this.openPopup();
}
}
// 外部方法: 手动设置选中日期
setDate(dateStr) {
const parsed = this.parseDateStr(dateStr);
if (parsed) {
this.selectedDate = parsed;
this.currentYear = parsed.year;
this.currentMonth = parsed.month;
this.renderCalendarGrid();
if (this.options.onSelect) this.options.onSelect(dateStr);
}
}
// 销毁插件,移除DOM
destroy() {
if (this.popupEl && this.popupEl.parentNode) this.popupEl.remove();
if (this.triggerEl) {
// 解绑事件
this.triggerEl.removeEventListener('click', this.togglePopup);
}
}
}
// 页面加载完毕后,初始化三个独立日历插件实例 (演示)
document.addEventListener('DOMContentLoaded', () => {
// 实例1: 绑定第一个按钮容器 (生成一个按钮)
const container1 = document.getElementById('calendar-demo-1');
if (container1) {
const btn = document.createElement('button');
btn.className = 'calendar-plugin-trigger';
btn.innerHTML = '📅 选择日期 (实例1)';
container1.appendChild(btn);
new CalendarPopup({
triggerElement: btn,
onSelect: (dateStr) => {
console.log('实例1选中日期:', dateStr);
// 动态改变按钮文本
if (dateStr) btn.innerHTML = `📅 ${dateStr}`;
else btn.innerHTML = '📅 选择日期';
},
initialDate: null
});
}
// 实例2: 右侧独立按钮
const container2 = document.getElementById('calendar-demo-2');
if (container2) {
const btn2 = document.createElement('button');
btn2.className = 'calendar-plugin-trigger';
btn2.innerHTML = '🗓️ 请选择日期 (右侧)';
container2.appendChild(btn2);
new CalendarPopup({
triggerElement: btn2,
onSelect: (dateStr) => {
if (dateStr) btn2.innerHTML = `🗓️ ${dateStr}`;
else btn2.innerHTML = '🗓️ 请选择日期';
}
});
}
// 实例3: 演示置灰今天日期,截图样式完全展示2026年4月风格
const container3 = document.getElementById('calendar-demo-3');
if (container3) {
const btn3 = document.createElement('button');
btn3.className = 'calendar-plugin-trigger';
btn3.innerHTML = '📆 日历 (截图样式)';
container3.appendChild(btn3);
const picker3 = new CalendarPopup({
triggerElement: btn3,
onSelect: (dateStr) => {
if (dateStr) btn3.innerHTML = `📆 ${dateStr}`;
else btn3.innerHTML = '📆 日历 (截图样式)';
},
initialDate: null
});
// 为了保证日历展示默认就是2026年4月风格 (已经在类内部默认设置 currentYear=2026, month=3)
// 同时清空选中完美呈现
picker3.selectedDate = null;
picker3.currentYear = 2026;
picker3.currentMonth = 3;
if (picker3.popupEl) picker3.renderCalendarGrid();
}
});
</script>
</body>
</html>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment