index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. <template>
  2. <wrapper
  3. fill
  4. margin
  5. padding
  6. background
  7. >
  8. <schema-table
  9. ref="table"
  10. :schema="schema"
  11. />
  12. <confirm-dialog
  13. ref="addDialog"
  14. title="新增合同"
  15. @confirm="onSave"
  16. >
  17. <div class="c-grid-form u-align-self--center">
  18. <span class="c-grid-form__label required">名称</span>
  19. <el-input
  20. v-model.trim="defaultName"
  21. placeholder="最多50个字符"
  22. maxlength="50"
  23. clearable
  24. />
  25. </div>
  26. </confirm-dialog>
  27. <confirm-dialog
  28. ref="editDialog"
  29. :title="contractName"
  30. size="large fixed"
  31. @confirm="onSaveEdit"
  32. >
  33. <div class="l-flex__fill l-flex">
  34. <div class="l-flex__none l-flex--col c-sibling-item">
  35. <div class="c-contract-dialog__header u-bold">基本数据(影响初次导入)</div>
  36. <div class="l-flex__fill u-overflow-y--auto">
  37. <div
  38. v-if="contract"
  39. class="c-grid-form mini"
  40. >
  41. <div class="c-grid-form__label">上刊日期</div>
  42. <div class="l-flex--row c-event__option">
  43. <el-date-picker
  44. v-model="contract.date"
  45. type="daterange"
  46. range-separator="至"
  47. value-format="yyyy-MM-dd"
  48. :editable="false"
  49. :clearable="false"
  50. />
  51. </div>
  52. <div class="c-grid-form__label">上播方式</div>
  53. <schema-select
  54. v-model="contract.time.type"
  55. :schema="timeTypeSelectSchema"
  56. />
  57. <template v-if="contract.time.type === 2">
  58. <div class="c-grid-form__label">时段</div>
  59. <el-time-picker
  60. key="range-picker"
  61. v-model="contract.time.range"
  62. is-range
  63. format="HH:mm"
  64. value-format="HH:mm"
  65. :clearable="false"
  66. />
  67. </template>
  68. <template v-if="contract.time.type === 3">
  69. <div class="c-grid-form__label">时间点</div>
  70. <el-time-picker
  71. key="time-picker"
  72. v-model="contract.time.point"
  73. value-format="HH:mm:ss"
  74. :clearable="false"
  75. />
  76. </template>
  77. <div class="c-grid-form__label">上播时长(s)</div>
  78. <el-input-number
  79. v-model="contract.duration"
  80. class="c-grid-form__info"
  81. data-info="范围:1~86400"
  82. :min="1"
  83. :max="86400"
  84. step-strictly
  85. />
  86. <div class="c-grid-form__label">上播次数</div>
  87. <el-input-number
  88. v-model="contract.count"
  89. :min="1"
  90. step-strictly
  91. />
  92. <div class="c-grid-form__label">关联的文件</div>
  93. <div class="c-grid-form__option">
  94. <el-upload
  95. ref="upload"
  96. class="c-contract-dialog__upload"
  97. action="none"
  98. :auto-upload="false"
  99. :show-file-list="false"
  100. :on-change="onChange"
  101. >
  102. <i class="el-icon-plus" />
  103. </el-upload>
  104. <div>
  105. <div
  106. v-for="(file, index) in files"
  107. :key="file.id"
  108. class="l-flex--row c-contract-dialog__file"
  109. >
  110. <i
  111. class="l-flex__none c-contract-dialog__del mini el-icon-minus has-active u-pointer"
  112. @click="onDelFile(file, index)"
  113. />
  114. <div
  115. class="l-flex__fill has-active u-ellipsis u-pointer"
  116. @click="onDownload(file)"
  117. >{{ file.name }}</div>
  118. </div>
  119. </div>
  120. </div>
  121. </div>
  122. </div>
  123. </div>
  124. <div class="l-flex__fill l-flex--col c-sibling-item far">
  125. <div class="l-flex--row c-contract-dialog__header">
  126. <span class="c-sibling-item u-bold">上播内容</span>
  127. <i
  128. class="c-sibling-item el-icon-circle-plus-outline u-color--blue u-pointer"
  129. @click="onAddAsset"
  130. />
  131. </div>
  132. <draggable
  133. v-model="assets"
  134. class="l-flex__fill u-overflow-y--auto"
  135. handle=".mover"
  136. animation="300"
  137. >
  138. <div
  139. v-for="(asset, index) in assets"
  140. :key="asset.key"
  141. class="l-flex--row c-sibling-item--v c-contract-dialog__item"
  142. >
  143. <div class="l-flex__auto l-flex--row mover">
  144. <i class="l-flex__none c-contract-dialog__mover el-icon-sort has-active" />
  145. <div
  146. class="l-flex__auto c-contract-dialog__name u-ellipsis has-active u-pointer"
  147. @click="onViewAssetItem(asset)"
  148. >
  149. {{ asset.tagInfo }} {{ asset.name }}
  150. </div>
  151. </div>
  152. <template v-if="needDuration">
  153. <span class="l-flex__none u-color--info">上播时长(s):</span>
  154. <el-input-number
  155. v-model="asset.duration"
  156. class="l-flex__none c-contract-dialog__seconds"
  157. controls-position="right"
  158. :min="1"
  159. :max="86400"
  160. :step="1"
  161. :disabled="asset.disabled"
  162. step-strictly
  163. />
  164. </template>
  165. <i
  166. class="l-flex__none c-contract-dialog__del el-icon-delete has-active u-pointer"
  167. @click="onDelAsset(asset, index)"
  168. />
  169. </div>
  170. </draggable>
  171. </div>
  172. </div>
  173. </confirm-dialog>
  174. <table-dialog
  175. ref="assetTableDialog"
  176. title="上播内容选择"
  177. :schema="assetTableSchema"
  178. @choosen="onChoosenAsset"
  179. />
  180. <preview-dialog ref="previewDialog" />
  181. </wrapper>
  182. </template>
  183. <script>
  184. import {
  185. AssetTagInfo,
  186. AssetType,
  187. TimeType,
  188. SCREEN_TIME_KEY
  189. } from '@/constant'
  190. import {
  191. parseTime,
  192. parseDuration,
  193. parseTaskTime,
  194. getTaskTimeInfo,
  195. transformToTaskTime,
  196. offsetDate,
  197. calculateDay,
  198. getAssetDuration
  199. } from '@/utils'
  200. import {
  201. getAssetUrl,
  202. getContracts,
  203. addContract,
  204. updateContract,
  205. getContract,
  206. deleteContract,
  207. bindFileToContract,
  208. deleteFileFromContract
  209. } from '@/api/asset'
  210. import { assetTableMixin } from '@/mixins/asset-table'
  211. import Draggable from 'vuedraggable'
  212. export default {
  213. name: 'AdContract',
  214. components: {
  215. Draggable
  216. },
  217. mixins: [assetTableMixin],
  218. data () {
  219. return {
  220. schema: {
  221. buttons: [
  222. { type: 'add', on: this.onAdd }
  223. ],
  224. list: getContracts,
  225. transform: this.transform,
  226. cols: [
  227. { prop: 'name', label: '名称', render: (data, h) => h('edit-input', {
  228. props: {
  229. value: `${data.name}`,
  230. maxlength: 50
  231. },
  232. on: { edit: val => this.onEditName(data, val) }
  233. }), 'min-width': 160 },
  234. { prop: 'dateRange', label: '上刊日期', 'width': 240 },
  235. { prop: 'timeRange', label: '上播时间', 'min-width': 100 },
  236. { prop: 'count', label: '上播次数', 'min-width': 100 },
  237. { prop: 'durationInfo', label: '上播时长', 'min-width': 100 },
  238. { type: 'invoke', render: [
  239. { label: '详情', on: this.onEdit },
  240. { label: '删除', on: this.onDel }
  241. ] }
  242. ]
  243. },
  244. defaultName: '',
  245. contract: null,
  246. timeTypeSelectSchema: {
  247. options: [
  248. { value: TimeType.SCREEN, label: '开机期间' },
  249. { value: TimeType.RANGE, label: '时段' },
  250. { value: TimeType.POINT, label: '时间点' }
  251. ]
  252. },
  253. files: [],
  254. assets: []
  255. }
  256. },
  257. computed: {
  258. contractName () {
  259. return this.contract?.name
  260. },
  261. needDuration () {
  262. return this.assets.length > 1
  263. }
  264. },
  265. methods: {
  266. onAdd () {
  267. this.defaultName = ''
  268. this.$refs.addDialog.show()
  269. },
  270. onSave (done) {
  271. if (!this.defaultName) {
  272. this.$message({
  273. type: 'warning',
  274. message: '请填写合同名称'
  275. })
  276. return
  277. }
  278. addContract({
  279. name: this.defaultName,
  280. startDate: parseTime(new Date(), '{y}-{m}-{d}'),
  281. day: 1,
  282. startTime: SCREEN_TIME_KEY,
  283. endTime: SCREEN_TIME_KEY,
  284. duration: 5,
  285. count: 100
  286. }).then(() => {
  287. done()
  288. this.$refs.table.pageTo()
  289. })
  290. },
  291. transform (contract) {
  292. contract.durationInfo = parseDuration(contract.duration)
  293. contract.dateRange = `${contract.startDate} 至 ${offsetDate(contract.startDate, contract.day)}`
  294. contract.timeRange = getTaskTimeInfo(contract)
  295. return contract
  296. },
  297. onEditName (contract, { newVal, oldVal }) {
  298. if (newVal === oldVal) {
  299. return
  300. }
  301. if (!newVal) {
  302. this.$message({
  303. type: 'warning',
  304. message: '请填写合同名称'
  305. })
  306. return
  307. }
  308. contract.name = newVal
  309. updateContract(contract.id, {
  310. attrs: {
  311. name: newVal
  312. }
  313. }).catch(() => {
  314. contract.name = oldVal
  315. })
  316. },
  317. onEdit (contract) {
  318. getContract(contract.id).then(({ data: { files, assets, ...data } }) => {
  319. this.$contract = contract
  320. this.contract = {
  321. ...data,
  322. date: [data.startDate, offsetDate(data.startDate, data.day)],
  323. time: parseTaskTime({ startTime: data.startTime, endTime: data.endTime })
  324. }
  325. this.files = files
  326. this.$assetMap = {}
  327. this.assets = assets.map(({ id, keyName, orderNo, duration, minioData }) => {
  328. this.$assetMap[id] = `${orderNo}_${duration}`
  329. return {
  330. key: id,
  331. id,
  332. tagInfo: AssetTagInfo[minioData.tag],
  333. type: minioData.type,
  334. keyName,
  335. name: minioData.originalName,
  336. duration,
  337. disabled: minioData.type === AssetType.VIDEO
  338. }
  339. })
  340. this.$refs.editDialog.show()
  341. })
  342. },
  343. onSaveEdit (done) {
  344. const { date, time, duration, count } = this.contract
  345. const attrs = {
  346. ...transformToTaskTime(time),
  347. startDate: date[0],
  348. day: calculateDay(date[0], date[1]) + 1,
  349. duration,
  350. count
  351. }
  352. const data = {}
  353. if (['startDate', 'day', 'startTime', 'endTime', 'duration', 'count'].some(key => attrs[key] !== this.$contract[key])) {
  354. data.attrs = attrs
  355. }
  356. let totalDuration = 0
  357. const delIdMap = {
  358. ...this.$assetMap
  359. }
  360. const assets = this.assets.map(({ id, keyName, duration }, index) => {
  361. totalDuration += duration
  362. const data = {
  363. keyName,
  364. duration,
  365. orderNo: index
  366. }
  367. if (id) {
  368. data.id = id
  369. }
  370. return data
  371. }).filter(({ id, orderNo, duration }) => {
  372. if (id) {
  373. delete delIdMap[id]
  374. console.log(`${orderNo}_${duration}`, this.$assetMap[id])
  375. if (`${orderNo}_${duration}` === this.$assetMap[id]) {
  376. return false
  377. }
  378. }
  379. return true
  380. })
  381. if (assets.length > 0) {
  382. data.assets = assets
  383. }
  384. const delIds = Object.keys(delIdMap)
  385. if (delIds.length > 0) {
  386. data.deleteIds = delIds
  387. }
  388. if (this.assets.length > 1 && totalDuration % duration !== 0) {
  389. this.$confirm(
  390. '上播时长与上播内容总时长不一致,上播内容无法完整播放',
  391. '继续保存',
  392. { type: 'warning' }
  393. ).then(() => {
  394. this.updateContract(data, done)
  395. })
  396. return
  397. }
  398. this.updateContract(data, done)
  399. },
  400. updateContract (data, done) {
  401. if (!Object.keys(data).length) {
  402. done()
  403. return
  404. }
  405. updateContract(this.$contract.id, data).then(() => {
  406. done()
  407. if (data.attrs) {
  408. this.transform(Object.assign(this.$contract, data.attrs))
  409. }
  410. })
  411. },
  412. onAddAsset () {
  413. this.$refs.assetTableDialog.show()
  414. },
  415. onChoosenAsset ({ value, done }) {
  416. const { tag, type, keyName, originalName } = value
  417. this.assets.push({
  418. key: `${Date.now()}_${Math.random().toString(16).slice(2)}`,
  419. tagInfo: AssetTagInfo[tag],
  420. disabled: type === AssetType.VIDEO,
  421. name: originalName,
  422. type,
  423. keyName,
  424. duration: getAssetDuration(value)
  425. })
  426. done()
  427. },
  428. onDelAsset (asset, index) {
  429. this.assets.splice(index, 1)
  430. },
  431. onChange ({ raw }) {
  432. this.$refs.upload.clearFiles()
  433. bindFileToContract(this.$contract.id, raw).then(({ data }) => {
  434. this.files.push(data)
  435. })
  436. },
  437. onDelFile (file, index) {
  438. deleteFileFromContract(file).then(() => {
  439. this.files.splice(index, 1)
  440. })
  441. },
  442. onDownload (file) {
  443. const a = document.createElement('a')
  444. a.style.display = 'none'
  445. a.setAttribute('target', '_blank')
  446. a.setAttribute('download', file.name)
  447. a.href = getAssetUrl(file.enclosureUrl)
  448. document.body.appendChild(a)
  449. a.click()
  450. document.body.removeChild(a)
  451. },
  452. onDel (contract) {
  453. deleteContract(contract).then(() => {
  454. this.$refs.table.decrease(1)
  455. })
  456. }
  457. }
  458. }
  459. </script>
  460. <style lang="scss" scoped>
  461. .c-contract-dialog {
  462. &__header {
  463. padding-bottom: 8px;
  464. margin-bottom: $spacing;
  465. color: $black;
  466. font-size: 18px;
  467. line-height: 1;
  468. border-bottom: 1px solid $border;
  469. }
  470. &__item {
  471. padding: 4px 0;
  472. border: 1px solid $gray;
  473. border-radius: $radius--mini;
  474. &.sortable-chosen {
  475. border-color: #c6e2ff;
  476. background-color: $blue--light;
  477. }
  478. }
  479. &__mover {
  480. padding: 10px 16px;
  481. font-size: 18px;
  482. cursor: move;
  483. }
  484. &__name {
  485. padding: 16px 10px 16px 0;
  486. }
  487. &__seconds {
  488. width: 120px;
  489. }
  490. &__del {
  491. display: inline-flex;
  492. justify-content: center;
  493. align-items: center;
  494. width: 50px;
  495. height: 40px;
  496. color: $gray;
  497. font-size: 18px;
  498. &.mini {
  499. width: 40px;
  500. }
  501. &:hover {
  502. color: $primary;
  503. }
  504. }
  505. &__upload {
  506. display: inline-block;
  507. border: 1px dashed #c0ccda;
  508. border-radius: 6px;
  509. background-color: #fbfdff;
  510. cursor: pointer;
  511. ::v-deep {
  512. .el-upload {
  513. display: inline-flex;
  514. justify-content: center;
  515. align-items: center;
  516. width: 40px;
  517. height: 40px;
  518. }
  519. }
  520. &:hover {
  521. color: $blue;
  522. border-color: $blue;
  523. }
  524. }
  525. &__file {
  526. width: 350px;
  527. & + & {
  528. border-top: 1px solid $border;
  529. }
  530. }
  531. }
  532. </style>