atmcompanyline.vue 6.8 KB

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