| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483 |
- // 全域變數
- let currentPage = 1;
- let currentView = 'list';
- let issues = [];
- let projects = [];
- // 頁面載入完成後初始化
- document.addEventListener('DOMContentLoaded', function() {
- loadProjects();
- loadIssues();
- loadStats();
- });
- // 載入專案列表
- async function loadProjects() {
- try {
- const response = await fetch('/api/projects');
- const result = await response.json();
-
- if (result.success) {
- projects = result.data;
- updateProjectSelects();
- } else {
- showAlert('載入專案失敗: ' + result.message, 'error');
- }
- } catch (error) {
- console.error('載入專案錯誤:', error);
- showAlert('載入專案時發生錯誤', 'error');
- }
- }
- // 更新專案選擇器
- function updateProjectSelects() {
- const projectFilter = document.getElementById('projectFilter');
- const issueProject = document.getElementById('issueProject');
-
- // 清空現有選項
- projectFilter.innerHTML = '<option value="">全部專案</option>';
- issueProject.innerHTML = '<option value="">選擇專案</option>';
-
- // 添加專案選項
- projects.forEach(project => {
- const option1 = new Option(project.name, project.id);
- const option2 = new Option(project.name, project.id);
- projectFilter.appendChild(option1);
- issueProject.appendChild(option2);
- });
- }
- // 載入問題列表
- async function loadIssues(page = 1) {
- currentPage = page;
- showLoading(true);
-
- try {
- const projectId = document.getElementById('projectFilter').value;
- const status = document.getElementById('statusFilter').value;
- const priority = document.getElementById('priorityFilter').value;
-
- let url = `/api/issues?page=${page}&limit=10`;
- if (projectId) url += `&project_id=${projectId}`;
- if (status) url += `&status=${status}`;
- if (priority) url += `&priority=${priority}`;
-
- const response = await fetch(url);
- const result = await response.json();
-
- if (result.success) {
- issues = result.data;
- renderIssues();
- renderPagination(result.pagination);
- updateStats();
- } else {
- showAlert('載入問題失敗: ' + result.message, 'error');
- }
- } catch (error) {
- console.error('載入問題錯誤:', error);
- showAlert('載入問題時發生錯誤', 'error');
- } finally {
- showLoading(false);
- }
- }
- // 渲染問題列表
- function renderIssues() {
- const issuesList = document.getElementById('issuesList');
-
- if (issues.length === 0) {
- issuesList.innerHTML = `
- <div class="empty-state">
- <i class="fas fa-inbox"></i>
- <h3>沒有找到問題</h3>
- <p>目前沒有符合篩選條件的問題</p>
- </div>
- `;
- return;
- }
-
- const issuesHtml = issues.map(issue => `
- <div class="issue-item" onclick="showIssueDetail(${issue.id})">
- <div class="issue-header">
- <div>
- <div class="issue-title">${escapeHtml(issue.title)}</div>
- <div class="issue-meta">
- <span><i class="fas fa-tag"></i> #${issue.id}</span>
- <span><i class="fas fa-folder"></i> ${issue.project_name || '未分類'}</span>
- <span><i class="fas fa-user"></i> ${issue.assignee || '未指派'}</span>
- <span><i class="fas fa-calendar"></i> ${formatDate(issue.created_at)}</span>
- </div>
- </div>
- <div>
- <span class="status-badge status-${issue.status}">${getStatusText(issue.status)}</span>
- <span class="priority-badge priority-${issue.priority}">${getPriorityText(issue.priority)}</span>
- </div>
- </div>
- ${issue.description ? `<div class="issue-description">${escapeHtml(issue.description)}</div>` : ''}
- </div>
- `).join('');
-
- issuesList.innerHTML = issuesHtml;
- }
- // 渲染分頁
- function renderPagination(pagination) {
- const paginationDiv = document.getElementById('pagination');
-
- if (pagination.pages <= 1) {
- paginationDiv.innerHTML = '';
- return;
- }
-
- let paginationHtml = '';
-
- // 上一頁按鈕
- paginationHtml += `
- <button ${pagination.page === 1 ? 'disabled' : ''} onclick="loadIssues(${pagination.page - 1})">
- <i class="fas fa-chevron-left"></i> 上一頁
- </button>
- `;
-
- // 頁碼按鈕
- const startPage = Math.max(1, pagination.page - 2);
- const endPage = Math.min(pagination.pages, pagination.page + 2);
-
- for (let i = startPage; i <= endPage; i++) {
- paginationHtml += `
- <button class="${i === pagination.page ? 'active' : ''}" onclick="loadIssues(${i})">
- ${i}
- </button>
- `;
- }
-
- // 下一頁按鈕
- paginationHtml += `
- <button ${pagination.page === pagination.pages ? 'disabled' : ''} onclick="loadIssues(${pagination.page + 1})">
- 下一頁 <i class="fas fa-chevron-right"></i>
- </button>
- `;
-
- paginationDiv.innerHTML = paginationHtml;
- }
- // 載入統計資料
- async function loadStats() {
- try {
- const response = await fetch('/api/issues');
- const result = await response.json();
-
- if (result.success) {
- const stats = {
- open: 0,
- in_progress: 0,
- closed: 0,
- total: result.data.length
- };
-
- result.data.forEach(issue => {
- if (issue.status === 'open') stats.open++;
- else if (issue.status === 'in_progress') stats.in_progress++;
- else if (issue.status === 'closed') stats.closed++;
- });
-
- updateStatsDisplay(stats);
- }
- } catch (error) {
- console.error('載入統計錯誤:', error);
- }
- }
- // 更新統計顯示
- function updateStatsDisplay(stats) {
- document.getElementById('openCount').textContent = stats.open;
- document.getElementById('progressCount').textContent = stats.in_progress;
- document.getElementById('closedCount').textContent = stats.closed;
- document.getElementById('totalCount').textContent = stats.total;
- }
- // 更新統計(從當前問題列表)
- function updateStats() {
- const stats = {
- open: 0,
- in_progress: 0,
- closed: 0,
- total: issues.length
- };
-
- issues.forEach(issue => {
- if (issue.status === 'open') stats.open++;
- else if (issue.status === 'in_progress') stats.in_progress++;
- else if (issue.status === 'closed') stats.closed++;
- });
-
- updateStatsDisplay(stats);
- }
- // 顯示問題詳情
- async function showIssueDetail(issueId) {
- try {
- const response = await fetch(`/api/issues/${issueId}`);
- const result = await response.json();
-
- if (result.success) {
- const issue = result.data;
- const modal = document.getElementById('issueDetailModal');
- const title = document.getElementById('issueDetailTitle');
- const content = document.getElementById('issueDetailContent');
-
- title.textContent = `#${issue.id} ${issue.title}`;
-
- content.innerHTML = `
- <div class="issue-detail">
- <div class="issue-detail-header">
- <div class="issue-meta">
- <span><i class="fas fa-folder"></i> ${issue.project_name || '未分類'}</span>
- <span><i class="fas fa-user"></i> ${issue.assignee || '未指派'}</span>
- <span><i class="fas fa-calendar"></i> ${formatDate(issue.created_at)}</span>
- </div>
- <div>
- <span class="status-badge status-${issue.status}">${getStatusText(issue.status)}</span>
- <span class="priority-badge priority-${issue.priority}">${getPriorityText(issue.priority)}</span>
- </div>
- </div>
-
- <div class="issue-description">
- <h4>描述</h4>
- <p>${issue.description || '無描述'}</p>
- </div>
-
- ${issue.comments && issue.comments.length > 0 ? `
- <div class="issue-comments">
- <h4>評論 (${issue.comments.length})</h4>
- ${issue.comments.map(comment => `
- <div class="comment">
- <div class="comment-header">
- <strong>${escapeHtml(comment.author)}</strong>
- <span class="comment-date">${formatDate(comment.created_at)}</span>
- </div>
- <div class="comment-content">${escapeHtml(comment.content)}</div>
- </div>
- `).join('')}
- </div>
- ` : ''}
- </div>
- `;
-
- showModal('issueDetailModal');
- } else {
- showAlert('載入問題詳情失敗: ' + result.message, 'error');
- }
- } catch (error) {
- console.error('載入問題詳情錯誤:', error);
- showAlert('載入問題詳情時發生錯誤', 'error');
- }
- }
- // 顯示新增問題模態框
- function showCreateIssueModal() {
- showModal('createIssueModal');
- }
- // 顯示新增專案模態框
- function showCreateProjectModal() {
- showModal('createProjectModal');
- }
- // 建立問題
- async function createIssue() {
- const form = document.getElementById('createIssueForm');
- const formData = new FormData(form);
-
- const issueData = {
- title: formData.get('title'),
- description: formData.get('description'),
- project_id: formData.get('project_id') || null,
- priority: formData.get('priority'),
- assignee: formData.get('assignee'),
- reporter: formData.get('reporter')
- };
-
- if (!issueData.title) {
- showAlert('請填寫問題標題', 'error');
- return;
- }
-
- try {
- const response = await fetch('/api/issues', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify(issueData)
- });
-
- const result = await response.json();
-
- if (result.success) {
- showAlert('問題建立成功!', 'success');
- closeModal('createIssueModal');
- form.reset();
- loadIssues();
- loadStats();
- } else {
- showAlert('建立問題失敗: ' + result.message, 'error');
- }
- } catch (error) {
- console.error('建立問題錯誤:', error);
- showAlert('建立問題時發生錯誤', 'error');
- }
- }
- // 建立專案
- async function createProject() {
- const form = document.getElementById('createProjectForm');
- const formData = new FormData(form);
-
- const projectData = {
- name: formData.get('name'),
- description: formData.get('description'),
- status: formData.get('status')
- };
-
- if (!projectData.name) {
- showAlert('請填寫專案名稱', 'error');
- return;
- }
-
- try {
- const response = await fetch('/api/projects', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify(projectData)
- });
-
- const result = await response.json();
-
- if (result.success) {
- showAlert('專案建立成功!', 'success');
- closeModal('createProjectModal');
- form.reset();
- loadProjects();
- } else {
- showAlert('建立專案失敗: ' + result.message, 'error');
- }
- } catch (error) {
- console.error('建立專案錯誤:', error);
- showAlert('建立專案時發生錯誤', 'error');
- }
- }
- // 切換檢視模式
- function toggleView(view, el) {
- currentView = view;
- // 更新按鈕 active 樣式
- const buttons = document.querySelectorAll('.view-toggle .btn');
- buttons.forEach(btn => btn.classList.remove('active'));
- el.classList.add('active');
- // 切換容器樣式
- const issuesList = document.getElementById('issuesList');
- if (view === 'grid') {
- issuesList.classList.remove('issues-list');
- issuesList.classList.add('issues-grid');
- } else {
- issuesList.classList.remove('issues-grid');
- issuesList.classList.add('issues-list');
- }
- renderIssues();
- }
- // 顯示模態框
- function showModal(modalId) {
- const modal = document.getElementById(modalId);
- modal.classList.add('show');
- modal.style.display = 'flex';
- }
- // 關閉模態框
- function closeModal(modalId) {
- const modal = document.getElementById(modalId);
- modal.classList.remove('show');
- modal.style.display = 'none';
- }
- // 顯示載入指示器
- function showLoading(show) {
- const loading = document.getElementById('loadingIndicator');
- loading.style.display = show ? 'block' : 'none';
- }
- // 顯示警告訊息
- function showAlert(message, type = 'info') {
- // 移除現有的警告
- const existingAlert = document.querySelector('.alert');
- if (existingAlert) {
- existingAlert.remove();
- }
-
- const alert = document.createElement('div');
- alert.className = `alert alert-${type}`;
- alert.textContent = message;
-
- document.querySelector('.container').insertBefore(alert, document.querySelector('.stats-grid'));
-
- // 3秒後自動移除
- setTimeout(() => {
- if (alert.parentNode) {
- alert.remove();
- }
- }, 3000);
- }
- // 工具函數
- function escapeHtml(text) {
- const div = document.createElement('div');
- div.textContent = text;
- return div.innerHTML;
- }
- function formatDate(dateString) {
- const date = new Date(dateString);
- return date.toLocaleDateString('zh-TW', {
- year: 'numeric',
- month: '2-digit',
- day: '2-digit',
- hour: '2-digit',
- minute: '2-digit'
- });
- }
- function getStatusText(status) {
- const statusMap = {
- 'open': '待處理',
- 'in_progress': '處理中',
- 'closed': '已完成'
- };
- return statusMap[status] || status;
- }
- function getPriorityText(priority) {
- const priorityMap = {
- 'high': '高',
- 'medium': '中',
- 'low': '低'
- };
- return priorityMap[priority] || priority;
- }
- // 點擊模態框外部關閉
- window.onclick = function(event) {
- const modals = document.querySelectorAll('.modal');
- modals.forEach(modal => {
- if (event.target === modal) {
- modal.classList.remove('show');
- modal.style.display = 'none';
- }
- });
- }
|