接口跑通,基础功能全部实现

This commit is contained in:
2026-03-16 16:14:08 +08:00
parent f81aec48ca
commit 2f2d5c3795
38 changed files with 3352 additions and 1754 deletions

View File

@@ -1,30 +1,365 @@
{% extends "base.html" %}
{% block title %}Add Task - Weibo-HotSign{% endblock %}
{% block title %}添加任务 - 微博超话签到{% endblock %}
{% block extra_css %}
<style>
.task-container { max-width: 560px; margin: 0 auto; }
/* 时间轮盘 */
.wheel-section {
display: flex;
gap: 20px;
justify-content: center;
align-items: flex-start;
margin: 24px 0 28px;
}
.wheel-col { text-align: center; }
.wheel-col .wheel-label {
font-size: 13px;
font-weight: 600;
color: #94a3b8;
margin-bottom: 10px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.wheel-wrapper {
position: relative;
width: 90px;
height: 180px;
overflow: hidden;
border-radius: 20px;
background: #f8fafc;
border: 1.5px solid #e2e8f0;
}
.wheel-wrapper::before, .wheel-wrapper::after {
content: '';
position: absolute;
left: 0; right: 0;
height: 60px;
z-index: 2;
pointer-events: none;
}
.wheel-wrapper::before {
top: 0;
background: linear-gradient(to bottom, rgba(248,250,252,1), rgba(248,250,252,0));
}
.wheel-wrapper::after {
bottom: 0;
background: linear-gradient(to top, rgba(248,250,252,1), rgba(248,250,252,0));
}
.wheel-highlight {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
width: 78px; height: 44px;
border-radius: 14px;
background: linear-gradient(135deg, rgba(99,102,241,0.1), rgba(168,85,247,0.1));
border: 2px solid rgba(99,102,241,0.3);
z-index: 1;
pointer-events: none;
}
.wheel-scroll {
position: relative;
z-index: 1;
height: 100%;
overflow-y: scroll;
scroll-snap-type: y mandatory;
-ms-overflow-style: none;
scrollbar-width: none;
}
.wheel-scroll::-webkit-scrollbar { display: none; }
.wheel-item {
height: 44px;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
font-weight: 600;
color: #cbd5e1;
scroll-snap-align: center;
transition: color 0.15s, transform 0.15s;
cursor: pointer;
user-select: none;
}
.wheel-item.active {
color: #6366f1;
transform: scale(1.1);
}
/* 重复模式 */
.repeat-options {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
}
.repeat-chip {
padding: 8px 18px;
border-radius: 14px;
border: 1.5px solid #e2e8f0;
background: #fff;
font-size: 14px;
font-weight: 500;
color: #64748b;
cursor: pointer;
transition: all 0.2s;
user-select: none;
}
.repeat-chip:hover { border-color: #818cf8; color: #6366f1; }
.repeat-chip.selected {
background: linear-gradient(135deg, #6366f1, #818cf8);
color: white;
border-color: transparent;
box-shadow: 0 2px 8px rgba(99,102,241,0.25);
}
/* 星期选择 */
.weekday-row {
display: flex;
gap: 6px;
margin-top: 12px;
}
.weekday-btn {
width: 44px; height: 44px;
border-radius: 14px;
border: 1.5px solid #e2e8f0;
background: #fff;
font-size: 14px;
font-weight: 600;
color: #64748b;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.weekday-btn:hover { border-color: #818cf8; }
.weekday-btn.selected {
background: linear-gradient(135deg, #6366f1, #818cf8);
color: white;
border-color: transparent;
}
/* 预览 */
.cron-preview {
margin-top: 20px;
padding: 16px 20px;
background: #f8fafc;
border-radius: 16px;
border: 1.5px solid #e2e8f0;
font-size: 14px;
}
.cron-preview code {
display: inline-block;
background: #eef2ff;
color: #6366f1;
padding: 3px 10px;
border-radius: 8px;
font-family: 'SF Mono', Monaco, monospace;
font-size: 14px;
font-weight: 600;
}
.cron-preview .desc { color: #64748b; margin-top: 6px; }
</style>
{% endblock %}
{% block content %}
<div style="max-width: 600px; margin: 0 auto;">
<h1 style="margin-bottom: 30px;">Add Signin Task</h1>
<div class="task-container">
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:24px;">
<h1 style="font-size:24px; font-weight:700; color:#1e293b;">⏰ 添加签到任务</h1>
<a href="{{ url_for('account_detail', account_id=account_id) }}" class="btn btn-secondary">返回</a>
</div>
<div class="card">
<form method="POST">
<div class="form-group">
<label for="cron_expression">Cron Expression</label>
<input type="text" id="cron_expression" name="cron_expression" required placeholder="e.g., 0 9 * * *">
<small style="color: #999; display: block; margin-top: 8px;">
Standard cron format (minute hour day month weekday)<br>
Examples:<br>
<code>0 9 * * *</code> - Every day at 9:00 AM<br>
<code>0 9 * * 1-5</code> - Weekdays at 9:00 AM<br>
<code>0 9,21 * * *</code> - Every day at 9:00 AM and 9:00 PM
</small>
</div>
<div class="card-header">选择签到时间</div>
<!-- 时间轮盘 -->
<div class="wheel-section">
<div class="wheel-col">
<div class="wheel-label"></div>
<div class="wheel-wrapper">
<div class="wheel-highlight"></div>
<div class="wheel-scroll" id="hour-wheel"></div>
</div>
</div>
<div style="font-size:32px; font-weight:700; color:#cbd5e1; padding-top:50px;">:</div>
<div class="wheel-col">
<div class="wheel-label"></div>
<div class="wheel-wrapper">
<div class="wheel-highlight"></div>
<div class="wheel-scroll" id="minute-wheel"></div>
</div>
</div>
</div>
<!-- 重复模式 -->
<div class="card-header" style="margin-top:8px;">重复方式</div>
<div class="repeat-options">
<div class="repeat-chip selected" data-mode="daily" onclick="setRepeat('daily')">每天</div>
<div class="repeat-chip" data-mode="weekdays" onclick="setRepeat('weekdays')">工作日</div>
<div class="repeat-chip" data-mode="custom" onclick="setRepeat('custom')">自定义</div>
</div>
<div id="weekday-picker" style="display:none; margin-top:12px;">
<div class="weekday-row">
<div class="weekday-btn" data-day="1" onclick="toggleDay(this)"></div>
<div class="weekday-btn" data-day="2" onclick="toggleDay(this)"></div>
<div class="weekday-btn" data-day="3" onclick="toggleDay(this)"></div>
<div class="weekday-btn" data-day="4" onclick="toggleDay(this)"></div>
<div class="weekday-btn" data-day="5" onclick="toggleDay(this)"></div>
<div class="weekday-btn" data-day="6" onclick="toggleDay(this)"></div>
<div class="weekday-btn" data-day="0" onclick="toggleDay(this)"></div>
</div>
</div>
<!-- Cron 预览 -->
<div class="cron-preview">
<div>生成的 Cron 表达式: <code id="cron-display">0 8 * * *</code></div>
<div class="desc" id="cron-desc">每天 08:00 执行签到</div>
</div>
<!-- 提交 -->
<form method="POST" id="task-form" style="margin-top:20px;">
<input type="hidden" id="cron_expression" name="cron_expression" value="0 8 * * *">
<div class="btn-group">
<button type="submit" class="btn btn-primary">Create Task</button>
<a href="{{ url_for('account_detail', account_id=account_id) }}" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-primary" style="flex:1; text-align:center;">创建任务</button>
<a href="{{ url_for('account_detail', account_id=account_id) }}" class="btn btn-secondary" style="text-align:center;">取消</a>
</div>
</form>
</div>
</div>
<script>
let selectedHour = 8;
let selectedMinute = 0;
let repeatMode = 'daily';
let customDays = new Set();
// 初始化轮盘
function initWheel(containerId, count, padFn, onSelect) {
const container = document.getElementById(containerId);
// 添加前后空白占位(让第一个和最后一个能滚到中间)
const padCount = 2;
for (let i = 0; i < padCount; i++) {
const spacer = document.createElement('div');
spacer.className = 'wheel-item';
spacer.style.visibility = 'hidden';
container.appendChild(spacer);
}
for (let i = 0; i < count; i++) {
const item = document.createElement('div');
item.className = 'wheel-item';
item.textContent = padFn(i);
item.dataset.value = i;
item.addEventListener('click', () => {
item.scrollIntoView({behavior:'smooth', block:'center'});
});
container.appendChild(item);
}
for (let i = 0; i < padCount; i++) {
const spacer = document.createElement('div');
spacer.className = 'wheel-item';
spacer.style.visibility = 'hidden';
container.appendChild(spacer);
}
// 滚动监听
let scrollTimer;
container.addEventListener('scroll', () => {
clearTimeout(scrollTimer);
scrollTimer = setTimeout(() => {
const items = container.querySelectorAll('.wheel-item:not([style*="hidden"])');
const center = container.scrollTop + container.clientHeight / 2;
let closest = null, minDist = Infinity;
items.forEach(el => {
const dist = Math.abs(el.offsetTop + 22 - center);
if (dist < minDist) { minDist = dist; closest = el; }
});
if (closest && closest.dataset.value !== undefined) {
items.forEach(el => el.classList.remove('active'));
closest.classList.add('active');
onSelect(parseInt(closest.dataset.value));
}
}, 60);
});
return container;
}
function pad2(n) { return n.toString().padStart(2, '0'); }
const hourWheel = initWheel('hour-wheel', 24, pad2, v => { selectedHour = v; updateCron(); });
const minuteWheel = initWheel('minute-wheel', 60, pad2, v => { selectedMinute = v; updateCron(); });
// 初始滚动到默认值
setTimeout(() => {
scrollToValue(hourWheel, selectedHour);
scrollToValue(minuteWheel, selectedMinute);
}, 100);
function scrollToValue(container, value) {
const items = container.querySelectorAll('.wheel-item');
items.forEach(el => {
if (el.dataset.value == value) {
el.scrollIntoView({block:'center'});
el.classList.add('active');
}
});
}
function setRepeat(mode) {
repeatMode = mode;
document.querySelectorAll('.repeat-chip').forEach(c => c.classList.remove('selected'));
document.querySelector(`.repeat-chip[data-mode="${mode}"]`).classList.add('selected');
document.getElementById('weekday-picker').style.display = mode === 'custom' ? 'block' : 'none';
updateCron();
}
function toggleDay(el) {
const day = el.dataset.day;
if (customDays.has(day)) {
customDays.delete(day);
el.classList.remove('selected');
} else {
customDays.add(day);
el.classList.add('selected');
}
updateCron();
}
const dayNames = ['日','一','二','三','四','五','六'];
function updateCron() {
let dow = '*';
let desc = '';
if (repeatMode === 'daily') {
dow = '*';
desc = '每天';
} else if (repeatMode === 'weekdays') {
dow = '1-5';
desc = '工作日';
} else {
if (customDays.size === 0) {
dow = '*';
desc = '每天';
} else if (customDays.size === 7) {
dow = '*';
desc = '每天';
} else {
const sorted = [...customDays].sort((a,b) => a - b);
dow = sorted.join(',');
desc = '每周' + sorted.map(d => dayNames[d]).join('、');
}
}
const cron = `${selectedMinute} ${selectedHour} * * ${dow}`;
const timeStr = pad2(selectedHour) + ':' + pad2(selectedMinute);
document.getElementById('cron-display').textContent = cron;
document.getElementById('cron-desc').textContent = `${desc} ${timeStr} 执行签到`;
document.getElementById('cron_expression').value = cron;
}
</script>
{% endblock %}