Creation chat
ClarkZhang
支持中文
02-25 11:45
暂无回复
No comments yet
登录 后可以评论
Assistant
(HTML code has been generated and updated; see above for the current version.)
▼ Code
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>月经周期记录器</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
primary: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a',
}
}
}
}
}
</script>
<style>
.calendar-day.today {
background-color: #dbeafe;
}
.dark .calendar-day.today {
background-color: #1e3a8a;
}
.calendar-day.period {
background-color: #fecaca;
}
.dark .calendar-day.period {
background-color: #7f1d1d;
}
.calendar-day.current-period {
background-color: #f87171;
color: white;
}
.dark .calendar-day.current-period {
background-color: #b91c1c;
}
.calendar-day.predicted-period {
background-color: #fed7d7;
}
.dark .calendar-day.predicted-period {
background-color: #7f1d1d;
}
</style>
</head>
<body class="min-h-screen bg-slate-50 dark:bg-slate-900 text-slate-900 dark:text-slate-100">
<!-- 主题切换按钮 -->
<button id="theme-toggle-btn" class="fixed top-4 right-4 z-10 p-2 rounded-md hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-colors flex items-center justify-center" aria-label="切换主题" onclick="toggleTheme()">
<svg id="icon-moon" class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998z"/></svg>
<svg id="icon-sun" class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24" aria-hidden="true" style="display:none"><circle cx="12" cy="12" r="4"></circle><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"></path></svg>
</button>
<script>
function toggleTheme(){
var el=document.documentElement;
var isDark=!el.classList.contains('dark');
el.classList.toggle('dark',isDark);
localStorage.setItem('theme',isDark?'dark':'light');
applyTheme(isDark);
}
function applyTheme(isDark){
document.getElementById('icon-moon').style.display=isDark?'none':'block';
document.getElementById('icon-sun').style.display=isDark?'block':'none';
}
document.addEventListener('DOMContentLoaded',function(){
var saved=localStorage.getItem('theme');
var isDark=saved==='dark'||(!saved&&window.matchMedia('(prefers-color-scheme: dark)').matches);
document.documentElement.classList.toggle('dark',isDark);
applyTheme(isDark);
});
</script>
<!-- 自定义弹窗 -->
<div id="custom-modal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4 hidden" aria-hidden="true">
<div class="rounded-xl bg-white dark:bg-slate-800 p-6 shadow-xl max-w-sm w-full border border-slate-200 dark:border-slate-700">
<p id="custom-modal-msg" class="text-slate-700 dark:text-slate-200 mb-4"></p>
<button type="button" onclick="hideModal()" class="w-full rounded-lg bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600">确定</button>
</div>
</div>
<script>
function showModal(msg){
var el=document.getElementById('custom-modal');
document.getElementById('custom-modal-msg').textContent=msg||'';
el.classList.remove('hidden');
}
function hideModal(){
document.getElementById('custom-modal').classList.add('hidden');
}
</script>
<header class="bg-white dark:bg-slate-800 shadow-sm py-6">
<div class="container mx-auto px-4">
<h1 class="text-3xl font-bold text-center text-blue-600 dark:text-blue-400">月经周期记录器</h1>
<p class="text-center text-slate-600 dark:text-slate-400 mt-2">记录您的周期并预测下次月经</p>
</div>
</header>
<main class="container mx-auto px-4 py-8">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- 日历区域 -->
<div class="lg:col-span-2">
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-md p-6">
<div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-semibold">日历</h2>
<div class="flex space-x-2">
<button id="prev-month" class="p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700" aria-label="上个月">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
</button>
<button id="next-month" class="p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700" aria-label="下个月">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</button>
</div>
</div>
<div id="calendar-header" class="grid grid-cols-7 mb-2 text-center font-medium text-slate-500 dark:text-slate-400">
<div>日</div>
<div>一</div>
<div>二</div>
<div>三</div>
<div>四</div>
<div>五</div>
<div>六</div>
</div>
<div id="calendar" class="grid grid-cols-7 gap-1"></div>
</div>
</div>
<!-- 控制面板和统计 -->
<div class="space-y-6">
<!-- 记录月经 -->
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-md p-6">
<h2 class="text-xl font-semibold mb-4">记录月经</h2>
<div class="mb-4">
<label class="block text-sm font-medium mb-2">选择日期范围</label>
<div class="flex space-x-2">
<input type="date" id="start-date" class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 p-2 text-slate-900 dark:text-slate-100">
<input type="date" id="end-date" class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 p-2 text-slate-900 dark:text-slate-100">
</div>
</div>
<button id="record-period" class="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-lg transition-colors">
记录月经
</button>
</div>
<!-- 周期统计 -->
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-md p-6">
<h2 class="text-xl font-semibold mb-4">周期统计</h2>
<div class="space-y-3">
<div class="flex justify-between">
<span class="text-slate-600 dark:text-slate-400">平均周期长度:</span>
<span id="avg-cycle" class="font-medium">-</span>
</div>
<div class="flex justify-between">
<span class="text-slate-600 dark:text-slate-400">平均经期长度:</span>
<span id="avg-period" class="font-medium">-</span>
</div>
<div class="flex justify-between">
<span class="text-slate-600 dark:text-slate-400">下次月经:</span>
<span id="next-period" class="font-medium">-</span>
</div>
<div class="flex justify-between">
<span class="text-slate-600 dark:text-slate-400">距离下次:</span>
<span id="days-until" class="font-medium">-</span>
</div>
</div>
</div>
<!-- 图例说明 -->
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-md p-6">
<h2 class="text-xl font-semibold mb-4">图例说明</h2>
<div class="space-y-2">
<div class="flex items-center">
<div class="w-4 h-4 bg-red-200 rounded mr-2"></div>
<span>当前经期</span>
</div>
<div class="flex items-center">
<div class="w-4 h-4 bg-red-100 rounded mr-2"></div>
<span>历史经期</span>
</div>
<div class="flex items-center">
<div class="w-4 h-4 bg-red-300 rounded mr-2"></div>
<span>预测经期</span>
</div>
<div class="flex items-center">
<div class="w-4 h-4 bg-blue-100 rounded mr-2"></div>
<span>今天</span>
</div>
</div>
</div>
</div>
</div>
</main>
<script>
// DOM 元素
const calendarEl = document.getElementById('calendar');
const prevMonthBtn = document.getElementById('prev-month');
const nextMonthBtn = document.getElementById('next-month');
const recordPeriodBtn = document.getElementById('record-period');
const startDateInput = document.getElementById('start-date');
const endDateInput = document.getElementById('end-date');
const avgCycleEl = document.getElementById('avg-cycle');
const avgPeriodEl = document.getElementById('avg-period');
const nextPeriodEl = document.getElementById('next-period');
const daysUntilEl = document.getElementById('days-until');
// 状态变量
let currentDate = new Date();
let periods = JSON.parse(localStorage.getItem('periods')) || [];
let currentMonth = currentDate.getMonth();
let currentYear = currentDate.getFullYear();
// 初始化
function init() {
// 设置默认日期为今天
const today = new Date();
const todayStr = today.toISOString().split('T')[0];
startDateInput.value = todayStr;
endDateInput.value = todayStr;
renderCalendar();
updateStats();
}
// 渲染日历
function renderCalendar() {
// 清空日历
calendarEl.innerHTML = '';
// 获取月份的第一天和最后一天
const firstDay = new Date(currentYear, currentMonth, 1);
const lastDay = new Date(currentYear, currentMonth + 1, 0);
// 获取第一天是星期几 (0 = 星期日)
const firstDayOfWeek = firstDay.getDay();
// 获取今天的日期用于比较
const today = new Date();
const todayStr = new Date(today.getFullYear(), today.getMonth(), today.getDate()).toISOString().split('T')[0];
// 添加空白单元格(月份开始前的天数)
for (let i = 0; i < firstDayOfWeek; i++) {
const emptyCell = document.createElement('div');
emptyCell.classList.add('calendar-day', 'h-12', 'rounded-lg');
calendarEl.appendChild(emptyCell);
}
// 为月份中的每一天添加单元格
for (let day = 1; day <= lastDay.getDate(); day++) {
const date = new Date(currentYear, currentMonth, day);
const dateStr = date.toISOString().split('T')[0];
const dayCell = document.createElement('div');
dayCell.classList.add('calendar-day', 'h-12', 'flex', 'items-center', 'justify-center', 'rounded-lg', 'cursor-pointer', 'hover:bg-slate-100', 'dark:hover:bg-slate-700', 'transition-colors');
dayCell.textContent = day;
dayCell.dataset.date = dateStr;
// 检查是否为今天
if (dateStr === todayStr) {
dayCell.classList.add('today', 'border', 'border-blue-300', 'dark:border-blue-500');
}
// 检查是否为当前经期
if (isDateInCurrentPeriod(date)) {
dayCell.classList.add('current-period');
}
// 检查是否为历史经期
else if (isDateInPreviousPeriod(date)) {
dayCell.classList.add('period');
}
// 检查是否为预测经期
else if (isDateInPredictedPeriod(date)) {
dayCell.classList.add('predicted-period');
}
dayCell.addEventListener('click', () => handleDayClick(dateStr));
calendarEl.appendChild(dayCell);
}
}
// 检查日期是否为当前经期
function isDateInCurrentPeriod(date) {
const today = new Date();
const todayStr = new Date(today.getFullYear(), today.getMonth(), today.getDate()).toISOString().split('T')[0];
const dateStr = date.toISOString().split('T')[0];
// 查找日期是否在任何记录的经期内
for (const period of periods) {
const start = new Date(period.start);
const end = new Date(period.end);
if (date >= start && date <= end) {
return true;
}
}
return false;
}
// 检查日期是否为历史经期
function isDateInPreviousPeriod(date) {
const today = new Date();
const todayStr = new Date(today.getFullYear(), today.getMonth(), today.getDate()).toISOString().split('T')[0];
const dateStr = date.toISOString().split('T')[0];
// 查找日期是否在任何记录的经期内(排除当前经期)
for (const period of periods) {
const start = new Date(period.start);
const end = new Date(period.end);
if (date >= start && date <= end) {
// 检查这个经期是否在今天之前
if (end < today) {
return true;
}
}
}
return false;
}
// 检查日期是否为预测经期
function isDateInPredictedPeriod(date) {
// 只有记录了至少一个经期才进行预测
if (periods.length === 0) return false;
// 获取最后记录的经期
const sortedPeriods = [...periods].sort((a, b) => new Date(b.start) - new Date(a.start));
const lastPeriod = sortedPeriods[0];
// 计算平均周期长度
const avgCycleLength = calculateAverageCycleLength();
// 如果无法计算平均值,则不进行预测
if (avgCycleLength === 0) return false;
// 预测下次经期开始时间(最后经期结束 + 平均周期长度)
const lastEndDate = new Date(lastPeriod.end);
const predictedStart = new Date(lastEndDate);
predictedStart.setDate(predictedStart.getDate() + avgCycleLength);
// 预测下次经期结束时间(预测开始 + 平均经期长度)
const avgPeriodLength = calculateAveragePeriodLength();
const predictedEnd = new Date(predictedStart);
predictedEnd.setDate(predictedEnd.getDate() + avgPeriodLength - 1);
// 检查日期是否在预测范围内
return date >= predictedStart && date <= predictedEnd;
}
// 处理日期点击
function handleDayClick(dateStr) {
const clickedDate = new Date(dateStr);
const today = new Date();
const todayStr = new Date(today.getFullYear(), today.getMonth(), today.getDate()).toISOString().split('T')[0];
// 不允许选择未来日期
if (dateStr > todayStr) {
showModal('您不能记录未来的经期。');
return;
}
// 将开始和结束日期设置为点击的日期
startDateInput.value = dateStr;
endDateInput.value = dateStr;
}
// 记录经期
function recordPeriod() {
const start = startDateInput.value;
const end = endDateInput.value;
if (!start || !end) {
showModal('请选择开始和结束日期。');
return;
}
// 验证日期
const startDate = new Date(start);
const endDate = new Date(end);
const today = new Date();
if (startDate > endDate) {
showModal('开始日期不能晚于结束日期。');
return;
}
if (startDate > today || endDate > today) {
showModal('您不能记录未来的经期。');
return;
}
// 检查是否有重叠的经期
for (const period of periods) {
const periodStart = new Date(period.start);
const periodEnd = new Date(period.end);
if (
(startDate >= periodStart && startDate <= periodEnd) ||
(endDate >= periodStart && endDate <= periodEnd) ||
(startDate <= periodStart && endDate >= periodEnd)
) {
showModal('此经期与现有记录重叠,请调整日期。');
return;
}
}
// 添加经期到列表
periods.push({ start, end });
// 保存到 localStorage
localStorage.setItem('periods', JSON.stringify(periods));
// 重新渲染日历和更新统计
renderCalendar();
updateStats();
// 显示确认信息
showModal('经期记录成功!');
}
// 计算平均周期长度
function calculateAverageCycleLength() {
if (periods.length < 2) return 0;
// 按开始日期排序
const sortedPeriods = [...periods].sort((a, b) => new Date(a.start) - new Date(b.start));
// 计算连续经期之间的差异
const differences = [];
for (let i = 1; i < sortedPeriods.length; i++) {
const prevEnd = new Date(sortedPeriods[i-1].end);
const currStart = new Date(sortedPeriods[i].start);
// 加1以包含开始和结束日期
const diff = Math.round((currStart - prevEnd) / (1000 * 60 * 60 * 24)) + 1;
differences.push(diff);
}
// 计算平均值
const sum = differences.reduce((a, b) => a + b, 0);
return Math.round(sum / differences.length);
}
// 计算平均经期长度
function calculateAveragePeriodLength() {
if (periods.length === 0) return 0;
const lengths = periods.map(period => {
const start = new Date(period.start);
const end = new Date(period.end);
return Math.round((end - start) / (1000 * 60 * 60 * 24)) + 1;
});
const sum = lengths.reduce((a, b) => a + b, 0);
return Math.round(sum / lengths.length);
}
// 计算下次经期
function calculateNextPeriod() {
if (periods.length === 0) return null;
// 按开始日期排序
const sortedPeriods = [...periods].sort((a, b) => new Date(a.start) - new Date(b.start));
// 获取最后记录的经期
const lastPeriod = sortedPeriods[sortedPeriods.length - 1];
// 计算平均周期长度
const avgCycleLength = calculateAverageCycleLength();
if (avgCycleLength === 0) return null;
// 计算下次经期开始时间
const lastEndDate = new Date(lastPeriod.end);
const nextStart = new Date(lastEndDate);
nextStart.setDate(nextStart.getDate() + avgCycleLength);
return nextStart;
}
// 更新统计信息
function updateStats() {
const avgCycleLength = calculateAverageCycleLength();
const avgPeriodLength = calculateAveragePeriodLength();
const nextPeriod = calculateNextPeriod();
// 更新平均周期长度
avgCycleEl.textContent = avgCycleLength > 0 ? `${avgCycleLength} 天` : '-';
// 更新平均经期长度
avgPeriodEl.textContent = avgPeriodLength > 0 ? `${avgPeriodLength} 天` : '-';
// 更新下次经期
if (nextPeriod) {
nextPeriodEl.textContent = nextPeriod.toLocaleDateString('zh-CN', {
month: 'short',
day: 'numeric',
year: 'numeric'
});
// 计算距离下次经期的天数
const today = new Date();
const diffTime = nextPeriod - today;
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (diffDays > 0) {
daysUntilEl.textContent = `${diffDays} 天`;
} else if (diffDays === 0) {
daysUntilEl.textContent = '今天';
} else {
daysUntilEl.textContent = `${Math.abs(diffDays)} 天前`;
}
} else {
nextPeriodEl.textContent = '-';
daysUntilEl.textContent = '-';
}
}
// 事件监听器
prevMonthBtn.addEventListener('click', () => {
currentMonth--;
if (currentMonth < 0) {
currentMonth = 11;
currentYear--;
}
renderCalendar();
});
nextMonthBtn.addEventListener('click', () => {
currentMonth++;
if (currentMonth > 11) {
currentMonth = 0;
currentYear++;
}
renderCalendar();
});
recordPeriodBtn.addEventListener('click', recordPeriod);
// 初始化应用
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>
02-25 11:48
暂无回复
No comments yet
登录 后可以评论
ClarkZhang
create an application to record menstruation period
02-25 11:40
暂无回复
No comments yet
登录 后可以评论
Assistant
I'll create a clean, functional menstrual cycle tracking application with dark mode support. Here's the complete implementation:
(HTML code has been generated and updated; see above for the current version.)
This menstrual cycle tracking application includes:
1. **Responsive Design** - Works on mobile and desktop with Tailwind CSS
2. **Dark Mode Support** - Toggle between light and dark themes
3. **Calendar View** - Visual month-by-month calendar showing:
- Current period days (red)
- Previous period days (light red)
- Predicted future period days (pale red)
- Today's date highlighted
4. **Period Recording** - Record start and end dates for menstrual periods
5. **Cycle Statistics** - Shows:
- Average cycle length
- Average period length
- Predicted next period date
- Days until next period
6. **Data Persistence** - Uses localStorage to save period data between sessions
7. **Validation** - Prevents invalid date selections and overlapping periods
8. **Visual Feedback** - Clear color coding for different period states
9. **User-Friendly Interface** - Simple controls and intuitive navigation
The app automatically calculates cycle predictions based on recorded data and provides a clean, modern interface for tracking menstrual health. All data is stored locally in the browser.
▼ Code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Menstruation Tracker</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
primary: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a',
}
}
}
}
}
</script>
<style>
.calendar-day.today {
background-color: #dbeafe;
}
.dark .calendar-day.today {
background-color: #1e3a8a;
}
.calendar-day.period {
background-color: #fecaca;
}
.dark .calendar-day.period {
background-color: #7f1d1d;
}
.calendar-day.current-period {
background-color: #f87171;
color: white;
}
.dark .calendar-day.current-period {
background-color: #b91c1c;
}
.calendar-day.predicted-period {
background-color: #fed7d7;
}
.dark .calendar-day.predicted-period {
background-color: #7f1d1d;
}
</style>
</head>
<body class="min-h-screen bg-slate-50 dark:bg-slate-900 text-slate-900 dark:text-slate-100">
<!-- Theme toggle -->
<button id="theme-toggle-btn" class="fixed top-4 right-4 z-10 p-2 rounded-md hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-colors flex items-center justify-center" aria-label="Toggle theme" onclick="toggleTheme()">
<svg id="icon-moon" class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998z"/></svg>
<svg id="icon-sun" class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24" aria-hidden="true" style="display:none"><circle cx="12" cy="12" r="4"></circle><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"></path></svg>
</button>
<script>
function toggleTheme(){
var el=document.documentElement;
var isDark=!el.classList.contains('dark');
el.classList.toggle('dark',isDark);
localStorage.setItem('theme',isDark?'dark':'light');
applyTheme(isDark);
}
function applyTheme(isDark){
document.getElementById('icon-moon').style.display=isDark?'none':'block';
document.getElementById('icon-sun').style.display=isDark?'block':'none';
}
document.addEventListener('DOMContentLoaded',function(){
var saved=localStorage.getItem('theme');
var isDark=saved==='dark'||(!saved&&window.matchMedia('(prefers-color-scheme: dark)').matches);
document.documentElement.classList.toggle('dark',isDark);
applyTheme(isDark);
});
</script>
<!-- Custom modal -->
<div id="custom-modal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4 hidden" aria-hidden="true">
<div class="rounded-xl bg-white dark:bg-slate-800 p-6 shadow-xl max-w-sm w-full border border-slate-200 dark:border-slate-700">
<p id="custom-modal-msg" class="text-slate-700 dark:text-slate-200 mb-4"></p>
<button type="button" onclick="hideModal()" class="w-full rounded-lg bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600">OK</button>
</div>
</div>
<script>
function showModal(msg){
var el=document.getElementById('custom-modal');
document.getElementById('custom-modal-msg').textContent=msg||'';
el.classList.remove('hidden');
}
function hideModal(){
document.getElementById('custom-modal').classList.add('hidden');
}
</script>
<header class="bg-white dark:bg-slate-800 shadow-sm py-6">
<div class="container mx-auto px-4">
<h1 class="text-3xl font-bold text-center text-blue-600 dark:text-blue-400">Menstruation Tracker</h1>
<p class="text-center text-slate-600 dark:text-slate-400 mt-2">Track your cycle and predict your next period</p>
</div>
</header>
<main class="container mx-auto px-4 py-8">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Calendar Section -->
<div class="lg:col-span-2">
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-md p-6">
<div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-semibold">Calendar</h2>
<div class="flex space-x-2">
<button id="prev-month" class="p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
</button>
<button id="next-month" class="p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</button>
</div>
</div>
<div id="calendar-header" class="grid grid-cols-7 mb-2 text-center font-medium text-slate-500 dark:text-slate-400">
<div>Sun</div>
<div>Mon</div>
<div>Tue</div>
<div>Wed</div>
<div>Thu</div>
<div>Fri</div>
<div>Sat</div>
</div>
<div id="calendar" class="grid grid-cols-7 gap-1"></div>
</div>
</div>
<!-- Controls and Stats -->
<div class="space-y-6">
<!-- Period Controls -->
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-md p-6">
<h2 class="text-xl font-semibold mb-4">Record Period</h2>
<div class="mb-4">
<label class="block text-sm font-medium mb-2">Select Date Range</label>
<div class="flex space-x-2">
<input type="date" id="start-date" class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 p-2 text-slate-900 dark:text-slate-100">
<input type="date" id="end-date" class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 p-2 text-slate-900 dark:text-slate-100">
</div>
</div>
<button id="record-period" class="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-lg transition-colors">
Record Period
</button>
</div>
<!-- Cycle Stats -->
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-md p-6">
<h2 class="text-xl font-semibold mb-4">Cycle Statistics</h2>
<div class="space-y-3">
<div class="flex justify-between">
<span class="text-slate-600 dark:text-slate-400">Average Cycle Length:</span>
<span id="avg-cycle" class="font-medium">-</span>
</div>
<div class="flex justify-between">
<span class="text-slate-600 dark:text-slate-400">Average Period Length:</span>
<span id="avg-period" class="font-medium">-</span>
</div>
<div class="flex justify-between">
<span class="text-slate-600 dark:text-slate-400">Next Period:</span>
<span id="next-period" class="font-medium">-</span>
</div>
<div class="flex justify-between">
<span class="text-slate-600 dark:text-slate-400">Days Until Next Period:</span>
<span id="days-until" class="font-medium">-</span>
</div>
</div>
</div>
<!-- Legend -->
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-md p-6">
<h2 class="text-xl font-semibold mb-4">Legend</h2>
<div class="space-y-2">
<div class="flex items-center">
<div class="w-4 h-4 bg-red-200 rounded mr-2"></div>
<span>Current Period</span>
</div>
<div class="flex items-center">
<div class="w-4 h-4 bg-red-100 rounded mr-2"></div>
<span>Previous Period</span>
</div>
<div class="flex items-center">
<div class="w-4 h-4 bg-red-300 rounded mr-2"></div>
<span>Predicted Period</span>
</div>
<div class="flex items-center">
<div class="w-4 h-4 bg-blue-100 rounded mr-2"></div>
<span>Today</span>
</div>
</div>
</div>
</div>
</div>
</main>
<script>
// DOM Elements
const calendarEl = document.getElementById('calendar');
const prevMonthBtn = document.getElementById('prev-month');
const nextMonthBtn = document.getElementById('next-month');
const recordPeriodBtn = document.getElementById('record-period');
const startDateInput = document.getElementById('start-date');
const endDateInput = document.getElementById('end-date');
const avgCycleEl = document.getElementById('avg-cycle');
const avgPeriodEl = document.getElementById('avg-period');
const nextPeriodEl = document.getElementById('next-period');
const daysUntilEl = document.getElementById('days-until');
// State
let currentDate = new Date();
let periods = JSON.parse(localStorage.getItem('periods')) || [];
let currentMonth = currentDate.getMonth();
let currentYear = currentDate.getFullYear();
// Initialize
function init() {
// Set default dates to today
const today = new Date();
const todayStr = today.toISOString().split('T')[0];
startDateInput.value = todayStr;
endDateInput.value = todayStr;
renderCalendar();
updateStats();
}
// Render calendar
function renderCalendar() {
// Clear calendar
calendarEl.innerHTML = '';
// Get first day of month and last day
const firstDay = new Date(currentYear, currentMonth, 1);
const lastDay = new Date(currentYear, currentMonth + 1, 0);
// Get day of week for first day (0 = Sunday)
const firstDayOfWeek = firstDay.getDay();
// Get today's date for comparison
const today = new Date();
const todayStr = new Date(today.getFullYear(), today.getMonth(), today.getDate()).toISOString().split('T')[0];
// Add empty cells for days before the first day
for (let i = 0; i < firstDayOfWeek; i++) {
const emptyCell = document.createElement('div');
emptyCell.classList.add('calendar-day', 'h-12', 'rounded-lg');
calendarEl.appendChild(emptyCell);
}
// Add cells for each day of the month
for (let day = 1; day <= lastDay.getDate(); day++) {
const date = new Date(currentYear, currentMonth, day);
const dateStr = date.toISOString().split('T')[0];
const dayCell = document.createElement('div');
dayCell.classList.add('calendar-day', 'h-12', 'flex', 'items-center', 'justify-center', 'rounded-lg', 'cursor-pointer', 'hover:bg-slate-100', 'dark:hover:bg-slate-700', 'transition-colors');
dayCell.textContent = day;
dayCell.dataset.date = dateStr;
// Check if today
if (dateStr === todayStr) {
dayCell.classList.add('today', 'border', 'border-blue-300', 'dark:border-blue-500');
}
// Check if in current period
if (isDateInCurrentPeriod(date)) {
dayCell.classList.add('current-period');
}
// Check if in previous period
else if (isDateInPreviousPeriod(date)) {
dayCell.classList.add('period');
}
// Check if predicted period
else if (isDateInPredictedPeriod(date)) {
dayCell.classList.add('predicted-period');
}
dayCell.addEventListener('click', () => handleDayClick(dateStr));
calendarEl.appendChild(dayCell);
}
}
// Check if date is in current period
function isDateInCurrentPeriod(date) {
const today = new Date();
const todayStr = new Date(today.getFullYear(), today.getMonth(), today.getDate()).toISOString().split('T')[0];
const dateStr = date.toISOString().split('T')[0];
// Find if date is in any recorded period
for (const period of periods) {
const start = new Date(period.start);
const end = new Date(period.end);
if (date >= start && date <= end) {
return true;
}
}
return false;
}
// Check if date is in previous period
function isDateInPreviousPeriod(date) {
const today = new Date();
const todayStr = new Date(today.getFullYear(), today.getMonth(), today.getDate()).toISOString().split('T')[0];
const dateStr = date.toISOString().split('T')[0];
// Find if date is in any recorded period (excluding current)
for (const period of periods) {
const start = new Date(period.start);
const end = new Date(period.end);
if (date >= start && date <= end) {
// Check if this period is before today
if (end < today) {
return true;
}
}
}
return false;
}
// Check if date is in predicted period
function isDateInPredictedPeriod(date) {
// Only predict if we have at least one period recorded
if (periods.length === 0) return false;
// Get the last recorded period
const sortedPeriods = [...periods].sort((a, b) => new Date(b.start) - new Date(a.start));
const lastPeriod = sortedPeriods[0];
// Calculate average cycle length
const avgCycleLength = calculateAverageCycleLength();
// If we can't calculate average, don't predict
if (avgCycleLength === 0) return false;
// Predict next period start (last period end + avg cycle length)
const lastEndDate = new Date(lastPeriod.end);
const predictedStart = new Date(lastEndDate);
predictedStart.setDate(predictedStart.getDate() + avgCycleLength);
// Predict next period end (predicted start + average period length)
const avgPeriodLength = calculateAveragePeriodLength();
const predictedEnd = new Date(predictedStart);
predictedEnd.setDate(predictedEnd.getDate() + avgPeriodLength - 1);
// Check if date falls in predicted range
return date >= predictedStart && date <= predictedEnd;
}
// Handle day click
function handleDayClick(dateStr) {
const clickedDate = new Date(dateStr);
const today = new Date();
const todayStr = new Date(today.getFullYear(), today.getMonth(), today.getDate()).toISOString().split('T')[0];
// Don't allow selecting future dates
if (dateStr > todayStr) {
showModal('You cannot record a period in the future.');
return;
}
// Set both start and end date to clicked date
startDateInput.value = dateStr;
endDateInput.value = dateStr;
}
// Record period
function recordPeriod() {
const start = startDateInput.value;
const end = endDateInput.value;
if (!start || !end) {
showModal('Please select both start and end dates.');
return;
}
// Validate dates
const startDate = new Date(start);
const endDate = new Date(end);
const today = new Date();
if (startDate > endDate) {
showModal('Start date cannot be after end date.');
return;
}
if (startDate > today || endDate > today) {
showModal('You cannot record a period in the future.');
return;
}
// Check for overlapping periods
for (const period of periods) {
const periodStart = new Date(period.start);
const periodEnd = new Date(period.end);
if (
(startDate >= periodStart && startDate <= periodEnd) ||
(endDate >= periodStart && endDate <= periodEnd) ||
(startDate <= periodStart && endDate >= periodEnd)
) {
showModal('This period overlaps with an existing record. Please adjust the dates.');
return;
}
}
// Add period to list
periods.push({ start, end });
// Save to localStorage
localStorage.setItem('periods', JSON.stringify(periods));
// Re-render calendar and update stats
renderCalendar();
updateStats();
// Show confirmation
showModal('Period recorded successfully!');
}
// Calculate average cycle length
function calculateAverageCycleLength() {
if (periods.length < 2) return 0;
// Sort periods by start date
const sortedPeriods = [...periods].sort((a, b) => new Date(a.start) - new Date(b.start));
// Calculate differences between consecutive periods
const differences = [];
for (let i = 1; i < sortedPeriods.length; i++) {
const prevEnd = new Date(sortedPeriods[i-1].end);
const currStart = new Date(sortedPeriods[i].start);
// Add 1 to include both start and end days
const diff = Math.round((currStart - prevEnd) / (1000 * 60 * 60 * 24)) + 1;
differences.push(diff);
}
// Calculate average
const sum = differences.reduce((a, b) => a + b, 0);
return Math.round(sum / differences.length);
}
// Calculate average period length
function calculateAveragePeriodLength() {
if (periods.length === 0) return 0;
const lengths = periods.map(period => {
const start = new Date(period.start);
const end = new Date(period.end);
return Math.round((end - start) / (1000 * 60 * 60 * 24)) + 1;
});
const sum = lengths.reduce((a, b) => a + b, 0);
return Math.round(sum / lengths.length);
}
// Calculate next period
function calculateNextPeriod() {
if (periods.length === 0) return null;
// Sort periods by start date
const sortedPeriods = [...periods].sort((a, b) => new Date(a.start) - new Date(b.start));
// Get the last recorded period
const lastPeriod = sortedPeriods[sortedPeriods.length - 1];
// Calculate average cycle length
const avgCycleLength = calculateAverageCycleLength();
if (avgCycleLength === 0) return null;
// Calculate next period start
const lastEndDate = new Date(lastPeriod.end);
const nextStart = new Date(lastEndDate);
nextStart.setDate(nextStart.getDate() + avgCycleLength);
return nextStart;
}
// Update statistics
function updateStats() {
const avgCycleLength = calculateAverageCycleLength();
const avgPeriodLength = calculateAveragePeriodLength();
const nextPeriod = calculateNextPeriod();
// Update average cycle length
avgCycleEl.textContent = avgCycleLength > 0 ? `${avgCycleLength} days` : '-';
// Update average period length
avgPeriodEl.textContent = avgPeriodLength > 0 ? `${avgPeriodLength} days` : '-';
// Update next period
if (nextPeriod) {
nextPeriodEl.textContent = nextPeriod.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
});
// Calculate days until next period
const today = new Date();
const diffTime = nextPeriod - today;
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (diffDays > 0) {
daysUntilEl.textContent = `${diffDays} days`;
} else if (diffDays === 0) {
daysUntilEl.textContent = 'Today';
} else {
daysUntilEl.textContent = `${Math.abs(diffDays)} days ago`;
}
} else {
nextPeriodEl.textContent = '-';
daysUntilEl.textContent = '-';
}
}
// Event Listeners
prevMonthBtn.addEventListener('click', () => {
currentMonth--;
if (currentMonth < 0) {
currentMonth = 11;
currentYear--;
}
renderCalendar();
});
nextMonthBtn.addEventListener('click', () => {
currentMonth++;
if (currentMonth > 11) {
currentMonth = 0;
currentYear++;
}
renderCalendar();
});
recordPeriodBtn.addEventListener('click', recordPeriod);
// Initialize app
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>
02-25 11:43
暂无回复
No comments yet
登录 后可以评论
Comments
暂无回复
No comments yet
登录 后可以评论