script.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  1. // 全域變數
  2. let currentPage = 1;
  3. let currentView = 'list';
  4. let issues = [];
  5. let projects = [];
  6. // 頁面載入完成後初始化
  7. document.addEventListener('DOMContentLoaded', function() {
  8. loadProjects();
  9. loadIssues();
  10. loadStats();
  11. });
  12. // 載入專案列表
  13. async function loadProjects() {
  14. try {
  15. const response = await fetch('/api/projects');
  16. const result = await response.json();
  17. if (result.success) {
  18. projects = result.data;
  19. updateProjectSelects();
  20. } else {
  21. showAlert('載入專案失敗: ' + result.message, 'error');
  22. }
  23. } catch (error) {
  24. console.error('載入專案錯誤:', error);
  25. showAlert('載入專案時發生錯誤', 'error');
  26. }
  27. }
  28. // 更新專案選擇器
  29. function updateProjectSelects() {
  30. const projectFilter = document.getElementById('projectFilter');
  31. const issueProject = document.getElementById('issueProject');
  32. // 清空現有選項
  33. projectFilter.innerHTML = '<option value="">全部專案</option>';
  34. issueProject.innerHTML = '<option value="">選擇專案</option>';
  35. // 添加專案選項
  36. projects.forEach(project => {
  37. const option1 = new Option(project.name, project.id);
  38. const option2 = new Option(project.name, project.id);
  39. projectFilter.appendChild(option1);
  40. issueProject.appendChild(option2);
  41. });
  42. }
  43. // 載入問題列表
  44. async function loadIssues(page = 1) {
  45. currentPage = page;
  46. showLoading(true);
  47. try {
  48. const projectId = document.getElementById('projectFilter').value;
  49. const status = document.getElementById('statusFilter').value;
  50. const priority = document.getElementById('priorityFilter').value;
  51. let url = `/api/issues?page=${page}&limit=10`;
  52. if (projectId) url += `&project_id=${projectId}`;
  53. if (status) url += `&status=${status}`;
  54. if (priority) url += `&priority=${priority}`;
  55. const response = await fetch(url);
  56. const result = await response.json();
  57. if (result.success) {
  58. issues = result.data;
  59. renderIssues();
  60. renderPagination(result.pagination);
  61. updateStats();
  62. } else {
  63. showAlert('載入問題失敗: ' + result.message, 'error');
  64. }
  65. } catch (error) {
  66. console.error('載入問題錯誤:', error);
  67. showAlert('載入問題時發生錯誤', 'error');
  68. } finally {
  69. showLoading(false);
  70. }
  71. }
  72. // 渲染問題列表
  73. function renderIssues() {
  74. const issuesList = document.getElementById('issuesList');
  75. if (issues.length === 0) {
  76. issuesList.innerHTML = `
  77. <div class="empty-state">
  78. <i class="fas fa-inbox"></i>
  79. <h3>沒有找到問題</h3>
  80. <p>目前沒有符合篩選條件的問題</p>
  81. </div>
  82. `;
  83. return;
  84. }
  85. const issuesHtml = issues.map(issue => `
  86. <div class="issue-item" onclick="showIssueDetail(${issue.id})">
  87. <div class="issue-header">
  88. <div>
  89. <div class="issue-title">${escapeHtml(issue.title)}</div>
  90. <div class="issue-meta">
  91. <span><i class="fas fa-tag"></i> #${issue.id}</span>
  92. <span><i class="fas fa-folder"></i> ${issue.project_name || '未分類'}</span>
  93. <span><i class="fas fa-user"></i> ${issue.assignee || '未指派'}</span>
  94. <span><i class="fas fa-calendar"></i> ${formatDate(issue.created_at)}</span>
  95. </div>
  96. </div>
  97. <div>
  98. <span class="status-badge status-${issue.status}">${getStatusText(issue.status)}</span>
  99. <span class="priority-badge priority-${issue.priority}">${getPriorityText(issue.priority)}</span>
  100. </div>
  101. </div>
  102. ${issue.description ? `<div class="issue-description">${escapeHtml(issue.description)}</div>` : ''}
  103. </div>
  104. `).join('');
  105. issuesList.innerHTML = issuesHtml;
  106. }
  107. // 渲染分頁
  108. function renderPagination(pagination) {
  109. const paginationDiv = document.getElementById('pagination');
  110. if (pagination.pages <= 1) {
  111. paginationDiv.innerHTML = '';
  112. return;
  113. }
  114. let paginationHtml = '';
  115. // 上一頁按鈕
  116. paginationHtml += `
  117. <button ${pagination.page === 1 ? 'disabled' : ''} onclick="loadIssues(${pagination.page - 1})">
  118. <i class="fas fa-chevron-left"></i> 上一頁
  119. </button>
  120. `;
  121. // 頁碼按鈕
  122. const startPage = Math.max(1, pagination.page - 2);
  123. const endPage = Math.min(pagination.pages, pagination.page + 2);
  124. for (let i = startPage; i <= endPage; i++) {
  125. paginationHtml += `
  126. <button class="${i === pagination.page ? 'active' : ''}" onclick="loadIssues(${i})">
  127. ${i}
  128. </button>
  129. `;
  130. }
  131. // 下一頁按鈕
  132. paginationHtml += `
  133. <button ${pagination.page === pagination.pages ? 'disabled' : ''} onclick="loadIssues(${pagination.page + 1})">
  134. 下一頁 <i class="fas fa-chevron-right"></i>
  135. </button>
  136. `;
  137. paginationDiv.innerHTML = paginationHtml;
  138. }
  139. // 載入統計資料
  140. async function loadStats() {
  141. try {
  142. const response = await fetch('/api/issues');
  143. const result = await response.json();
  144. if (result.success) {
  145. const stats = {
  146. open: 0,
  147. in_progress: 0,
  148. closed: 0,
  149. total: result.data.length
  150. };
  151. result.data.forEach(issue => {
  152. if (issue.status === 'open') stats.open++;
  153. else if (issue.status === 'in_progress') stats.in_progress++;
  154. else if (issue.status === 'closed') stats.closed++;
  155. });
  156. updateStatsDisplay(stats);
  157. }
  158. } catch (error) {
  159. console.error('載入統計錯誤:', error);
  160. }
  161. }
  162. // 更新統計顯示
  163. function updateStatsDisplay(stats) {
  164. document.getElementById('openCount').textContent = stats.open;
  165. document.getElementById('progressCount').textContent = stats.in_progress;
  166. document.getElementById('closedCount').textContent = stats.closed;
  167. document.getElementById('totalCount').textContent = stats.total;
  168. }
  169. // 更新統計(從當前問題列表)
  170. function updateStats() {
  171. const stats = {
  172. open: 0,
  173. in_progress: 0,
  174. closed: 0,
  175. total: issues.length
  176. };
  177. issues.forEach(issue => {
  178. if (issue.status === 'open') stats.open++;
  179. else if (issue.status === 'in_progress') stats.in_progress++;
  180. else if (issue.status === 'closed') stats.closed++;
  181. });
  182. updateStatsDisplay(stats);
  183. }
  184. // 顯示問題詳情
  185. async function showIssueDetail(issueId) {
  186. try {
  187. const response = await fetch(`/api/issues/${issueId}`);
  188. const result = await response.json();
  189. if (result.success) {
  190. const issue = result.data;
  191. const modal = document.getElementById('issueDetailModal');
  192. const title = document.getElementById('issueDetailTitle');
  193. const content = document.getElementById('issueDetailContent');
  194. title.textContent = `#${issue.id} ${issue.title}`;
  195. content.innerHTML = `
  196. <div class="issue-detail">
  197. <div class="issue-detail-header">
  198. <div class="issue-meta">
  199. <span><i class="fas fa-folder"></i> ${issue.project_name || '未分類'}</span>
  200. <span><i class="fas fa-user"></i> ${issue.assignee || '未指派'}</span>
  201. <span><i class="fas fa-calendar"></i> ${formatDate(issue.created_at)}</span>
  202. </div>
  203. <div>
  204. <span class="status-badge status-${issue.status}">${getStatusText(issue.status)}</span>
  205. <span class="priority-badge priority-${issue.priority}">${getPriorityText(issue.priority)}</span>
  206. </div>
  207. </div>
  208. <div class="issue-description">
  209. <h4>描述</h4>
  210. <p>${issue.description || '無描述'}</p>
  211. </div>
  212. ${issue.comments && issue.comments.length > 0 ? `
  213. <div class="issue-comments">
  214. <h4>評論 (${issue.comments.length})</h4>
  215. ${issue.comments.map(comment => `
  216. <div class="comment">
  217. <div class="comment-header">
  218. <strong>${escapeHtml(comment.author)}</strong>
  219. <span class="comment-date">${formatDate(comment.created_at)}</span>
  220. </div>
  221. <div class="comment-content">${escapeHtml(comment.content)}</div>
  222. </div>
  223. `).join('')}
  224. </div>
  225. ` : ''}
  226. </div>
  227. `;
  228. showModal('issueDetailModal');
  229. } else {
  230. showAlert('載入問題詳情失敗: ' + result.message, 'error');
  231. }
  232. } catch (error) {
  233. console.error('載入問題詳情錯誤:', error);
  234. showAlert('載入問題詳情時發生錯誤', 'error');
  235. }
  236. }
  237. // 顯示新增問題模態框
  238. function showCreateIssueModal() {
  239. showModal('createIssueModal');
  240. }
  241. // 顯示新增專案模態框
  242. function showCreateProjectModal() {
  243. showModal('createProjectModal');
  244. }
  245. // 建立問題
  246. async function createIssue() {
  247. const form = document.getElementById('createIssueForm');
  248. const formData = new FormData(form);
  249. const issueData = {
  250. title: formData.get('title'),
  251. description: formData.get('description'),
  252. project_id: formData.get('project_id') || null,
  253. priority: formData.get('priority'),
  254. assignee: formData.get('assignee'),
  255. reporter: formData.get('reporter')
  256. };
  257. if (!issueData.title) {
  258. showAlert('請填寫問題標題', 'error');
  259. return;
  260. }
  261. try {
  262. const response = await fetch('/api/issues', {
  263. method: 'POST',
  264. headers: {
  265. 'Content-Type': 'application/json'
  266. },
  267. body: JSON.stringify(issueData)
  268. });
  269. const result = await response.json();
  270. if (result.success) {
  271. showAlert('問題建立成功!', 'success');
  272. closeModal('createIssueModal');
  273. form.reset();
  274. loadIssues();
  275. loadStats();
  276. } else {
  277. showAlert('建立問題失敗: ' + result.message, 'error');
  278. }
  279. } catch (error) {
  280. console.error('建立問題錯誤:', error);
  281. showAlert('建立問題時發生錯誤', 'error');
  282. }
  283. }
  284. // 建立專案
  285. async function createProject() {
  286. const form = document.getElementById('createProjectForm');
  287. const formData = new FormData(form);
  288. const projectData = {
  289. name: formData.get('name'),
  290. description: formData.get('description'),
  291. status: formData.get('status')
  292. };
  293. if (!projectData.name) {
  294. showAlert('請填寫專案名稱', 'error');
  295. return;
  296. }
  297. try {
  298. const response = await fetch('/api/projects', {
  299. method: 'POST',
  300. headers: {
  301. 'Content-Type': 'application/json'
  302. },
  303. body: JSON.stringify(projectData)
  304. });
  305. const result = await response.json();
  306. if (result.success) {
  307. showAlert('專案建立成功!', 'success');
  308. closeModal('createProjectModal');
  309. form.reset();
  310. loadProjects();
  311. } else {
  312. showAlert('建立專案失敗: ' + result.message, 'error');
  313. }
  314. } catch (error) {
  315. console.error('建立專案錯誤:', error);
  316. showAlert('建立專案時發生錯誤', 'error');
  317. }
  318. }
  319. // 切換檢視模式
  320. function toggleView(view, el) {
  321. currentView = view;
  322. // 更新按鈕 active 樣式
  323. const buttons = document.querySelectorAll('.view-toggle .btn');
  324. buttons.forEach(btn => btn.classList.remove('active'));
  325. el.classList.add('active');
  326. // 切換容器樣式
  327. const issuesList = document.getElementById('issuesList');
  328. if (view === 'grid') {
  329. issuesList.classList.remove('issues-list');
  330. issuesList.classList.add('issues-grid');
  331. } else {
  332. issuesList.classList.remove('issues-grid');
  333. issuesList.classList.add('issues-list');
  334. }
  335. renderIssues();
  336. }
  337. // 顯示模態框
  338. function showModal(modalId) {
  339. const modal = document.getElementById(modalId);
  340. modal.classList.add('show');
  341. modal.style.display = 'flex';
  342. }
  343. // 關閉模態框
  344. function closeModal(modalId) {
  345. const modal = document.getElementById(modalId);
  346. modal.classList.remove('show');
  347. modal.style.display = 'none';
  348. }
  349. // 顯示載入指示器
  350. function showLoading(show) {
  351. const loading = document.getElementById('loadingIndicator');
  352. loading.style.display = show ? 'block' : 'none';
  353. }
  354. // 顯示警告訊息
  355. function showAlert(message, type = 'info') {
  356. // 移除現有的警告
  357. const existingAlert = document.querySelector('.alert');
  358. if (existingAlert) {
  359. existingAlert.remove();
  360. }
  361. const alert = document.createElement('div');
  362. alert.className = `alert alert-${type}`;
  363. alert.textContent = message;
  364. document.querySelector('.container').insertBefore(alert, document.querySelector('.stats-grid'));
  365. // 3秒後自動移除
  366. setTimeout(() => {
  367. if (alert.parentNode) {
  368. alert.remove();
  369. }
  370. }, 3000);
  371. }
  372. // 工具函數
  373. function escapeHtml(text) {
  374. const div = document.createElement('div');
  375. div.textContent = text;
  376. return div.innerHTML;
  377. }
  378. function formatDate(dateString) {
  379. const date = new Date(dateString);
  380. return date.toLocaleDateString('zh-TW', {
  381. year: 'numeric',
  382. month: '2-digit',
  383. day: '2-digit',
  384. hour: '2-digit',
  385. minute: '2-digit'
  386. });
  387. }
  388. function getStatusText(status) {
  389. const statusMap = {
  390. 'open': '待處理',
  391. 'in_progress': '處理中',
  392. 'closed': '已完成'
  393. };
  394. return statusMap[status] || status;
  395. }
  396. function getPriorityText(priority) {
  397. const priorityMap = {
  398. 'high': '高',
  399. 'medium': '中',
  400. 'low': '低'
  401. };
  402. return priorityMap[priority] || priority;
  403. }
  404. // 點擊模態框外部關閉
  405. window.onclick = function(event) {
  406. const modals = document.querySelectorAll('.modal');
  407. modals.forEach(modal => {
  408. if (event.target === modal) {
  409. modal.classList.remove('show');
  410. modal.style.display = 'none';
  411. }
  412. });
  413. }