CommandInfo.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
  1. <template>
  2. <div class="command-info">
  3. <div class="header">
  4. <div class="go-back" @click="router.push(`/project-info/${crtProject!.id}`)">
  5. <van-button size="small"><van-icon name="arrow-left" />返回</van-button>
  6. </div>
  7. <div class="btn-group">
  8. <van-button size="small" type="primary" @click="handleAddCommand('condition')">增加条件</van-button>
  9. <van-button size="small" type="primary" @click="handleAddCommand('delay')">增加延时</van-button>
  10. <van-button size="small" type="primary" @click="handleAddCommand('exec')">增加执行</van-button>
  11. <van-button size="small" type="danger" @click="handleAddCommand('del')">删除任务</van-button>
  12. <van-icon name="question-o" size="24" @click="showHelpDoc = true" />
  13. </div>
  14. </div>
  15. <div class="container">
  16. <div class="card-group" v-if="crtCommand!.stepList.length">
  17. <div class="card" v-for="(item, index) in crtCommand!.stepList">
  18. <div class="card-header">
  19. <div class="step">
  20. <div class="num">{{ index + 1 }}</div>
  21. </div>
  22. <div class="label">{{ textMap[item.type] }}</div>
  23. </div>
  24. <template v-if="item.type === 'condition' || item.type === 'exec'">
  25. <div class="item" v-for="(cItem, cIndex) in item.list">
  26. <div class="tip" v-show="cIndex !== 0 && item.type !== 'exec'">或</div>
  27. <div class="wrap">
  28. <div class="content">
  29. <template v-if="cItem.type !== 'time'">
  30. <van-field
  31. v-model="cItem.label"
  32. is-link
  33. readonly
  34. placeholder="请选择"
  35. style="width: 30%"
  36. @click="
  37. showSelect(
  38. cItem.label.includes('DI') || cItem.label.includes('AI') ? 'inputLabel' : 'outputLabel',
  39. index,
  40. cIndex,
  41. 'label'
  42. )
  43. "
  44. />
  45. <template v-if="cItem.label.includes('DI') || cItem.label.includes('DO')">
  46. <van-field
  47. v-model="cItem.value"
  48. is-link
  49. readonly
  50. placeholder="请选择"
  51. style="width: 70%"
  52. @click="showSelect('commandValue', index, cIndex, 'value')"
  53. />
  54. </template>
  55. <template v-if="cItem.label.includes('AI') || cItem.label.includes('AO')">
  56. <van-field
  57. v-if="item.type === 'condition'"
  58. v-model="cItem.operation"
  59. is-link
  60. readonly
  61. placeholder="请选择"
  62. style="width: 35%"
  63. @click="showSelect('operation', index, cIndex, 'operation')"
  64. />
  65. <div class="operation" v-if="item.type === 'exec'">=</div>
  66. <van-field
  67. v-model="cItem.value"
  68. placeholder="请输入"
  69. type="number"
  70. :style="{ width: item.type === 'exec' ? '60%' : '35%' }"
  71. />
  72. </template>
  73. </template>
  74. <template v-else>
  75. <van-field
  76. v-model="cItem.label"
  77. is-link
  78. readonly
  79. placeholder="请选择"
  80. style="width: 60%"
  81. @click="showSelect('timeLabel', index, cIndex, 'label')"
  82. />
  83. <van-field
  84. v-model="cItem.value"
  85. is-link
  86. readonly
  87. placeholder="请选择"
  88. style="width: 40%"
  89. @click="showSelect('timeValue', index, cIndex, 'value')"
  90. />
  91. </template>
  92. </div>
  93. <div class="del-icon">
  94. <van-icon name="cross" @click="handleEditCommand('delChild', index, cIndex)" />
  95. </div>
  96. </div>
  97. </div>
  98. </template>
  99. <template v-else-if="item.type === 'delay'">
  100. <div class="delay-content">延时<van-stepper v-model="item.value" />秒</div>
  101. </template>
  102. <div class="btn-group">
  103. <template v-if="item.type === 'condition'">
  104. <van-button size="small" type="primary" @click="handleEditCommand('condition', index)"
  105. >增加条件</van-button
  106. >
  107. <van-button size="small" type="primary" @click="handleEditCommand('delay', index)">增加定时</van-button>
  108. </template>
  109. <van-button
  110. v-if="item.type === 'exec'"
  111. size="small"
  112. type="primary"
  113. @click="handleEditCommand('exec', index)"
  114. >添加执行</van-button
  115. >
  116. <van-button size="small" type="danger" @click="handleEditCommand('del', index)"
  117. >&nbsp;删除&nbsp;</van-button
  118. >
  119. </div>
  120. </div>
  121. </div>
  122. <div class="empty" v-else>当前没有可执行任务</div>
  123. <div class="footer" v-if="crtCommand!.stepList.length">
  124. <div class="tip">如果条件不满足,将会从第一步开始重新执行</div>
  125. <van-button type="primary" :disabled="!crtCommand!.stepList.length" @click="handleSaveCommand()"
  126. >保存</van-button
  127. >
  128. </div>
  129. </div>
  130. <van-popup v-model:show="showOptionsPicker" destroy-on-close round position="bottom">
  131. <van-picker
  132. v-if="pickerType !== 'timeValue'"
  133. v-model="pickerValue"
  134. :columns="columnsMap[pickerType]"
  135. @cancel="showOptionsPicker = false"
  136. @confirm="onConfirm"
  137. />
  138. <van-time-picker
  139. v-else
  140. @cancel="showOptionsPicker = false"
  141. @confirm="onConfirm"
  142. v-model="currentTime"
  143. title="选择时间"
  144. />
  145. </van-popup>
  146. <van-calendar v-model:show="showCalendar" type="range" @confirm="calendarConfirm" />
  147. <van-popup v-model:show="showHelpDoc" round :style="{ width: '80%' }">
  148. <div class="help-label">帮助文档</div>
  149. <div class="help-content">
  150. &nbsp;&nbsp;&nbsp;&nbsp;增加条件:意思为增加设备运行的条件,当设备满足设置的条件后才可执行其他操作,可设置DI状态、AI数值范围,以及定时触发。<br />
  151. &nbsp;&nbsp;&nbsp;&nbsp;增加执行:增加执行为控制设备做某些操作,可单独添加,也可在设置条件和延时后添加。<br />
  152. &nbsp;&nbsp;&nbsp;&nbsp;注意:条件和延时下面需添加执行才能完成自动化控制。整个运行逻辑为从上往下顺序执行。
  153. </div>
  154. </van-popup>
  155. </div>
  156. </template>
  157. <script setup lang="ts">
  158. import { useGlobalStore } from '@/stores/global'
  159. import type { CommandType, ConditionType, ProjectType } from '@/types/global'
  160. import { deepClone } from '@/utils/tools'
  161. import { storeToRefs } from 'pinia'
  162. import { v4 as uuid4 } from 'uuid'
  163. import { showConfirmDialog } from 'vant'
  164. const router = useRouter()
  165. const route = useRoute()
  166. const { projectList } = storeToRefs(useGlobalStore())
  167. const crtCommand = ref<CommandType>()
  168. const crtProject = ref<ProjectType>()
  169. const textMap: Record<string, string> = {
  170. condition: '条件',
  171. delay: '延时',
  172. exec: '执行'
  173. }
  174. watch(
  175. () => route,
  176. () => {
  177. const project = projectList.value.find((item) => item.id === route.params.pid)
  178. crtProject.value = project
  179. if (project) {
  180. const command = project.commandList!.find((item) => item.id === route.params.id) || []
  181. crtCommand.value = deepClone(command) as CommandType
  182. console.log(crtCommand.value)
  183. }
  184. },
  185. {
  186. deep: true,
  187. immediate: true,
  188. once: true
  189. }
  190. )
  191. const showOptionsPicker = ref(false)
  192. const pickerValue = ref([])
  193. const currentTime = ref(['00', '00'])
  194. const pickerType = ref('')
  195. const columnsMap: Record<string, any> = {
  196. inputLabel: [
  197. { text: 'DI1', value: 'DI1' },
  198. { text: 'DI2', value: 'DI2' },
  199. { text: 'AI1', value: 'AI1' },
  200. { text: 'AI2', value: 'AI2' }
  201. ],
  202. outputLabel: [
  203. { text: 'DO1', value: 'DO1' },
  204. { text: 'DO2', value: 'DO2' },
  205. { text: 'AO1', value: 'AO1' },
  206. { text: 'AO2', value: 'AO2' }
  207. ],
  208. commandValue: [
  209. { text: '闭合', value: '闭合' },
  210. { text: '打开', value: '打开' }
  211. ],
  212. operation: [
  213. { text: '≥', value: '≥' },
  214. { text: '≤', value: '≤' },
  215. { text: '=', value: '=' },
  216. { text: '≠', value: '≠' }
  217. ],
  218. timeLabel: [
  219. { text: '每天', value: '每天' },
  220. { text: '仅一次', value: '仅一次' },
  221. { text: '周一', value: '周一' },
  222. { text: '周二', value: '周二' },
  223. { text: '周三', value: '周三' },
  224. { text: '周四', value: '周四' },
  225. { text: '周五', value: '周五' },
  226. { text: '周六', value: '周六' },
  227. { text: '周日', value: '周日' },
  228. { text: '自定义', value: '自定义' }
  229. ]
  230. }
  231. type ChangeInfo = {
  232. stepIndex: number
  233. index: number
  234. key: keyof ConditionType
  235. }
  236. const changeInfo: ChangeInfo = {
  237. stepIndex: 0,
  238. index: 0,
  239. key: 'label' as keyof ConditionType
  240. }
  241. const showSelect = (colType: string, stepIndex: number, index: number, key: keyof ConditionType) => {
  242. pickerType.value = colType
  243. changeInfo.stepIndex = stepIndex
  244. changeInfo.index = index
  245. changeInfo.key = key
  246. showOptionsPicker.value = true
  247. }
  248. const showCalendar = ref(false)
  249. const onConfirm = () => {
  250. console.log(pickerValue.value)
  251. if (crtCommand.value) {
  252. if (pickerType.value !== 'timeValue') {
  253. if (pickerValue.value.length && pickerValue.value[0] === '自定义') {
  254. showCalendar.value = true
  255. } else {
  256. crtCommand.value.stepList[changeInfo.stepIndex].list![changeInfo.index][changeInfo.key] = pickerValue.value[0]
  257. if (pickerType.value.includes('AI') || pickerType.value.includes('DI')) {
  258. crtCommand.value.stepList[changeInfo.stepIndex].list![changeInfo.index].value = ''
  259. }
  260. }
  261. } else {
  262. crtCommand.value.stepList[changeInfo.stepIndex].list![changeInfo.index][changeInfo.key] =
  263. currentTime.value.join(':')
  264. }
  265. currentTime.value = ['00', '00']
  266. pickerValue.value = []
  267. }
  268. showOptionsPicker.value = false
  269. }
  270. const calendarConfirm = (vals: any) => {
  271. const [start, end] = vals
  272. if (!crtCommand.value) return
  273. crtCommand.value.stepList[changeInfo.stepIndex].list![changeInfo.index][changeInfo.key] =
  274. `${new Date(start).toLocaleDateString()}-${new Date(end).toLocaleDateString()}`
  275. showCalendar.value = false
  276. }
  277. const handleEditCommand = (type: string, index: number, cIndex?: number) => {
  278. if (!crtCommand.value) return
  279. const x = crtCommand.value.x
  280. console.log(crtCommand.value.stepList)
  281. let y = 0
  282. const crtStep = crtCommand.value.stepList[index]
  283. if (crtStep.type === 'delay') {
  284. y = crtCommand.value.y + 100
  285. } else {
  286. y = crtCommand.value.y + 100 * crtCommand.value.stepList[index].list!.length
  287. }
  288. switch (type) {
  289. case 'condition':
  290. crtCommand.value.stepList[index].list!.push({
  291. id: '' + Date.now(),
  292. type: 'input',
  293. label: 'DI1',
  294. value: '',
  295. x,
  296. y
  297. })
  298. break
  299. case 'exec':
  300. crtCommand.value.stepList[index].list!.push({
  301. id: '' + Date.now(),
  302. type: 'output',
  303. label: 'DO1',
  304. value: '',
  305. x,
  306. y
  307. })
  308. break
  309. case 'delay':
  310. crtCommand.value.stepList[index].list!.push({
  311. id: uuid4(),
  312. type: 'time',
  313. label: '每天',
  314. operation: '=',
  315. value: '',
  316. x,
  317. y
  318. })
  319. break
  320. case 'delChild':
  321. crtCommand.value.stepList[index].list!.splice(cIndex!, 1)
  322. break
  323. case 'del':
  324. crtCommand.value.stepList.splice(index, 1)
  325. break
  326. }
  327. }
  328. const handleAddCommand = (type: string) => {
  329. if (!crtCommand.value) return
  330. if (type === 'del') {
  331. if (crtProject.value && crtCommand.value) {
  332. showConfirmDialog({
  333. title: '提示',
  334. message: '确定要删除这个任务吗'
  335. })
  336. .then(() => {
  337. // on confirm
  338. console.log(crtProject.value, crtCommand.value)
  339. crtProject.value!.commandList = crtProject.value!.commandList.filter(
  340. (item) => crtCommand.value?.id !== item.id
  341. )
  342. router.push(`/project-info/${crtProject.value!.id}`)
  343. })
  344. .catch(() => {
  345. // on cancel
  346. })
  347. }
  348. return
  349. }
  350. const endCMD = crtCommand.value.stepList[crtCommand.value.stepList.length - 1]
  351. const isExistTime = endCMD?.list?.some((item) => item.type === 'time')
  352. const x = endCMD?.x || crtCommand.value.x
  353. console.log(endCMD, x)
  354. const y = 100 + crtProject.value!.commandList.length * 2000
  355. switch (type) {
  356. case 'condition':
  357. crtCommand.value.stepList.push({
  358. id: '' + Date.now(),
  359. type: 'condition',
  360. list: [],
  361. x: isExistTime ? (endCMD.x || 0) + 3000 : x + 400,
  362. y
  363. })
  364. break
  365. case 'delay':
  366. crtCommand.value.stepList.push({
  367. id: '' + Date.now(),
  368. type: 'delay',
  369. value: '1000',
  370. unit: 's',
  371. x: isExistTime ? (endCMD.x || 0) + 3000 : x + 400,
  372. y
  373. })
  374. break
  375. case 'exec':
  376. crtCommand.value.stepList.push({
  377. id: '' + Date.now(),
  378. type: 'exec',
  379. list: [],
  380. x: isExistTime ? (endCMD.x || 0) + 3000 : x + 400,
  381. y
  382. })
  383. break
  384. }
  385. }
  386. const handleSaveCommand = () => {
  387. console.log(crtCommand.value)
  388. let isExistNull = false
  389. crtCommand.value!.stepList.forEach((item) => {
  390. if (item.type === 'delay') {
  391. if (item.value === '' || item.value === null || item.value === undefined) {
  392. isExistNull = true
  393. }
  394. } else {
  395. item.list!.forEach((cItem) => {
  396. type keyType = keyof ConditionType
  397. for (let key in cItem) {
  398. if (cItem[key as keyType] === '' || cItem[key as keyType] === null || cItem[key as keyType] === undefined) {
  399. isExistNull = true
  400. }
  401. }
  402. })
  403. }
  404. })
  405. if (isExistNull) return showFailToast('请填写完整')
  406. const project = projectList.value.find((item) => item.id === route.params.pid)
  407. if (project) {
  408. const command = project.commandList!.find((item) => item.id === route.params.id)
  409. if (command) {
  410. command.stepList = crtCommand.value!.stepList
  411. showSuccessToast('保存成功')
  412. }
  413. }
  414. }
  415. const showHelpDoc = ref(false)
  416. </script>
  417. <style scoped>
  418. .command-info {
  419. padding: 16px 0;
  420. height: 100%;
  421. overflow: hidden;
  422. }
  423. .header {
  424. padding: 0 16px;
  425. margin-bottom: 16px;
  426. display: flex;
  427. align-items: center;
  428. justify-content: space-between;
  429. }
  430. .btn-group {
  431. display: flex;
  432. align-items: center;
  433. gap: 8px;
  434. }
  435. .card {
  436. padding: 16px;
  437. margin-bottom: 8px;
  438. display: flex;
  439. flex-direction: column;
  440. gap: 8px;
  441. border-radius: 4px;
  442. background: #fff;
  443. }
  444. .card-header {
  445. display: flex;
  446. justify-content: space-between;
  447. color: #666;
  448. }
  449. .container {
  450. padding: 0 16px;
  451. height: calc(100% - 34px);
  452. overflow: auto;
  453. }
  454. .step {
  455. display: flex;
  456. align-items: center;
  457. }
  458. .step .num {
  459. margin: 0 4px;
  460. width: 24px;
  461. height: 24px;
  462. line-height: 24px;
  463. text-align: center;
  464. border-radius: 50%;
  465. color: #4fb5f9;
  466. border: 1px solid #4fb5f9;
  467. }
  468. .card .wrap {
  469. display: flex;
  470. gap: 8px;
  471. align-items: center;
  472. /* margin-bottom: 8px; */
  473. }
  474. .tip {
  475. margin: 0 0 8px 8px;
  476. width: 100%;
  477. color: #999;
  478. }
  479. .card .item .content {
  480. flex: 1;
  481. display: flex;
  482. align-items: center;
  483. gap: 8px;
  484. }
  485. .card .content .operation {
  486. margin: 0 8px;
  487. }
  488. .card .item .del-icon {
  489. width: 20px;
  490. height: 20px;
  491. display: flex;
  492. justify-content: center;
  493. align-items: center;
  494. border: 1px solid #ff0000;
  495. border-radius: 50%;
  496. color: #ff0000;
  497. }
  498. .card .item .van-cell {
  499. border: 1px solid #ccc;
  500. border-radius: 4px;
  501. }
  502. .card .delay-content {
  503. padding: 16px;
  504. }
  505. .van-stepper {
  506. margin: 0 16px;
  507. }
  508. .empty {
  509. height: 400px;
  510. text-align: center;
  511. line-height: 400px;
  512. font-size: 24px;
  513. color: #999;
  514. }
  515. .footer {
  516. padding: 8px 0;
  517. display: flex;
  518. flex-direction: column;
  519. justify-content: center;
  520. align-items: center;
  521. gap: 8px;
  522. overflow: hidden;
  523. }
  524. .footer .van-button {
  525. width: 70%;
  526. }
  527. .help-label {
  528. margin: 24px 16px 16px;
  529. font-size: 18px;
  530. font-weight: 600;
  531. }
  532. .help-content {
  533. padding: 0 24px 24px;
  534. line-height: 2;
  535. }
  536. </style>