index.vue 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. <template>
  2. <div class="monitor" :class="{ 'is-critical': isFull }">
  3. <!-- 顶部状态栏 -->
  4. <header class="monitor-bar">
  5. <div class="bar-left">
  6. <span class="bar-dot" />
  7. <span class="bar-label">智慧停车监控</span>
  8. </div>
  9. <div class="bar-right">
  10. <span class="bar-status">{{ isFull ? '已满' : '运营中' }}</span>
  11. <span class="bar-divider">|</span>
  12. <span class="bar-time">{{ now }}</span>
  13. </div>
  14. </header>
  15. <!-- 核心读表区 -->
  16. <section class="readouts">
  17. <div class="readout-card spaces" :class="{ full: isFull }">
  18. <p class="readout-label">车位余量</p>
  19. <p class="readout-value">
  20. <span class="readout-num used">{{ data.used_spaces }}</span>
  21. <span class="readout-sep">/</span>
  22. <span class="readout-num total">{{ data.total_spaces }}</span>
  23. </p>
  24. <p class="readout-meta" v-if="isFull">全场满</p>
  25. <p class="readout-meta" v-else>{{ data.total_spaces - data.used_spaces }} 空位</p>
  26. </div>
  27. <div class="readout-card park">
  28. <p class="readout-label">在场车辆</p>
  29. <p class="readout-value">{{ data.in_park }} <span class="readout-unit">台</span></p>
  30. </div>
  31. <div class="readout-card income">
  32. <p class="readout-label">今日收入</p>
  33. <p class="readout-value">
  34. <span class="readout-currency">¥</span>{{ formatNum(data.today_income) }}
  35. </p>
  36. </div>
  37. <div class="readout-card flow">
  38. <p class="readout-label">今日进出</p>
  39. <p class="readout-value">
  40. <span class="readout-num entry">{{ data.today_entry }}</span>
  41. <span class="readout-arrow">→</span>
  42. <span class="readout-num exit">{{ data.today_exit }}</span>
  43. </p>
  44. </div>
  45. </section>
  46. <!-- 设备状态 -->
  47. <section class="devices-panel">
  48. <h3 class="panel-title">设备状态</h3>
  49. <div class="device-grid">
  50. <div
  51. v-for="d in data.devices"
  52. :key="d.channel_name"
  53. class="device-row"
  54. :class="{ offline: d.device_status !== 'online' }"
  55. >
  56. <span class="device-indicator" :class="d.device_status" />
  57. <span class="device-channel">{{ d.channel_name }}</span>
  58. <span class="device-dir">{{ d.direction === 'in' ? '入口' : '出口' }}</span>
  59. <span class="device-name">{{ d.device_name }}</span>
  60. </div>
  61. <div v-if="!data.devices || data.devices.length === 0" class="device-empty">
  62. 暂无设备连接
  63. </div>
  64. </div>
  65. </section>
  66. </div>
  67. </template>
  68. <script setup>
  69. import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
  70. import { getMonitor } from '@/api/dashboard'
  71. const data = reactive({
  72. total_spaces: 0, used_spaces: 0, in_park: 0,
  73. today_income: 0, today_entry: 0, today_exit: 0,
  74. devices: []
  75. })
  76. const now = ref('')
  77. let dataTimer = null
  78. let clockTimer = null
  79. const isFull = computed(() => data.total_spaces > 0 && data.used_spaces >= data.total_spaces)
  80. function formatNum(n) { return (n || 0).toFixed(2) }
  81. function updateClock() {
  82. now.value = new Date().toLocaleString('zh-CN', {
  83. hour: '2-digit', minute: '2-digit', second: '2-digit'
  84. })
  85. }
  86. async function fetchData() {
  87. try {
  88. const res = await getMonitor()
  89. if (res.code === 0) Object.assign(data, res.data)
  90. } catch (e) { /* keep last state */ }
  91. }
  92. onMounted(() => {
  93. updateClock()
  94. fetchData()
  95. clockTimer = setInterval(updateClock, 1000)
  96. dataTimer = setInterval(fetchData, 10000)
  97. })
  98. onUnmounted(() => {
  99. clearInterval(clockTimer)
  100. clearInterval(dataTimer)
  101. })
  102. </script>
  103. <style scoped>
  104. /* === Theme tokens === */
  105. .monitor {
  106. --bg: #0c1922;
  107. --panel: #152534;
  108. --text: #e2ecf5;
  109. --muted: #8ca8c4;
  110. --green: #00e6a0;
  111. --amber: #fca33b;
  112. --red: #ff4757;
  113. background: var(--bg);
  114. min-height: 100vh;
  115. padding: 24px 32px;
  116. color: var(--text);
  117. font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
  118. transition: background .6s;
  119. }
  120. .monitor.is-critical {
  121. --bg: #1a0f0f;
  122. --panel: #241414;
  123. }
  124. /* === Bar === */
  125. .monitor-bar {
  126. display: flex; justify-content: space-between; align-items: center;
  127. padding-bottom: 20px; border-bottom: 1px solid #1e3240; margin-bottom: 28px;
  128. }
  129. .bar-left { display: flex; align-items: center; gap: 10px; }
  130. .bar-dot {
  131. width: 8px; height: 8px; border-radius: 50%; background: var(--green);
  132. box-shadow: 0 0 6px var(--green);
  133. }
  134. .is-critical .bar-dot { background: var(--red); box-shadow: 0 0 6px var(--red); }
  135. .bar-label { font-size: 18px; font-weight: 700; letter-spacing: .04em; color: #ffffff; }
  136. .bar-right { display: flex; gap: 12px; align-items: center; font-size: 13px; color: #b0c8de; }
  137. .bar-status { color: var(--green); }
  138. .is-critical .bar-status { color: var(--red); }
  139. .bar-divider { color: #2a3d4e; }
  140. .bar-time { font-variant-numeric: tabular-nums; color: #ffffff; font-size: 15px; font-weight: 600; }
  141. /* === Readouts === */
  142. .readouts {
  143. display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 28px;
  144. }
  145. .readout-card {
  146. background: var(--panel);
  147. border: 1px solid #1a2e3c;
  148. border-radius: 10px;
  149. padding: 24px 20px 20px;
  150. text-align: center;
  151. transition: border-color .4s, box-shadow .4s;
  152. }
  153. .readout-card.full {
  154. border-color: var(--red);
  155. box-shadow: inset 0 0 40px rgba(255,71,87,.06), 0 0 20px rgba(255,71,87,.08);
  156. animation: pulse-red 2s ease-in-out infinite;
  157. }
  158. @keyframes pulse-red {
  159. 0%, 100% { box-shadow: inset 0 0 30px rgba(255,71,87,.04), 0 0 16px rgba(255,71,87,.06); }
  160. 50% { box-shadow: inset 0 0 50px rgba(255,71,87,.08), 0 0 28px rgba(255,71,87,.1); }
  161. }
  162. /* Label */
  163. .readout-label {
  164. font-size: 13px; text-transform: uppercase; letter-spacing: .08em;
  165. color: #a0bedb; margin: 0 0 10px; font-weight: 600;
  166. }
  167. /* Value */
  168. .readout-value {
  169. margin: 0;
  170. font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
  171. font-size: 36px; font-weight: 600; letter-spacing: -.02em;
  172. color: #eaf4fb;
  173. line-height: 1.1;
  174. }
  175. .spaces .readout-num.used { color: #eaf4fb; }
  176. .full .spaces .readout-num.used { color: var(--red); text-shadow: 0 0 12px rgba(255,71,87,.4); }
  177. .readout-sep { font-size: 28px; color: #3a5068; margin: 0 4px; font-weight: 400; }
  178. .readout-num.total { color: var(--muted); font-weight: 400; }
  179. .readout-unit { font-size: 16px; color: var(--muted); font-weight: 400; letter-spacing: 0; }
  180. .readout-currency { font-size: 20px; color: var(--green); margin-right: 2px; font-weight: 400; }
  181. .readout-arrow { color: #3a5068; margin: 0 6px; font-size: 22px; }
  182. .readout-num.entry { color: var(--green); }
  183. .readout-num.exit { color: var(--amber); }
  184. .readout-meta { margin: 8px 0 0; font-size: 12px; color: var(--muted); }
  185. .full .readout-meta { color: var(--red); font-weight: 500; }
  186. /* Income glow */
  187. .income .readout-value {
  188. color: var(--green);
  189. text-shadow: 0 0 18px rgba(0,214,143,.15);
  190. }
  191. /* === Devices === */
  192. .devices-panel {
  193. background: var(--panel); border: 1px solid #1a2e3c; border-radius: 10px;
  194. padding: 20px 24px 16px;
  195. }
  196. .panel-title {
  197. margin: 0 0 14px; font-size: 13px; text-transform: uppercase;
  198. letter-spacing: .08em; color: #a0bedb; font-weight: 600;
  199. }
  200. .device-grid {
  201. display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 10px;
  202. }
  203. .device-row {
  204. display: flex; align-items: center; gap: 10px;
  205. padding: 10px 14px;
  206. background: #0e1a23; border-radius: 6px;
  207. border-left: 3px solid var(--green);
  208. transition: border-color .3s;
  209. }
  210. .device-row.offline { border-left-color: var(--red); }
  211. .device-indicator {
  212. width: 7px; height: 7px; border-radius: 50%; background: var(--green);
  213. box-shadow: 0 0 5px var(--green); flex-shrink: 0;
  214. }
  215. .device-indicator.offline { background: var(--red); box-shadow: 0 0 5px var(--red); }
  216. .device-channel { font-weight: 600; font-size: 13px; min-width: 80px; color: #dce8f2; }
  217. .device-dir {
  218. font-size: 11px; color: #8aa8c2; background: #162330;
  219. padding: 2px 8px; border-radius: 3px;
  220. }
  221. .device-name { color: #849eb6; font-size: 13px; margin-left: auto; }
  222. .device-empty { color: #8aa8c2; font-size: 13px; padding: 8px; }
  223. /* === Responsive === */
  224. @media (max-width: 900px) {
  225. .readouts { grid-template-columns: repeat(2, 1fr); }
  226. .readout-value { font-size: 28px; }
  227. }
  228. </style>