CommandInfo.vue 22 KB

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