GateControl.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. <template>
  2. <div v-if="visible" id="gate-control-panel" class="sm-panel gate-control-panel">
  3. <div class="sm-panel-header">
  4. <span class="panel-title">闸门启闭控制</span>
  5. <el-icon
  6. class="close-icon"
  7. @click="$emit('close')"
  8. title="关闭"
  9. >
  10. <CircleClose />
  11. </el-icon>
  12. </div>
  13. <div class="gate-control-content">
  14. <!-- 全部控制 -->
  15. <div class="all-control-section">
  16. <div class="all-control-label">全部闸门控制</div>
  17. <div class="all-control-buttons">
  18. <el-button
  19. type="primary"
  20. size="small"
  21. :disabled="isAnyAnimating"
  22. @click="openAllGates"
  23. class="all-control-btn"
  24. >
  25. <el-icon><ArrowUp /></el-icon>
  26. 全部开启
  27. </el-button>
  28. <el-button
  29. type="danger"
  30. size="small"
  31. :disabled="isAnyAnimating"
  32. @click="closeAllGates"
  33. class="all-control-btn"
  34. >
  35. <el-icon><ArrowDown /></el-icon>
  36. 全部关闭
  37. </el-button>
  38. </div>
  39. </div>
  40. <!-- 闸门列表(可收缩) -->
  41. <div class="gates-section">
  42. <div class="gates-label" @click="toggleGateList">
  43. <span class="gates-label-text">闸门列表 (共 {{ gates.length }} 个)</span>
  44. <el-icon
  45. class="collapse-icon"
  46. :class="{ collapsed: gateListCollapsed }"
  47. >
  48. <ArrowDown />
  49. </el-icon>
  50. </div>
  51. <div v-if="gates.length === 0" style="color: #999; padding: 20px; text-align: center;">
  52. 暂无闸门数据
  53. </div>
  54. <div v-show="!gateListCollapsed" class="gates-list">
  55. <div
  56. v-for="gate in gates"
  57. :key="gate.name"
  58. class="gate-item"
  59. >
  60. <div class="gate-header">
  61. <span class="gate-name">{{ gate.name }}</span>
  62. <span class="gate-status" :class="getGateStatusClass(gate.position)">
  63. {{ getGateStatusText(gate.position) }}
  64. </span>
  65. </div>
  66. <div class="gate-position">
  67. <span class="gate-position-value">{{ gate.position }}%</span>
  68. </div>
  69. <el-slider
  70. v-model="gate.sliderValue"
  71. :min="0"
  72. :max="100"
  73. :step="1"
  74. :disabled="gate.isAnimating"
  75. @change="(value) => handleGateSliderChange(gate, value)"
  76. class="gate-slider"
  77. />
  78. <div class="gate-controls">
  79. <el-button
  80. size="small"
  81. :disabled="gate.isAnimating || gate.position >= 100"
  82. @click="openGate(gate)"
  83. icon="ArrowUp"
  84. />
  85. <el-button
  86. size="small"
  87. :disabled="gate.isAnimating || gate.position <= 0"
  88. @click="closeGate(gate)"
  89. icon="ArrowDown"
  90. />
  91. <el-button
  92. size="small"
  93. :disabled="gate.isAnimating"
  94. @click="setGatePosition(gate, 50)"
  95. >50%</el-button>
  96. </div>
  97. <div v-if="gate.isAnimating" class="gate-progress">
  98. <el-progress
  99. :percentage="gate.animationProgress"
  100. :stroke-width="4"
  101. :color="gate.position < gate.targetPosition ? '#67c23a' : '#f56c6c'"
  102. />
  103. </div>
  104. </div>
  105. </div>
  106. </div>
  107. <!-- 快捷操作 -->
  108. <div class="quick-section">
  109. <div class="quick-label">全部快捷操作</div>
  110. <div class="quick-buttons">
  111. <el-button
  112. size="small"
  113. :disabled="isAnyAnimating"
  114. @click="setAllPositions(25)"
  115. >25%</el-button>
  116. <el-button
  117. size="small"
  118. :disabled="isAnyAnimating"
  119. @click="setAllPositions(50)"
  120. >50%</el-button>
  121. <el-button
  122. size="small"
  123. :disabled="isAnyAnimating"
  124. @click="setAllPositions(75)"
  125. >75%</el-button>
  126. </div>
  127. </div>
  128. </div>
  129. </div>
  130. </template>
  131. <script>
  132. import { ref, computed, onMounted, onUnmounted } from 'vue'
  133. import { CircleClose, ArrowUp, ArrowDown } from '@element-plus/icons-vue'
  134. export default {
  135. name: 'GateControl',
  136. components: {
  137. CircleClose,
  138. ArrowUp,
  139. ArrowDown
  140. },
  141. props: {
  142. visible: {
  143. type: Boolean,
  144. default: false
  145. },
  146. initialPosition: {
  147. type: Number,
  148. default: 0,
  149. validator: (value) => value >= 0 && value <= 100
  150. }
  151. },
  152. emits: ['close', 'positionChange'],
  153. setup(props, { emit }) {
  154. // 将组件实例注册到全局变量
  155. onMounted(() => {
  156. window.gateControlInstance = {
  157. updateGateList: updateGateList
  158. };
  159. console.log('🚪 GateControl 组件已挂载,全局实例已注册');
  160. });
  161. onUnmounted(() => {
  162. // 清理全局实例
  163. if (window.gateControlInstance) {
  164. window.gateControlInstance = null;
  165. }
  166. });
  167. // 闸门列表
  168. const gates = ref([])
  169. const gateListCollapsed = ref(false)
  170. // 切换闸门列表展开/收缩
  171. const toggleGateList = () => {
  172. gateListCollapsed.value = !gateListCollapsed.value
  173. }
  174. // 检查是否有任何闸门正在动画
  175. const isAnyAnimating = computed(() => {
  176. return gates.value.some(gate => gate.isAnimating)
  177. })
  178. // 更新闸门列表
  179. const updateGateList = (gateNames) => {
  180. console.log('🔄 GateControl - 更新闸门列表:', gateNames)
  181. console.log('🔄 GateControl - 闸门数量:', gateNames?.length || 0)
  182. console.log('🔄 GateControl - 当前 gates.value:', gates.value)
  183. if (!gateNames || gateNames.length === 0) {
  184. console.warn('⚠️ GateControl - 闸门名称列表为空')
  185. return
  186. }
  187. gates.value = gateNames.map(name => ({
  188. name: name,
  189. position: props.initialPosition,
  190. sliderValue: props.initialPosition,
  191. isAnimating: false,
  192. animationProgress: 0,
  193. targetPosition: props.initialPosition,
  194. animationFrame: null
  195. }))
  196. console.log('✅ GateControl - 闸门列表已更新,共', gates.value.length, '个闸门')
  197. }
  198. // 获取闸门状态样式类
  199. const getGateStatusClass = (position) => {
  200. if (position === 0) return 'closed'
  201. if (position === 100) return 'opened'
  202. return 'opening'
  203. }
  204. // 获取闸门状态文本
  205. const getGateStatusText = (position) => {
  206. if (position === 0) return '关闭'
  207. if (position === 100) return '全开'
  208. return '调节中'
  209. }
  210. // 缓动函数
  211. const easeInOutCubic = (t) => {
  212. return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2
  213. }
  214. // 动画单个闸门
  215. const animateGate = (gate, toPosition) => {
  216. if (gate.isAnimating) return
  217. if (gate.position === toPosition) return
  218. gate.isAnimating = true
  219. const startPosition = gate.position
  220. gate.targetPosition = toPosition
  221. const startTime = performance.now()
  222. const duration = 3000 // 3秒动画
  223. const animate = (currentTime) => {
  224. const elapsed = currentTime - startTime
  225. const progress = Math.min(elapsed / duration, 1)
  226. const easeProgress = easeInOutCubic(progress)
  227. gate.position = Math.round(
  228. startPosition + (toPosition - startPosition) * easeProgress
  229. )
  230. gate.sliderValue = gate.position
  231. gate.animationProgress = Math.round(progress * 100)
  232. // 实时通知父组件闸门位置变化(动画过程中持续更新)
  233. emit('positionChange', {
  234. gateName: gate.name,
  235. position: gate.position
  236. })
  237. if (progress < 1) {
  238. gate.animationFrame = requestAnimationFrame(animate)
  239. } else {
  240. gate.isAnimating = false
  241. }
  242. }
  243. gate.animationFrame = requestAnimationFrame(animate)
  244. }
  245. // 开启单个闸门
  246. const openGate = (gate) => {
  247. animateGate(gate, 100)
  248. }
  249. // 关闭单个闸门
  250. const closeGate = (gate) => {
  251. animateGate(gate, 0)
  252. }
  253. // 设置单个闸门位置
  254. const setGatePosition = (gate, position) => {
  255. animateGate(gate, position)
  256. }
  257. // 处理滑块变化
  258. const handleGateSliderChange = (gate, value) => {
  259. animateGate(gate, value)
  260. }
  261. // 开启所有闸门
  262. const openAllGates = () => {
  263. gates.value.forEach(gate => {
  264. if (!gate.isAnimating) {
  265. animateGate(gate, 100)
  266. }
  267. })
  268. }
  269. // 关闭所有闸门
  270. const closeAllGates = () => {
  271. gates.value.forEach(gate => {
  272. if (!gate.isAnimating) {
  273. animateGate(gate, 0)
  274. }
  275. })
  276. }
  277. // 设置所有闸门位置
  278. const setAllPositions = (position) => {
  279. gates.value.forEach(gate => {
  280. if (!gate.isAnimating) {
  281. animateGate(gate, position)
  282. }
  283. })
  284. }
  285. // 组件卸载时清理动画
  286. onUnmounted(() => {
  287. gates.value.forEach(gate => {
  288. if (gate.animationFrame) {
  289. cancelAnimationFrame(gate.animationFrame)
  290. }
  291. })
  292. })
  293. return {
  294. gates,
  295. gateListCollapsed,
  296. isAnyAnimating,
  297. updateGateList,
  298. getGateStatusClass,
  299. getGateStatusText,
  300. toggleGateList,
  301. openGate,
  302. closeGate,
  303. setGatePosition,
  304. handleGateSliderChange,
  305. openAllGates,
  306. closeAllGates,
  307. setAllPositions
  308. }
  309. }
  310. }
  311. </script>
  312. <style scoped>
  313. .gate-control-panel {
  314. width: 320px;
  315. max-height: 80vh; /* 使用视口高度的80%作为最大高度 */
  316. top: 100px;
  317. right: 20px;
  318. padding: 15px;
  319. overflow-y: auto;
  320. }
  321. .sm-panel-header {
  322. display: flex;
  323. justify-content: space-between;
  324. align-items: center;
  325. margin-bottom: 15px;
  326. padding-bottom: 10px;
  327. border-bottom: 1px solid #eee;
  328. }
  329. .panel-title {
  330. font-size: 16px;
  331. font-weight: bold;
  332. color: #303133;
  333. }
  334. .close-icon {
  335. cursor: pointer;
  336. color: #999;
  337. font-size: 18px;
  338. transition: color 0.2s;
  339. }
  340. .close-icon:hover {
  341. color: #666;
  342. }
  343. .gate-control-content {
  344. display: flex;
  345. flex-direction: column;
  346. gap: 15px;
  347. }
  348. /* 全部控制 */
  349. .all-control-section {
  350. display: flex;
  351. justify-content: space-between;
  352. align-items: center;
  353. padding: 10px;
  354. background-color: #f8f9fa;
  355. border-radius: 4px;
  356. }
  357. .all-control-label {
  358. font-size: 14px;
  359. color: #606266;
  360. }
  361. .all-control-buttons {
  362. display: flex;
  363. gap: 8px;
  364. }
  365. .all-control-btn {
  366. display: flex;
  367. align-items: center;
  368. gap: 4px;
  369. }
  370. /* 闸门列表 */
  371. .gates-section {
  372. display: flex;
  373. flex-direction: column;
  374. gap: 10px;
  375. }
  376. .gates-label {
  377. font-size: 14px;
  378. color: #606266;
  379. font-weight: 500;
  380. display: flex;
  381. align-items: center;
  382. justify-content: space-between;
  383. cursor: pointer;
  384. user-select: none;
  385. padding: 4px 0;
  386. }
  387. .gates-label:hover {
  388. color: #409eff;
  389. }
  390. .collapse-icon {
  391. font-size: 14px;
  392. transition: transform 0.25s ease;
  393. }
  394. .collapse-icon.collapsed {
  395. transform: rotate(-90deg);
  396. }
  397. .gates-list {
  398. display: flex;
  399. flex-direction: column;
  400. gap: 12px;
  401. max-height: calc(80vh - 200px); /* 根据面板高度减去其他部分的高度 */
  402. overflow-y: auto;
  403. }
  404. .gate-item {
  405. padding: 12px;
  406. background-color: #fafafa;
  407. border-radius: 6px;
  408. border: 1px solid #e4e7ed;
  409. }
  410. .gate-header {
  411. display: flex;
  412. justify-content: space-between;
  413. align-items: center;
  414. margin-bottom: 8px;
  415. }
  416. .gate-name {
  417. font-size: 14px;
  418. font-weight: 500;
  419. color: #303133;
  420. }
  421. .gate-status {
  422. font-size: 12px;
  423. padding: 2px 8px;
  424. border-radius: 10px;
  425. }
  426. .gate-status.closed {
  427. background-color: #fef0f0;
  428. color: #f56c6c;
  429. }
  430. .gate-status.opened {
  431. background-color: #f0f9eb;
  432. color: #67c23a;
  433. }
  434. .gate-status.opening {
  435. background-color: #fdf6ec;
  436. color: #e6a23c;
  437. }
  438. .gate-position {
  439. text-align: right;
  440. margin-bottom: 8px;
  441. }
  442. .gate-position-value {
  443. font-size: 18px;
  444. font-weight: bold;
  445. color: #409eff;
  446. }
  447. .gate-slider {
  448. margin-bottom: 8px;
  449. }
  450. .gate-controls {
  451. display: flex;
  452. gap: 6px;
  453. }
  454. .gate-controls el-button {
  455. flex: 1;
  456. }
  457. .gate-progress {
  458. margin-top: 8px;
  459. }
  460. /* 快捷操作 */
  461. .quick-section {
  462. display: flex;
  463. flex-direction: column;
  464. gap: 10px;
  465. }
  466. .quick-label {
  467. font-size: 14px;
  468. color: #606266;
  469. }
  470. .quick-buttons {
  471. display: flex;
  472. gap: 10px;
  473. }
  474. .quick-buttons el-button {
  475. flex: 1;
  476. }
  477. </style>