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>
This commit is contained in:
donald
2025-12-03 23:53:24 +08:00
commit db0f0bbfe7
50 changed files with 11883 additions and 0 deletions

430
templates/js/api.js Normal file
View File

@@ -0,0 +1,430 @@
/**
* API 服務層 - 處理所有後端 API 呼叫
*/
const API_BASE_URL = '/api/v1';
class ApiService {
constructor() {
this.baseUrl = API_BASE_URL;
}
/**
* 取得認證 Token
*/
getToken() {
return localStorage.getItem('token');
}
/**
* 設定認證 Token
*/
setToken(token) {
localStorage.setItem('token', token);
}
/**
* 清除認證 Token
*/
clearToken() {
localStorage.removeItem('token');
localStorage.removeItem('user');
}
/**
* 建立請求標頭
*/
getHeaders(includeAuth = true) {
const headers = {
'Content-Type': 'application/json'
};
if (includeAuth) {
const token = this.getToken();
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
}
return headers;
}
/**
* 發送 API 請求
*/
async request(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
const config = {
headers: this.getHeaders(options.auth !== false),
...options
};
try {
const response = await fetch(url, config);
// 處理 401 未授權
if (response.status === 401) {
this.clearToken();
window.location.reload();
throw new Error('登入已過期,請重新登入');
}
// 處理 204 無內容
if (response.status === 204) {
return { success: true };
}
const data = await response.json();
if (!response.ok) {
throw new Error(data.detail || '請求失敗');
}
return data;
} catch (error) {
console.error('API Error:', error);
throw error;
}
}
/**
* GET 請求
*/
async get(endpoint, params = {}) {
const queryString = new URLSearchParams(params).toString();
const url = queryString ? `${endpoint}?${queryString}` : endpoint;
return this.request(url, { method: 'GET' });
}
/**
* POST 請求
*/
async post(endpoint, data = {}, options = {}) {
return this.request(endpoint, {
method: 'POST',
body: JSON.stringify(data),
...options
});
}
/**
* PUT 請求
*/
async put(endpoint, data = {}) {
return this.request(endpoint, {
method: 'PUT',
body: JSON.stringify(data)
});
}
/**
* DELETE 請求
*/
async delete(endpoint) {
return this.request(endpoint, { method: 'DELETE' });
}
/**
* 上傳檔案
*/
async upload(endpoint, formData) {
const token = this.getToken();
const headers = {};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'POST',
headers,
body: formData
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.detail || '上傳失敗');
}
return response.json();
}
}
// 建立全域 API 實例 (必須在其他 API 模組之前)
const api = new ApiService();
// ============ 認證 API ============
const authApi = {
/**
* 登入
*/
async login(username, password, authType = 'local') {
const response = await api.post('/auth/login', {
username,
password,
auth_type: authType
}, { auth: false });
if (response.token) {
api.setToken(response.token);
localStorage.setItem('user', JSON.stringify(response.user));
}
return response;
},
/**
* 登出
*/
async logout() {
try {
await api.post('/auth/logout');
} finally {
api.clearToken();
}
},
/**
* 取得當前用戶
*/
async getMe() {
return api.get('/auth/me');
},
/**
* 檢查是否已登入
*/
isLoggedIn() {
return !!api.getToken();
},
/**
* 取得本地儲存的用戶資訊
*/
getUser() {
const user = localStorage.getItem('user');
return user ? JSON.parse(user) : null;
}
};
// ============ 用戶管理 API ============
const usersApi = {
/**
* 取得用戶列表
*/
async getList(params = {}) {
return api.get('/users', params);
},
/**
* 取得單一用戶
*/
async getById(userId) {
return api.get(`/users/${userId}`);
},
/**
* 建立用戶
*/
async create(userData) {
return api.post('/users', userData);
},
/**
* 更新用戶
*/
async update(userId, userData) {
return api.put(`/users/${userId}`, userData);
},
/**
* 刪除用戶
*/
async delete(userId) {
return api.delete(`/users/${userId}`);
}
};
// ============ 群組管理 API ============
const groupsApi = {
/**
* 取得群組列表
*/
async getList(params = {}) {
return api.get('/groups', params);
},
/**
* 取得群組詳情
*/
async getById(groupId) {
return api.get(`/groups/${groupId}`);
},
/**
* 建立群組
*/
async create(groupData) {
return api.post('/groups', groupData);
},
/**
* 更新群組
*/
async update(groupId, groupData) {
return api.put(`/groups/${groupId}`, groupData);
},
/**
* 刪除群組
*/
async delete(groupId) {
return api.delete(`/groups/${groupId}`);
},
/**
* 取得群組關鍵字
*/
async getKeywords(groupId) {
return api.get(`/groups/${groupId}/keywords`);
},
/**
* 新增關鍵字
*/
async addKeyword(groupId, keyword) {
return api.post(`/groups/${groupId}/keywords`, { keyword });
},
/**
* 刪除關鍵字
*/
async deleteKeyword(groupId, keywordId) {
return api.delete(`/groups/${groupId}/keywords/${keywordId}`);
}
};
// ============ 訂閱管理 API ============
const subscriptionsApi = {
/**
* 取得我的訂閱
*/
async getMySubscriptions() {
return api.get('/subscriptions');
},
/**
* 更新訂閱
*/
async update(subscriptions) {
return api.put('/subscriptions', { subscriptions });
}
};
// ============ 報告管理 API ============
const reportsApi = {
/**
* 取得報告列表
*/
async getList(params = {}) {
return api.get('/reports', params);
},
/**
* 取得今日報告
*/
async getToday() {
return api.get('/reports/today');
},
/**
* 取得報告詳情
*/
async getById(reportId) {
return api.get(`/reports/${reportId}`);
},
/**
* 更新報告
*/
async update(reportId, reportData) {
return api.put(`/reports/${reportId}`, reportData);
},
/**
* 發布報告
*/
async publish(reportId) {
return api.post(`/reports/${reportId}/publish`);
},
/**
* 重新產生 AI 摘要
*/
async regenerateSummary(reportId) {
return api.post(`/reports/${reportId}/regenerate-summary`);
},
/**
* 匯出報告 PDF
*/
async exportPdf(reportId) {
const token = api.getToken();
const response = await fetch(`${API_BASE_URL}/reports/${reportId}/export`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error('匯出失敗');
}
return response.blob();
}
};
// ============ 系統設定 API ============
const settingsApi = {
/**
* 取得系統設定
*/
async get() {
return api.get('/settings');
},
/**
* 更新系統設定
*/
async update(settings) {
return api.put('/settings', settings);
},
/**
* 測試 LLM 連線
*/
async testLlm() {
return api.post('/settings/llm/test');
},
/**
* 上傳 PDF Logo
*/
async uploadLogo(file) {
const formData = new FormData();
formData.append('logo', file);
return api.upload('/settings/pdf/logo', formData);
},
/**
* 取得管理員儀表板數據
*/
async getAdminDashboard() {
return api.get('/settings/dashboard/admin');
}
};
// 匯出所有 API 到 window
window.api = api;
window.authApi = authApi;
window.usersApi = usersApi;
window.groupsApi = groupsApi;
window.subscriptionsApi = subscriptionsApi;
window.reportsApi = reportsApi;
window.settingsApi = settingsApi;

1010
templates/js/app.js Normal file

File diff suppressed because it is too large Load Diff