[HTML] 纯文本查看 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>2026预算日历</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
body {
padding: 10px; /* 减少body内边距,适配小屏 */
background-color: #f5f5f5;
overflow-x: hidden; /* 禁止页面整体横向滚动 */
}
.container {
width: 100%; /* 改为100%宽度,适配所有屏幕 */
max-width: 500px;
margin: 0 auto;
background: white;
padding: 15px; /* 减少内边距 */
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
font-size: 18px; /* 缩小标题字体 */
text-align: center;
margin-bottom: 15px;
color: #333;
}
.refresh-btn {
width: 100%;
background: #ff9800;
color: white;
border: none;
padding: 8px; /* 减少按钮内边距 */
border-radius: 6px;
font-size: 15px;
cursor: pointer;
margin-bottom: 12px;
}
.input-group {
margin-bottom: 12px;
}
label {
display: block;
margin-bottom: 4px;
font-weight: 500;
color: #555;
font-size: 14px;
}
input[type="number"],
select,
.custom-input {
width: 100%;
padding: 8px; /* 减少输入框内边距 */
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
}
.expense-item {
display: flex;
gap: 8px; /* 缩小间距 */
align-items: center;
margin-bottom: 8px;
padding: 8px;
background: #f9f9f9;
border-radius: 6px;
}
.expense-item select,
.expense-item input {
flex: 1;
}
.expense-amount {
width: 70px !important; /* 缩小金额输入框 */
}
.remove-btn {
background: #ff4444;
color: white;
border: none;
width: 28px; /* 缩小删除按钮 */
height: 28px;
border-radius: 50%;
cursor: pointer;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
}
.add-btn {
background: #2196F3;
color: white;
border: none;
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 4px;
}
.calculate-btn {
width: 100%;
background: #4CAF50;
color: white;
border: none;
padding: 10px;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
margin-bottom: 15px;
}
.calendar {
display: none;
margin-top: 15px;
width: 100%; /* 日历容器占满宽度 */
overflow-x: auto; /* 小屏时允许横向滚动日历 */
-webkit-overflow-scrolling: touch; /* 适配iOS顺滑滚动 */
}
.calendar h2 {
font-size: 16px;
text-align: center;
margin-bottom: 8px;
color: #333;
}
.calendar-header {
display: grid;
grid-template-columns: repeat(7, minmax(30px, 1fr)); /* 关键:列宽自适应,最小30px */
text-align: center;
font-weight: bold;
margin-bottom: 8px;
color: #666;
font-size: 12px; /* 缩小表头字体 */
}
.calendar-days {
display: grid;
grid-template-columns: repeat(7, minmax(30px, 1fr)); /* 关键:列宽自适应 */
gap: 3px; /* 缩小日期间距 */
}
.calendar-day {
min-height: 35px; /* 最小高度,保证可点击 */
aspect-ratio: 1; /* 保持正方形 */
border: 1px solid #eee;
padding: 2px; /* 大幅减少内边距 */
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border-radius: 4px;
background: #f9f9f9;
}
.day-number {
font-weight: bold;
margin-bottom: 1px;
font-size: 11px; /* 缩小日期数字 */
}
.day-amount {
font-size: 9px; /* 缩小金额字体 */
color: #2196F3;
}
.empty-day {
border: none;
background: transparent;
}
.friday {
background: #ffebee;
}
.weekend {
background: #e3f2fd;
}
.error {
color: #ff4444;
margin-bottom: 12px;
display: none;
font-size: 14px;
}
.custom-category-container {
display: none;
margin-top: 4px;
}
.custom-category-input {
width: 100%;
padding: 6px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 13px;
}
</style>
</head>
<body>
<div class="container">
<h1>2026年月度预算日历</h1>
<button class="refresh-btn" id="refresh-btn">🔄 清除缓存并刷新</button>
<div class="input-group">
<label for="total-budget">每月可用预算(元)</label>
<input type="number" id="total-budget" min="0" step="0.01" required>
</div>
<div class="input-group">
<label>固定开支</label>
<div id="expense-list">
<!-- 动态添加开支项 -->
</div>
<button class="add-btn" id="add-expense">+ 添加固定开支</button>
</div>
<div class="error" id="error-message"></div>
<button class="calculate-btn" id="calculate-btn">计算并显示日历</button>
<div class="calendar" id="calendar-container">
<h2>2026年1月预算日历</h2>
<div class="calendar-header">
<div>周一</div>
<div>周二</div>
<div>周三</div>
<div>周四</div>
<div>周五</div>
<div>周六</div>
<div>周日</div>
</div>
<div class="calendar-days" id="calendar-days">
<!-- 动态生成日历 -->
</div>
</div>
</div>
<script>
// 初始化开支项
document.addEventListener('DOMContentLoaded', function() {
const expenseList = document.getElementById('expense-list');
const addExpenseBtn = document.getElementById('add-expense');
const calculateBtn = document.getElementById('calculate-btn');
const calendarContainer = document.getElementById('calendar-container');
const calendarDays = document.getElementById('calendar-days');
const errorMessage = document.getElementById('error-message');
const refreshBtn = document.getElementById('refresh-btn');
// 开支类别
const expenseCategories = [
'电费', '燃气费', '水费', '加油费',
'物业费', '停车费', '预存款', '其他项'
];
// 强制刷新逻辑
refreshBtn.addEventListener('click', function() {
if (confirm('确定要清除缓存并刷新页面吗?所有未提交的输入会丢失!')) {
window.location.reload(true);
}
});
// 添加按钮仅创建项,不立即检查重复
addExpenseBtn.addEventListener('click', function() {
addExpenseItem(); // 直接创建,不检查重复
});
// 初始化添加一个开支项
addExpenseItem();
// 计算按钮点击事件:先检查所有项是否重复,再计算
calculateBtn.addEventListener('click', function() {
// 第一步:检查所有开支项是否有重复类别
const duplicateCategory = checkAllExpenseDuplicate();
if (duplicateCategory) {
errorMessage.textContent = `发现重复的开支类别:${duplicateCategory},请修改后再计算!`;
errorMessage.style.display = 'block';
return; // 阻止后续计算
}
// 第二步:执行正常计算逻辑
calculateAndDisplayCalendar();
});
// 新增:检查所有开支项是否有重复类别(全局校验)
function checkAllExpenseDuplicate() {
const categoryList = [];
const expenseItems = document.querySelectorAll('.expense-item');
// 遍历所有项,收集类别
expenseItems.forEach(item => {
const categorySelect = item.querySelector('.expense-category');
let category = categorySelect.value;
if (category === '其他项') {
const customInput = item.querySelector('.custom-category-input');
category = customInput.value.trim() || '其他项';
}
// 空类别跳过(未输入的其他项)
if (category) {
categoryList.push(category);
}
});
// 检查是否有重复
const uniqueCategories = new Set(categoryList);
if (categoryList.length !== uniqueCategories.size) {
// 找到重复的类别
for (let cat of categoryList) {
if (categoryList.filter(c => c === cat).length > 1) {
return cat; // 返回第一个重复的类别
}
}
}
return null; // 无重复
}
// 检查单个项重复的函数(保留,备用)
function checkExpenseDuplicate(newItem) {
if (!newItem) return false;
const newCategorySelect = newItem.querySelector('.expense-category');
let newCategory = newCategorySelect.value;
if (newCategory === '其他项') {
const customInput = newItem.querySelector('.custom-category-input');
newCategory = customInput.value.trim() || '其他项';
}
if (!newCategory) return false;
const existingCategories = [];
const expenseItems = document.querySelectorAll('.expense-item');
expenseItems.forEach(item => {
if (item === newItem) return;
const categorySelect = item.querySelector('.expense-category');
let category = categorySelect.value;
if (category === '其他项') {
const customInput = item.querySelector('.custom-category-input');
category = customInput.value.trim() || '其他项';
}
existingCategories.push(category);
});
return existingCategories.includes(newCategory);
}
// 添加开支项函数
function addExpenseItem() {
const expenseItem = document.createElement('div');
expenseItem.className = 'expense-item';
const categorySelect = document.createElement('select');
categorySelect.className = 'expense-category';
expenseCategories.forEach(category => {
const option = document.createElement('option');
option.value = category;
option.textContent = category;
categorySelect.appendChild(option);
});
const customCategoryContainer = document.createElement('div');
customCategoryContainer.className = 'custom-category-container';
const customCategoryInput = document.createElement('input');
customCategoryInput.type = 'text';
customCategoryInput.className = 'custom-category-input';
customCategoryInput.placeholder = '请输入自定义类别(10字以内)';
customCategoryInput.maxLength = 10;
customCategoryContainer.appendChild(customCategoryInput);
const amountInput = document.createElement('input');
amountInput.type = 'number';
amountInput.className = 'expense-amount';
amountInput.min = '0';
amountInput.step = '0.01';
amountInput.placeholder = '金额';
const removeBtn = document.createElement('button');
removeBtn.className = 'remove-btn';
removeBtn.textContent = '-';
removeBtn.addEventListener('click', function() {
if (expenseList.children.length > 1) {
expenseItem.remove();
} else {
alert('至少保留一个开支项,可以将金额设为0');
}
});
// 监听类别选择变化
categorySelect.addEventListener('change', function() {
if (this.value === '其他项') {
customCategoryContainer.style.display = 'block';
} else {
customCategoryContainer.style.display = 'none';
}
});
// 组装开支项
expenseItem.appendChild(categorySelect);
expenseItem.appendChild(customCategoryContainer);
expenseItem.appendChild(amountInput);
expenseItem.appendChild(removeBtn);
expenseList.appendChild(expenseItem);
return expenseItem;
}
// 计算并显示日历
function calculateAndDisplayCalendar() {
try {
errorMessage.style.display = 'none';
const totalBudget = parseFloat(document.getElementById('total-budget').value);
if (isNaN(totalBudget) || totalBudget < 0) {
throw new Error('请输入有效的月度预算金额(大于等于0)');
}
let totalExpenses = 0;
const expenseItems = document.querySelectorAll('.expense-item');
expenseItems.forEach(item => {
const amountInput = item.querySelector('.expense-amount');
const amount = parseFloat(amountInput.value) || 0;
if (amount < 0) {
throw new Error('固定开支金额不能为负数');
}
totalExpenses += amount;
});
if (totalExpenses > totalBudget) {
throw new Error(`固定开支总额(${totalExpenses.toFixed(2)}元)超过了月度预算(${totalBudget.toFixed(2)}元)`);
}
const availableBudget = Math.round(totalBudget - totalExpenses);
const monthStats = {
weekdays: 0,
fridays: 0,
weekends: 0
};
for (let day = 1; day <= 31; day++) {
const date = new Date(2026, 0, day);
const dayOfWeek = date.getDay();
if (dayOfWeek === 5) {
monthStats.fridays++;
} else if (dayOfWeek === 0 || dayOfWeek === 6) {
monthStats.weekends++;
} else {
monthStats.weekdays++;
}
}
const fridayFixedAmount = 100;
const weekendMultiplier = 1.5;
const fridayTotal = fridayFixedAmount * monthStats.fridays;
if (fridayTotal > availableBudget) {
throw new Error(`预算不足,仅周五固定支出就需要${fridayTotal}元`);
}
let baseAmount = (availableBudget - fridayTotal) /
(monthStats.weekdays + weekendMultiplier * monthStats.weekends);
let dailyAmounts = [];
for (let day = 1; day <= 31; day++) {
const date = new Date(2026, 0, day);
const dayOfWeek = date.getDay();
let amount;
if (dayOfWeek === 5) {
amount = fridayFixedAmount;
} else if (dayOfWeek === 0 || dayOfWeek === 6) {
amount = baseAmount * weekendMultiplier;
} else {
amount = baseAmount;
}
dailyAmounts.push({ day, dayOfWeek, amount });
}
dailyAmounts = dailyAmounts.map(item => ({
...item,
amount: Math.floor(item.amount)
}));
let totalAllocated = dailyAmounts.reduce((sum, item) => sum + item.amount, 0);
let diff = availableBudget - totalAllocated;
if (diff > 0) {
const sortedIndices = dailyAmounts
.map((_, idx) => idx)
.sort((a, b) => dailyAmounts[a].amount - dailyAmounts[b].amount);
for (let i = 0; i < diff && i < sortedIndices.length; i++) {
dailyAmounts[sortedIndices[i]].amount += 1;
}
}
generateCalendar(dailyAmounts);
calendarContainer.style.display = 'block';
} catch (error) {
errorMessage.textContent = error.message;
errorMessage.style.display = 'block';
calendarContainer.style.display = 'none';
}
}
// 生成日历
function generateCalendar(dailyAmounts) {
calendarDays.innerHTML = '';
const firstDayOfMonth = new Date(2026, 0, 1).getDay();
const daysInMonth = 31;
let emptyDays = firstDayOfMonth === 0 ? 6 : firstDayOfMonth - 1;
for (let i = 0; i < emptyDays; i++) {
const emptyDay = document.createElement('div');
emptyDay.className = 'calendar-day empty-day';
calendarDays.appendChild(emptyDay);
}
dailyAmounts.forEach(item => {
const { day, dayOfWeek, amount } = item;
const dayElement = document.createElement('div');
dayElement.className = 'calendar-day';
if (dayOfWeek === 5) {
dayElement.classList.add('friday');
} else if (dayOfWeek === 0 || dayOfWeek === 6) {
dayElement.classList.add('weekend');
}
const dayNumber = document.createElement('div');
dayNumber.className = 'day-number';
dayNumber.textContent = day;
const dayAmountElement = document.createElement('div');
dayAmountElement.className = 'day-amount';
dayAmountElement.textContent = amount + '元';
dayElement.appendChild(dayNumber);
dayElement.appendChild(dayAmountElement);
calendarDays.appendChild(dayElement);
});
}
});
</script>
</body>
</html>