index.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647
  1. <template>
  2. <wrapper
  3. :vertical="false"
  4. fill
  5. margin
  6. padding
  7. background
  8. >
  9. <device-tree-single
  10. class="c-sibling-item c-sidebar auto-width u-font-size--sm"
  11. @change="onChange"
  12. />
  13. <schema-table
  14. v-if="deviceId"
  15. ref="table"
  16. class="c-sibling-item far"
  17. row-key="id"
  18. :schema="schema"
  19. />
  20. <table-dialog
  21. ref="contentDialog"
  22. size="medium"
  23. title="上播内容"
  24. :schema="contentSchema"
  25. />
  26. <asset-task-dialog
  27. ref="taskDialog"
  28. @view="onViewAsset"
  29. @confirm="onSubmitAssetTask"
  30. />
  31. <radio-table-dialog
  32. ref="contractTableDialog"
  33. title="新增合同任务"
  34. message="请选择合同"
  35. :schema="contractTableSchema"
  36. @confirm="onSubmitContractTask"
  37. />
  38. <preview-dialog ref="previewDialog" />
  39. <confirm-dialog
  40. ref="timeDialog"
  41. title="上播时间设置"
  42. @confirm="onConfirmTime"
  43. >
  44. <div class="c-grid-form auto u-align-self--center">
  45. <div class="c-grid-form__label">上播方式</div>
  46. <schema-select
  47. v-model="taskTime.type"
  48. class="u-width"
  49. :schema="taskTimeTypeSelectSchema"
  50. />
  51. <template v-if="taskTime.type === 2">
  52. <div class="c-grid-form__label">时段</div>
  53. <el-time-picker
  54. key="task-range-picker"
  55. v-model="taskTime.range"
  56. class="u-width"
  57. is-range
  58. format="HH:mm"
  59. value-format="HH:mm"
  60. :clearable="false"
  61. />
  62. </template>
  63. <template v-if="taskTime.type === 3">
  64. <div class="c-grid-form__label">时间点</div>
  65. <el-time-picker
  66. key="task-time-picker"
  67. v-model="taskTime.point"
  68. class="u-width"
  69. value-format="HH:mm:ss"
  70. :clearable="false"
  71. />
  72. </template>
  73. </div>
  74. </confirm-dialog>
  75. </wrapper>
  76. </template>
  77. <script>
  78. import {
  79. TimeType,
  80. SCREEN_TIME_KEY,
  81. TaskFromType,
  82. TaskFromTypeInfo,
  83. AssetTag,
  84. AssetTagInfo,
  85. AssetType,
  86. AssetTypeInfo
  87. } from '@/constant'
  88. import {
  89. parseDuration,
  90. parseTaskTime,
  91. getTaskTimeInfo,
  92. transformToTaskTime,
  93. offsetDate,
  94. calculateDay,
  95. getAssetThumb
  96. } from '@/utils'
  97. import {
  98. getContract,
  99. getContracts
  100. } from '@/api/asset'
  101. import {
  102. getTasks,
  103. addTask,
  104. deleteTask,
  105. updateTask,
  106. getOrderDetail
  107. } from '../api'
  108. import AssetTaskDialog from './components/AssetTaskDialog.vue'
  109. export default {
  110. name: 'AdOrderTask',
  111. components: {
  112. AssetTaskDialog
  113. },
  114. data () {
  115. const canEdit = this.$store.getters.isOperator
  116. const canAudit = this.$store.getters.isGroupAdmin
  117. return {
  118. deviceId: '',
  119. schema: {
  120. buttons: canEdit
  121. ? [
  122. { type: 'add', label: '素材任务', on: this.onAddAssetTask },
  123. { type: 'add', label: '合同任务', on: this.onAddContractTask }
  124. ]
  125. : null,
  126. list: this.getTasks,
  127. transform: this.transform,
  128. cols: [
  129. { prop: 'type', type: 'refresh', width: 80 },
  130. { prop: 'name' },
  131. { label: '上刊日期', 'min-width': 220, render: (data, h) => canEdit && data.allowed && data.from === TaskFromType.ASSET
  132. ? h('el-date-picker', {
  133. staticClass: 'o-date-picker',
  134. props: {
  135. value: [data.startDate, data.endDate],
  136. type: 'daterange',
  137. rangeSeparator: '至',
  138. valueFormat: 'yyyy-MM-dd',
  139. editable: false,
  140. clearable: false
  141. },
  142. on: {
  143. input: val => this.onDateEdit(data, val)
  144. }
  145. })
  146. : `${data.startDate} 至 ${data.endDate}`, align: 'center' },
  147. { label: '上播时间', render: (data, h) => canEdit && data.allowed && data.from === TaskFromType.ASSET
  148. ? h('div', {
  149. staticClass: 'o-date-picker jc',
  150. on: {
  151. click: () => this.onEditTime(data)
  152. }
  153. }, data.range)
  154. : data.range, 'min-width': 120, align: 'center' },
  155. { label: '上播时长(s)', render: (data, h) => canEdit && this.canEditDuration(data)
  156. ? h('edit-input', {
  157. staticClass: 'border',
  158. props: {
  159. value: `${data.duration}`,
  160. align: 'center'
  161. },
  162. on: {
  163. edit: val => this.onSimpleEdit(data, 'duration', val)
  164. }
  165. })
  166. : data.duration, 'min-width': 100, align: 'center' },
  167. { label: '上播次数', render: (data, h) => canEdit && data.allowed && data.from === TaskFromType.ASSET
  168. ? h('edit-input', {
  169. staticClass: 'border',
  170. props: {
  171. value: `${data.count}`,
  172. align: 'center'
  173. },
  174. on: {
  175. edit: val => this.onSimpleEdit(data, 'count', val)
  176. }
  177. })
  178. : data.count, align: 'center' },
  179. { label: '审核次数', render: (data, h) => canAudit && data.allowed
  180. ? h('edit-input', {
  181. staticClass: 'border',
  182. props: {
  183. value: `${data.auditCount}`,
  184. align: 'center'
  185. },
  186. on: {
  187. edit: val => this.onAuditEdit(data, val)
  188. }
  189. })
  190. : '-', align: 'center' },
  191. { prop: 'tag', type: 'tag', on: canAudit && this.onAudit },
  192. { type: 'invoke', render: [
  193. { label: '查看', allow: ({ allowed }) => allowed, on: this.onViewTask },
  194. { label: '删除', allow: ({ from }) => from !== TaskFromType.ORDER, on: this.onDel }
  195. ], width: 92 }
  196. ]
  197. },
  198. contentSchema: {
  199. list: this.getContentAssets,
  200. cols: [
  201. { prop: 'tagType', label: '文件', width: 100, align: 'center' },
  202. { prop: 'fileType', label: '', width: 80 },
  203. { prop: 'file', label: '', type: 'asset', on: this.onViewAsset },
  204. { prop: 'adDuration', label: '上播时长', align: 'center' },
  205. { type: 'invoke', render: [
  206. { label: '查看', allow: ({ file }) => !!file, on: this.onViewAsset }
  207. ] }
  208. ]
  209. },
  210. contractTableSchema: {
  211. condition: { name: '' },
  212. list: getContracts,
  213. transform: this.transformContract,
  214. filters: [
  215. { key: 'name', type: 'search', placeholder: '合同名称' }
  216. ],
  217. cols: [
  218. { prop: 'name', label: '名称' },
  219. { prop: 'dateRange', label: '上刊日期', 'width': 200 },
  220. { prop: 'timeRange', label: '上播时间' },
  221. { prop: 'count', label: '上播次数' },
  222. { prop: 'durationInfo', label: '上播时长' }
  223. ]
  224. },
  225. taskTimeTypeSelectSchema: {
  226. options: [
  227. { value: TimeType.SCREEN, label: '开机期间' },
  228. { value: TimeType.RANGE, label: '时段' },
  229. { value: TimeType.POINT, label: '时间点' }
  230. ]
  231. },
  232. taskTime: {}
  233. }
  234. },
  235. watch: {
  236. deviceId () {
  237. this.$refs.table?.pageTo(1)
  238. }
  239. },
  240. methods: {
  241. onChange ({ id }) {
  242. this.deviceId = id
  243. },
  244. getTasks (params) {
  245. return getTasks({
  246. deviceId: this.deviceId,
  247. ...params
  248. })
  249. },
  250. canEditDuration ({ allowed, from, fromInfo }) {
  251. return allowed && from === TaskFromType.ASSET && fromInfo?.type === AssetType.IMAGE
  252. },
  253. transform (task) {
  254. const { id, from, fromId, fromInfo, startDate, startTime, endTime, day, duration, count, auditCount, enable } = task
  255. const allowed = this.isValidTask(from, fromInfo)
  256. return {
  257. id,
  258. from,
  259. fromId,
  260. fromInfo,
  261. type: this.getDataType(task),
  262. name: this.getDataName(task),
  263. startDate,
  264. endDate: offsetDate(startDate, day),
  265. startTime,
  266. endTime,
  267. range: getTaskTimeInfo({ startTime, endTime }),
  268. count,
  269. duration,
  270. auditCount,
  271. enable,
  272. tag: allowed
  273. ? this.getEnableTag(enable)
  274. : {
  275. type: 'danger',
  276. label: '异常',
  277. msg: '源数据丢失'
  278. },
  279. allowed
  280. }
  281. },
  282. isValidTask (from, fromInfo) {
  283. switch (from) {
  284. case TaskFromType.ORDER:
  285. return true
  286. default:
  287. return !!fromInfo
  288. }
  289. },
  290. getDataType ({ from, fromInfo }) {
  291. if (from === TaskFromType.ASSET && fromInfo) {
  292. return AssetTypeInfo[fromInfo.type]
  293. }
  294. return TaskFromTypeInfo[from]
  295. },
  296. getDataName ({ from, fromInfo }) {
  297. switch (from) {
  298. case TaskFromType.ORDER:
  299. return '自助广告'
  300. default:
  301. return fromInfo?.name || '-'
  302. }
  303. },
  304. getEnableTag (enable) {
  305. return {
  306. type: enable ? 'success' : 'primary',
  307. label: enable ? '已审核' : '待审核'
  308. }
  309. },
  310. transformContract (contract) {
  311. contract.durationInfo = parseDuration(contract.duration)
  312. contract.dateRange = `${contract.startDate} 至 ${offsetDate(contract.startDate, contract.day)}`
  313. contract.timeRange = getTaskTimeInfo(contract)
  314. return contract
  315. },
  316. onViewTask (task) {
  317. switch (task.from) {
  318. case TaskFromType.ORDER:
  319. getOrderDetail(task.fromId).then(({ data }) => {
  320. this.showContentDialog(data.assets.map(this.transformOrderAsset))
  321. })
  322. break
  323. case TaskFromType.ASSET:
  324. this.onViewAsset({
  325. file: {
  326. type: task.fromInfo.type,
  327. url: task.fromId
  328. }
  329. })
  330. break
  331. case TaskFromType.CONTRACT:
  332. getContract(task.fromId).then(({ data }) => {
  333. this.showContentDialog(data.assets.map(this.transformContractAsset))
  334. })
  335. break
  336. default:
  337. break
  338. }
  339. },
  340. showContentDialog (assets) {
  341. if (assets.length === 1) {
  342. assets[0].adDuration = '独占'
  343. }
  344. this.$assets = assets
  345. this.$refs.contentDialog.show()
  346. },
  347. getContentAssets () {
  348. const assets = this.$assets
  349. return Promise.resolve({
  350. data: assets,
  351. totalCount: assets.length
  352. })
  353. },
  354. transformOrderAsset ({ keyName, type, adDuration, thumb }) {
  355. const isImage = type === AssetType.IMAGE
  356. return type
  357. ? {
  358. tagType: AssetTagInfo[AssetTag.AD],
  359. fileType: AssetTypeInfo[type],
  360. file: {
  361. type,
  362. url: keyName,
  363. thumb: isImage ? keyName : thumb,
  364. origin: !isImage
  365. },
  366. adDuration: parseDuration(adDuration)
  367. }
  368. : {
  369. tagType: '素材已删除',
  370. adDuration: parseDuration(adDuration)
  371. }
  372. },
  373. transformContractAsset ({ duration, minioData }) {
  374. if (minioData) {
  375. const { tag, type, keyName } = minioData
  376. return {
  377. tagType: AssetTagInfo[tag],
  378. fileType: AssetTypeInfo[type],
  379. file: {
  380. type,
  381. url: keyName,
  382. thumb: getAssetThumb(minioData)
  383. },
  384. adDuration: parseDuration(duration)
  385. }
  386. }
  387. return {
  388. tagType: '素材已删除',
  389. adDuration: parseDuration(duration)
  390. }
  391. },
  392. onViewAsset ({ file }) {
  393. this.$refs.previewDialog.show(file)
  394. },
  395. onDel ({ id, type, name }) {
  396. deleteTask(id, `${type} ${name}`).then(() => {
  397. this.$refs.table.decrease(1)
  398. })
  399. },
  400. onAddAssetTask () {
  401. this.$refs.taskDialog.show()
  402. },
  403. onSubmitAssetTask ({ value, done }) {
  404. this.addTask(value).then(done)
  405. },
  406. onAddContractTask () {
  407. this.$refs.contractTableDialog.show()
  408. },
  409. onSubmitContractTask ({ value, done }) {
  410. this.addTask({
  411. from: TaskFromType.CONTRACT,
  412. fromId: value.id,
  413. startDate: value.startDate,
  414. day: value.day,
  415. startTime: value.startTime,
  416. endTime: value.endTime,
  417. count: value.count,
  418. duration: value.duration
  419. }).then(done)
  420. },
  421. addTask (task) {
  422. return addTask({
  423. ...task,
  424. auditCount: task.count,
  425. deviceIdList: [this.deviceId],
  426. enable: false
  427. }).then(() => {
  428. this.$refs.table.pageTo(1)
  429. })
  430. },
  431. onAudit (task) {
  432. const { allowed, enable, startTime, endTime, duration, auditCount } = task
  433. if (!allowed) {
  434. return
  435. }
  436. if (!enable) {
  437. const totalDuration = duration * auditCount * 1000
  438. if (startTime && endTime && endTime !== SCREEN_TIME_KEY) {
  439. const remind = new Date(`2000-01-01 ${endTime}`) - new Date(`2000-01-01 ${startTime}`)
  440. if (remind < totalDuration) {
  441. this.$message({
  442. type: 'warning',
  443. message: '所选时段无法满足上播次数'
  444. })
  445. return
  446. }
  447. }
  448. }
  449. this.$confirm(
  450. enable ? '停止使用后将不可参与自动编单' : '启用后将可参与自动编单',
  451. enable ? '停用任务' : '启用任务',
  452. { type: 'warning' }
  453. ).then(() => {
  454. this.onUpdateEnable(task)
  455. })
  456. },
  457. onUpdateEnable (task) {
  458. const { id, enable } = task
  459. updateTask({
  460. id,
  461. enable: !enable
  462. }).then(() => {
  463. task.enable = !enable
  464. task.tag = this.getEnableTag(!enable)
  465. })
  466. },
  467. onSimpleEdit (task, key, { newVal, oldVal }) {
  468. if (!newVal || !/^\d+$/.test(newVal) || Number(newVal) < 1) {
  469. return
  470. }
  471. if (newVal !== oldVal) {
  472. task[key] = Number(newVal)
  473. updateTask({
  474. id: task.id,
  475. enable: false,
  476. [key]: newVal
  477. }).then(
  478. () => {
  479. if (task.enable) {
  480. task.enable = false
  481. task.tag = this.getEnableTag(false)
  482. }
  483. },
  484. () => {
  485. task[key] = oldVal
  486. }
  487. )
  488. }
  489. },
  490. onDateEdit (task, val) {
  491. const { id, enable, startDate, endDate } = task
  492. if (startDate !== val[0] || endDate !== val[1]) {
  493. const day = calculateDay(val[0], val[1]) + 1
  494. updateTask({
  495. id,
  496. enable: false,
  497. startDate: val[0],
  498. day
  499. }).then(
  500. () => {
  501. if (enable) {
  502. task.enable = false
  503. task.tag = this.getEnableTag(false)
  504. }
  505. task.startDate = val[0]
  506. task.endDate = val[1]
  507. task.day = day
  508. }
  509. )
  510. }
  511. },
  512. onAuditEdit (task, { newVal, oldVal }) {
  513. if (!newVal || !/^\d+$/.test(newVal) || Number(newVal) < 1) {
  514. return
  515. }
  516. if (newVal !== oldVal) {
  517. const auditCount = Number(newVal)
  518. if (task.from === TaskFromType.ORDER && auditCount < task.count) {
  519. this.$message({
  520. type: 'warning',
  521. message: '订单的审核次数不能小于上播次数'
  522. })
  523. return
  524. }
  525. if (task.from === TaskFromType.CONTRACT && auditCount < task.count) {
  526. this.$message({
  527. type: 'warning',
  528. message: '合同的审核次数不能小于上播次数'
  529. })
  530. return
  531. }
  532. task.auditCount = auditCount
  533. updateTask({
  534. id: task.id,
  535. enable: false,
  536. auditCount
  537. }).then(
  538. () => {
  539. if (task.enable) {
  540. task.enable = false
  541. task.tag = this.getEnableTag(false)
  542. }
  543. },
  544. () => {
  545. task.auditCount = oldVal
  546. }
  547. )
  548. }
  549. },
  550. onEditTime (task) {
  551. this.$task = task
  552. this.taskTime = parseTaskTime(task)
  553. this.$refs.timeDialog.show()
  554. },
  555. onConfirmTime (done) {
  556. const { id, enable, startTime, endTime } = this.$task
  557. const { startTime: targetStartTime, endTime: targetEndTime } = transformToTaskTime(this.taskTime)
  558. if (!targetStartTime) {
  559. this.$message({
  560. type: 'warning',
  561. message: '请选择上播时间'
  562. })
  563. return
  564. }
  565. if (startTime !== targetStartTime || endTime !== targetEndTime) {
  566. updateTask({
  567. id,
  568. enable: false,
  569. startTime: targetStartTime,
  570. endTime: targetEndTime
  571. }).then(
  572. () => {
  573. if (enable) {
  574. this.$task.enable = false
  575. this.$task.tag = this.getEnableTag(false)
  576. }
  577. this.$task.startTime = targetStartTime
  578. this.$task.endTime = targetEndTime
  579. this.$task.range = getTaskTimeInfo({ startTime: targetStartTime, endTime: targetEndTime })
  580. done()
  581. }
  582. )
  583. } else {
  584. done()
  585. }
  586. }
  587. }
  588. }
  589. </script>
  590. <style lang="scss" scoped>
  591. .c-task {
  592. line-height: 1;
  593. border-radius: $radius--sm;
  594. border: 1px solid #dcdfe6;
  595. &:hover {
  596. border-color: #c0c4cc;
  597. }
  598. &__name {
  599. position: absolute;
  600. width: 100%;
  601. height: 100%;
  602. }
  603. }
  604. ::v-deep .o-date-picker {
  605. display: flex;
  606. justify-content: space-between;
  607. align-items: center;
  608. width: auto;
  609. min-height: $height;
  610. padding: 3px 0;
  611. border: 1px solid $primary;
  612. border-radius: $radius--sm;
  613. background-color: $blue--light;
  614. &.jc {
  615. justify-content: center;
  616. }
  617. &:hover {
  618. border-color: $gray--dark;
  619. }
  620. .el-range-input {
  621. flex: 1 1 auto;
  622. width: auto;
  623. min-width: 0;
  624. background-color: $blue--light;
  625. }
  626. .el-range-separator {
  627. flex: 0 0 auto;
  628. width: auto;
  629. min-width: 0;
  630. }
  631. .el-input__icon {
  632. display: none;
  633. }
  634. }
  635. </style>