body {
font-family: 'Arial', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 10px;
}
.container {
max-width: 400px;
margin: 0 auto;
background: white;
border-radius: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #4CAF50, #45a049);
color: white;
padding: 20px;
text-align: center;
}
.year-selector {
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
margin-top: 10px;
}
.year-btn {
background: rgba(255,255,255,0.2);
border: none;
color: white;
padding: 8px 12px;
border-radius: 50%;
cursor: pointer;
font-size: 18px;
}
.year-btn:hover {
background: rgba(255,255,255,0.3);
}
.current-year {
font-size: 24px;
font-weight: bold;
}
.content {
padding: 20px;
}
.month-grid {
display: grid;
grid-template-columns: 1fr;
gap: 15px;
}
.month-card {
background: #f8f9fa;
border-radius: 15px;
padding: 15px;
border: 2px solid #e9ecef;
}
.month-title {
font-size: 18px;
font-weight: bold;
color: #495057;
margin-bottom: 12px;
text-align: center;
}
.card-buttons {
display: flex;
flex-direction: column;
gap: 8px;
}
.card-btn {
padding: 12px;
border: none;
border-radius: 10px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.3s ease;
position: relative;
width: 100%;
text-align: left;
}
.card-btn.unpaid {
background: #ff6b6b;
color: white;
}
.card-btn.paid {
background: #51cf66;
color: white;
cursor: default;
}
.card-btn:hover:not(.paid) {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.payment-info {
font-size: 11px;
margin-top: 4px;
opacity: 0.9;
}
.cancel-btn {
background: #ffd43b;
color: #495057;
font-size: 12px;
padding: 6px 12px;
border: none;
border-radius: 5px;
cursor: pointer;
margin-top: 8px;
display: inline-block;
}
.cancel-btn:hover {
background: #ffcd02;
}
.settings-btn {
background: rgba(255,255,255,0.2);
border: none;
color: white;
padding: 8px 12px;
border-radius: 10px;
cursor: pointer;
font-size: 14px;
margin-top: 10px;
}
.settings-btn:hover {
background: rgba(255,255,255,0.3);
}
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
}
.modal-content {
background-color: white;
margin: 15% auto;
padding: 20px;
border-radius: 15px;
width: 90%;
max-width: 400px;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.close {
color: #aaa;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.close:hover {
color: black;
}
.card-list {
margin-bottom: 20px;
}
.card-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
margin: 5px 0;
background: #f8f9fa;
border-radius: 8px;
}
.card-name {
flex: 1;
padding: 5px;
border: 1px solid #ddd;
border-radius: 5px;
margin-right: 10px;
}
.delete-btn {
background: #ff6b6b;
color: white;
border: none;
padding: 8px 12px;
border-radius: 5px;
cursor: pointer;
font-size: 12px;
}
.delete-btn:hover {
background: #ff5252;
}
.delete-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.add-card {
display: flex;
gap: 10px;
margin-top: 15px;
}
.add-input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
.add-btn {
background: #4CAF50;
color: white;
border: none;
padding: 10px 15px;
border-radius: 5px;
cursor: pointer;
}
.stats {
background: #e9ecef;
padding: 15px;
margin: 15px 0;
border-radius: 10px;
text-align: center;
}
.stats-item {
display: inline-block;
margin: 0 15px;
}
.stats-number {
font-size: 24px;
font-weight: bold;
color: #495057;
}
.stats-label {
font-size: 12px;
color: #6c757d;
}
.toggle-past-months {
background: #4CAF50;
color: white;
border: none;
padding: 10px;
border-radius: 10px;
cursor: pointer;
font-size: 14px;
margin-bottom: 15px;
width: 100%;
text-align: center;
transition: all 0.3s ease;
}
.toggle-past-months:hover {
background: #45a049;
}
.past-months {
transition: max-height 0.3s ease;
overflow: hidden;
}
.past-months.collapsed {
max-height: 0;
}
</style>
💳 信用卡繳費紀錄
◀
2025
▶
⚙️ 管理信用卡
<div class="content">
<div class="stats">
<div class="stats-item">
<div class="stats-number" id="paidCount">0</div>
<div class="stats-label">本月已繳費</div>
</div>
<div class="stats-item">
<div class="stats-number" id="unpaidCount">0</div>
<div class="stats-label">本月未繳費</div>
</div>
</div>
<button class="toggle-past-months" id="togglePastMonths" onclick="togglePastMonths()">展開過往月份</button>
<div class="month-grid" id="monthGrid">
<!-- 當前月份卡片會在這裡動態生成 -->
</div>
<div class="past-months collapsed" id="pastMonths">
<!-- 過往月份卡片會在這裡動態生成 -->
</div>
</div>
</div>
<!-- 設定模態框 -->
<div id="settingsModal" class="modal" role="dialog" aria-modal="true">
<div class="modal-content">
<div class="modal-header">
<h2>管理信用卡</h2>
<span class="close" onclick="closeSettings()">×</span>
</div>
<div class="card-list" id="cardList">
<!-- 信用卡列表會在這裡動態生成 -->
</div>
<div class="add-card">
<input type="text" id="newCardName" class="add-input" placeholder="輸入新信用卡名稱">
<button class="add-btn" onclick="addCard()">新增</button>
</div>
</div>
</div>
<script>
let currentYear = 2025;
let cardNames = ['中國信託', '台新銀行', '玉山銀行'];
const months = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'];
let paymentRecords = {};
let isPastMonthsExpanded = false;
// 載入資料並同步
function loadData() {
const savedRecords = localStorage.getItem('paymentRecords');
const savedCards = localStorage.getItem('cardNames');
const savedExpanded = localStorage.getItem('isPastMonthsExpanded');
if (savedCards) cardNames = JSON.parse(savedCards);
if (savedRecords) {
paymentRecords = JSON.parse(savedRecords);
// 同步 cardNames 和 paymentRecords
Object.keys(paymentRecords).forEach(year => {
Object.keys(paymentRecords[year]).forEach(month => {
Object.keys(paymentRecords[year][month]).forEach(card => {
if (!cardNames.includes(card)) {
delete paymentRecords[year][month][card];
}
});
});
});
}
if (savedExpanded) isPastMonthsExpanded = JSON.parse(savedExpanded);
}
// 儲存資料並清理舊年份
function saveData() {
const currentYearNum = new Date().getFullYear();
const yearsToKeep = [currentYearNum, currentYearNum - 1, currentYearNum - 2];
const cleanedRecords = {};
yearsToKeep.forEach(year => {
if (paymentRecords[year]) {
cleanedRecords[year] = paymentRecords[year];
}
});
paymentRecords = cleanedRecords;
localStorage.setItem('paymentRecords', JSON.stringify(paymentRecords));
localStorage.setItem('cardNames', JSON.stringify(cardNames));
localStorage.setItem('isPastMonthsExpanded', JSON.stringify(isPastMonthsExpanded));
}
// 初始化資料結構
function initializeRecords() {
if (!paymentRecords[currentYear]) {
paymentRecords[currentYear] = {};
for (let month = 1; month <= 12; month++) {
paymentRecords[currentYear][month] = {};
cardNames.forEach(card => {
paymentRecords[currentYear][month][card] = null;
});
}
} else {
// 確保所有信用卡和月份存在
for (let month = 1; month <= 12; month++) {
if (!paymentRecords[currentYear][month]) {
paymentRecords[currentYear][month] = {};
}
cardNames.forEach(card => {
if (paymentRecords[currentYear][month][card] === undefined) {
paymentRecords[currentYear][month][card] = null;
}
});
// 移除不存在的信用卡
Object.keys(paymentRecords[currentYear][month]).forEach(card => {
if (!cardNames.includes(card)) {
delete paymentRecords[currentYear][month][card];
}
});
}
}
saveData();
}
// 切換年份
function changeYear(direction) {
const newYear = currentYear + direction;
const currentYearNum = new Date().getFullYear();
if (newYear < currentYearNum - 2 || newYear > currentYearNum + 10) {
alert('年份超出允許範圍!僅支援近三年及未來十年。');
return;
}
currentYear = newYear;
document.getElementById('currentYear').textContent = currentYear;
initializeRecords();
renderMonths();
updateStats();
}
// 切換繳費狀態
function togglePayment(month, cardIndex) {
const cardName = cardNames[cardIndex];
if (!cardName || !paymentRecords[currentYear] || !paymentRecords[currentYear][month]) {
console.error('無效的資料結構', { currentYear, month, cardIndex, cardName });
alert('無法標記繳費:資料結構異常!');
return;
}
const now = new Date();
paymentRecords[currentYear][month][cardName] = {
date: now.toLocaleDateString('zh-TW'),
time: now.toLocaleTimeString('zh-TW', { hour: '2-digit', minute: '2-digit' })
};
renderMonths();
updateStats();
saveData();
}
// 取消繳費
function cancelPayment(event, month, cardIndex) {
event.stopPropagation();
event.preventDefault();
const cardName = cardNames[cardIndex];
if (!cardName || !paymentRecords[currentYear] || !paymentRecords[currentYear][month]) {
console.error('無效的資料結構', { currentYear, month, cardIndex, cardName });
alert('無法取消繳費:資料結構異常!');
return;
}
if (!paymentRecords[currentYear][month][cardName]) {
alert('此信用卡尚未標記為已繳費!');
return;
}
if (confirm(`確定要取消 ${months[month-1]} ${cardName} 的繳費紀錄嗎?`)) {
paymentRecords[currentYear][month][cardName] = null;
renderMonths();
updateStats();
saveData();
}
}
// 展開/收合過往月份
function togglePastMonths() {
isPastMonthsExpanded = !isPastMonthsExpanded;
const pastMonths = document.getElementById('pastMonths');
const toggleButton = document.getElementById('togglePastMonths');
if (isPastMonthsExpanded) {
pastMonths.classList.remove('collapsed');
toggleButton.textContent = '收合過往月份';
} else {
pastMonths.classList.add('collapsed');
toggleButton.textContent = '展開過往月份';
}
saveData();
}
// 渲染月份卡片
function renderMonths() {
const monthGrid = document.getElementById('monthGrid');
const pastMonths = document.getElementById('pastMonths');
monthGrid.innerHTML = '';
pastMonths.innerHTML = '';
// 確保 currentYear 已初始化
initializeRecords();
const currentMonth = new Date().getFullYear() === currentYear ? new Date().getMonth() + 1 : null;
for (let month = 1; month <= 12; month++) {
const monthCard = document.createElement('div');
monthCard.className = 'month-card';
let buttonsHTML = '';
cardNames.forEach((cardName, cardIndex) => {
const record = paymentRecords[currentYear][month][cardName] || null;
const isPaid = record !== null;
let buttonContent = '';
if (isPaid) {
buttonContent = `
<div style="margin-bottom: 5px;">${cardName} ✓</div>
<div class="payment-info">繳費時間: ${record.date} ${record.time}</div>
<button class="cancel-btn" onclick="cancelPayment(event, ${month}, ${cardIndex})">取消繳費</button>
`;
} else {
buttonContent = `${cardName} (未繳費)`;
}
buttonsHTML += `
<div class="card-btn ${isPaid ? 'paid' : 'unpaid'}"
${isPaid ? '' : `onclick="togglePayment(${month}, ${cardIndex})"`}>
${buttonContent}
</div>
`;
});
monthCard.innerHTML = `
<div class="month-title">${months[month-1]}</div>
<div class="card-buttons">
${buttonsHTML}
</div>
`;
if (month === currentMonth) {
monthGrid.appendChild(monthCard);
} else {
pastMonths.appendChild(monthCard);
}
}
const toggleButton = document.getElementById('togglePastMonths');
if (isPastMonthsExpanded) {
pastMonths.classList.remove('collapsed');
toggleButton.textContent = '收合過往月份';
} else {
pastMonths.classList.add('collapsed');
toggleButton.textContent = '展開過往月份';
}
}
// 更新統計數據(僅計算當前月份)
function updateStats() {
let paidCount = 0;
let unpaidCount = 0;
const currentYearNum = new Date().getFullYear();
const currentMonth = currentYear === currentYearNum ? new Date().getMonth() + 1 : 1;
if (paymentRecords[currentYear] && paymentRecords[currentYear][currentMonth]) {
cardNames.forEach(cardName => {
if (paymentRecords[currentYear][currentMonth][cardName]) {
paidCount++;
} else {
unpaidCount++;
}
});
} else {
unpaidCount = cardNames.length;
}
document.getElementById('paidCount').textContent = paidCount;
document.getElementById('unpaidCount').textContent = unpaidCount;
}
// 開啟設定模態框
function openSettings() {
renderCardList();
document.getElementById('settingsModal').style.display = 'block';
}
// 關閉設定模態框
function closeSettings() {
document.getElementById('settingsModal').style.display = 'none';
}
// 渲染信用卡列表
function renderCardList() {
const cardList = document.getElementById('cardList');
cardList.innerHTML = '';
cardNames.forEach((cardName, index) => {
const cardItem = document.createElement('div');
cardItem.className = 'card-item';
cardItem.innerHTML = `
<input type="text" class="card-name" value="${cardName}"
onchange="updateCardName(${index}, this.value)">
<button class="delete-btn" onclick="deleteCard(${index})"
${cardNames.length <= 1 ? 'disabled' : ''}>刪除</button>
`;
cardList.appendChild(cardItem);
});
}
// 更新信用卡名稱
function updateCardName(index, newName) {
if (newName.trim() === '') {
alert('信用卡名稱不能為空!');
renderCardList();
return;
}
const trimmedName = newName.trim();
if (cardNames.some((name, i) => i !== index && name === trimmedName)) {
alert('這張信用卡名稱已經存在!');
renderCardList();
return;
}
const oldName = cardNames[index];
cardNames[index] = trimmedName;
Object.keys(paymentRecords).forEach(year => {
Object.keys(paymentRecords[year]).forEach(month => {
if (paymentRecords[year][month][oldName] !== undefined) {
paymentRecords[year][month][trimmedName] = paymentRecords[year][month][oldName];
delete paymentRecords[year][month][oldName];
}
});
});
renderMonths();
updateStats();
saveData();
}
// 刪除信用卡
function deleteCard(index) {
if (cardNames.length <= 1) {
alert('至少需要保留一張信用卡!');
return;
}
if (confirm(`確定要刪除 ${cardNames[index]} 嗎?這將會刪除所有相關的繳費紀錄。`)) {
const deletedCardName = cardNames[index];
cardNames.splice(index, 1);
Object.keys(paymentRecords).forEach(year => {
Object.keys(paymentRecords[year]).forEach(month => {
delete paymentRecords[year][month][deletedCardName];
});
});
renderCardList();
renderMonths();
updateStats();
saveData();
}
}
// 新增信用卡
function addCard() {
const newCardName = document.getElementById('newCardName').value.trim();
if (newCardName === '') {
alert('請輸入信用卡名稱!');
return;
}
if (cardNames.includes(newCardName)) {
alert('這張信用卡已經存在!');
return;
}
cardNames.push(newCardName);
Object.keys(paymentRecords).forEach(year => {
Object.keys(paymentRecords[year]).forEach(month => {
paymentRecords[year][month][newCardName] = null;
});
});
document.getElementById('newCardName').value = '';
renderCardList();
renderMonths();
updateStats();
saveData();
}
// 點擊模態框外部關閉
window.onclick = function(event) {
const modal = document.getElementById('settingsModal');
if (event.target === modal) {
closeSettings();
}
}
// 初始化
document.addEventListener('DOMContentLoaded', () => {
loadData();
initializeRecords();
renderMonths();
updateStats();
});
</script>