React Table Row 在mobile device 無法換行

AI 詠唱出來的第一版 UI, 如果是只有使用 desktop device only, 是沒問題, table row 的顯示方式在 mobile device 上顯示不友善, 因為建言編號沒換行, 造成版面被擠壓, 換成 div 的顯示方式, 讓 mobile deivce 針對同一筆row, 可以顯示為多行.

Key Improvements:

  1. Responsive Layout:
    • Desktop View: Maintains a clean, professional table layout with sortable columns.
    • Mobile View: Automatically switches to a card-based layout. This prevents the “squeezed” look of the table on small screens and ensures that all information (ID, subject, status, and deadline) is clearly readable.
  2. Unified Styling:
    • The background colors for table rows and cards are now consistent across both User and Management modes to avoid visual confusion.
    • Status badges and typography have been polished for better clarity.
  3. Mobile Fixes:
    • Language Switcher: Fixed the issue where the language switcher was not clickable on mobile devices. It now uses a robust React state management with a backdrop to handle closing the menu.
    • Search & Filters: Adjusted the search input and status filter buttons to be more space-efficient on smaller screens.
  4. Management Dashboard:
    • Refactored the management view to include the same responsive card system.
    • Enhanced the status filter UI with clear counts and interactive feedback.

The application has been verified to build and lint correctly, ensuring a stable experience across all devices.


異動的程式碼:

         <div className="bg-white border border-slate-200 rounded-xl overflow-hidden shadow-sm">
-          <div className="overflow-x-auto">
+          {/* Desktop Table View */}
+          <div className="hidden md:block overflow-x-auto">
             <table className="w-full text-left border-collapse">
               <thead>
                 <tr className="bg-slate-50 text-slate-500 text-sm uppercase tracking-wider font-semibold">
@@ -1170,6 +1171,72 @@ export default function App() {
               </tbody>
             </table>
           </div>
+
+          {/* Mobile Card View */}
+          <div className="md:hidden divide-y divide-slate-100">
+            {currentItems.length > 0 ? (
+              currentItems.map((s) => {
+                const deadlineInfo = getDeadlineInfo(s);
+                let translatedDeadlineText = deadlineInfo.text;
+                if (deadlineInfo.isDueToday) {
+                  translatedDeadlineText = t.dueToday;
+                } else if (deadlineInfo.isOverdue) {
+                  translatedDeadlineText = `${t.overdue} ${Math.abs(getWorkingDaysDifference(TODAY, new Date(deadlineInfo.deadlineDate)))} ${t.days}`;
+                } else {
+                  translatedDeadlineText = `${t.remaining} ${getWorkingDaysDifference(TODAY, new Date(deadlineInfo.deadlineDate))} ${t.days}`;
+                }
+
+                return (
+                  <div
+                    key={s.id}
+                    onClick={() => handleSuggestionClick(s)}
+                    className="p-4 hover:bg-slate-50 active:bg-slate-100 transition-colors cursor-pointer space-y-3"
+                  >
+                    <div className="flex justify-between items-start">
+                      <div className="flex flex-col space-y-1">
+                        <span className="text-[10px] font-mono text-slate-400 uppercase tracking-wider">{s.id}</span>
+                        <h3 className="font-bold text-slate-900 leading-tight">{s.subject}</h3>
+                        {s.isExtended && (
+                          <span className="text-[10px] text-blue-500 font-bold uppercase tracking-tighter">{t.extendedOnce}</span>
+                        )}
+                      </div>
+                      <StatusBadge status={s.status} language={language} isUserMode={true} />
+                    </div>
+
+                    <div className="flex justify-between items-center text-xs">
+                      <div className="flex items-center text-slate-500">
+                        <Building2 size={14} className="mr-1" />
+                        <span>{s.department}</span>
+                      </div>
+
+                      <div className="text-right">
+                        {s.status === 'unprocessed' ? (
+                          <span className="text-slate-400 italic">{t.suggestionReceived}</span>
+                        ) : (
+                          <div className="flex flex-col items-end">
+                            <div className={`flex items-center space-x-1 font-medium
+                              ${deadlineInfo.isDueToday ? 'text-red-600' : ''}
+                              ${deadlineInfo.isOverdue ? 'text-red-500 italic' : ''}
+                              ${!deadlineInfo.isOverdue && !deadlineInfo.isDueToday ? 'text-slate-600' : ''}
+                            `}>
+                              {deadlineInfo.isDueToday || deadlineInfo.isOverdue ? <AlertTriangle size={12} /> : <Clock size={12} className="text-orange-400" />}
+                              <span>{translatedDeadlineText}</span>
+                            </div>
+                            <span className="text-[10px] text-slate-400">{deadlineInfo.deadlineDate}</span>
+                          </div>
+                        )}
+                      </div>
+                    </div>
+                  </div>
+                );
+              })
+            ) : (
+              <div className="p-12 text-center text-slate-400">
+                {t.noResults}
+              </div>
+            )}
+          </div>
+        </div>
           <div className="px-6 py-4 bg-slate-50 border-t border-slate-100 flex flex-col sm:flex-row justify-between items-center gap-4 text-sm text-slate-500">
             <div className="flex items-center space-x-4">
               <p>{t.all} {sortedSuggestions.length} {t.totalResults}</p>
@@ -1192,10 +1259,9 @@ export default function App() {
               </button>
             </div>
           </div>
-        </div>
-      </motion.div>
-    );
-  };
+        </motion.div>
+      );
+    };

   const renderManagement = () => {
     const t = translations[language];
@@ -1328,13 +1394,15 @@ export default function App() {
                     setSearchQuery(e.target.value);
                     setCurrentPage(1);
                   }}
-                  className="pl-9 pr-4 py-1.5 rounded-lg border border-slate-200 text-sm focus:ring-2 focus:ring-slate-800 outline-none"
+                  className="pl-9 pr-4 py-1.5 rounded-lg border border-slate-200 text-sm focus:ring-2 focus:ring-slate-800 outline-none w-40 sm:w-64"
                 />
-          <div className="overflow-x-auto">
+
+          {/* Desktop Table View */}
+          <div className="hidden md:block overflow-x-auto">
             <table className="w-full text-left border-collapse">
               <thead>
                 <tr className="bg-slate-50 text-slate-500 text-sm uppercase tracking-wider font-semibold">
@@ -1374,7 +1442,6 @@ export default function App() {
                 {currentItems.length > 0 ? (
                   currentItems.map((s) => {
                     const deadlineInfo = getDeadlineInfo(s);
-                    // Translate deadline text
                     let translatedDeadlineText = deadlineInfo.text;
                     if (deadlineInfo.isDueToday) {
                       translatedDeadlineText = t.dueToday;
@@ -1388,30 +1455,44 @@ export default function App() {
                       <tr
                         key={s.id}
                         onClick={() => handleSuggestionClick(s)}
-                        className="hover:bg-slate-50 transition-colors cursor-pointer border-b border-slate-50 last:border-0"
+                        className="hover:bg-slate-50 transition-colors cursor-pointer group border-b border-slate-50 last:border-0"
                       >
-                        <td className="px-6 py-4 text-sm font-mono text-slate-500">{s.id}</td>
-                        <td className="px-6 py-4 font-medium text-slate-900">{s.subject}</td>
-                        <td className="px-6 py-4 text-slate-600">{s.department}</td>
-                        <td className="px-6 py-4">
+                        <td className="px-6 py-5 text-sm font-mono text-slate-500">
+                          <span>{s.id}</span>
+                        </td>
+                        <td className="px-6 py-5 font-medium text-slate-900">
+                          <div className="flex flex-col">
+                            <span>{s.subject}</span>
+                            {s.isExtended && (
+                              <span className="text-[10px] text-blue-500 font-bold uppercase tracking-tighter">{t.extendedOnce}</span>
+                            )}
+                          </div>
+                        </td>
+                        <td className="px-6 py-5 text-slate-600">{s.department}</td>
+                        <td className="px-6 py-5">
                           <StatusBadge status={s.status} language={language} isUserMode={false} />
                         </td>
-                        <td className="px-6 py-4 text-right">
+                        <td className="px-6 py-5 text-right">
                           <div className="flex flex-col items-end space-y-0.5">
                             {s.status === 'unprocessed' ? (
-                              <div className="text-slate-400 text-xs italic">
+                              <div className="text-slate-400 text-sm italic">
                                 {t.suggestionReceived}
                               </div>
                             ) : (
                               <>
-                                <div className={`flex items-center justify-end space-x-1 text-xs
+                                <div className={`flex items-center justify-end space-x-1
                                   ${deadlineInfo.isDueToday ? 'text-red-600 font-medium' : ''}
                                   ${deadlineInfo.isOverdue ? 'text-red-500 font-medium italic' : ''}
                                   ${!deadlineInfo.isOverdue && !deadlineInfo.isDueToday ? 'text-slate-600' : ''}
                                 `}>
+                                  {!deadlineInfo.isOverdue && !deadlineInfo.isDueToday && <Clock size={16} className="text-orange-400" />}
+                                  {deadlineInfo.isDueToday && <AlertTriangle size={16} />}
+                                  {deadlineInfo.isOverdue && <AlertTriangle size={16} />}
                                   <span>{translatedDeadlineText}</span>
                                 </div>
-                                <div className="text-[10px] text-slate-400">{deadlineInfo.deadlineDate}</div>
+                                <div className="text-[10px] text-slate-400">
+                                  {t.deadlineLabel}: {deadlineInfo.deadlineDate}
+                                </div>
                               </>
                             )}
                           </div>
@@ -1429,27 +1510,92 @@ export default function App() {
               </tbody>
             </table>
           </div>
-          <div className="px-6 py-4 bg-slate-50 border-t border-slate-100 flex flex-col sm:flex-row justify-between items-center gap-4 text-sm text-slate-500">
-            <div className="flex items-center space-x-4">
-              <p>{t.all} {sortedSuggestions.length} {t.totalResults}</p>
-            </div>
-            <div className="flex items-center space-x-4">
-              <button
-                onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
-                disabled={currentPage === 1}
-                className="p-1 rounded hover:bg-slate-200 transition-colors disabled:opacity-30"
-              >
-                <ChevronLeft size={20} />
-              </button>
-              <span className="font-medium text-slate-900">{t.page} {currentPage} {t.of} {totalPages || 1} {t.pageSuffix}</span>
-              <button
-                onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
-                disabled={currentPage === totalPages || totalPages === 0}
-                className="p-1 rounded hover:bg-slate-200 transition-colors disabled:opacity-30"
-              >
-                <ChevronRight size={20} />
-              </button>
-            </div>
+
+          {/* Mobile Card View */}
+          <div className="md:hidden divide-y divide-slate-100">
+            {currentItems.length > 0 ? (
+              currentItems.map((s) => {
+                const deadlineInfo = getDeadlineInfo(s);
+                let translatedDeadlineText = deadlineInfo.text;
+                if (deadlineInfo.isDueToday) {
+                  translatedDeadlineText = t.dueToday;
+                } else if (deadlineInfo.isOverdue) {
+                  translatedDeadlineText = `${t.overdue} ${Math.abs(getWorkingDaysDifference(TODAY, new Date(deadlineInfo.deadlineDate)))} ${t.days}`;
+                } else {
+                  translatedDeadlineText = `${t.remaining} ${getWorkingDaysDifference(TODAY, new Date(deadlineInfo.deadlineDate))} ${t.days}`;
+                }
+
+                return (
+                  <div
+                    key={s.id}
+                    onClick={() => handleSuggestionClick(s)}
+                    className="p-4 hover:bg-slate-50 active:bg-slate-100 transition-colors cursor-pointer space-y-3"
+                  >
+                    <div className="flex justify-between items-start">
+                      <div className="flex flex-col space-y-1">
+                        <span className="text-[10px] font-mono text-slate-400 uppercase tracking-wider">{s.id}</span>
+                        <h3 className="font-bold text-slate-900 leading-tight">{s.subject}</h3>
+                        {s.isExtended && (
+                          <span className="text-[10px] text-blue-500 font-bold uppercase tracking-tighter">{t.extendedOnce}</span>
+                        )}
+                      </div>
+                      <StatusBadge status={s.status} language={language} isUserMode={false} />
+                    </div>
+
+                    <div className="flex justify-between items-center text-xs">
+                      <div className="flex items-center text-slate-500">
+                        <Building2 size={14} className="mr-1" />
+                        <span>{s.department}</span>
+                      </div>
+                      <div className="text-right">
+                        {s.status === 'unprocessed' ? (
+                          <span className="text-slate-400 italic">{t.suggestionReceived}</span>
+                        ) : (
+                          <div className="flex flex-col items-end">
+                            <div className={`flex items-center space-x-1 font-medium
+                              ${deadlineInfo.isDueToday ? 'text-red-600' : ''}
+                              ${deadlineInfo.isOverdue ? 'text-red-500 italic' : ''}
+                              ${!deadlineInfo.isOverdue && !deadlineInfo.isDueToday ? 'text-slate-600' : ''}
+                            `}>
+                              {deadlineInfo.isDueToday || deadlineInfo.isOverdue ? <AlertTriangle size={12} /> : <Clock size={12} className="text-orange-400" />}
+                              <span>{translatedDeadlineText}</span>
+                            </div>
+                            <span className="text-[10px] text-slate-400">{deadlineInfo.deadlineDate}</span>
+                          </div>
+                        )}
+                      </div>
+                    </div>
+                  </div>
+                );
+              })
+            ) : (
+              <div className="p-12 text-center text-slate-400">
+                {t.noResults}
+              </div>
+            )}
+          </div>
+        </div>
+              </div>
+            )}
+          </div>
+        </div>
+        <div className="px-6 py-4 bg-slate-50 border-t border-slate-100 flex flex-col sm:flex-row justify-between items-center gap-4 text-sm text-slate-500">
+          <div className="flex items-center space-x-4">
+            <p>{t.all} {sortedSuggestions.length} {t.totalResults}</p>
+          </div>
+          <div className="flex items-center space-x-4">
+            <button
+              onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
+              disabled={currentPage === 1}
+              className="p-1 rounded hover:bg-slate-200 transition-colors disabled:opacity-30"
+            >
+              <ChevronLeft size={20} />
+            </button>
+            <span className="font-medium text-slate-900">{t.page} {currentPage} {t.of} {totalPages || 1} {t.pageSuffix}</span>
+            <button
+              onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
+              disabled={currentPage === totalPages || totalPages === 0}
+              className="p-1 rounded hover:bg-slate-200 transition-colors disabled:opacity-30"
+            >
+              <ChevronRight size={20} />
+            </button>
           </div>
         </div>

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *