task.vue 20 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133
  1. <template>
  2. <div class="system-task">
  3. <el-row class="box scroller1" type="flex" :gutter="10">
  4. <!-- 系统,用户自定义,已停止 -->
  5. <el-col v-for="(item, index) in list" :key="index">
  6. <div class="block" :class="[`_${item.key}`]">
  7. <div class="header">
  8. <!-- 图标 -->
  9. <i class="icon" :class="item.icon"></i>
  10. <!-- 标题 -->
  11. <span class="label">{{ item.label }}</span>
  12. <!-- 数量 -->
  13. <span class="num">({{ item.pagination.total }})</span>
  14. <span class="flex1"></span>
  15. <!-- 操作按钮 -->
  16. <ul class="op-btn">
  17. <li v-if="item.loading">
  18. <i class="el-icon-loading"></i>
  19. </li>
  20. <li
  21. v-else
  22. @click="refreshTask({ page: 1 })"
  23. class="refresh-btn"
  24. v-permission="perm.delete"
  25. >
  26. <i class="el-icon-refresh"></i>
  27. <span>刷新</span>
  28. </li>
  29. <li @click="edit(item.params)" class="add-btn" v-permission="perm.add">
  30. <i class="el-icon-plus"></i>
  31. <span>添加</span>
  32. </li>
  33. </ul>
  34. </div>
  35. <div class="container scroller1">
  36. <draggable
  37. v-model="list[index].list"
  38. v-bind="drag.options"
  39. tag="ul"
  40. :data-type="item.params.type"
  41. :data-status="item.params.status"
  42. @end="(e) => onDragEnd(e, item)"
  43. >
  44. <li
  45. v-for="item2 in list[index].list"
  46. :key="item2.id"
  47. :data-id="item2.id"
  48. @contextmenu.stop.prevent="openCM($event, item2)"
  49. class="_drag"
  50. >
  51. <div class="h">
  52. <span class="type _warning" v-show="item2.status === 0">
  53. {{ item2.type | task_type }}
  54. </span>
  55. <span class="name">{{ item2.name }}</span>
  56. </div>
  57. <div class="remark">{{ item2.remark }}</div>
  58. <div class="f">
  59. <template v-if="item2.status">
  60. <span class="date">{{ item2.nextRunTime || "..." }}</span>
  61. <span class="start">进行中</span>
  62. </template>
  63. <template v-else>
  64. <span>...</span>
  65. <span class="stop">已停止</span>
  66. </template>
  67. </div>
  68. <el-row type="flex" class="op">
  69. <el-col v-if="item2.status === 0" @click.native="start(item2)">
  70. <i class="el-icon-video-play"></i>
  71. <span>开始</span>
  72. </el-col>
  73. <el-col
  74. v-else
  75. @click.native="stop(item2)"
  76. v-permission="perm.stop"
  77. >
  78. <i class="el-icon-video-pause"></i>
  79. <span>暂停</span>
  80. </el-col>
  81. <el-col
  82. @click.native="edit(item2)"
  83. v-permission="{
  84. and: [perm.update, perm.info]
  85. }"
  86. >
  87. <i class="el-icon-edit"></i>
  88. <span>编辑</span>
  89. </el-col>
  90. <el-col @click.native="findLog(item2)" v-permission="perm.log">
  91. <i class="el-icon-tickets"></i>
  92. <span>查看日志</span>
  93. </el-col>
  94. </el-row>
  95. </li>
  96. <li v-if="list[index].list.length == 0">
  97. <div class="empty">暂无数据</div>
  98. </li>
  99. </draggable>
  100. <el-button
  101. class="more"
  102. type="text"
  103. size="mini"
  104. @click="moreTask(index, item)"
  105. v-if="item.pagination.total >= item.pagination.size"
  106. >查看更多</el-button
  107. >
  108. </div>
  109. <div class="footer">
  110. <button class="btn-add" @click="edit(item.params)" v-permission="perm.add">
  111. <i class="el-icon-plus"></i>
  112. </button>
  113. </div>
  114. </div>
  115. </el-col>
  116. <!-- 日志 -->
  117. <el-col v-permission="perm.log">
  118. <div class="block _log">
  119. <div class="header">
  120. <!-- 标题 -->
  121. <span class="label">日志</span>
  122. <!-- 数量 -->
  123. <span class="num">({{ logs.pagination.total }})</span>
  124. <span class="flex1"></span>
  125. <!-- 是否异常 -->
  126. <el-checkbox-group
  127. class="status"
  128. v-model="logs.filters.status"
  129. @change="filterLog"
  130. >
  131. <el-checkbox :label="0">异常</el-checkbox>
  132. </el-checkbox-group>
  133. <!-- 操作按钮 -->
  134. <ul class="op-btn">
  135. <li @click="refreshLog({ page: 1 })">
  136. <i class="el-icon-refresh"></i>
  137. <span>刷新</span>
  138. </li>
  139. <li v-if="logs.current" class="_current-log" @click="allLog">
  140. <span>{{ logs.current.name }}</span>
  141. <i class="el-icon-close"></i>
  142. </li>
  143. </ul>
  144. </div>
  145. <div
  146. class="container"
  147. v-loading="logs.loading"
  148. element-loading-text="拼命加载中"
  149. >
  150. <ul class="scroller1" v-infinite-scroll="moreLog">
  151. <li
  152. v-for="(item, index) in logs.list"
  153. :key="index"
  154. :class="{ _error: item.status == 0 }"
  155. @click="expandLog(item)"
  156. >
  157. <div class="h">
  158. <span class="name">{{ index + 1 }} · {{ item.taskName }}</span>
  159. </div>
  160. <div class="remark" :class="{ _ellipsis: !item._expand }">
  161. {{ item.detail || "..." }}
  162. </div>
  163. <div class="f">
  164. <span>执行时间:{{ item.createTime }}</span>
  165. </div>
  166. </li>
  167. <li v-if="logs.list.length == 0">
  168. <div class="empty">暂无数据</div>
  169. </li>
  170. </ul>
  171. </div>
  172. </div>
  173. </el-col>
  174. </el-row>
  175. </div>
  176. </template>
  177. <script>
  178. import draggable from "vuedraggable";
  179. import { checkPerm } from "cool/modules/base";
  180. import { Form, ContextMenu } from "cl-admin-crud";
  181. export default {
  182. name: "system-task",
  183. components: {
  184. draggable
  185. },
  186. data() {
  187. return {
  188. list: [
  189. {
  190. key: "sys",
  191. label: "系统任务",
  192. icon: "el-icon-s-tools",
  193. list: [],
  194. loading: false,
  195. params: {
  196. type: 0,
  197. status: 1
  198. },
  199. pagination: {
  200. size: 10,
  201. page: 1,
  202. total: 0
  203. }
  204. },
  205. {
  206. key: "user",
  207. label: "用户自定义任务",
  208. icon: "el-icon-user-solid",
  209. list: [],
  210. loading: false,
  211. params: {
  212. type: 1,
  213. status: 1
  214. },
  215. pagination: {
  216. size: 10,
  217. page: 1,
  218. total: 0
  219. }
  220. },
  221. {
  222. key: "stop",
  223. label: "已停止任务",
  224. list: [],
  225. loading: false,
  226. params: {
  227. type: null,
  228. status: 0
  229. },
  230. pagination: {
  231. size: 10,
  232. page: 1,
  233. total: 0
  234. }
  235. }
  236. ],
  237. logs: {
  238. loading: false,
  239. list: [],
  240. pagination: {
  241. size: 10,
  242. page: 1
  243. },
  244. params: {},
  245. filters: {
  246. status: []
  247. },
  248. current: null
  249. },
  250. drag: {
  251. options: {
  252. group: "Task",
  253. animation: 300,
  254. ghostClass: "Ghost",
  255. dragClass: "Drag",
  256. draggable: "._drag"
  257. }
  258. }
  259. };
  260. },
  261. filters: {
  262. task_type(i) {
  263. return i === 0 ? "系统" : "用户";
  264. }
  265. },
  266. mounted() {
  267. this.refreshTask({ page: 1 });
  268. },
  269. computed: {
  270. perm() {
  271. return this.$service.task.info.permission;
  272. }
  273. },
  274. methods: {
  275. // 任务拖动
  276. onDragEnd({ to, item }) {
  277. const status = to.getAttribute("data-status");
  278. const type = to.getAttribute("data-type");
  279. const id = item.getAttribute("data-id");
  280. if (status == 0) {
  281. this.stop({ id });
  282. }
  283. if (status == 1) {
  284. this.start({ id, type });
  285. }
  286. },
  287. // 右键菜单
  288. openCM(e, { id, status, type, name }) {
  289. let menus = [
  290. {
  291. label: "立即执行",
  292. perm: ["once"],
  293. "suffix-icon": "el-icon-video-play",
  294. callback: (_, close) => {
  295. this.once({ id });
  296. close();
  297. }
  298. },
  299. {
  300. label: "编辑",
  301. perm: ["update", "info"],
  302. "suffix-icon": "el-icon-edit",
  303. callback: (_, close) => {
  304. this.edit({ id, type });
  305. close();
  306. }
  307. },
  308. {
  309. label: "删除",
  310. perm: ["delete"],
  311. "suffix-icon": "el-icon-delete",
  312. callback: (_, close) => {
  313. this.delete({ id });
  314. close();
  315. }
  316. },
  317. {
  318. label: "查看日志",
  319. perm: ["log"],
  320. "suffix-icon": "el-icon-tickets",
  321. callback: (_, close) => {
  322. this.findLog({ id, name });
  323. close();
  324. }
  325. }
  326. ];
  327. if (status == 1) {
  328. menus.splice(1, 0, {
  329. label: "暂停",
  330. perm: ["stop"],
  331. "suffix-icon": "el-icon-video-pause",
  332. callback: (_, close) => {
  333. this.stop({ id, type });
  334. close();
  335. }
  336. });
  337. } else {
  338. menus.splice(1, 0, {
  339. label: "开始",
  340. perm: ["start"],
  341. "suffix-icon": "el-icon-video-play",
  342. callback: (_, close) => {
  343. this.start({ id, type });
  344. close();
  345. }
  346. });
  347. }
  348. ContextMenu.open(e, {
  349. list: menus.filter((e) => {
  350. return checkPerm({
  351. and: e.perm.map((a) => this.perm[a])
  352. });
  353. })
  354. });
  355. return false;
  356. },
  357. // 编辑任务
  358. async edit(params) {
  359. const { id, type } = params || {};
  360. let info = {
  361. type
  362. };
  363. if (id) {
  364. info = await this.$service.task.info.info({ id });
  365. }
  366. if (info.every) {
  367. info.every /= 1000;
  368. }
  369. if (!info.limit) {
  370. info.limit = undefined;
  371. }
  372. const { setForm } = Form.open({
  373. title: `编辑任务`,
  374. width: "600px",
  375. props: {
  376. "label-width": "80px"
  377. },
  378. items: [
  379. {
  380. label: "名称",
  381. prop: "name",
  382. value: info.name,
  383. component: {
  384. name: "el-input",
  385. attrs: {
  386. placeholder: "请输入名称"
  387. }
  388. },
  389. rules: {
  390. required: true,
  391. message: "名称不能为空"
  392. }
  393. },
  394. {
  395. label: "类型",
  396. prop: "taskType",
  397. value: info.taskType || 0,
  398. component: {
  399. name: "el-select",
  400. options: [
  401. {
  402. label: "cron",
  403. value: 0
  404. },
  405. {
  406. label: "时间间隔",
  407. value: 1
  408. }
  409. ],
  410. on: {
  411. change: (v) => {
  412. if (v == 0) {
  413. setForm("limit", undefined);
  414. setForm("every", undefined);
  415. } else {
  416. setForm("cron", undefined);
  417. }
  418. }
  419. }
  420. }
  421. },
  422. {
  423. label: "cron",
  424. prop: "cron",
  425. hidden: ({ scope }) => {
  426. return scope.taskType == 1;
  427. },
  428. value: info.cron,
  429. component: {
  430. name: "el-input",
  431. attrs: {
  432. placeholder: "* * * * * *"
  433. }
  434. },
  435. rules: {
  436. required: true,
  437. message: "cron不能为空"
  438. }
  439. },
  440. {
  441. label: "次数",
  442. prop: "limit",
  443. value: info.limit,
  444. hidden: ({ scope }) => {
  445. return scope.taskType == 0;
  446. },
  447. component: {
  448. name: "el-input-number",
  449. props: {
  450. min: 1,
  451. max: 10000
  452. }
  453. }
  454. },
  455. {
  456. label: "间隔(秒)",
  457. prop: "every",
  458. value: info.every,
  459. hidden: ({ scope }) => {
  460. return scope.taskType == 0;
  461. },
  462. component: {
  463. name: "el-input-number",
  464. props: {
  465. min: 1,
  466. max: 100000000
  467. }
  468. },
  469. rules: {
  470. required: true,
  471. message: "执行间隔不能为空"
  472. }
  473. },
  474. {
  475. label: "service",
  476. prop: "service",
  477. value: info.service,
  478. component: {
  479. name: "el-input",
  480. attrs: {
  481. placeholder: "sys.test.add(params)"
  482. }
  483. }
  484. },
  485. {
  486. label: "开始时间",
  487. prop: "startDate",
  488. value: info.startDate,
  489. component: {
  490. name: "el-date-picker",
  491. props: {
  492. type: "datetime"
  493. }
  494. }
  495. },
  496. {
  497. label: "结束时间",
  498. prop: "endDate",
  499. value: info.endDate,
  500. component: {
  501. name: "el-date-picker",
  502. props: {
  503. type: "datetime"
  504. }
  505. }
  506. },
  507. {
  508. label: "备注",
  509. prop: "remark",
  510. value: info.remark,
  511. component: {
  512. name: "el-input",
  513. props: {
  514. type: "textarea"
  515. }
  516. }
  517. },
  518. {
  519. label: "状态",
  520. prop: "status",
  521. value: info.status === 0 ? 0 : 1,
  522. component: {
  523. name: "el-radio-group",
  524. options: [
  525. {
  526. label: "停止",
  527. value: 0
  528. },
  529. {
  530. label: "运行",
  531. value: 1
  532. }
  533. ]
  534. }
  535. }
  536. ],
  537. on: {
  538. submit: (data, { close, done }) => {
  539. if (!data.limit) {
  540. data.limit = null;
  541. }
  542. this.$service.task.info[id ? "update" : "add"]({
  543. ...info,
  544. ...data,
  545. every: data.every * 1000
  546. })
  547. .then(() => {
  548. this.refreshTask();
  549. this.$message.success("保存成功");
  550. close();
  551. })
  552. .catch((err) => {
  553. this.$message.error(err);
  554. done();
  555. });
  556. }
  557. }
  558. });
  559. },
  560. // 删除任务
  561. delete({ id }) {
  562. this.$confirm("此操作将永久删除该任务,是否继续?", "提示", {
  563. type: "warning"
  564. })
  565. .then(() => {
  566. this.$service.task.info.delete({ ids: [id] }).then(() => {
  567. this.refreshTask();
  568. });
  569. })
  570. .catch(() => {});
  571. },
  572. // 开始任务
  573. start({ id, type }) {
  574. this.$service.task.info
  575. .start({ id, type })
  576. .then(() => {
  577. this.refreshTask();
  578. })
  579. .catch((err) => {
  580. this.$message.error(err);
  581. });
  582. },
  583. // 停止任务
  584. stop({ id }) {
  585. this.$service.task.info
  586. .stop({ id })
  587. .then(() => {
  588. this.refreshTask();
  589. })
  590. .catch((err) => {
  591. this.$message.error(err);
  592. });
  593. },
  594. // 任务执行一次
  595. once({ id }) {
  596. this.$service.task.info
  597. .once({ id })
  598. .then(() => {
  599. this.refreshTask();
  600. })
  601. .catch((err) => {
  602. this.$message.error(err);
  603. });
  604. },
  605. expandLog(e) {
  606. this.$set(e, "_expand", !e._expand);
  607. },
  608. // 刷新任务
  609. refreshTask(params, options) {
  610. const { index, more } = options || {};
  611. const arr =
  612. index === undefined || index === null ? this.list.map((e, i) => i) : [index];
  613. arr.forEach(async (k) => {
  614. let item = this.list[k];
  615. Object.assign(item.params, {
  616. ...item.pagination,
  617. ...params
  618. });
  619. this.$set(item, "loading", true);
  620. let res = await this.$service.task.info.page(item.params);
  621. this.moreList(res, item);
  622. if (!more) {
  623. this.$el.querySelector(`.block._${item.key} .container`).scroll(0, 0);
  624. }
  625. item.loading = false;
  626. });
  627. },
  628. // 更多任务
  629. moreTask(index) {
  630. this.refreshTask(null, { index, more: true });
  631. },
  632. // 刷新日志
  633. async refreshLog(newParams, options) {
  634. if (this.logs.loading) {
  635. return false;
  636. }
  637. if (!checkPerm(this.perm.log)) {
  638. return false;
  639. }
  640. const { params, pagination } = this.logs;
  641. const { more } = options || {};
  642. Object.assign(params, {
  643. ...pagination,
  644. ...newParams
  645. });
  646. this.logs.loading = true;
  647. let res = await this.$service.task.info.log(params);
  648. this.moreList(res, this.logs);
  649. if (!more) {
  650. this.$el.querySelector(".block._log .container ul").scroll(0, 0);
  651. }
  652. this.logs.loading = false;
  653. },
  654. // 更多日志
  655. moreLog() {
  656. this.refreshLog(null, { more: true });
  657. },
  658. // 查看任务对应的日志
  659. findLog(e) {
  660. this.logs.current = e;
  661. this.refreshLog({ page: 1, id: e.id });
  662. },
  663. // 所有日志
  664. allLog() {
  665. this.logs.current = null;
  666. this.refreshLog({ page: 1, id: null });
  667. },
  668. // 过滤日志
  669. filterLog([v]) {
  670. this.refreshLog({ page: 1, status: v === undefined ? 1 : 0 });
  671. },
  672. moreList(res, { list, pagination }) {
  673. const { page, size } = res.pagination;
  674. const len = res.list.length;
  675. const max = list.length;
  676. if (page == 1) {
  677. list.splice(0, max, ...res.list);
  678. } else {
  679. let start = max - (max % size);
  680. let end = start + len;
  681. list.splice(start, end, ...res.list);
  682. }
  683. if (len == size) {
  684. res.pagination.page += 1;
  685. }
  686. Object.assign(pagination, res.pagination);
  687. return page != 1;
  688. }
  689. }
  690. };
  691. </script>
  692. <style lang="scss" scoped>
  693. .Ghost {
  694. opacity: 0.7;
  695. }
  696. .Drag {
  697. border: 1px dashed #000 !important;
  698. background: #fff !important;
  699. }
  700. .system-task {
  701. .box {
  702. height: 100%;
  703. overflow-x: auto;
  704. .el-col {
  705. height: 100%;
  706. width: 413px;
  707. }
  708. }
  709. .block {
  710. height: 100%;
  711. width: 400px;
  712. .header {
  713. display: flex;
  714. align-items: center;
  715. height: 40px;
  716. background-color: #f0f0f0;
  717. border-top-left-radius: 10px;
  718. border-top-right-radius: 10px;
  719. position: relative;
  720. top: 5px;
  721. z-index: 1;
  722. padding: 0 10px 5px 10px;
  723. i {
  724. font-size: 18px;
  725. }
  726. .label {
  727. font-size: 12px;
  728. margin: 0 5px;
  729. letter-spacing: 0.5px;
  730. }
  731. .num {
  732. font-size: 12px;
  733. }
  734. .flex1 {
  735. flex: 1;
  736. }
  737. .op-btn {
  738. display: flex;
  739. li {
  740. display: flex;
  741. align-items: center;
  742. list-style: none;
  743. cursor: pointer;
  744. padding: 2px 10px;
  745. background-color: #fff;
  746. border-radius: 3px;
  747. margin-left: 5px;
  748. &:hover {
  749. background-color: #dedede;
  750. color: #444;
  751. }
  752. i {
  753. font-size: 13px;
  754. margin-right: 2px;
  755. }
  756. span {
  757. font-size: 12px;
  758. }
  759. }
  760. }
  761. }
  762. .container {
  763. max-height: calc(100% - 90px);
  764. overflow-y: auto;
  765. margin-bottom: 5px;
  766. z-index: 2;
  767. position: relative;
  768. background-color: #f7f7f7;
  769. ul {
  770. li {
  771. list-style: none;
  772. background-color: #fff;
  773. border-radius: 5px;
  774. margin-bottom: 5px;
  775. padding: 10px 15px;
  776. font-size: 14px;
  777. letter-spacing: 0.5px;
  778. border: 1px solid #f7f7f7;
  779. &:last-child {
  780. margin-bottom: 0;
  781. }
  782. &._drag {
  783. cursor: pointer;
  784. }
  785. &:hover {
  786. .op {
  787. height: 30px;
  788. }
  789. }
  790. .h {
  791. display: flex;
  792. align-items: center;
  793. font-size: 14px;
  794. margin-bottom: 10px;
  795. .type {
  796. font-size: 12px;
  797. border-radius: 3px;
  798. padding: 1px 2px;
  799. margin-right: 5px;
  800. &._warning {
  801. background-color: $color-warning;
  802. color: #fff;
  803. }
  804. }
  805. }
  806. .remark {
  807. font-size: 12px;
  808. color: #666;
  809. margin-bottom: 20px;
  810. }
  811. .empty {
  812. text-align: center;
  813. font-size: 13px;
  814. color: #666;
  815. margin: 10px 0;
  816. }
  817. .f {
  818. display: flex;
  819. align-items: center;
  820. justify-content: space-between;
  821. position: relative;
  822. .date {
  823. font-size: 12px;
  824. color: #fff;
  825. background-color: $color-primary;
  826. border-radius: 2px;
  827. margin-left: 40px;
  828. padding: 1px 3px;
  829. &::before {
  830. content: "NEXT";
  831. position: absolute;
  832. left: 0;
  833. top: 1px;
  834. color: #222;
  835. }
  836. }
  837. .start,
  838. .stop {
  839. display: flex;
  840. align-items: center;
  841. font-size: 12px;
  842. margin-left: 30px;
  843. position: relative;
  844. &::before {
  845. content: "";
  846. display: block;
  847. height: 6px;
  848. width: 6px;
  849. border-radius: 6px;
  850. position: absolute;
  851. left: -15px;
  852. }
  853. }
  854. .start {
  855. color: #67c23a;
  856. &::before {
  857. background-color: #67c23a;
  858. }
  859. }
  860. .stop {
  861. color: #f56c6c;
  862. &::before {
  863. background-color: #f56c6c;
  864. }
  865. }
  866. }
  867. .op {
  868. height: 0;
  869. margin-top: 15px;
  870. transition: height 0.3s;
  871. overflow: hidden;
  872. .el-col {
  873. height: 30px;
  874. display: flex;
  875. justify-content: center;
  876. align-items: center;
  877. &:hover {
  878. background-color: #f7f7f7;
  879. }
  880. span {
  881. font-size: 12px;
  882. color: #666;
  883. }
  884. i {
  885. font-size: 16px;
  886. margin-right: 5px;
  887. }
  888. }
  889. }
  890. &._error {
  891. background-color: $color-danger;
  892. color: #fff;
  893. .remark {
  894. color: #fff !important;
  895. }
  896. }
  897. }
  898. }
  899. .more {
  900. display: block;
  901. margin: 10px auto;
  902. }
  903. }
  904. .footer {
  905. height: 36px;
  906. .btn-add {
  907. height: 34px;
  908. width: 100%;
  909. border-radius: 3px;
  910. border: 0;
  911. background-color: #fff;
  912. cursor: pointer;
  913. i {
  914. font-size: 16px;
  915. color: #999;
  916. }
  917. &:hover {
  918. box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
  919. }
  920. }
  921. }
  922. }
  923. .block._stop {
  924. .header {
  925. .add-btn {
  926. display: none;
  927. }
  928. }
  929. .container {
  930. max-height: calc(100% - 50px);
  931. }
  932. .footer {
  933. display: none;
  934. }
  935. }
  936. .block._log {
  937. .header {
  938. .status {
  939. .el-checkbox {
  940. margin-right: 10px;
  941. }
  942. }
  943. .op-btn {
  944. li {
  945. display: flex;
  946. align-items: center;
  947. height: 20px;
  948. &._current-log {
  949. span {
  950. display: block;
  951. max-width: 100px;
  952. overflow: hidden;
  953. text-overflow: ellipsis;
  954. white-space: nowrap;
  955. }
  956. i {
  957. margin-left: 2px;
  958. }
  959. &:hover {
  960. i {
  961. color: $color-danger;
  962. }
  963. }
  964. }
  965. }
  966. }
  967. }
  968. .container {
  969. height: calc(100% - 50px);
  970. max-height: calc(100% - 50px);
  971. ul {
  972. height: 100%;
  973. li {
  974. .remark {
  975. color: #999;
  976. &._ellipsis {
  977. overflow: hidden;
  978. text-overflow: ellipsis;
  979. display: -webkit-box;
  980. -webkit-box-orient: vertical;
  981. -webkit-line-clamp: 2;
  982. }
  983. }
  984. .f {
  985. font-size: 12px;
  986. }
  987. &:hover {
  988. .remark {
  989. color: #444;
  990. }
  991. }
  992. }
  993. }
  994. }
  995. }
  996. }
  997. </style>