CommandInfo.vue 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854
  1. <template>
  2. <div class="command-info">
  3. <div class="header">
  4. <div class="go-back">
  5. <van-icon size="16" name="arrow-left" @click="router.push(`/project-info/${crtProject!.id}`)" />
  6. <div class="command-name">{{ crtCommand?.name }}</div>
  7. <van-icon size="16" name="edit" @click.stop="showEditPopup = true" />
  8. </div>
  9. <div class="btn-group">
  10. <van-icon name="question-o" size="24" @click="showHelpDoc = true" />
  11. <van-button
  12. type="primary"
  13. size="small"
  14. :disabled="!crtCommand!.stepList.length && !isExistCnt"
  15. @click="handleSaveCommand"
  16. >保存</van-button
  17. >
  18. </div>
  19. </div>
  20. <div class="container" ref="stepListRef">
  21. <div class="card-group" v-if="crtCommand!.stepList.length">
  22. <VueDraggableNext :list="crtCommand!.stepList" animation="250" :delay="500">
  23. <div class="card" v-for="(item, index) in crtCommand!.stepList">
  24. <div class="card-header">
  25. <div class="step">
  26. <div class="num">{{ index + 1 }}</div>
  27. </div>
  28. <div class="label">{{ textMap[item.type] }}</div>
  29. </div>
  30. <template v-if="item.type === 'condition' || item.type === 'exec'">
  31. <div class="item" v-for="(cItem, cIndex) in item.list">
  32. <div class="tip" v-show="cIndex !== 0 && item.type !== 'exec'">或</div>
  33. <div class="wrap">
  34. <div class="content">
  35. <template v-if="cItem.type !== 'time'">
  36. <van-field
  37. v-model="cItem.label"
  38. is-link
  39. readonly
  40. placeholder="请选择"
  41. style="width: 30%"
  42. @click="
  43. showSelect(
  44. cItem.label.includes('DI') || cItem.label.includes('AI') ? 'inputLabel' : 'outputLabel',
  45. index,
  46. cIndex,
  47. 'label',
  48. cItem.label
  49. )
  50. "
  51. />
  52. <template v-if="cItem.label.includes('DI') || cItem.label.includes('DO')">
  53. <van-field
  54. v-model="cItem.value"
  55. is-link
  56. readonly
  57. placeholder="请选择"
  58. style="width: 70%"
  59. @click="showSelect('commandValue', index, cIndex, 'value', cItem.value)"
  60. />
  61. </template>
  62. <template v-if="cItem.label.includes('AI') || cItem.label.includes('AO')">
  63. <van-field
  64. v-if="item.type === 'condition'"
  65. v-model="cItem.operation"
  66. is-link
  67. readonly
  68. placeholder="请选择"
  69. style="width: 35%"
  70. @click="showSelect('operation', index, cIndex, 'operation', cItem.operation!)"
  71. />
  72. <div class="operation" v-if="item.type === 'exec'">=</div>
  73. <van-field
  74. v-model="cItem.value"
  75. placeholder="请输入"
  76. :number="2.2"
  77. type="number"
  78. :style="{ width: item.type === 'exec' ? '60%' : '35%' }"
  79. />
  80. </template>
  81. </template>
  82. <template v-else>
  83. <van-field
  84. v-model="cItem.label"
  85. is-link
  86. readonly
  87. placeholder="请选择"
  88. style="width: 60%"
  89. @click="showSelect('timeLabel', index, cIndex, 'label', cItem.label)"
  90. />
  91. <van-field
  92. v-model="cItem.operation"
  93. is-link
  94. readonly
  95. placeholder="请选择"
  96. style="width: 35%"
  97. @click="showSelect('timeOperation', index, cIndex, 'operation', cItem.operation!)"
  98. />
  99. <van-field
  100. v-model="cItem.value"
  101. is-link
  102. readonly
  103. placeholder="请选择"
  104. style="width: 40%"
  105. @click="showSelect('timeValue', index, cIndex, 'value', cItem.value)"
  106. />
  107. </template>
  108. </div>
  109. <div class="del-icon" v-show="item.list?.length !== 1">
  110. <van-icon name="cross" @click="handleEditCommand('delChild', index, cIndex)" />
  111. </div>
  112. </div>
  113. </div>
  114. </template>
  115. <template v-else-if="item.type === 'delay'">
  116. <div class="delay-content">延时<van-stepper v-model="item.value" />秒</div>
  117. </template>
  118. <div class="btn-group">
  119. <template v-if="item.type === 'condition'">
  120. <van-button size="small" type="primary" @click="handleEditCommand('condition', index)"
  121. >增加条件</van-button
  122. >
  123. <van-button size="small" type="primary" @click="handleEditCommand('delay', index)">增加定时</van-button>
  124. </template>
  125. <van-button
  126. v-if="item.type === 'exec'"
  127. size="small"
  128. type="primary"
  129. @click="handleEditCommand('exec', index)"
  130. >添加执行</van-button
  131. >
  132. <van-button size="small" type="danger" @click="handleEditCommand('del', index)"
  133. >&nbsp;删除&nbsp;</van-button
  134. >
  135. </div>
  136. </div>
  137. </VueDraggableNext>
  138. </div>
  139. <div class="empty" v-else>当前没有可执行任务</div>
  140. <div class="footer" v-if="crtCommand!.stepList.length || isExistCnt">
  141. <div class="tip">如果没有执行命令,则本条任务不会被编译;如果条件不满足,将会从第一步开始重新执行</div>
  142. </div>
  143. </div>
  144. <div class="handle-group" v-show="showBtnGroup">
  145. <van-button size="small" type="primary" @click="handleAddCommand('condition')">增加条件</van-button>
  146. <van-button size="small" type="primary" @click="handleAddCommand('delay')">增加延时</van-button>
  147. <van-button size="small" type="primary" @click="handleAddCommand('exec')">增加执行</van-button>
  148. <van-button size="small" type="danger" @click="handleAddCommand('del')">删除任务</van-button>
  149. <div class="pack" @click="showBtnGroup = false"><van-icon name="arrow" /></div>
  150. </div>
  151. <div class="unfold" @click="showBtnGroup = true" v-show="!showBtnGroup"><van-icon name="plus" /></div>
  152. <van-popup v-model:show="showOptionsPicker" destroy-on-close round position="bottom">
  153. <van-picker
  154. v-if="pickerType !== 'timeValue'"
  155. v-model="pickerValue"
  156. :columns="columnsMap[pickerType]"
  157. @cancel="showOptionsPicker = false"
  158. @confirm="onConfirm"
  159. />
  160. <van-time-picker
  161. v-else
  162. @cancel="showOptionsPicker = false"
  163. @confirm="onConfirm"
  164. v-model="currentTime"
  165. title="选择时间"
  166. />
  167. </van-popup>
  168. <van-calendar v-model:show="showCalendar" :type="calendarType" @confirm="calendarConfirm" />
  169. <van-popup v-model:show="showHelpDoc" round :style="{ width: '80%' }">
  170. <div class="help-label">帮助文档</div>
  171. <div class="help-content">
  172. &nbsp;&nbsp;&nbsp;&nbsp;增加条件:意思为增加设备运行的条件,当设备满足设置的条件后才可执行其他操作,可设置DI状态、AI数值范围,以及定时触发。<br />
  173. &nbsp;&nbsp;&nbsp;&nbsp;增加执行:增加执行为控制设备做某些操作,可单独添加,也可在设置条件和延时后添加。<br />
  174. &nbsp;&nbsp;&nbsp;&nbsp;注意:条件和延时下面需添加执行才能完成自动化控制。整个运行逻辑为从上往下顺序执行。
  175. </div>
  176. </van-popup>
  177. <van-popup v-model:show="showEditPopup" round :style="{ width: '80%' }">
  178. <div class="label" style="margin: 16px">编辑任务名称</div>
  179. <van-form @submit="onSubmit">
  180. <van-cell-group inset>
  181. <van-field
  182. border
  183. v-model="newCommandName"
  184. name="newCommandName"
  185. placeholder="任务名称"
  186. maxlength="20"
  187. :rules="[{ required: true, message: '请填写任务名称' }]"
  188. />
  189. </van-cell-group>
  190. <div class="van-button-group">
  191. <van-button @click="handleCancel" size="small">取消</van-button>
  192. <van-button type="primary" native-type="submit" size="small">确定</van-button>
  193. </div>
  194. </van-form>
  195. </van-popup>
  196. <van-popup v-model:show="showUnsaveTipPopup" round :style="{ width: '80%' }" :close-on-click-overlay="false">
  197. <div class="label">当前页面内容未保存,是否保存?</div>
  198. <div class="btn-group">
  199. <van-button size="small" @click="handleExit">退出</van-button>
  200. <van-button size="small" type="primary" @click="handleSaveCommand">保存</van-button>
  201. <van-button size="small" type="success" @click="handeSaveAndExit">保存并退出</van-button>
  202. </div>
  203. </van-popup>
  204. </div>
  205. </template>
  206. <script setup lang="ts">
  207. import { useGlobalStore } from '@/stores/global'
  208. import type { CommandType, ConditionType, ProjectType } from '@/types/global'
  209. import { deepClone, deepEqual } from '@/utils/tools'
  210. import { storeToRefs } from 'pinia'
  211. import { v4 as uuid4 } from 'uuid'
  212. import { showConfirmDialog } from 'vant'
  213. import { VueDraggableNext } from 'vue-draggable-next'
  214. const router = useRouter()
  215. const route = useRoute()
  216. const { projectList } = storeToRefs(useGlobalStore())
  217. const crtCommand = ref<CommandType>()
  218. const crtProject = ref<ProjectType>()
  219. const textMap: Record<string, string> = {
  220. condition: '条件',
  221. delay: '延时',
  222. exec: '执行'
  223. }
  224. const isExistCnt = ref(false)
  225. watch(
  226. () => route,
  227. () => {
  228. crtProject.value = projectList.value.find((item) => item.id === route.params.pid)
  229. console.log(route, projectList.value)
  230. if (crtProject.value) {
  231. const command = crtProject.value.commandList!.find((item) => item.id === route.params.id)!
  232. if (command.stepList.length) {
  233. isExistCnt.value = true
  234. }
  235. crtCommand.value = deepClone(command) as CommandType
  236. console.log(crtCommand.value)
  237. }
  238. },
  239. {
  240. deep: true,
  241. immediate: true,
  242. once: true
  243. }
  244. )
  245. const showOptionsPicker = ref(false)
  246. const pickerValue = ref<string[]>([])
  247. const currentTime = ref(['00', '00'])
  248. const pickerType = ref('')
  249. const columnsMap: Record<string, any> = {
  250. inputLabel: [
  251. { text: 'DI0', value: 'DI0' },
  252. { text: 'DI1', value: 'DI1' },
  253. { text: 'DI2', value: 'DI2' },
  254. { text: 'DI3', value: 'DI3' },
  255. { text: 'DI4', value: 'DI4' },
  256. { text: 'DI5', value: 'DI5' },
  257. { text: 'DI6', value: 'DI6' },
  258. { text: 'DI7', value: 'DI7' },
  259. { text: 'DI8', value: 'DI8' },
  260. { text: 'DI9', value: 'DI9' },
  261. { text: 'AI0', value: 'AI0' },
  262. { text: 'AI1', value: 'AI1' },
  263. { text: 'AI2', value: 'AI2' },
  264. { text: 'AI3', value: 'AI3' }
  265. ],
  266. outputLabel: [
  267. { text: 'DO0', value: 'DO0' },
  268. { text: 'DO1', value: 'DO1' },
  269. { text: 'DO2', value: 'DO2' },
  270. { text: 'DO3', value: 'DO3' },
  271. { text: 'DO4', value: 'DO4' },
  272. { text: 'DO5', value: 'DO5' },
  273. { text: 'DO6', value: 'DO6' },
  274. { text: 'DO7', value: 'DO7' },
  275. { text: 'DO8', value: 'DO8' },
  276. { text: 'DO9', value: 'DO9' },
  277. { text: 'AO0', value: 'AO0' },
  278. { text: 'AO1', value: 'AO1' }
  279. ],
  280. commandValue: [
  281. { text: '闭合', value: '闭合' },
  282. { text: '打开', value: '打开' }
  283. ],
  284. operation: [
  285. { text: '≥', value: '≥' },
  286. { text: '>', value: '>' },
  287. { text: '≤', value: '≤' },
  288. { text: '<', value: '<' },
  289. { text: '=', value: '=' },
  290. { text: '≠', value: '≠' }
  291. ],
  292. timeOperation: [
  293. { text: '等于', value: '等于' },
  294. { text: '早于', value: '早于' },
  295. { text: '晚于', value: '晚于' }
  296. ],
  297. timeLabel: [
  298. { text: '每天', value: '每天' },
  299. { text: '特定某一天', value: '特定某一天' },
  300. { text: '周一', value: '周一' },
  301. { text: '周二', value: '周二' },
  302. { text: '周三', value: '周三' },
  303. { text: '周四', value: '周四' },
  304. { text: '周五', value: '周五' },
  305. { text: '周六', value: '周六' },
  306. { text: '周日', value: '周日' },
  307. { text: '自定义', value: '自定义' }
  308. ]
  309. }
  310. type ChangeInfo = {
  311. stepIndex: number
  312. index: number
  313. key: keyof ConditionType
  314. }
  315. const changeInfo: ChangeInfo = {
  316. stepIndex: 0,
  317. index: 0,
  318. key: 'label' as keyof ConditionType
  319. }
  320. const showSelect = (colType: string, stepIndex: number, index: number, key: keyof ConditionType, value: string) => {
  321. pickerType.value = colType
  322. changeInfo.stepIndex = stepIndex
  323. changeInfo.index = index
  324. changeInfo.key = key
  325. showOptionsPicker.value = true
  326. if (pickerType.value !== 'timeValue') {
  327. pickerValue.value = [value]
  328. } else {
  329. currentTime.value = value.split(':')
  330. }
  331. }
  332. const showCalendar = ref(false)
  333. const calendarType = ref<'single' | 'range'>('single')
  334. const onConfirm = () => {
  335. console.log(pickerValue.value, currentTime.value, pickerType.value)
  336. if (crtCommand.value) {
  337. if (pickerType.value !== 'timeValue') {
  338. if (pickerValue.value.length && pickerValue.value[0] === '自定义') {
  339. showCalendar.value = true
  340. calendarType.value = 'range'
  341. } else if (pickerValue.value.length && pickerValue.value[0] === '特定某一天') {
  342. showCalendar.value = true
  343. calendarType.value = 'single'
  344. } else {
  345. crtCommand.value.stepList[changeInfo.stepIndex].list![changeInfo.index][changeInfo.key] = pickerValue.value[0]
  346. if (pickerValue.value[0].includes('AI') || pickerValue.value[0].includes('AO')) {
  347. crtCommand.value.stepList[changeInfo.stepIndex].list![changeInfo.index].value = '0'
  348. crtCommand.value.stepList[changeInfo.stepIndex].list![changeInfo.index].operation = '='
  349. } else if (pickerValue.value[0].includes('DI') || pickerValue.value[0].includes('DO')) {
  350. crtCommand.value.stepList[changeInfo.stepIndex].list![changeInfo.index].value = '闭合'
  351. }
  352. }
  353. } else {
  354. crtCommand.value.stepList[changeInfo.stepIndex].list![changeInfo.index][changeInfo.key] =
  355. currentTime.value.join(':')
  356. }
  357. currentTime.value = ['00', '00']
  358. pickerValue.value = []
  359. }
  360. showOptionsPicker.value = false
  361. }
  362. const calendarConfirm = (vals: any) => {
  363. if (!crtCommand.value) return
  364. if (Array.isArray(vals)) {
  365. const [start, end] = vals
  366. crtCommand.value.stepList[changeInfo.stepIndex].list![changeInfo.index][changeInfo.key] =
  367. `${new Date(start).toLocaleDateString()}-${new Date(end).toLocaleDateString()}`
  368. showCalendar.value = false
  369. } else {
  370. crtCommand.value.stepList[changeInfo.stepIndex].list![changeInfo.index][changeInfo.key] =
  371. `${new Date(vals).toLocaleDateString()}`
  372. showCalendar.value = false
  373. }
  374. }
  375. const handleEditCommand = (type: string, index: number, cIndex?: number) => {
  376. if (!crtCommand.value) return
  377. // const x = crtCommand.value.x
  378. console.log(crtCommand.value.stepList)
  379. let y = 0
  380. const crtStep = crtCommand.value.stepList[index]
  381. // if (crtStep.type === 'delay') {
  382. // y = crtCommand.value.y + 100
  383. // } else {
  384. // y = crtCommand.value.y + 100 * crtCommand.value.stepList[index].list!.length
  385. // }
  386. switch (type) {
  387. case 'condition':
  388. crtCommand.value.stepList[index].list!.push({
  389. id: '' + Date.now(),
  390. type: 'input',
  391. label: 'DI0',
  392. value: '闭合',
  393. // x,
  394. // y
  395. })
  396. break
  397. case 'exec':
  398. crtCommand.value.stepList[index].list!.push({
  399. id: '' + Date.now(),
  400. type: 'output',
  401. label: 'DO0',
  402. value: '打开',
  403. // x,
  404. // y
  405. })
  406. break
  407. case 'delay':
  408. crtCommand.value.stepList[index].list!.push({
  409. id: '' + Date.now(),
  410. type: 'time',
  411. label: '每天',
  412. operation: '等于',
  413. value: '08:00',
  414. // x,
  415. // y
  416. })
  417. break
  418. case 'delChild':
  419. crtCommand.value.stepList[index].list!.splice(cIndex!, 1)
  420. break
  421. case 'del':
  422. crtCommand.value.stepList.splice(index, 1)
  423. break
  424. }
  425. }
  426. const stepListRef = ref<HTMLDivElement>()
  427. let flag = false
  428. let timer: any
  429. const handleAddCommand = async (type: string) => {
  430. if (!crtCommand.value) return
  431. if (type === 'del') {
  432. if (crtProject.value && crtCommand.value) {
  433. showConfirmDialog({
  434. title: '提示',
  435. message: '确定要删除这个任务吗'
  436. })
  437. .then(() => {
  438. // on confirm
  439. if (crtProject.value!.commandList.length === 1) return showFailToast('请至少保留一个任务!')
  440. console.log(crtProject.value, crtCommand.value)
  441. crtProject.value!.commandList = crtProject.value!.commandList.filter(
  442. (item) => crtCommand.value?.id !== item.id
  443. )
  444. router.push(`/project-info/${crtProject.value!.id}`)
  445. })
  446. .catch(() => {
  447. // on cancel
  448. })
  449. }
  450. return
  451. }
  452. if (flag) return
  453. if (timer) clearTimeout(timer)
  454. flag = true
  455. const endCMD = crtCommand.value.stepList[crtCommand.value.stepList.length - 1]
  456. const isExistTime = endCMD?.list?.some((item) => item.type === 'time')
  457. // const x = endCMD?.x || crtCommand.value.x
  458. // console.log(endCMD, x)
  459. // const y = 100 + crtProject.value!.commandList.length * 2000
  460. const id = '' + Date.now()
  461. switch (type) {
  462. case 'condition':
  463. crtCommand.value.stepList.push({
  464. id,
  465. type: 'condition',
  466. list: [
  467. {
  468. id,
  469. label: 'DI0',
  470. value: '闭合',
  471. type: 'input',
  472. // x,
  473. // y: y + 100
  474. }
  475. ],
  476. // x: isExistTime ? (endCMD.x || 0) + 3000 : x + 400,
  477. // y
  478. })
  479. break
  480. case 'delay':
  481. crtCommand.value.stepList.push({
  482. id,
  483. type: 'delay',
  484. value: '5',
  485. unit: 's',
  486. // x: isExistTime ? (endCMD.x || 0) + 3000 : x + 400,
  487. // y
  488. })
  489. break
  490. case 'exec':
  491. crtCommand.value.stepList.push({
  492. id,
  493. type: 'exec',
  494. list: [
  495. {
  496. id,
  497. label: 'DO0',
  498. value: '打开',
  499. type: 'output',
  500. // x,
  501. // y: y + 100
  502. }
  503. ],
  504. // x: isExistTime ? (endCMD.x || 0) + 3000 : x + 400,
  505. // y
  506. })
  507. break
  508. }
  509. await nextTick()
  510. if (stepListRef.value && stepListRef.value.scrollHeight > stepListRef.value.clientHeight) {
  511. stepListRef.value.scroll({ top: stepListRef.value.scrollHeight, behavior: 'smooth' })
  512. }
  513. timer = setTimeout(() => {
  514. flag = false
  515. }, 300)
  516. }
  517. const handleSaveCommand = () => {
  518. console.log(crtCommand.value)
  519. if (showUnsaveTipPopup.value) {
  520. showUnsaveTipPopup.value = false
  521. }
  522. let isExistNull = false
  523. crtCommand.value!.stepList.forEach((item) => {
  524. if (item.type === 'delay') {
  525. if (item.value === '' || item.value === null || item.value === undefined) {
  526. isExistNull = true
  527. }
  528. } else {
  529. item.list!.forEach((cItem) => {
  530. type keyType = keyof ConditionType
  531. for (let key in cItem) {
  532. if (cItem[key as keyType] === '' || cItem[key as keyType] === null || cItem[key as keyType] === undefined) {
  533. isExistNull = true
  534. }
  535. }
  536. })
  537. }
  538. })
  539. if (isExistNull) return showFailToast('请填写完整')
  540. const project = projectList.value.find((item) => item.id === route.params.pid)
  541. if (project) {
  542. const command = project.commandList!.find((item) => item.id === route.params.id)
  543. console.log(crtProject.value?.isCompiled)
  544. if (project.isCompiled) {
  545. const isCompiled = deepEqual(
  546. project.commandList.find((item) => item.id === crtCommand.value?.id),
  547. crtCommand.value
  548. )
  549. if (!isCompiled) {
  550. localStorage.removeItem('imgobj' + crtProject.value!.id)
  551. }
  552. project!.isCompiled = isCompiled
  553. }
  554. if (command) {
  555. command.stepList = deepClone(crtCommand.value!.stepList)
  556. showSuccessToast('保存成功')
  557. }
  558. }
  559. }
  560. const showHelpDoc = ref(false)
  561. const showEditPopup = ref(false)
  562. const newCommandName = ref('')
  563. const onSubmit = (values: Record<string, string>) => {
  564. console.log(values)
  565. const command = crtProject.value!.commandList!.find((item) => item.id === route.params.id)!
  566. console.log(crtProject.value?.commandList)
  567. const isExist = crtProject.value?.commandList.some(
  568. (item) => item.name === values.newCommandName && item.id !== command.id
  569. )
  570. if (isExist) {
  571. showFailToast('任务名称已存在')
  572. return
  573. } else {
  574. command!.name = values.newCommandName
  575. crtCommand.value!.name = values.newCommandName
  576. handleCancel()
  577. }
  578. }
  579. const handleCancel = async () => {
  580. showEditPopup.value = false
  581. await nextTick()
  582. newCommandName.value = ''
  583. }
  584. const showBtnGroup = ref(true)
  585. const showUnsaveTipPopup = ref(false)
  586. let toRouterPath = ''
  587. let isExit = false
  588. onBeforeRouteLeave((to, from, next) => {
  589. console.log(to, from)
  590. if (isExit) return next()
  591. const project = projectList.value.find((item) => item.id === route.params.pid)
  592. if (!project) return next()
  593. const isCompiled = deepEqual(
  594. project.commandList.find((item) => item.id === crtCommand.value?.id),
  595. crtCommand.value
  596. )
  597. if (isCompiled) return next()
  598. showUnsaveTipPopup.value = true
  599. toRouterPath = to.path
  600. })
  601. const handleExit = () => {
  602. showUnsaveTipPopup.value = false
  603. isExit = true
  604. router.push(toRouterPath)
  605. }
  606. const handeSaveAndExit = () => {
  607. handleSaveCommand()
  608. handleExit()
  609. }
  610. const handleBeforeUnload = (e: BeforeUnloadEvent) => {
  611. const project = projectList.value.find((item) => item.id === route.params.pid)
  612. if (project) {
  613. const isCompiled = deepEqual(
  614. project.commandList.find((item) => item.id === crtCommand.value?.id),
  615. crtCommand.value
  616. )
  617. if (!isCompiled) {
  618. e.preventDefault()
  619. }
  620. }
  621. }
  622. onMounted(() => {
  623. window.addEventListener('beforeunload', handleBeforeUnload)
  624. document.addEventListener('visibilitychange', (e) => {
  625. if (document.visibilityState === 'hidden') {
  626. console.log('页面被隐藏或关闭')
  627. handleBeforeUnload(e)
  628. // 在这里执行保存或其他操作
  629. }
  630. })
  631. })
  632. onUnmounted(() => {
  633. window.removeEventListener('beforeunload', handleBeforeUnload)
  634. })
  635. </script>
  636. <style scoped>
  637. .command-info {
  638. height: 100%;
  639. overflow: hidden;
  640. user-select: none;
  641. }
  642. .header {
  643. padding: 16px;
  644. margin-bottom: 16px;
  645. display: flex;
  646. align-items: center;
  647. justify-content: space-between;
  648. background-color: #fff;
  649. }
  650. .go-back {
  651. display: flex;
  652. align-items: center;
  653. gap: 8px;
  654. }
  655. .btn-group {
  656. display: flex;
  657. align-items: center;
  658. gap: 8px;
  659. }
  660. .btn-group .van-button {
  661. padding: 2px 16px;
  662. }
  663. .card {
  664. padding: 16px;
  665. margin-bottom: 8px;
  666. display: flex;
  667. flex-direction: column;
  668. gap: 8px;
  669. border-radius: 4px;
  670. background: #fff;
  671. }
  672. .card-header {
  673. display: flex;
  674. justify-content: space-between;
  675. color: #666;
  676. }
  677. .container {
  678. padding: 0 16px;
  679. height: calc(100% - 86px);
  680. overflow: auto;
  681. }
  682. .step {
  683. display: flex;
  684. align-items: center;
  685. }
  686. .step .num {
  687. margin: 0 4px;
  688. width: 24px;
  689. height: 24px;
  690. line-height: 24px;
  691. text-align: center;
  692. border-radius: 50%;
  693. color: #4fb5f9;
  694. border: 1px solid #4fb5f9;
  695. }
  696. .card .wrap {
  697. display: flex;
  698. gap: 8px;
  699. align-items: center;
  700. /* margin-bottom: 8px; */
  701. }
  702. .tip {
  703. margin: 0 0 8px 8px;
  704. width: 100%;
  705. color: #999;
  706. }
  707. .card .item .content {
  708. flex: 1;
  709. display: flex;
  710. align-items: center;
  711. gap: 8px;
  712. }
  713. .card .content .operation {
  714. margin: 0 8px;
  715. }
  716. .card .item .del-icon {
  717. width: 20px;
  718. height: 20px;
  719. display: flex;
  720. justify-content: center;
  721. align-items: center;
  722. border: 1px solid #ff0000;
  723. border-radius: 50%;
  724. color: #ff0000;
  725. }
  726. .card .item .van-cell {
  727. border: 1px solid #ccc;
  728. border-radius: 4px;
  729. }
  730. .card .delay-content {
  731. padding: 16px;
  732. }
  733. .van-stepper {
  734. margin: 0 16px;
  735. }
  736. .empty {
  737. height: 400px;
  738. text-align: center;
  739. line-height: 400px;
  740. font-size: 24px;
  741. color: #999;
  742. }
  743. .footer {
  744. padding: 8px 0;
  745. display: flex;
  746. flex-direction: column;
  747. justify-content: center;
  748. align-items: center;
  749. gap: 8px;
  750. overflow: hidden;
  751. }
  752. .footer .van-button {
  753. width: 70%;
  754. }
  755. .help-label {
  756. margin: 24px 16px 16px;
  757. font-size: 18px;
  758. font-weight: 600;
  759. }
  760. .help-content {
  761. padding: 0 24px 24px;
  762. line-height: 2;
  763. }
  764. .van-button-group {
  765. margin: 16px;
  766. display: flex;
  767. justify-content: flex-end;
  768. gap: 8px;
  769. }
  770. .handle-group {
  771. position: fixed;
  772. bottom: 64px;
  773. right: 16px;
  774. padding: 16px;
  775. background-color: rgba(255, 255, 255);
  776. border-radius: 4px;
  777. display: flex;
  778. justify-content: center;
  779. align-items: center;
  780. gap: 8px;
  781. max-width: 90%;
  782. transition: all 0.3s;
  783. box-shadow: 0 0 2px 2px #ccc;
  784. }
  785. .unfold {
  786. position: fixed;
  787. bottom: 72px;
  788. right: 16px;
  789. background-color: rgba(255, 255, 255);
  790. border-radius: 4px;
  791. display: flex;
  792. justify-content: center;
  793. align-items: center;
  794. gap: 8px;
  795. width: 48px;
  796. height: 48px;
  797. font-size: 28px;
  798. transition: all 0.3s;
  799. color: #4fb5f9;
  800. box-shadow: 0 0 2px 2px #ccc;
  801. }
  802. .pack {
  803. display: flex;
  804. width: 28px;
  805. justify-content: center;
  806. align-items: center;
  807. border-radius: 2px;
  808. }
  809. .pack:active {
  810. background: #efefef;
  811. }
  812. .van-popup .label {
  813. margin: 24px;
  814. font-size: 18px;
  815. }
  816. .van-popup .btn-group {
  817. margin: 24px;
  818. display: flex;
  819. justify-content: flex-end;
  820. }
  821. </style>