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:
430
templates/js/api.js
Normal file
430
templates/js/api.js
Normal 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;
|
||||
Reference in New Issue
Block a user