waterdataline.vue 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. <template>
  2. <div class="container mx-auto px-4 py-8">
  3. <div class="bg-white rounded-xl shadow-lg overflow-hidden">
  4. <div class="p-6 border-b border-gray-200">
  5. <h1 class="text-[clamp(1.5rem,3vw,2.5rem)] font-bold text-gray-800 text-center">水质检测数据列表</h1>
  6. <p class="text-gray-500 text-center mt-2">实时监测与分析水质指标</p>
  7. </div>
  8. <!-- 加载状态 -->
  9. <div v-if="loading" class="py-20 flex justify-center items-center">
  10. <div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
  11. </div>
  12. <!-- 错误状态 -->
  13. <div v-else-if="error" class="p-8 bg-red-50 border-l-4 border-red-400 text-red-700">
  14. <div class="flex">
  15. <div class="flex-shrink-0">
  16. <i class="fa fa-exclamation-triangle text-red-500 text-xl"></i>
  17. </div>
  18. <div class="ml-3">
  19. <h3 class="text-sm font-medium text-red-800">加载失败</h3>
  20. <div class="mt-2 text-sm text-red-700">
  21. <p>{{ error }}</p>
  22. </div>
  23. <div class="mt-4">
  24. <button @click="fetchData" class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition duration-150 ease-in-out">
  25. <i class="fa fa-refresh mr-2"></i>重试
  26. </button>
  27. </div>
  28. </div>
  29. </div>
  30. </div>
  31. <!-- 数据表格 -->
  32. <div v-else-if="filteredData.length > 0" class="overflow-x-auto">
  33. <table class="min-w-full divide-y divide-gray-200">
  34. <thead class="bg-gray-50">
  35. <tr>
  36. <th
  37. v-for="(col, index) in displayColumns"
  38. :key="index"
  39. class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 transition-colors"
  40. @click="sortData(col.key)"
  41. >
  42. <div class="flex items-center justify-between">
  43. {{ col.label }}
  44. <span v-if="sortKey === col.key" class="ml-1 text-gray-400">
  45. {{ sortOrder === 'asc' ? '↑' : '↓' }}
  46. </span>
  47. </div>
  48. </th>
  49. </tr>
  50. </thead>
  51. <tbody class="bg-white divide-y divide-gray-200">
  52. <tr v-for="(item, rowIndex) in sortedData" :key="rowIndex"
  53. class="hover:bg-gray-50 transition-colors duration-150">
  54. <td
  55. v-for="(col, colIndex) in displayColumns"
  56. :key="colIndex"
  57. class="px-6 py-4 whitespace-nowrap text-sm"
  58. >
  59. <div class="flex items-center">
  60. <div class="text-gray-900 font-medium">
  61. {{ item[col.key] !== null ? item[col.key] : '-' }}
  62. </div>
  63. </div>
  64. </td>
  65. </tr>
  66. </tbody>
  67. </table>
  68. </div>
  69. <!-- 空数据状态 -->
  70. <div v-else class="p-8 text-center">
  71. <div class="flex flex-col items-center justify-center">
  72. <div class="text-gray-400 mb-4">
  73. <i class="fa fa-database text-5xl"></i>
  74. </div>
  75. <h3 class="text-lg font-medium text-gray-900 mb-1">暂无有效数据</h3>
  76. <p class="text-gray-500">已过滤全空行</p>
  77. </div>
  78. </div>
  79. <!-- 数据表格 + 统计 -->
  80. <div class="p-4 bg-gray-50 border-t border-gray-200">
  81. <div class="flex flex-col md:flex-row justify-between items-center">
  82. <div class="text-sm text-gray-500 mb-2 md:mb-0">
  83. 共 <span class="font-medium text-gray-900">{{ filteredData.length }}</span> 条数据
  84. </div>
  85. </div>
  86. </div>
  87. </div>
  88. </div>
  89. </template>
  90. <script setup>
  91. import { ref, computed, onMounted } from 'vue';
  92. import axios from 'axios';
  93. // 定义固定列配置
  94. const displayColumns = ref([
  95. { key: 'water_assay_ID', label: '检测ID' },
  96. { key: 'pH', label: 'PH值' },
  97. { key: 'Cr', label: '铬含量' },
  98. { key: 'As', label: '砷含量' },
  99. { key: 'Cd', label: '镉含量' },
  100. { key: 'Hg', label: '汞含量' },
  101. { key: 'Pb', label: '铅含量' },
  102. { key: 'sample_code', label: '样本编码' },
  103. { key: 'water_sample_ID', label: '水样ID' },
  104. ]);
  105. // 状态管理
  106. const waterData = ref([]);
  107. const loading = ref(true);
  108. const error = ref(null);
  109. const sortKey = ref('');
  110. const sortOrder = ref('asc');
  111. // 获取数据
  112. const fetchData = async () => {
  113. try {
  114. loading.value = true;
  115. error.value = null;
  116. const response = await axios.get('http://localhost:3000/table/Water_assay_data');
  117. waterData.value = response.data.data || response.data;
  118. } catch (err) {
  119. error.value = err.message || '无法连接到服务器,请检查接口是否可用';
  120. } finally {
  121. loading.value = false;
  122. }
  123. };
  124. // 过滤全空行
  125. const filteredData = computed(() => {
  126. return waterData.value.filter(item => {
  127. return displayColumns.value.some(col => item[col.key] !== null && item[col.key] !== '-');
  128. });
  129. });
  130. // 排序功能
  131. const sortedData = computed(() => {
  132. if (!sortKey.value) return filteredData.value;
  133. return [...filteredData.value].sort((a, b) => {
  134. const valA = a[sortKey.value];
  135. const valB = b[sortKey.value];
  136. if (valA < valB) return sortOrder.value === 'asc' ? -1 : 1;
  137. if (valA > valB) return sortOrder.value === 'asc' ? 1 : -1;
  138. return 0;
  139. });
  140. });
  141. // 切换排序
  142. const sortData = (key) => {
  143. if (sortKey.value === key) {
  144. sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc';
  145. } else {
  146. sortKey.value = key;
  147. sortOrder.value = 'asc';
  148. }
  149. };
  150. // 组件挂载
  151. onMounted(() => {
  152. fetchData();
  153. });
  154. </script>
  155. <style scoped>
  156. /* 布局 */
  157. .container {
  158. max-width: 1280px;
  159. margin: 0 auto;
  160. padding: 32px 16px;
  161. }
  162. .overflow-x-auto { overflow-x: auto; }
  163. /* 卡片 */
  164. .bg-white { background-color: #fff; }
  165. .rounded-xl { border-radius: 1rem; }
  166. .shadow-lg {
  167. box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1),
  168. 0 4px 6px -4px rgba(0,0,0,0.1);
  169. }
  170. /* 文字 */
  171. .text-center { text-align: center; }
  172. .text-lg { font-size: 1.125rem; }
  173. .font-bold { font-weight: 700; }
  174. .text-gray-800 { color: #111827; }
  175. /* 动画 */
  176. .animate-spin {
  177. animation: spin 1s linear infinite;
  178. }
  179. @keyframes spin {
  180. from { transform: rotate(0deg); }
  181. to { transform: rotate(360deg); }
  182. }
  183. /* 表格 */
  184. table { width: 100%; }
  185. .px-6 { padding: 0 1.5rem; }
  186. .py-4 { padding: 1rem 0; }
  187. .hover\:bg-gray-50:hover { background-color: #f9fafb; }
  188. /* 响应式 */
  189. @media (max-width: 640px) {
  190. .container { padding: 32px 8px; }
  191. .px-6 { padding: 0 0.75rem; }
  192. }
  193. table {
  194. border-collapse: collapse; /* 合并边框线 */
  195. }
  196. th, td {
  197. border: 1px solid #d1d5db; /* 灰色边框 */
  198. text-align: center; /* 内容居中 */
  199. padding: 12px 8px; /* 内边距优化 */
  200. }
  201. </style>