Initial daily news digest web
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
const $ = (selector) => document.querySelector(selector);
|
||||
const escapeHtml = (value) => String(value ?? '').replace(/[&<>'"]/g, (char) => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }[char]));
|
||||
const categoryNames = { general: '综合', ai_agent: 'AI 与 Agent', agri_hardware: '农业硬件', agri_solutions: '方案落地', agri_services: '农业服务', policy_market: '政策市场' };
|
||||
|
||||
function today() { return new Date().toISOString().slice(0, 10); }
|
||||
function formatDate(value) { return value ? new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: 'long', day: 'numeric', weekday: 'short' }).format(new Date(`${value}T00:00:00`)) : '—'; }
|
||||
function formatTime(value) { return value ? new Intl.DateTimeFormat('zh-CN', { month: 'short', day: 'numeric' }).format(new Date(value)) : '近期'; }
|
||||
|
||||
async function loadHealth() {
|
||||
try {
|
||||
const response = await fetch('/api/health');
|
||||
const label = $('#healthLabel');
|
||||
if (response.ok) label.innerHTML = '<span class="status-dot"></span>数据库连接正常';
|
||||
else throw new Error();
|
||||
} catch { $('#healthLabel').innerHTML = '<span class="status-dot error"></span>数据库暂不可用'; }
|
||||
}
|
||||
|
||||
function renderArticle(article) {
|
||||
const title = article.translated_title || article.title || '无标题';
|
||||
const summary = article.translated_summary || article.summary || '暂无摘要';
|
||||
const category = categoryNames[article.section] || categoryNames[article.category] || '综合';
|
||||
return `<article class="article">
|
||||
<div class="article-meta"><span class="tag">${escapeHtml(category)}</span><span>${escapeHtml(article.source_name || '未知来源')}</span><span>·</span><span>${escapeHtml(formatTime(article.published_at))}</span></div>
|
||||
<h3><a href="${escapeHtml(article.url)}" target="_blank" rel="noreferrer">${escapeHtml(title)}</a></h3>
|
||||
<p>${escapeHtml(summary)}</p>
|
||||
</article>`;
|
||||
}
|
||||
|
||||
function renderReports(payload) {
|
||||
$('#reportCount').textContent = payload.total ?? 0;
|
||||
const reports = payload.reports || [];
|
||||
const articleCount = reports.reduce((sum, report) => sum + (report.articles || []).length, 0);
|
||||
$('#articleCount').textContent = articleCount;
|
||||
$('#sourceCount').textContent = new Set(reports.flatMap((report) => (report.articles || []).map((item) => item.source_name).filter(Boolean))).size;
|
||||
if (!reports.length) {
|
||||
$('#reportList').innerHTML = '<div class="empty-state"><strong>还没有符合条件的日报</strong><span>n8n 完成第一次抓取后,内容会出现在这里。</span></div>';
|
||||
return;
|
||||
}
|
||||
$('#reportList').innerHTML = reports.map((report) => `<article class="report-card card">
|
||||
<div class="report-top"><div><div class="report-date">${escapeHtml(formatDate(report.report_date))}</div><h2>${escapeHtml(report.title)}</h2><p class="report-intro">${escapeHtml(report.introduction || '今日资讯已按主题整理。')}</p></div><span class="report-status">${report.status === 'published' ? '已发布' : '草稿'}</span></div>
|
||||
<div class="article-grid">${(report.articles || []).map(renderArticle).join('')}</div>
|
||||
</article>`).join('');
|
||||
}
|
||||
|
||||
async function loadReports() {
|
||||
const params = new URLSearchParams();
|
||||
const date = $('#dateInput').value;
|
||||
const q = $('#searchInput').value.trim();
|
||||
const category = $('#categoryInput').value;
|
||||
if (date) params.set('date', date);
|
||||
if (q) params.set('q', q);
|
||||
if (category) params.set('category', category);
|
||||
$('#reportList').innerHTML = '<div class="empty-state"><span>正在读取日报…</span></div>';
|
||||
try {
|
||||
const response = await fetch(`/api/reports?${params}`);
|
||||
const payload = await response.json();
|
||||
if (!response.ok) throw new Error(payload.error || '读取失败');
|
||||
renderReports(payload);
|
||||
} catch (error) {
|
||||
$('#reportList').innerHTML = `<div class="empty-state"><strong>暂时无法读取日报</strong><span>${escapeHtml(error.message)}</span></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
$('#todayLabel').textContent = formatDate(today());
|
||||
$('#dateInput').value = today();
|
||||
$('#dateInput').addEventListener('change', loadReports);
|
||||
$('#categoryInput').addEventListener('change', loadReports);
|
||||
let searchTimer;
|
||||
$('#searchInput').addEventListener('input', () => { clearTimeout(searchTimer); searchTimer = setTimeout(loadReports, 280); });
|
||||
$('#clearFilters').addEventListener('click', () => { $('#dateInput').value = ''; $('#categoryInput').value = ''; $('#searchInput').value = ''; loadReports(); });
|
||||
loadHealth();
|
||||
loadReports();
|
||||
@@ -0,0 +1,62 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="theme-color" content="#123c34">
|
||||
<title>每日情报 · Daily Digest</title>
|
||||
<link rel="stylesheet" href="/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/">
|
||||
<span class="brand-mark">D</span>
|
||||
<span><strong>Daily Digest</strong><small>每日行业情报</small></span>
|
||||
</a>
|
||||
<nav><a class="nav-link active" href="/">日报</a><a class="nav-link" href="/settings">设置管理</a></nav>
|
||||
</header>
|
||||
|
||||
<main class="shell">
|
||||
<section class="hero">
|
||||
<div class="hero-copy">
|
||||
<div class="eyebrow"><span class="live-dot"></span> DAILY SIGNALS</div>
|
||||
<h1>今天值得读的<br><em>行业情报</em></h1>
|
||||
<p>把 AI、Agent 与农业产业链的重要动态,按主题整理成一份可追踪的日报。</p>
|
||||
</div>
|
||||
<div class="hero-side">
|
||||
<div class="date-label">今日日期</div>
|
||||
<div id="todayLabel" class="today-date">—</div>
|
||||
<div id="healthLabel" class="health-label"><span class="status-dot"></span>正在检查数据库</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="toolbar card">
|
||||
<div class="toolbar-heading"><span class="section-kicker">ARCHIVE</span><h2>日报档案</h2></div>
|
||||
<div class="filters">
|
||||
<label class="search-box"><span>⌕</span><input id="searchInput" type="search" placeholder="搜索标题、摘要或关键词" aria-label="搜索日报"></label>
|
||||
<label class="date-box"><span>日期</span><input id="dateInput" type="date" aria-label="选择日期"></label>
|
||||
<select id="categoryInput" aria-label="选择栏目">
|
||||
<option value="">全部栏目</option>
|
||||
<option value="ai_agent">AI 与 Agent</option>
|
||||
<option value="agri_hardware">农业硬件</option>
|
||||
<option value="agri_solutions">农业方案落地</option>
|
||||
<option value="agri_services">农业服务</option>
|
||||
<option value="policy_market">政策与市场</option>
|
||||
</select>
|
||||
<button id="clearFilters" class="button button-ghost" type="button">清除</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="summary-grid" aria-label="日报统计">
|
||||
<div class="stat-card"><span class="stat-label">报告数量</span><strong id="reportCount">—</strong><small>按筛选条件</small></div>
|
||||
<div class="stat-card"><span class="stat-label">今日文章</span><strong id="articleCount">—</strong><small>已收录资讯</small></div>
|
||||
<div class="stat-card accent-card"><span class="stat-label">信息状态</span><strong id="sourceCount">—</strong><small>来源持续更新中</small></div>
|
||||
</section>
|
||||
|
||||
<section id="reportList" class="report-list" aria-live="polite"></section>
|
||||
</main>
|
||||
|
||||
<footer class="footer"><span>DAILY DIGEST</span><span>资讯原文版权归原作者所有</span></footer>
|
||||
<script src="/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,49 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="theme-color" content="#123c34">
|
||||
<title>设置管理 · Daily Digest</title>
|
||||
<link rel="stylesheet" href="/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/"><span class="brand-mark">D</span><span><strong>Daily Digest</strong><small>每日行业情报</small></span></a>
|
||||
<nav><a class="nav-link" href="/">日报</a><a class="nav-link active" href="/settings">设置管理</a></nav>
|
||||
</header>
|
||||
|
||||
<main class="shell settings-shell">
|
||||
<section class="page-heading"><div class="eyebrow">CONTROL ROOM</div><h1>推送设置</h1><p>管理日报收件邮箱,为每个邮箱选择一个或多个内容模块。</p></section>
|
||||
|
||||
<section class="token-card card">
|
||||
<div><span class="section-kicker">PRIVATE ACCESS</span><h2>管理令牌</h2><p>令牌只保存在当前浏览器会话中,用于保护邮箱管理接口。</p></div>
|
||||
<div class="token-controls"><input id="tokenInput" type="password" autocomplete="off" placeholder="输入服务器 APP_ADMIN_TOKEN"><button id="saveToken" class="button button-dark" type="button">保存令牌</button></div>
|
||||
<div id="tokenMessage" class="form-message" role="status"></div>
|
||||
</section>
|
||||
|
||||
<div class="settings-grid">
|
||||
<section class="card form-card">
|
||||
<div class="card-heading"><div><span class="section-kicker">RECIPIENT</span><h2 id="formTitle">添加收件邮箱</h2></div><button id="cancelEdit" class="text-button hidden" type="button">取消编辑</button></div>
|
||||
<form id="recipientForm">
|
||||
<input id="recipientId" type="hidden">
|
||||
<label>邮箱地址<input id="emailInput" type="email" required placeholder="name@example.com"></label>
|
||||
<label>显示名称<span class="optional">可选</span><input id="nameInput" type="text" maxlength="120" placeholder="我的日报"></label>
|
||||
<div class="form-row"><label>发送时间<input id="timeInput" type="time" value="07:30"></label><label>时区<input id="timezoneInput" type="text" value="Asia/Shanghai"></label></div>
|
||||
<label class="check-line"><input id="allModulesInput" type="checkbox"><span>订阅全部模块</span></label>
|
||||
<div class="module-picker"><div class="picker-label">选择推送模块 <span>可多选</span></div><div id="moduleOptions" class="module-options"></div></div>
|
||||
<button class="button button-primary full-button" type="submit"><span id="submitLabel">保存邮箱设置</span><span>↗</span></button>
|
||||
<div id="formMessage" class="form-message" role="alert"></div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="card recipients-card">
|
||||
<div class="card-heading"><div><span class="section-kicker">DELIVERY LIST</span><h2>已配置邮箱</h2></div><span id="recipientCount" class="count-pill">0</span></div>
|
||||
<div id="recipientList" class="recipient-list"><div class="loading-line">正在加载…</div></div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="footer"><span>DAILY DIGEST</span><span>只向已启用邮箱发送日报</span></footer>
|
||||
<script src="/settings.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,62 @@
|
||||
const $ = (selector) => document.querySelector(selector);
|
||||
const escapeHtml = (value) => String(value ?? '').replace(/[&<>'"]/g, (char) => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }[char]));
|
||||
let modules = [];
|
||||
let editingId = null;
|
||||
|
||||
function token() { return sessionStorage.getItem('daily_digest_admin_token') || ''; }
|
||||
function headers(json = false) { const value = { 'X-Admin-Token': token() }; if (json) value['Content-Type'] = 'application/json'; return value; }
|
||||
function message(selector, text, type = '') { const el = $(selector); el.textContent = text; el.className = `form-message ${type}`; }
|
||||
|
||||
async function api(path, options = {}) {
|
||||
const response = await fetch(path, { ...options, headers: { ...headers(Boolean(options.body)), ...(options.headers || {}) } });
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok) throw new Error(payload.error || `请求失败 (${response.status})`);
|
||||
return payload;
|
||||
}
|
||||
|
||||
function renderModuleOptions(selected = []) {
|
||||
$('#moduleOptions').innerHTML = modules.map((module) => `<label class="module-option"><input type="checkbox" value="${escapeHtml(module.module_key)}" ${selected.includes(module.module_key) ? 'checked' : ''}><span>${escapeHtml(module.module_name)}</span></label>`).join('');
|
||||
}
|
||||
function selectedModules() { return [...document.querySelectorAll('#moduleOptions input:checked')].map((input) => input.value); }
|
||||
|
||||
function renderRecipients(recipients) {
|
||||
$('#recipientCount').textContent = recipients.length;
|
||||
if (!recipients.length) { $('#recipientList').innerHTML = '<div class="empty-state"><strong>还没有收件邮箱</strong><span>在左侧添加第一个日报收件人。</span></div>'; return; }
|
||||
const names = Object.fromEntries(modules.map((module) => [module.module_key, module.module_name]));
|
||||
$('#recipientList').innerHTML = recipients.map((recipient) => `<div class="recipient-item">
|
||||
<div><div class="recipient-email">${escapeHtml(recipient.email)} ${recipient.enabled ? '' : '<span class="tag">已停用</span>'}</div><div class="recipient-sub">${escapeHtml(recipient.display_name || '未设置名称')} · 每天 ${escapeHtml(String(recipient.send_time).slice(0, 5))} · ${escapeHtml(recipient.timezone)}</div><div class="chips">${recipient.send_all_modules ? '<span class="chip">全部模块</span>' : (recipient.modules || []).map((key) => `<span class="chip">${escapeHtml(names[key] || key)}</span>`).join('')}</div></div>
|
||||
<div class="recipient-actions"><button class="icon-button" type="button" data-edit="${recipient.id}" title="编辑">✎</button><button class="icon-button delete" type="button" data-delete="${recipient.id}" title="删除">×</button></div>
|
||||
</div>`).join('');
|
||||
document.querySelectorAll('[data-edit]').forEach((button) => button.addEventListener('click', () => editRecipient(recipients.find((item) => item.id === Number(button.dataset.edit)))));
|
||||
document.querySelectorAll('[data-delete]').forEach((button) => button.addEventListener('click', () => removeRecipient(Number(button.dataset.delete))));
|
||||
}
|
||||
|
||||
async function loadRecipients() {
|
||||
try { renderRecipients((await api('/api/recipients')).recipients); }
|
||||
catch (error) { $('#recipientList').innerHTML = `<div class="empty-state"><strong>无法读取邮箱设置</strong><span>${escapeHtml(error.message)}</span></div>`; }
|
||||
}
|
||||
|
||||
function editRecipient(recipient) {
|
||||
editingId = recipient.id; $('#formTitle').textContent = '编辑收件邮箱'; $('#submitLabel').textContent = '保存修改'; $('#cancelEdit').classList.remove('hidden');
|
||||
$('#recipientId').value = recipient.id; $('#emailInput').value = recipient.email; $('#emailInput').disabled = true; $('#nameInput').value = recipient.display_name || ''; $('#timeInput').value = String(recipient.send_time).slice(0, 5); $('#timezoneInput').value = recipient.timezone || 'Asia/Shanghai'; $('#allModulesInput').checked = recipient.send_all_modules; renderModuleOptions(recipient.modules || []); window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
function resetForm() { editingId = null; $('#formTitle').textContent = '添加收件邮箱'; $('#submitLabel').textContent = '保存邮箱设置'; $('#cancelEdit').classList.add('hidden'); $('#recipientForm').reset(); $('#emailInput').disabled = false; $('#timeInput').value = '07:30'; $('#timezoneInput').value = 'Asia/Shanghai'; renderModuleOptions([]); message('#formMessage', ''); }
|
||||
|
||||
async function removeRecipient(id) {
|
||||
if (!window.confirm('确定删除这个收件邮箱吗?历史发送记录会保留,但以后不会再发送。')) return;
|
||||
try { await api(`/api/recipients/${id}`, { method: 'DELETE' }); message('#tokenMessage', '邮箱已删除', 'success'); await loadRecipients(); }
|
||||
catch (error) { message('#tokenMessage', error.message, 'error'); }
|
||||
}
|
||||
|
||||
$('#saveToken').addEventListener('click', async () => { const value = $('#tokenInput').value.trim(); if (!value) return message('#tokenMessage', '请输入管理令牌', 'error'); sessionStorage.setItem('daily_digest_admin_token', value); message('#tokenMessage', '令牌已保存,正在验证…'); await loadRecipients(); });
|
||||
$('#tokenInput').value = token();
|
||||
$('#cancelEdit').addEventListener('click', resetForm);
|
||||
$('#allModulesInput').addEventListener('change', (event) => { document.querySelectorAll('#moduleOptions input').forEach((input) => { input.disabled = event.target.checked; }); });
|
||||
$('#recipientForm').addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
const body = { email: $('#emailInput').value.trim(), display_name: $('#nameInput').value.trim(), send_time: $('#timeInput').value, timezone: $('#timezoneInput').value.trim(), send_all_modules: $('#allModulesInput').checked, modules: selectedModules() };
|
||||
try { await api(editingId ? `/api/recipients/${editingId}` : '/api/recipients', { method: editingId ? 'PUT' : 'POST', body: JSON.stringify(body) }); message('#formMessage', editingId ? '邮箱设置已更新' : '邮箱已添加', 'success'); resetForm(); await loadRecipients(); }
|
||||
catch (error) { message('#formMessage', error.message, 'error'); }
|
||||
});
|
||||
|
||||
(async function init() { try { modules = (await fetch('/api/modules').then((response) => response.json())).modules || []; renderModuleOptions(); await loadRecipients(); } catch (error) { message('#tokenMessage', error.message, 'error'); } })();
|
||||
@@ -0,0 +1,88 @@
|
||||
:root {
|
||||
--ink: #17342e;
|
||||
--muted: #70827d;
|
||||
--line: #dbe6e0;
|
||||
--paper: #f6f8f5;
|
||||
--card: #ffffff;
|
||||
--green: #1d594c;
|
||||
--green-dark: #123c34;
|
||||
--mint: #cfe7d9;
|
||||
--orange: #e57b4b;
|
||||
--shadow: 0 14px 36px rgba(31, 67, 56, .07);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: var(--paper); color: var(--ink); font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; line-height: 1.55; }
|
||||
a { color: inherit; text-decoration: none; }
|
||||
button, input, select { font: inherit; }
|
||||
button { cursor: pointer; }
|
||||
.topbar { height: 76px; padding: 0 max(24px, calc((100vw - 1180px) / 2)); display: flex; align-items: center; justify-content: space-between; background: rgba(246,248,245,.9); border-bottom: 1px solid var(--line); }
|
||||
.brand { display: inline-flex; align-items: center; gap: 11px; }
|
||||
.brand-mark { display: grid; place-items: center; width: 33px; height: 33px; border-radius: 9px; color: #fff; background: var(--green-dark); font-weight: 800; letter-spacing: -.08em; }
|
||||
.brand strong, .brand small { display: block; line-height: 1.15; }
|
||||
.brand strong { font-size: 14px; letter-spacing: .04em; }
|
||||
.brand small { margin-top: 3px; color: var(--muted); font-size: 10px; letter-spacing: .12em; }
|
||||
nav { display: flex; gap: 8px; }
|
||||
.nav-link { padding: 9px 14px; border-radius: 10px; color: var(--muted); font-size: 13px; }
|
||||
.nav-link:hover, .nav-link.active { color: var(--green-dark); background: #e9f1eb; }
|
||||
.shell { width: min(1180px, calc(100% - 48px)); margin: 0 auto; padding: 54px 0 72px; }
|
||||
.hero { display: flex; justify-content: space-between; gap: 32px; padding: 26px 32px 34px; min-height: 238px; border-radius: 25px; color: #fff; background: var(--green-dark); overflow: hidden; position: relative; box-shadow: var(--shadow); }
|
||||
.hero::after { content: ""; width: 350px; height: 350px; position: absolute; right: -80px; bottom: -210px; border: 1px solid rgba(211,241,220,.24); border-radius: 50%; box-shadow: 0 0 0 36px rgba(211,241,220,.05), 0 0 0 72px rgba(211,241,220,.04); }
|
||||
.hero-copy, .hero-side { position: relative; z-index: 1; }
|
||||
.eyebrow, .section-kicker { color: var(--orange); font-size: 10px; font-weight: 800; letter-spacing: .2em; }
|
||||
.hero h1 { margin: 14px 0 10px; font-size: clamp(34px, 5vw, 56px); line-height: .98; letter-spacing: -.06em; }
|
||||
.hero h1 em { color: #bde7ca; font-style: normal; }
|
||||
.hero p { max-width: 470px; margin: 0; color: #b7d1c4; font-size: 14px; }
|
||||
.live-dot { display: inline-block; width: 7px; height: 7px; margin-right: 6px; border-radius: 50%; background: #8ee4a9; box-shadow: 0 0 0 5px rgba(142,228,169,.12); }
|
||||
.hero-side { align-self: flex-end; min-width: 180px; padding: 14px 0 0 30px; border-left: 1px solid rgba(207,231,217,.24); }
|
||||
.date-label { color: #8fb7a7; font-size: 11px; letter-spacing: .12em; }
|
||||
.today-date { margin: 3px 0 10px; font-size: 21px; font-weight: 700; }
|
||||
.health-label { color: #a9cfba; font-size: 11px; }
|
||||
.status-dot { display: inline-block; width: 6px; height: 6px; margin-right: 5px; border-radius: 50%; background: #8ee4a9; }
|
||||
.status-dot.error { background: #ff9e7c; }
|
||||
.card { background: var(--card); border: 1px solid var(--line); border-radius: 18px; box-shadow: var(--shadow); }
|
||||
.toolbar { display: flex; gap: 24px; align-items: center; justify-content: space-between; margin-top: -28px; padding: 19px 22px; position: relative; z-index: 2; }
|
||||
.toolbar-heading h2, .card-heading h2, .token-card h2 { margin: 3px 0 0; font-size: 19px; letter-spacing: -.03em; }
|
||||
.filters { display: flex; flex: 1; justify-content: flex-end; gap: 8px; }
|
||||
.search-box, .date-box, select { height: 40px; display: flex; align-items: center; border: 1px solid var(--line); border-radius: 9px; background: #fbfcfb; color: var(--muted); font-size: 12px; }
|
||||
.search-box { width: min(260px, 32vw); padding: 0 11px; gap: 7px; }
|
||||
.search-box span { font-size: 21px; line-height: 0; }
|
||||
.search-box input, .date-box input, select { min-width: 0; border: 0; outline: 0; color: var(--ink); background: transparent; }
|
||||
.search-box input { width: 100%; }
|
||||
.date-box { padding: 0 9px; gap: 5px; }
|
||||
.date-box input { width: 120px; font-size: 12px; }
|
||||
select { padding: 0 9px; }
|
||||
.button { border: 0; border-radius: 9px; padding: 10px 14px; font-size: 12px; font-weight: 700; transition: transform .15s, background .15s; }
|
||||
.button:hover { transform: translateY(-1px); }
|
||||
.button-primary { display: flex; justify-content: space-between; align-items: center; color: #fff; background: var(--green); }
|
||||
.button-dark { color: #fff; background: var(--green-dark); }
|
||||
.button-ghost { color: var(--green); background: #edf4ef; }
|
||||
.full-button { width: 100%; margin-top: 23px; }
|
||||
.summary-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 13px; margin: 21px 0 30px; }
|
||||
.stat-card { padding: 19px 21px; border: 1px solid var(--line); border-radius: 15px; background: #fff; }
|
||||
.stat-card strong { display: block; margin: 4px 0 0; font-size: 28px; letter-spacing: -.05em; }
|
||||
.stat-label, .stat-card small { color: var(--muted); font-size: 11px; }
|
||||
.stat-card small { display: block; margin-top: 2px; }
|
||||
.accent-card { background: #e5f1e7; border-color: #c5dfcb; }
|
||||
.report-list { display: grid; gap: 15px; }
|
||||
.report-card { padding: 25px 27px; }
|
||||
.report-top { display: flex; justify-content: space-between; gap: 20px; align-items: flex-start; border-bottom: 1px solid var(--line); padding-bottom: 17px; }
|
||||
.report-date { color: var(--muted); font-size: 12px; letter-spacing: .08em; }
|
||||
.report-card h2 { margin: 4px 0 5px; font-size: 22px; letter-spacing: -.04em; }
|
||||
.report-intro { margin: 0; color: var(--muted); font-size: 13px; }
|
||||
.report-status { padding: 5px 9px; border-radius: 99px; color: var(--green); background: #e4f1e8; font-size: 10px; white-space: nowrap; }
|
||||
.article-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1px 28px; margin-top: 11px; }
|
||||
.article { padding: 16px 0; border-bottom: 1px solid #edf1ee; }
|
||||
.article-meta { display: flex; gap: 7px; align-items: center; color: var(--muted); font-size: 10px; }
|
||||
.tag { display: inline-block; padding: 3px 7px; border-radius: 5px; color: var(--green); background: #e8f2ea; font-weight: 700; }
|
||||
.article h3 { margin: 7px 0 5px; font-size: 15px; line-height: 1.4; }
|
||||
.article h3 a:hover { color: var(--orange); }
|
||||
.article p { display: -webkit-box; margin: 0; overflow: hidden; color: var(--muted); font-size: 12px; line-height: 1.6; -webkit-box-orient: vertical; -webkit-line-clamp: 3; }
|
||||
.empty-state { padding: 64px 24px; text-align: center; border: 1px dashed #cbdad0; border-radius: 18px; color: var(--muted); background: rgba(255,255,255,.56); }
|
||||
.empty-state strong { display: block; margin-bottom: 5px; color: var(--ink); }
|
||||
.loading-line { color: var(--muted); font-size: 13px; }
|
||||
.footer { display: flex; justify-content: space-between; width: min(1180px, calc(100% - 48px)); margin: 0 auto; padding: 0 0 28px; color: #94a49e; font-size: 10px; letter-spacing: .1em; }
|
||||
.page-heading { margin: 8px 0 31px; }.page-heading h1 { margin: 9px 0 4px; font-size: 42px; letter-spacing: -.06em; }.page-heading p { margin: 0; color: var(--muted); font-size: 14px; }
|
||||
.settings-shell { padding-top: 46px; }.token-card { display: grid; grid-template-columns: 1fr minmax(310px, 430px); gap: 20px; align-items: center; padding: 22px 25px; margin-bottom: 18px; }.token-card p { margin: 7px 0 0; color: var(--muted); font-size: 12px; }.token-controls { display: flex; gap: 8px; }.token-controls input, .form-card input[type="email"], .form-card input[type="text"], .form-card input[type="time"] { min-width: 0; width: 100%; height: 40px; padding: 0 11px; border: 1px solid var(--line); border-radius: 8px; outline: none; color: var(--ink); background: #fbfcfb; }.token-controls input:focus, .form-card input:focus { border-color: #8ebda0; box-shadow: 0 0 0 3px #e8f2eb; }.settings-grid { display: grid; grid-template-columns: .88fr 1.12fr; gap: 18px; }.form-card, .recipients-card { padding: 24px; }.card-heading { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 23px; }.form-card form > label, .form-row label { display: block; margin-bottom: 15px; color: var(--ink); font-size: 12px; font-weight: 700; }.form-card label input { display: block; margin-top: 6px; }.optional, .picker-label span { margin-left: 5px; color: var(--muted); font-size: 10px; font-weight: 400; }.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }.check-line { display: flex !important; gap: 8px; align-items: center; font-weight: 600 !important; }.check-line input { accent-color: var(--green); }.module-picker { padding: 14px; border-radius: 11px; background: #f4f8f4; }.picker-label { margin-bottom: 9px; font-size: 12px; font-weight: 700; }.module-options { display: flex; flex-wrap: wrap; gap: 7px; }.module-option { position: relative; }.module-option input { position: absolute; opacity: 0; }.module-option span { display: block; padding: 7px 9px; border: 1px solid #d5e4d8; border-radius: 7px; color: #5d7169; background: #fff; font-size: 11px; cursor: pointer; }.module-option input:checked + span { border-color: #9bc7a8; color: var(--green-dark); background: #dcefe0; font-weight: 700; }.text-button { border: 0; color: var(--green); background: transparent; font-size: 12px; }.hidden { display: none !important; }.form-message { min-height: 18px; margin-top: 9px; color: var(--muted); font-size: 11px; }.form-message.error { color: #b74d37; }.form-message.success { color: var(--green); }.count-pill { min-width: 25px; padding: 3px 8px; border-radius: 20px; color: var(--green); background: #e6f1e8; font-size: 11px; text-align: center; }.recipient-list { display: grid; gap: 10px; }.recipient-item { display: flex; justify-content: space-between; gap: 12px; padding: 14px 0; border-bottom: 1px solid #edf1ee; }.recipient-item:last-child { border-bottom: 0; }.recipient-email { font-size: 13px; font-weight: 700; }.recipient-sub { margin-top: 4px; color: var(--muted); font-size: 11px; }.chips { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 8px; }.chip { padding: 3px 6px; border-radius: 5px; color: var(--green); background: #edf5ee; font-size: 10px; }.recipient-actions { display: flex; align-items: center; gap: 7px; }.icon-button { width: 30px; height: 30px; border: 1px solid var(--line); border-radius: 7px; color: var(--muted); background: #fff; }.icon-button:hover { color: var(--green); border-color: #9bc7a8; }.icon-button.delete:hover { color: #bd523c; border-color: #e9b0a1; }
|
||||
@media (max-width: 840px) { .hero { min-height: auto; flex-direction: column; }.hero-side { align-self: flex-start; padding: 14px 0 0; border-left: 0; border-top: 1px solid rgba(207,231,217,.24); width: 100%; }.toolbar { align-items: flex-start; flex-direction: column; margin-top: 15px; }.filters { width: 100%; justify-content: flex-start; flex-wrap: wrap; }.search-box { flex: 1; width: auto; }.settings-grid { grid-template-columns: 1fr; }.token-card { grid-template-columns: 1fr; }.article-grid { grid-template-columns: 1fr; } }
|
||||
@media (max-width: 540px) { .topbar { height: 66px; padding: 0 18px; }.nav-link { padding: 8px; font-size: 11px; }.shell { width: calc(100% - 28px); padding-top: 30px; }.hero { padding: 24px 22px 26px; border-radius: 20px; }.hero h1 { font-size: 39px; }.summary-grid { gap: 7px; }.stat-card { padding: 13px 12px; }.stat-card strong { font-size: 22px; }.stat-card small { font-size: 9px; }.report-card { padding: 20px 17px; }.report-top { flex-direction: column; gap: 9px; }.footer { width: calc(100% - 28px); }.page-heading h1 { font-size: 35px; }.token-card, .form-card, .recipients-card { padding: 19px; }.token-controls { flex-direction: column; } }
|
||||
Reference in New Issue
Block a user