Files
daily-news-app/templates/index.html
donald db0f0bbfe7 Initial commit: Daily News App
企業內部新聞彙整與分析系統
- 自動新聞抓取 (Digitimes, 經濟日報, 工商時報)
- AI 智慧摘要 (OpenAI/Claude/Ollama)
- 群組管理與訂閱通知
- 已清理 Python 快取檔案

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 23:53:24 +08:00

950 lines
32 KiB
HTML

<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>每日報導 APP</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft JhengHei", Arial, sans-serif;
background: #f5f5f5;
color: #333;
line-height: 1.6;
}
/* 登入頁面 */
#login-page {
display: flex;
min-height: 100vh;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
}
.login-card {
background: white;
padding: 3rem;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
width: 100%;
max-width: 400px;
}
.login-card h1 {
text-align: center;
margin-bottom: 2rem;
color: #2c3e50;
}
/* 導航列 */
.navbar {
background: #2c3e50;
color: white;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.navbar h1 {
font-size: 1.5rem;
}
.navbar-actions {
display: flex;
gap: 1rem;
align-items: center;
}
.lang-switch {
background: rgba(255,255,255,0.1);
border: 1px solid rgba(255,255,255,0.2);
color: white;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
}
.user-menu {
display: flex;
align-items: center;
gap: 0.5rem;
}
/* 按鈕 */
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.3s;
}
.btn-sm {
padding: 0.25rem 0.75rem;
font-size: 0.85rem;
}
.btn-primary {
background: #3498db;
color: white;
}
.btn-primary:hover {
background: #2980b9;
}
.btn-secondary {
background: #95a5a6;
color: white;
}
.btn-success {
background: #27ae60;
color: white;
}
.btn-success:hover {
background: #219a52;
}
.btn-danger {
background: #e74c3c;
color: white;
}
.btn-danger:hover {
background: #c0392b;
}
/* 容器 */
.container {
max-width: 1200px;
margin: 2rem auto;
padding: 0 1rem;
}
/* 卡片 */
.card {
background: white;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1.5rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 2px solid #ecf0f1;
}
.card-title {
font-size: 1.3rem;
font-weight: 600;
color: #2c3e50;
}
/* 表格 */
.table {
width: 100%;
border-collapse: collapse;
}
.table th,
.table td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid #ecf0f1;
}
.table th {
background: #f8f9fa;
font-weight: 600;
color: #2c3e50;
}
.table tr:hover {
background: #f8f9fa;
}
/* 標籤 */
.badge {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.85rem;
font-weight: 500;
}
.badge-success {
background: #d4edda;
color: #155724;
}
.badge-warning {
background: #fff3cd;
color: #856404;
}
.badge-danger {
background: #f8d7da;
color: #721c24;
}
.badge-info {
background: #d1ecf1;
color: #0c5460;
}
.badge-secondary {
background: #e9ecef;
color: #495057;
}
/* 表單 */
.form-group {
margin-bottom: 1.5rem;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #2c3e50;
}
.form-control {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
}
.form-control:focus {
outline: none;
border-color: #3498db;
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
}
/* 頁面切換 */
.page {
display: none;
}
.page.active {
display: block;
}
/* 標籤頁 */
.tabs {
display: flex;
border-bottom: 2px solid #ecf0f1;
margin-bottom: 1.5rem;
}
.tab {
padding: 1rem 1.5rem;
cursor: pointer;
border: none;
background: none;
font-size: 1rem;
color: #7f8c8d;
border-bottom: 3px solid transparent;
transition: all 0.3s;
}
.tab:hover {
color: #3498db;
}
.tab.active {
color: #3498db;
border-bottom-color: #3498db;
font-weight: 600;
}
/* 統計卡片 */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.stat-card {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: #3498db;
}
.stat-label {
color: #7f8c8d;
font-size: 0.9rem;
margin-top: 0.5rem;
}
/* 摘要區塊 */
.summary-box {
background: #f8f9fa;
padding: 1.5rem;
border-radius: 8px;
border-left: 4px solid #3498db;
margin: 1rem 0;
}
/* 文章列表 */
.article-item {
padding: 1rem;
border-bottom: 1px solid #ecf0f1;
display: flex;
justify-content: space-between;
align-items: center;
}
.article-item:last-child {
border-bottom: none;
}
.article-info {
flex: 1;
}
.article-title {
font-weight: 600;
margin-bottom: 0.5rem;
}
.article-meta {
color: #7f8c8d;
font-size: 0.9rem;
}
/* 切換按鈕 */
.switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 24px;
}
.slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #3498db;
}
input:checked + .slider:before {
transform: translateX(26px);
}
/* Modal */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 8px;
max-width: 800px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
padding: 2rem;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.modal-close {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #7f8c8d;
}
/* Loading Overlay */
#loading-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255,255,255,0.8);
align-items: center;
justify-content: center;
z-index: 2000;
}
.spinner {
width: 50px;
height: 50px;
border: 4px solid #ecf0f1;
border-top-color: #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Toast */
#toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 3000;
}
.toast {
background: #333;
color: white;
padding: 1rem 1.5rem;
border-radius: 4px;
margin-bottom: 0.5rem;
opacity: 0;
transition: opacity 0.3s;
}
.toast.show {
opacity: 1;
}
.toast-success {
background: #27ae60;
}
.toast-error {
background: #e74c3c;
}
.toast-info {
background: #3498db;
}
/* 關鍵字標籤 */
.keyword-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.5rem 1rem;
margin: 0.25rem;
}
.keyword-badge button {
background: none;
border: none;
cursor: pointer;
font-size: 1rem;
color: inherit;
}
/* 空狀態 */
.empty-state {
text-align: center;
padding: 3rem;
color: #7f8c8d;
}
/* 報告卡片 */
.report-card {
border-left: 4px solid #3498db;
}
/* 響應式 */
@media (max-width: 768px) {
.navbar {
flex-direction: column;
gap: 1rem;
}
.container {
padding: 0 0.5rem;
}
.stats-grid {
grid-template-columns: 1fr;
}
.tabs {
flex-wrap: wrap;
}
.tab {
flex: 1;
text-align: center;
padding: 0.75rem;
}
}
/* 分隔線 */
.section-divider {
margin: 2rem 0;
padding-top: 2rem;
border-top: 2px solid #ecf0f1;
}
</style>
</head>
<body>
<!-- Loading Overlay -->
<div id="loading-overlay">
<div class="spinner"></div>
</div>
<!-- Toast Container -->
<div id="toast-container"></div>
<!-- 登入頁面 -->
<div id="login-page">
<div class="login-card">
<h1>每日報導 APP</h1>
<form id="login-form" onsubmit="handleLoginSubmit(event)">
<div class="form-group">
<label class="form-label">帳號</label>
<input type="text" class="form-control" id="login-username" required placeholder="輸入帳號">
</div>
<div class="form-group">
<label class="form-label">密碼</label>
<input type="password" class="form-control" id="login-password" required placeholder="輸入密碼">
</div>
<div class="form-group">
<label class="form-label">認證方式</label>
<select class="form-control" id="login-auth-type">
<option value="local">本地認證</option>
<option value="ad">AD 認證</option>
</select>
</div>
<button type="submit" class="btn btn-primary" style="width: 100%; padding: 0.75rem;">登入</button>
</form>
</div>
</div>
<!-- 主應用程式 -->
<div id="main-app" style="display: none;">
<!-- 導航列 -->
<nav class="navbar">
<h1>每日報導 APP</h1>
<div class="navbar-actions">
<div class="user-menu">
<span id="user-display-name">載入中...</span>
<button class="btn btn-secondary" onclick="app.handleLogout()">登出</button>
</div>
</div>
</nav>
<div class="container">
<!-- 頁面選單 -->
<div class="tabs">
<button class="tab active" data-page="dashboard" onclick="app.showPage('dashboard')">儀表板</button>
<button class="tab" data-page="reports" onclick="app.showPage('reports')">報告管理</button>
<button class="tab" data-page="groups" onclick="app.showPage('groups')">群組管理</button>
<button class="tab" data-page="users" onclick="app.showPage('users')">用戶管理</button>
<button class="tab" data-page="settings" onclick="app.showPage('settings')">系統設定</button>
</div>
<!-- 儀表板頁面 -->
<div id="dashboard" class="page active">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value" id="stat-today-articles">-</div>
<div class="stat-label">今日新聞</div>
</div>
<div class="stat-card">
<div class="stat-value" id="stat-pending-reports">-</div>
<div class="stat-label">待審核報告</div>
</div>
<div class="stat-card">
<div class="stat-value" id="stat-published-reports">-</div>
<div class="stat-label">已發布報告</div>
</div>
<div class="stat-card">
<div class="stat-value" id="stat-active-users">-</div>
<div class="stat-label">活躍用戶</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h2 class="card-title">今日待審核報告</h2>
<button class="btn btn-primary" onclick="app.showPage('reports')">查看全部</button>
</div>
<table class="table">
<thead>
<tr>
<th>報告標題</th>
<th>群組</th>
<th>文章數</th>
<th>狀態</th>
<th>操作</th>
</tr>
</thead>
<tbody id="dashboard-reports-tbody">
<tr>
<td colspan="5" style="text-align: center; color: #7f8c8d;">載入中...</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 報告管理頁面 -->
<div id="reports" class="page">
<div class="card">
<div class="card-header">
<h2 class="card-title">報告管理</h2>
<div>
<input type="date" class="form-control" id="report-date-filter"
style="width: auto; display: inline-block; margin-right: 0.5rem;"
onchange="app.loadReports()">
<select class="form-control" id="report-group-filter"
style="width: auto; display: inline-block;"
onchange="app.loadReports()">
<option value="all">全部群組</option>
</select>
</div>
</div>
<div id="reports-list">
<div class="empty-state">載入中...</div>
</div>
</div>
</div>
<!-- 群組管理頁面 -->
<div id="groups" class="page">
<div class="card">
<div class="card-header">
<h2 class="card-title">群組管理</h2>
<button class="btn btn-primary" onclick="app.showNewGroupForm()">新增群組</button>
</div>
<table class="table">
<thead>
<tr>
<th>群組名稱</th>
<th>分類</th>
<th>關鍵字數</th>
<th>訂閱數</th>
<th>狀態</th>
<th>操作</th>
</tr>
</thead>
<tbody id="groups-tbody">
<tr>
<td colspan="6" style="text-align: center;">載入中...</td>
</tr>
</tbody>
</table>
</div>
<!-- 群組編輯表單 -->
<div class="card" id="group-form-card" style="display: none;">
<h3 class="card-title" style="margin-bottom: 1rem;">編輯群組</h3>
<form id="group-form" onsubmit="event.preventDefault(); app.saveGroup();">
<div class="form-group">
<label class="form-label">群組名稱</label>
<input type="text" class="form-control" id="group-name" required>
</div>
<div class="form-group">
<label class="form-label">描述</label>
<textarea class="form-control" id="group-description" rows="3"></textarea>
</div>
<div class="form-group">
<label class="form-label">分類</label>
<select class="form-control" id="group-category">
<option value="industry">產業別</option>
<option value="topic">主題</option>
</select>
</div>
<div class="form-group">
<label class="form-label">關鍵字</label>
<div id="group-keywords" style="margin-bottom: 0.5rem;"></div>
<div style="display: flex; gap: 0.5rem;">
<input type="text" class="form-control" id="new-keyword-input" placeholder="新增關鍵字...">
<button type="button" class="btn btn-primary"
onclick="app.addKeyword(document.getElementById('group-form').dataset.groupId)">新增</button>
</div>
</div>
<div class="form-group">
<label class="form-label">AI 摘要背景資訊</label>
<textarea class="form-control" id="group-ai-background" rows="4"
placeholder="提供給 AI 的背景資訊..."></textarea>
</div>
<div class="form-group">
<label class="form-label">AI 摘要方向</label>
<textarea class="form-control" id="group-ai-prompt" rows="4"
placeholder="指定 AI 摘要的重點方向..."></textarea>
</div>
<div style="display: flex; gap: 1rem;">
<button type="submit" class="btn btn-primary">儲存</button>
<button type="button" class="btn btn-secondary"
onclick="document.getElementById('group-form-card').style.display='none'">取消</button>
</div>
</form>
</div>
</div>
<!-- 用戶管理頁面 -->
<div id="users" class="page">
<div class="card">
<div class="card-header">
<h2 class="card-title">用戶管理</h2>
<button class="btn btn-primary" onclick="app.showNewUserForm()">新增用戶</button>
</div>
<div style="margin-bottom: 1rem;">
<input type="text" class="form-control" id="user-search" placeholder="搜尋用戶..."
style="max-width: 300px; display: inline-block;" onkeyup="app.loadUsers()">
<select class="form-control" id="user-role-filter"
style="width: auto; display: inline-block; margin-left: 0.5rem;"
onchange="app.loadUsers()">
<option value="all">全部角色</option>
<option value="admin">管理員</option>
<option value="editor">專員</option>
<option value="reader">讀者</option>
</select>
</div>
<table class="table">
<thead>
<tr>
<th>帳號</th>
<th>顯示名稱</th>
<th>Email</th>
<th>角色</th>
<th>認證方式</th>
<th>狀態</th>
<th>操作</th>
</tr>
</thead>
<tbody id="users-tbody">
<tr>
<td colspan="7" style="text-align: center;">載入中...</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 系統設定頁面 -->
<div id="settings" class="page">
<div class="card">
<h2 class="card-title" style="margin-bottom: 1.5rem;">系統設定</h2>
<!-- LLM 設定 -->
<div>
<h3 style="margin-bottom: 1rem; color: #2c3e50;">LLM 設定</h3>
<div class="form-group">
<label class="form-label">LLM 提供者</label>
<select class="form-control" id="llm-provider">
<option value="gemini">Google Gemini</option>
<option value="openai">OpenAI</option>
<option value="ollama">Ollama (地端)</option>
</select>
</div>
<div class="form-group">
<label class="form-label">API Key</label>
<input type="password" class="form-control" id="llm-api-key" placeholder="輸入 API Key...">
<small style="color: #7f8c8d; margin-top: 0.25rem; display: block;">API Key 將加密儲存</small>
</div>
<div class="form-group">
<label class="form-label">模型版本</label>
<input type="text" class="form-control" id="llm-model" placeholder="例如: gemini-1.5-pro">
</div>
<button class="btn btn-primary" onclick="app.testLlmConnection()">測試連線</button>
<button class="btn btn-success" style="margin-left: 0.5rem;" onclick="app.saveLlmSettings()">儲存設定</button>
</div>
<!-- PDF 模板設定 -->
<div class="section-divider">
<h3 style="margin-bottom: 1rem; color: #2c3e50;">PDF 模板設定</h3>
<div class="form-group">
<label class="form-label">公司 Logo</label>
<input type="file" class="form-control" id="pdf-logo-input" accept="image/png,image/jpeg,image/svg+xml">
<small style="color: #7f8c8d; margin-top: 0.25rem; display: block;">支援 PNG、JPEG、SVG 格式</small>
<button class="btn btn-primary" style="margin-top: 0.5rem;" onclick="app.uploadLogo()">上傳</button>
</div>
<div class="form-group">
<label class="form-label">頁首文字</label>
<input type="text" class="form-control" id="pdf-header" placeholder="每日報導">
</div>
<div class="form-group">
<label class="form-label">頁尾文字</label>
<input type="text" class="form-control" id="pdf-footer" placeholder="本報告僅供內部參考使用">
</div>
</div>
<!-- SMTP 設定 -->
<div class="section-divider">
<h3 style="margin-bottom: 1rem; color: #2c3e50;">SMTP 設定</h3>
<div class="form-group">
<label class="form-label">SMTP 伺服器</label>
<input type="text" class="form-control" id="smtp-host" placeholder="smtp.example.com">
</div>
<div class="form-group">
<label class="form-label">SMTP 埠號</label>
<input type="number" class="form-control" id="smtp-port" value="587">
</div>
<div class="form-group">
<label class="form-label">SMTP 帳號</label>
<input type="text" class="form-control" id="smtp-username" placeholder="smtp@example.com">
</div>
<div class="form-group">
<label class="form-label">SMTP 密碼</label>
<input type="password" class="form-control" id="smtp-password" placeholder="輸入密碼...">
</div>
<div class="form-group">
<label class="form-label">寄件者 Email</label>
<input type="email" class="form-control" id="smtp-from-email" placeholder="noreply@example.com">
</div>
<div class="form-group">
<label class="form-label">寄件者名稱</label>
<input type="text" class="form-control" id="smtp-from-name" placeholder="每日報導系統">
</div>
<button class="btn btn-success" onclick="app.saveSmtpSettings()">儲存設定</button>
</div>
</div>
</div>
</div>
</div>
<!-- 報告詳情 Modal -->
<div class="modal" id="report-detail-modal">
<div class="modal-content">
<div class="modal-header">
<h2>報告詳情</h2>
<button class="modal-close" onclick="app.closeModal('report-detail-modal')">&times;</button>
</div>
<div id="report-detail-content">
載入中...
</div>
</div>
</div>
<!-- 用戶編輯 Modal -->
<div class="modal" id="user-modal">
<div class="modal-content">
<div class="modal-header">
<h2>編輯用戶</h2>
<button class="modal-close" onclick="app.closeModal('user-modal')">&times;</button>
</div>
<form id="user-form" onsubmit="event.preventDefault(); app.saveUser();">
<div class="form-group">
<label class="form-label">帳號</label>
<input type="text" class="form-control" id="user-username" required>
</div>
<div class="form-group">
<label class="form-label">顯示名稱</label>
<input type="text" class="form-control" id="user-display-name-input" required>
</div>
<div class="form-group">
<label class="form-label">Email</label>
<input type="email" class="form-control" id="user-email">
</div>
<div class="form-group">
<label class="form-label">角色</label>
<select class="form-control" id="user-role">
<option value="1">管理員</option>
<option value="2">專員</option>
<option value="3">讀者</option>
</select>
</div>
<div class="form-group">
<label class="form-label">認證方式</label>
<select class="form-control" id="user-auth-type">
<option value="local">本地認證</option>
<option value="ad">AD 認證</option>
</select>
</div>
<div class="form-group" id="user-password-group">
<label class="form-label">密碼</label>
<input type="password" class="form-control" id="user-password" placeholder="留空則不修改">
</div>
<div class="form-group">
<label class="form-label">
<input type="checkbox" id="user-is-active" checked> 啟用帳號
</label>
</div>
<div style="display: flex; gap: 1rem;">
<button type="submit" class="btn btn-primary">儲存</button>
<button type="button" class="btn btn-secondary" onclick="app.closeModal('user-modal')">取消</button>
</div>
</form>
</div>
</div>
<!-- 載入 JavaScript -->
<script src="/static/js/api.js"></script>
<script src="/static/js/app.js"></script>
<script>
// 登入表單處理
function handleLoginSubmit(event) {
event.preventDefault();
const username = document.getElementById('login-username').value;
const password = document.getElementById('login-password').value;
const authType = document.getElementById('login-auth-type').value;
app.handleLogin(username, password, authType);
}
</script>
</body>
</html>