chore: 初始提交 - 移除所有硬编码凭据,统一从环境变量读取

This commit is contained in:
yangxiangyuan
2026-06-11 17:10:58 +08:00
commit c223c79b36
1699 changed files with 313940 additions and 0 deletions
+535
View File
@@ -0,0 +1,535 @@
// UI 渲染与交互
const UI = (function () {
const gridEl = () => document.getElementById('calendar-grid');
const monthLabelEl = () => document.getElementById('currentMonth');
const dayEventsEl = () => document.getElementById('dayEvents');
const WEEKDAYS = ['日', '一', '二', '三', '四', '五', '六'];
function isMobile() {
try {
return window.matchMedia && window.matchMedia('(max-width: 768px)').matches || (navigator && navigator.maxTouchPoints > 0);
} catch (_) { return false; }
}
// 创建详细悬停提示框
function createEventTooltip(event) {
let recurText = '';
try {
if (typeof parseRecurrenceStr === 'function') {
const rec = parseRecurrenceStr(event.recurrence || '');
if (rec && rec.type && rec.type !== 'NONE') {
const parts = [];
if (rec.type === 'DAILY') {
parts.push(rec.interval > 1 ? `每 ${rec.interval} 天` : '每天');
} else if (rec.type === 'WEEKLY') {
parts.push(rec.interval > 1 ? `每 ${rec.interval} 周` : '每周');
if (Array.isArray(rec.by) && rec.by.length) {
const map = ['一','二','三','四','五','六','日'];
const names = rec.by.map(n => map[(n - 1 + 7) % 7]).map(ch => '周' + ch);
parts.push(names.join('、'));
}
}
recurText = parts.join(',');
}
}
} catch (_) {}
const tooltip = document.createElement('div');
tooltip.className = 'event-tooltip';
tooltip.innerHTML = `
<div class="tooltip-header">
<strong>${escapeHtml(event.title || '无标题')}</strong>
${event.completed ? '<span class="badge bg-success ms-2">已完成</span>' : ''}
</div>
<div class="tooltip-content">
<table class="tooltip-table">
<tr><th>开始时间:</th><td>${formatDateTime(event.start)}</td></tr>
${event.end ? `<tr><th>结束时间:</th><td>${formatDateTime(event.end)}</td></tr>` : ''}
${event.content ? `<tr><th>内容:</th><td>${escapeHtmlWithLinks(event.content)}</td></tr>` : ''}
${event.remindAt ? `<tr><th>提醒时间:</th><td>${formatDateTime(event.remindAt)}</td></tr>` : ''}
${recurText ? `<tr><th>循环:</th><td>${recurText}</td></tr>` : ''}
<tr><th>状态:</th><td>${event.completed ? '已完成' : '进行中'}</td></tr>
</table>
</div>
`;
return tooltip;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function escapeHtmlWithLinks(text) {
const raw = String(text || '');
const escaped = escapeHtml(raw);
const linkPattern = /((?:https?:\/\/|\/tools\/)[^\s<]+)/g;
return escaped.replace(linkPattern, (m) => {
const href = m.startsWith('/tools/') ? m : m;
return `<a href="${href}" target="_blank" rel="noopener noreferrer">${m}</a>`;
}).replace(/\n/g, '<br>');
}
function formatDateTime(dateString) {
const date = new Date(dateString);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
function renderWeekdayHeader() {
const head = document.createElement('div');
head.className = 'calendar-weekday';
WEEKDAYS.forEach(w => {
const d = document.createElement('div');
d.textContent = w;
head.appendChild(d);
});
return head;
}
function monthStart(year, month) { return new Date(Date.UTC(year, month - 1, 1)); }
function monthEnd(year, month) { return new Date(Date.UTC(year, month, 0)); }
function loadEventIntoForm(e) {
if (!e) return;
const form = document.getElementById('eventForm');
if (!form) return;
form.eventLoaded = e;
const idInput = document.getElementById('eventId');
if (idInput) idInput.value = e.id || '';
const titleInput = document.getElementById('title');
if (titleInput) titleInput.value = e.title || '';
const contentInput = document.getElementById('content');
if (contentInput) contentInput.value = e.content || '';
const completedInput = document.getElementById('completedFlag');
if (completedInput) completedInput.checked = !!e.completed;
const startInput = document.getElementById('start');
if (startInput && e.start) startInput.value = toInputValue(e.start);
const endInput = document.getElementById('end');
if (endInput && e.end) endInput.value = toInputValue(e.end);
const typeSel = document.getElementById('recurrenceType');
const intervalInput = document.getElementById('recurrenceInterval');
const weekdayInputs = Array.from(document.querySelectorAll('#weekdayGroup [data-weekday]'));
const recStr = String(e.recurrence || '');
if (typeSel) {
typeSel.value = recStr.startsWith('DAILY') ? 'DAILY' : (recStr.startsWith('WEEKLY') ? 'WEEKLY' : 'NONE');
}
if (intervalInput) {
const m = /interval=(\d+)/.exec(recStr);
intervalInput.value = m ? Math.max(1, parseInt(m[1], 10)) : 1;
}
const by = (() => {
const m = /by=([0-9,]+)/.exec(recStr);
return m ? m[1].split(',').map(s => parseInt(s, 10)) : [];
})();
weekdayInputs.forEach(el => {
const v = parseInt(el.getAttribute('data-weekday'), 10);
el.checked = by.includes(v);
});
const enableReminderInput = document.getElementById('enableReminder');
const hasRemind = !!e.remindAt;
if (enableReminderInput) enableReminderInput.checked = hasRemind;
const dt = document.getElementById('remindDateTime');
const tm = document.getElementById('remindTime');
const typeVal = typeSel ? typeSel.value : 'NONE';
if (typeVal === 'NONE') {
if (dt) {
dt.classList.remove('d-none');
dt.value = e.remindAt ? toInputValue(e.remindAt) : '';
}
if (tm) tm.classList.add('d-none');
} else {
if (dt) dt.classList.add('d-none');
if (tm) {
tm.classList.remove('d-none');
tm.value = e.remindAt ? toTimeInputValue(e.remindAt) : '';
}
}
const btnDelete = document.getElementById('btnDelete');
if (btnDelete) btnDelete.classList.remove('d-none');
if (isMobile()) {
openEventModal(e);
}
}
// 在 UI.renderCalendar 的事件 pill 点击中,改为填充统一表单字段与解析循环
function renderCalendar(year, month, events) {
const grid = gridEl();
grid.innerHTML = '';
// 清理旧的星期头部,避免重复累加
Array.from(grid.parentElement.querySelectorAll('.calendar-weekday')).forEach(el => el.remove());
const head = renderWeekdayHeader();
grid.parentElement.insertBefore(head, grid);
// 性能优化:建立日期索引,避免在循环中重复遍历所有事件
const eventsByDate = new Map();
// 辅助:生成本地日期 Key YYYY-MM-DD
const toKey = (y, m, d) => `${y}-${String(m).padStart(2,'0')}-${String(d).padStart(2,'0')}`;
// 确定当月范围(本地时间)
const monthStart = new Date(year, month - 1, 1, 0, 0, 0, 0);
const monthEnd = new Date(year, month, 0, 23, 59, 59, 999);
const overlapsDay = (e, dateObj) => {
try {
if (!e || e.deleted || !e.start) return false;
const s = new Date(e.start);
const ep = e.end ? new Date(e.end) : new Date(e.start);
const ds = new Date(dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate(), 0, 0, 0, 0);
const de = new Date(dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate(), 23, 59, 59, 999);
return s <= de && ep >= ds;
} catch (_) { return false; }
};
events.forEach(e => {
if (!e || e.deleted || !e.start) return;
try {
const s = new Date(e.start);
const end = e.end ? new Date(e.end) : new Date(e.start);
// 计算事件涉及的日期范围(截断到当月)
// 转换为本地日期的 0 点进行比较
const sDay = new Date(s.getFullYear(), s.getMonth(), s.getDate());
const eDay = new Date(end.getFullYear(), end.getMonth(), end.getDate());
// 如果事件范围与当月无交集,跳过
if (eDay < monthStart || sDay > monthEnd) return;
const iterStart = sDay < monthStart ? monthStart : sDay;
const iterEnd = eDay > monthEnd ? monthEnd : eDay;
// 遍历涉及的每一天
for (let d = new Date(iterStart); d <= iterEnd; d.setDate(d.getDate() + 1)) {
if (!overlapsDay(e, d)) continue;
const key = toKey(d.getFullYear(), d.getMonth() + 1, d.getDate());
if (!eventsByDate.has(key)) eventsByDate.set(key, []);
eventsByDate.get(key).push(e);
}
} catch (_) {}
});
const start = new Date(Date.UTC(year, month - 1, 1));
const end = new Date(Date.UTC(year, month, 0));
const firstWeekday = start.getUTCDay();
const daysInMonth = end.getUTCDate();
// 补齐上月空白格
for (let i = 0; i < firstWeekday; i++) {
const cell = document.createElement('div');
cell.className = 'calendar-cell is-outside';
cell.innerHTML = `<div class="cell-header"><span></span></div>`;
grid.appendChild(cell);
}
const today = new Date();
const isToday = (d) => d.getFullYear() === today.getFullYear() && d.getMonth() === today.getMonth() && d.getDate() === today.getDate();
const frag = document.createDocumentFragment();
for (let day = 1; day <= daysInMonth; day++) {
const date = new Date(year, month - 1, day); // 本地时间对象用于 isToday 判断
const cell = document.createElement('div');
cell.className = 'calendar-cell' + (isToday(date) ? ' is-today' : '');
const header = document.createElement('div');
header.className = 'cell-header';
header.innerHTML = `<span>${day}</span>`;
const body = document.createElement('div');
body.className = 'events';
const key = toKey(year, month, day);
const esRaw = eventsByDate.get(key) || [];
const es = (function dedupDay(list) {
if (list.length <= 1) return list;
const seenDisplay = new Set();
const seenId = new Set();
return list.filter(e => {
// 优化:减少 Date 对象创建
const sStr = e.start || '';
// 简单提取时分,假设 ISO 格式
let hh = '00', mm = '00';
if (sStr.length >= 16) {
const tPart = new Date(sStr);
hh = String(tPart.getHours()).padStart(2, '0');
mm = String(tPart.getMinutes()).padStart(2, '0');
}
const title = (e.title || '').trim();
const done = e.completed ? '1' : '0';
const dk = `${title}|${hh}:${mm}|${done}`;
if (seenDisplay.has(dk)) return false;
if (e.id) {
if (seenId.has(e.id)) return false;
seenId.add(e.id);
}
seenDisplay.add(dk);
return true;
});
})(esRaw);
const MAX_PER_DAY = 50;
const visibleEs = es.slice(0, MAX_PER_DAY);
visibleEs.forEach(e => {
const pill = document.createElement('div');
pill.className = 'event-pill' + (e.completed ? ' is-done' : '');
pill.textContent = e.title;
if (!isMobile()) {
pill.addEventListener('mouseenter', () => {
const tooltip = createEventTooltip(e);
document.body.appendChild(tooltip);
const rect = pill.getBoundingClientRect();
tooltip.style.position = 'fixed';
tooltip.style.left = rect.right + 10 + 'px';
tooltip.style.top = rect.top + 'px';
tooltip.style.zIndex = '1000';
pill._currentTooltip = tooltip;
});
pill.addEventListener('mouseleave', () => {
if (pill._currentTooltip) {
document.body.removeChild(pill._currentTooltip);
pill._currentTooltip = null;
}
});
}
pill.addEventListener('click', () => {
loadEventIntoForm(e);
});
body.appendChild(pill);
});
if (es.length > MAX_PER_DAY) {
const more = document.createElement('div');
more.className = 'event-pill more';
more.textContent = `还有 ${es.length - MAX_PER_DAY} 项...`;
more.style.backgroundColor = '#f0f0f0';
more.style.color = '#888';
more.style.textAlign = 'center';
more.style.cursor = 'pointer';
more.style.fontSize = '0.85em';
body.appendChild(more);
}
cell.appendChild(header);
cell.appendChild(body);
cell.addEventListener('click', (ev) => {
const isPill = ev.target.closest('.event-pill');
if (!isPill) renderDayList(date, events);
});
frag.appendChild(cell);
}
grid.appendChild(frag);
monthLabelEl().textContent = `${year}年${month}月`;
}
function renderDayList(date, events) {
const list = dayEventsEl();
list.innerHTML = '';
function overlapsDay(e, dateObj) {
try {
if (!e || e.deleted || !e.start) return false;
const s = new Date(e.start);
const ep = e.end ? new Date(e.end) : new Date(e.start);
const ds = new Date(dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate(), 0, 0, 0, 0);
const de = new Date(dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate(), 23, 59, 59, 999);
return s <= de && ep >= ds;
} catch (_) { return false; }
}
const esRaw = events.filter(e => overlapsDay(e, date) && !e.deleted);
const es = (function dedupDay(list) {
const seenId = new Set();
const seenDisplay = new Set();
function displayKey(e) {
const d = e.start ? new Date(e.start) : null;
const hh = d ? String(d.getHours()).padStart(2, '0') : '00';
const mm = d ? String(d.getMinutes()).padStart(2, '0') : '00';
const title = (e.title || '').trim();
const done = e.completed ? '1' : '0';
return `${title}|${hh}:${mm}|${done}`;
}
return list.filter(e => {
const dk = displayKey(e);
if (seenDisplay.has(dk)) return false;
if (e.id) {
if (seenId.has(e.id)) return false;
seenId.add(e.id);
}
seenDisplay.add(dk);
return true;
});
})(esRaw);
if (!es.length) {
const li = document.createElement('li');
li.className = 'list-group-item text-muted small';
li.textContent = '当天暂无事件';
list.appendChild(li);
return;
}
es.forEach(e => {
const li = document.createElement('li');
li.className = 'list-group-item';
if (e.completed) li.classList.add('is-done');
const wrap = document.createElement('div');
wrap.className = 'event-list-content';
const titleEl = document.createElement('p');
titleEl.className = 'event-list-title';
titleEl.textContent = e.title || '';
const timeEl = document.createElement('p');
timeEl.className = 'event-list-time';
const s = e.start ? new Date(e.start) : null;
const ed = e.end ? new Date(e.end) : null;
const pad = (n) => String(n).padStart(2, '0');
const fmt = (d) => d ? `${pad(d.getHours())}:${pad(d.getMinutes())}` : '';
const sTxt = fmt(s);
const eTxt = fmt(ed);
timeEl.textContent = eTxt ? `${sTxt} - ${eTxt}` : sTxt;
const descEl = document.createElement('p');
descEl.className = 'event-list-desc';
descEl.innerHTML = escapeHtmlWithLinks(e.content || '');
wrap.appendChild(titleEl);
wrap.appendChild(timeEl);
if (descEl.textContent) wrap.appendChild(descEl);
li.appendChild(wrap);
li.addEventListener('click', () => {
loadEventIntoForm(e);
});
list.appendChild(li);
});
}
function toInputValue(iso) {
const d = new Date(iso);
const q = Math.round(d.getMinutes() / 15) * 15;
d.setMinutes(Math.min(45, Math.max(0, q)), 0, 0);
const pad = (n) => String(n).padStart(2, '0');
const yyyy = d.getFullYear();
const MM = pad(d.getMonth() + 1);
const dd = pad(d.getDate());
const hh = pad(d.getHours());
const mm = pad(d.getMinutes());
return `${yyyy}-${MM}-${dd}T${hh}:${mm}`;
}
function toTimeInputValue(iso) {
const d = new Date(iso);
const q = Math.round(d.getMinutes() / 15) * 15;
d.setMinutes(Math.min(45, Math.max(0, q)), 0, 0);
const pad = (n) => String(n).padStart(2, '0');
return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
function openEventModal(event) {
const backdrop = document.createElement('div');
backdrop.className = 'event-modal-backdrop';
const modal = document.createElement('div');
modal.className = 'event-modal';
const tableRows = `
<tr><th>开始时间:</th><td>${formatDateTime(event.start)}</td></tr>
${event.end ? `<tr><th>结束时间:</th><td>${formatDateTime(event.end)}</td></tr>` : ''}
${event.content ? `<tr><th>内容:</th><td>${escapeHtml(event.content)}</td></tr>` : ''}
${event.remindAt ? `<tr><th>提醒时间:</th><td>${formatDateTime(event.remindAt)}</td></tr>` : ''}
<tr><th>状态:</th><td>${event.completed ? '已完成' : '进行中'}</td></tr>
`;
modal.innerHTML = `
<div class="modal-header">
<div class="modal-title">${escapeHtml(event.title || '无标题')}</div>
<button type="button" class="modal-close" aria-label="关闭">×</button>
</div>
<div class="modal-body">
<table class="tooltip-table">${tableRows}</table>
</div>
`;
document.body.appendChild(backdrop);
document.body.appendChild(modal);
const close = () => { modal.remove(); backdrop.remove(); };
modal.querySelector('.modal-close').addEventListener('click', close);
backdrop.addEventListener('click', close);
}
function showGlobalModal(message) {
let backdrop = document.getElementById('globalModalBackdrop');
if (!backdrop) {
backdrop = document.createElement('div');
backdrop.id = 'globalModalBackdrop';
backdrop.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.35);display:flex;align-items:center;justify-content:center;z-index:2000;';
const box = document.createElement('div');
box.id = 'globalModalBox';
box.style.cssText = 'min-width:260px;max-width:90vw;padding:16px 20px;border-radius:10px;background:#1f2937;color:#fff;box-shadow:0 10px 25px rgba(0,0,0,.35);text-align:center;';
const spinner = document.createElement('div');
spinner.className = 'spinner-border text-light';
spinner.setAttribute('role', 'status');
spinner.style.cssText = 'width:2rem;height:2rem;';
const msg = document.createElement('div');
msg.id = 'globalModalMessage';
msg.style.cssText = 'margin-top:10px;font-size:14px;';
msg.textContent = message || '正在处理...';
const logPanel = document.createElement('div');
logPanel.id = 'globalModalLog';
logPanel.style.cssText = 'margin-top:10px;padding:10px;background:#374151;border-radius:5px;text-align:left;font-size:12px;max-height:200px;overflow-y:auto;white-space:pre-wrap;';
const copyBtn = document.createElement('button');
copyBtn.id = 'globalModalCopy';
copyBtn.style.cssText = 'margin-top:10px;padding:5px 10px;background:#4b5563;border:none;border-radius:5px;color:#fff;cursor:pointer;';
copyBtn.textContent = '复制日志';
copyBtn.addEventListener('click', () => {
try {
const text = logPanel.textContent || '';
if (!text) return;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).catch(() => {});
}
} catch (_) {}
});
box.appendChild(spinner);
box.appendChild(msg);
box.appendChild(logPanel);
box.appendChild(copyBtn);
backdrop.appendChild(box);
document.body.appendChild(backdrop);
} else {
const msg = document.getElementById('globalModalMessage');
if (msg) msg.textContent = message || '正在处理...';
const logPanel = document.getElementById('globalModalLog');
if (logPanel) logPanel.textContent = '';
}
}
function appendGlobalModalLog(text) {
const logPanel = document.getElementById('globalModalLog');
if (!logPanel) return;
const line = new Date().toLocaleTimeString() + ' - ' + text;
logPanel.textContent = (logPanel.textContent || '') + line + '\n';
logPanel.scrollTop = logPanel.scrollHeight;
}
function hideGlobalModal() {
const backdrop = document.getElementById('globalModalBackdrop');
if (backdrop && backdrop.parentNode) backdrop.parentNode.removeChild(backdrop);
}
function updateGlobalModalMessage(message) {
const msg = document.getElementById('globalModalMessage');
if (msg) msg.textContent = message || '';
}
function completeGlobalModal(message) {
try {
const box = document.getElementById('globalModalBox');
const spinner = box ? box.querySelector('.spinner-border') : null;
if (spinner && spinner.parentNode) spinner.parentNode.removeChild(spinner);
} catch (_) {}
updateGlobalModalMessage(message || '完成');
setTimeout(() => { try { hideGlobalModal(); } catch (_) {} }, 1000);
}
return { renderCalendar, renderDayList, toInputValue, showGlobalModal, appendGlobalModalLog, hideGlobalModal, updateGlobalModalMessage, completeGlobalModal };
})();
window.UI = UI;