123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724 |
- <template>
- <div class="command-info">
- <div class="header">
- <div class="go-back">
- <van-icon size="16" name="arrow-left" @click="router.push(`/project-info/${crtProject!.id}`)" />
- <div class="command-name">{{ crtCommand?.name }}</div>
- <van-icon size="16" name="edit" @click.stop="showEditPopup = true" />
- </div>
- <div class="btn-group">
- <van-icon name="question-o" size="24" @click="showHelpDoc = true" />
- </div>
- </div>
- <div class="container">
- <div class="card-group" v-if="crtCommand!.stepList.length">
- <VueDraggableNext :list="crtCommand!.stepList" animation="250" :delay="500">
- <div class="card" v-for="(item, index) in crtCommand!.stepList">
- <div class="card-header">
- <div class="step">
- 第
- <div class="num">{{ index + 1 }}</div>
- 步
- </div>
- <div class="label">{{ textMap[item.type] }}</div>
- </div>
- <template v-if="item.type === 'condition' || item.type === 'exec'">
- <div class="item" v-for="(cItem, cIndex) in item.list">
- <div class="tip" v-show="cIndex !== 0 && item.type !== 'exec'">或</div>
- <div class="wrap">
- <div class="content">
- <template v-if="cItem.type !== 'time'">
- <van-field
- v-model="cItem.label"
- is-link
- readonly
- placeholder="请选择"
- style="width: 30%"
- @click="
- showSelect(
- cItem.label.includes('DI') || cItem.label.includes('AI') ? 'inputLabel' : 'outputLabel',
- index,
- cIndex,
- 'label'
- )
- "
- />
- <template v-if="cItem.label.includes('DI') || cItem.label.includes('DO')">
- <van-field
- v-model="cItem.value"
- is-link
- readonly
- placeholder="请选择"
- style="width: 70%"
- @click="showSelect('commandValue', index, cIndex, 'value')"
- />
- </template>
- <template v-if="cItem.label.includes('AI') || cItem.label.includes('AO')">
- <van-field
- v-if="item.type === 'condition'"
- v-model="cItem.operation"
- is-link
- readonly
- placeholder="请选择"
- style="width: 35%"
- @click="showSelect('operation', index, cIndex, 'operation')"
- />
- <div class="operation" v-if="item.type === 'exec'">=</div>
- <van-field
- v-model="cItem.value"
- placeholder="请输入"
- :number="2.2"
- type="number"
- :style="{ width: item.type === 'exec' ? '60%' : '35%' }"
- />
- </template>
- </template>
- <template v-else>
- <van-field
- v-model="cItem.label"
- is-link
- readonly
- placeholder="请选择"
- style="width: 60%"
- @click="showSelect('timeLabel', index, cIndex, 'label')"
- />
- <van-field
- v-model="cItem.value"
- is-link
- readonly
- placeholder="请选择"
- style="width: 40%"
- @click="showSelect('timeValue', index, cIndex, 'value')"
- />
- </template>
- </div>
- <div class="del-icon" v-show="item.list?.length !== 1">
- <van-icon name="cross" @click="handleEditCommand('delChild', index, cIndex)" />
- </div>
- </div>
- </div>
- </template>
- <template v-else-if="item.type === 'delay'">
- <div class="delay-content">延时<van-stepper v-model="item.value" />秒</div>
- </template>
- <div class="btn-group">
- <template v-if="item.type === 'condition'">
- <van-button size="small" type="primary" @click="handleEditCommand('condition', index)"
- >增加条件</van-button
- >
- <van-button size="small" type="primary" @click="handleEditCommand('delay', index)">增加定时</van-button>
- </template>
- <van-button
- v-if="item.type === 'exec'"
- size="small"
- type="primary"
- @click="handleEditCommand('exec', index)"
- >添加执行</van-button
- >
- <van-button size="small" type="danger" @click="handleEditCommand('del', index)"
- > 删除 </van-button
- >
- </div>
- </div>
- </VueDraggableNext>
- </div>
- <div class="empty" v-else>当前没有可执行任务</div>
- <div class="footer" v-if="crtCommand!.stepList.length || isExistCnt">
- <div class="tip">如果没有执行命令,则本条任务不会被编译;如果条件不满足,将会从第一步开始重新执行</div>
- <van-button type="primary" :disabled="!crtCommand!.stepList.length && !isExistCnt" @click="handleSaveCommand()"
- >保存</van-button
- >
- </div>
- </div>
- <div class="handle-group" v-show="showBtnGroup">
- <van-button size="small" type="primary" @click="handleAddCommand('condition')">增加条件</van-button>
- <van-button size="small" type="primary" @click="handleAddCommand('delay')">增加延时</van-button>
- <van-button size="small" type="primary" @click="handleAddCommand('exec')">增加执行</van-button>
- <van-button size="small" type="danger" @click="handleAddCommand('del')">删除任务</van-button>
- <div class="pack" @click="showBtnGroup = false"><van-icon name="arrow" /></div>
- </div>
- <div class="unfold" @click="showBtnGroup = true" v-show="!showBtnGroup"><van-icon name="plus" /></div>
- <van-popup v-model:show="showOptionsPicker" destroy-on-close round position="bottom">
- <van-picker
- v-if="pickerType !== 'timeValue'"
- v-model="pickerValue"
- :columns="columnsMap[pickerType]"
- @cancel="showOptionsPicker = false"
- @confirm="onConfirm"
- />
- <van-time-picker
- v-else
- @cancel="showOptionsPicker = false"
- @confirm="onConfirm"
- v-model="currentTime"
- title="选择时间"
- />
- </van-popup>
- <van-calendar v-model:show="showCalendar" type="range" @confirm="calendarConfirm" />
- <van-popup v-model:show="showHelpDoc" round :style="{ width: '80%' }">
- <div class="help-label">帮助文档</div>
- <div class="help-content">
- 增加条件:意思为增加设备运行的条件,当设备满足设置的条件后才可执行其他操作,可设置DI状态、AI数值范围,以及定时触发。<br />
- 增加执行:增加执行为控制设备做某些操作,可单独添加,也可在设置条件和延时后添加。<br />
- 注意:条件和延时下面需添加执行才能完成自动化控制。整个运行逻辑为从上往下顺序执行。
- </div>
- </van-popup>
- <van-popup v-model:show="showEditPopup" round :style="{ width: '80%' }">
- <div class="label" style="margin: 16px">编辑任务名称</div>
- <van-form @submit="onSubmit">
- <van-cell-group inset>
- <van-field
- border
- v-model="newCommandName"
- name="newCommandName"
- placeholder="任务名称"
- maxlength="20"
- :rules="[{ required: true, message: '请填写任务名称' }]"
- />
- </van-cell-group>
- <div class="van-button-group">
- <van-button @click="handleCancel" size="small">取消</van-button>
- <van-button type="primary" native-type="submit" size="small">确定</van-button>
- </div>
- </van-form>
- </van-popup>
- </div>
- </template>
- <script setup lang="ts">
- import { useGlobalStore } from '@/stores/global'
- import type { CommandType, ConditionType, ProjectType } from '@/types/global'
- import { deepClone, deepEqual } from '@/utils/tools'
- import { storeToRefs } from 'pinia'
- import { v4 as uuid4 } from 'uuid'
- import { showConfirmDialog } from 'vant'
- import { VueDraggableNext } from 'vue-draggable-next'
- const router = useRouter()
- const route = useRoute()
- const { projectList } = storeToRefs(useGlobalStore())
- const crtCommand = ref<CommandType>()
- const crtProject = ref<ProjectType>()
- const textMap: Record<string, string> = {
- condition: '条件',
- delay: '延时',
- exec: '执行'
- }
- const isExistCnt = ref(false)
- watch(
- () => route,
- () => {
- crtProject.value = projectList.value.find((item) => item.id === route.params.pid)
- console.log(route, projectList.value)
- if (crtProject.value) {
- const command = crtProject.value.commandList!.find((item) => item.id === route.params.id)!
- if (command.stepList.length) {
- isExistCnt.value = true
- }
- crtCommand.value = deepClone(command) as CommandType
- console.log(crtCommand.value)
- }
- },
- {
- deep: true,
- immediate: true,
- once: true
- }
- )
- const showOptionsPicker = ref(false)
- const pickerValue = ref<string[]>([])
- const currentTime = ref(['00', '00'])
- const pickerType = ref('')
- const columnsMap: Record<string, any> = {
- inputLabel: [
- { text: 'DI0', value: 'DI0' },
- { text: 'DI1', value: 'DI1' },
- { text: 'DI2', value: 'DI2' },
- { text: 'DI3', value: 'DI3' },
- { text: 'DI4', value: 'DI4' },
- { text: 'DI5', value: 'DI5' },
- { text: 'DI6', value: 'DI6' },
- { text: 'DI7', value: 'DI7' },
- { text: 'DI8', value: 'DI8' },
- { text: 'DI9', value: 'DI9' },
- { text: 'AI0', value: 'AI0' },
- { text: 'AI1', value: 'AI1' },
- { text: 'AI2', value: 'AI2' },
- { text: 'AI3', value: 'AI3' }
- ],
- outputLabel: [
- { text: 'DO0', value: 'DO0' },
- { text: 'DO1', value: 'DO1' },
- { text: 'DO2', value: 'DO2' },
- { text: 'DO3', value: 'DO3' },
- { text: 'DO4', value: 'DO4' },
- { text: 'DO5', value: 'DO5' },
- { text: 'DO6', value: 'DO6' },
- { text: 'DO7', value: 'DO7' },
- { text: 'DO8', value: 'DO8' },
- { text: 'DO9', value: 'DO9' },
- { text: 'AO0', value: 'AO0' },
- { text: 'AO1', value: 'AO1' }
- ],
- commandValue: [
- { text: '闭合', value: '闭合' },
- { text: '打开', value: '打开' }
- ],
- operation: [
- { text: '≥', value: '≥' },
- { text: '≤', value: '≤' },
- { text: '=', value: '=' },
- { text: '≠', value: '≠' }
- ],
- timeLabel: [
- { text: '每天', value: '每天' },
- { text: '仅一次', value: '仅一次' },
- { text: '周一', value: '周一' },
- { text: '周二', value: '周二' },
- { text: '周三', value: '周三' },
- { text: '周四', value: '周四' },
- { text: '周五', value: '周五' },
- { text: '周六', value: '周六' },
- { text: '周日', value: '周日' },
- { text: '自定义', value: '自定义' }
- ]
- }
- type ChangeInfo = {
- stepIndex: number
- index: number
- key: keyof ConditionType
- }
- const changeInfo: ChangeInfo = {
- stepIndex: 0,
- index: 0,
- key: 'label' as keyof ConditionType
- }
- const showSelect = (colType: string, stepIndex: number, index: number, key: keyof ConditionType) => {
- pickerType.value = colType
- changeInfo.stepIndex = stepIndex
- changeInfo.index = index
- changeInfo.key = key
- showOptionsPicker.value = true
- }
- const showCalendar = ref(false)
- const onConfirm = () => {
- console.log(pickerValue.value, pickerType.value)
- if (crtCommand.value) {
- if (pickerType.value !== 'timeValue') {
- if (pickerValue.value.length && pickerValue.value[0] === '自定义') {
- showCalendar.value = true
- } else {
- crtCommand.value.stepList[changeInfo.stepIndex].list![changeInfo.index][changeInfo.key] = pickerValue.value[0]
- if (pickerValue.value[0].includes('AI')) {
- crtCommand.value.stepList[changeInfo.stepIndex].list![changeInfo.index].value = '0'
- crtCommand.value.stepList[changeInfo.stepIndex].list![changeInfo.index].operation = '='
- } else if (pickerValue.value[0].includes('DI')) {
- crtCommand.value.stepList[changeInfo.stepIndex].list![changeInfo.index].value = '闭合'
- }
- }
- } else {
- crtCommand.value.stepList[changeInfo.stepIndex].list![changeInfo.index][changeInfo.key] =
- currentTime.value.join(':')
- }
- currentTime.value = ['00', '00']
- pickerValue.value = []
- }
- showOptionsPicker.value = false
- }
- const calendarConfirm = (vals: any) => {
- const [start, end] = vals
- if (!crtCommand.value) return
- crtCommand.value.stepList[changeInfo.stepIndex].list![changeInfo.index][changeInfo.key] =
- `${new Date(start).toLocaleDateString()}-${new Date(end).toLocaleDateString()}`
- showCalendar.value = false
- }
- const handleEditCommand = (type: string, index: number, cIndex?: number) => {
- if (!crtCommand.value) return
- const x = crtCommand.value.x
- console.log(crtCommand.value.stepList)
- let y = 0
- const crtStep = crtCommand.value.stepList[index]
- if (crtStep.type === 'delay') {
- y = crtCommand.value.y + 100
- } else {
- y = crtCommand.value.y + 100 * crtCommand.value.stepList[index].list!.length
- }
- switch (type) {
- case 'condition':
- crtCommand.value.stepList[index].list!.push({
- id: '' + Date.now(),
- type: 'input',
- label: 'DI0',
- value: '闭合',
- x,
- y
- })
- break
- case 'exec':
- crtCommand.value.stepList[index].list!.push({
- id: '' + Date.now(),
- type: 'output',
- label: 'DO0',
- value: '打开',
- x,
- y
- })
- break
- case 'delay':
- crtCommand.value.stepList[index].list!.push({
- id: uuid4(),
- type: 'time',
- label: '每天',
- operation: '=',
- value: '08:00',
- x,
- y
- })
- break
- case 'delChild':
- crtCommand.value.stepList[index].list!.splice(cIndex!, 1)
- break
- case 'del':
- crtCommand.value.stepList.splice(index, 1)
- break
- }
- }
- const handleAddCommand = (type: string) => {
- if (!crtCommand.value) return
- if (type === 'del') {
- if (crtProject.value && crtCommand.value) {
- showConfirmDialog({
- title: '提示',
- message: '确定要删除这个任务吗'
- })
- .then(() => {
- // on confirm
- if (crtProject.value!.commandList.length === 1) return showFailToast('请至少保留一个任务!')
- console.log(crtProject.value, crtCommand.value)
- crtProject.value!.commandList = crtProject.value!.commandList.filter(
- (item) => crtCommand.value?.id !== item.id
- )
- router.push(`/project-info/${crtProject.value!.id}`)
- })
- .catch(() => {
- // on cancel
- })
- }
- return
- }
- const endCMD = crtCommand.value.stepList[crtCommand.value.stepList.length - 1]
- const isExistTime = endCMD?.list?.some((item) => item.type === 'time')
- const x = endCMD?.x || crtCommand.value.x
- console.log(endCMD, x)
- const y = 100 + crtProject.value!.commandList.length * 2000
- const id = '' + Date.now()
- switch (type) {
- case 'condition':
- crtCommand.value.stepList.push({
- id,
- type: 'condition',
- list: [
- {
- id,
- label: 'DI0',
- value: '闭合',
- type: 'input',
- x,
- y: y + 100
- }
- ],
- x: isExistTime ? (endCMD.x || 0) + 3000 : x + 400,
- y
- })
- break
- case 'delay':
- crtCommand.value.stepList.push({
- id,
- type: 'delay',
- value: '5',
- unit: 's',
- x: isExistTime ? (endCMD.x || 0) + 3000 : x + 400,
- y
- })
- break
- case 'exec':
- crtCommand.value.stepList.push({
- id,
- type: 'exec',
- list: [
- {
- id,
- label: 'DO0',
- value: '打开',
- type: 'output',
- x,
- y: y + 100
- }
- ],
- x: isExistTime ? (endCMD.x || 0) + 3000 : x + 400,
- y
- })
- break
- }
- }
- const handleSaveCommand = () => {
- console.log(crtCommand.value)
- let isExistNull = false
- crtCommand.value!.stepList.forEach((item) => {
- if (item.type === 'delay') {
- if (item.value === '' || item.value === null || item.value === undefined) {
- isExistNull = true
- }
- } else {
- item.list!.forEach((cItem) => {
- type keyType = keyof ConditionType
- for (let key in cItem) {
- if (cItem[key as keyType] === '' || cItem[key as keyType] === null || cItem[key as keyType] === undefined) {
- isExistNull = true
- }
- }
- })
- }
- })
- if (isExistNull) return showFailToast('请填写完整')
- const project = projectList.value.find((item) => item.id === route.params.pid)
- if (project) {
- const command = project.commandList!.find((item) => item.id === route.params.id)
- console.log(crtProject.value?.isCompiled)
- if (project.isCompiled) {
- const isCompiled = deepEqual(
- project.commandList.find((item) => item.id === crtCommand.value?.id),
- crtCommand.value
- )
- if (!isCompiled) {
- localStorage.removeItem('imgobj' + crtProject.value!.id)
- }
- project!.isCompiled = isCompiled
- }
- if (command) {
- command.stepList = crtCommand.value!.stepList
- showSuccessToast('保存成功')
- }
- }
- }
- const showHelpDoc = ref(false)
- const showEditPopup = ref(false)
- const newCommandName = ref('')
- const onSubmit = (values: Record<string, string>) => {
- console.log(values)
- const command = crtProject.value!.commandList!.find((item) => item.id === route.params.id)!
- console.log(crtProject.value?.commandList)
- const isExist = crtProject.value?.commandList.some(
- (item) => item.name === values.newCommandName && item.id !== command.id
- )
- if (isExist) {
- showFailToast('任务名称已存在')
- return
- } else {
- command!.name = values.newCommandName
- crtCommand.value!.name = values.newCommandName
- handleCancel()
- }
- }
- const handleCancel = async () => {
- showEditPopup.value = false
- await nextTick()
- newCommandName.value = ''
- }
- const showBtnGroup = ref(true)
- </script>
- <style scoped>
- .command-info {
- height: 100%;
- overflow: hidden;
- user-select: none;
- }
- .header {
- padding: 16px;
- margin-bottom: 16px;
- display: flex;
- align-items: center;
- justify-content: space-between;
- background-color: #fff;
- }
- .go-back {
- display: flex;
- align-items: center;
- gap: 8px;
- }
- .btn-group {
- display: flex;
- align-items: center;
- gap: 8px;
- }
- .card {
- padding: 16px;
- margin-bottom: 8px;
- display: flex;
- flex-direction: column;
- gap: 8px;
- border-radius: 4px;
- background: #fff;
- }
- .card-header {
- display: flex;
- justify-content: space-between;
- color: #666;
- }
- .container {
- padding: 0 16px;
- height: calc(100% - 86px);
- overflow: auto;
- }
- .step {
- display: flex;
- align-items: center;
- }
- .step .num {
- margin: 0 4px;
- width: 24px;
- height: 24px;
- line-height: 24px;
- text-align: center;
- border-radius: 50%;
- color: #4fb5f9;
- border: 1px solid #4fb5f9;
- }
- .card .wrap {
- display: flex;
- gap: 8px;
- align-items: center;
- /* margin-bottom: 8px; */
- }
- .tip {
- margin: 0 0 8px 8px;
- width: 100%;
- color: #999;
- }
- .card .item .content {
- flex: 1;
- display: flex;
- align-items: center;
- gap: 8px;
- }
- .card .content .operation {
- margin: 0 8px;
- }
- .card .item .del-icon {
- width: 20px;
- height: 20px;
- display: flex;
- justify-content: center;
- align-items: center;
- border: 1px solid #ff0000;
- border-radius: 50%;
- color: #ff0000;
- }
- .card .item .van-cell {
- border: 1px solid #ccc;
- border-radius: 4px;
- }
- .card .delay-content {
- padding: 16px;
- }
- .van-stepper {
- margin: 0 16px;
- }
- .empty {
- height: 400px;
- text-align: center;
- line-height: 400px;
- font-size: 24px;
- color: #999;
- }
- .footer {
- padding: 8px 0;
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- gap: 8px;
- overflow: hidden;
- }
- .footer .van-button {
- width: 70%;
- }
- .help-label {
- margin: 24px 16px 16px;
- font-size: 18px;
- font-weight: 600;
- }
- .help-content {
- padding: 0 24px 24px;
- line-height: 2;
- }
- .van-button-group {
- margin: 16px;
- display: flex;
- justify-content: flex-end;
- gap: 8px;
- }
- .handle-group {
- position: fixed;
- bottom: 64px;
- right: 16px;
- padding: 16px;
- background-color: rgba(255, 255, 255, 0.7);
- border-radius: 4px;
- display: flex;
- justify-content: center;
- align-items: center;
- gap: 8px;
- max-width: 90%;
- transition: all 0.3s;
- }
- .unfold {
- position: fixed;
- bottom: 72px;
- right: 16px;
- background-color: rgba(255, 255, 255, 0.7);
- border-radius: 4px;
- display: flex;
- justify-content: center;
- align-items: center;
- gap: 8px;
- width: 48px;
- height: 48px;
- font-size: 28px;
- transition: all 0.3s;
- }
- .pack {
- display: flex;
- width: 28px;
- justify-content: center;
- align-items: center;
- border-radius: 2px;
- }
- .pack:active {
- background: #efefef;
- }
- </style>
|