index.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. <template>
  2. <div class="l-flex__fill l-flex">
  3. <div class="l-flex__none c-takeover-debug u-overflow-y--auto">
  4. <div
  5. v-for="message in messages"
  6. :key="message.id"
  7. >
  8. {{ message.info }}
  9. </div>
  10. </div>
  11. <div
  12. v-loading="loading"
  13. class="l-flex__fill l-flex--col"
  14. >
  15. <div
  16. v-if="!loading && !valid"
  17. class="l-flex--col center"
  18. >
  19. <button
  20. v-if="online"
  21. class="o-button"
  22. @click="createClient"
  23. >
  24. 连接设备
  25. </button>
  26. <div
  27. v-else
  28. class="u-color--info"
  29. >
  30. 设备未上线
  31. </div>
  32. </div>
  33. <template v-if="valid">
  34. <div class="l-flex--row c-sibling-item--v">
  35. <button
  36. class="c-sibling-item o-button"
  37. @click="onAddAsset"
  38. >
  39. <i class="o-button__icon el-icon-circle-plus-outline" />
  40. 缓存资源
  41. </button>
  42. <button
  43. class="c-sibling-item o-button"
  44. @click="onAddWeb"
  45. >
  46. <i class="o-button__icon el-icon-circle-plus-outline" />
  47. 缓存网页
  48. </button>
  49. </div>
  50. <el-tabs
  51. v-model="active"
  52. class="c-sibling-item--v c-tabs has-bottom-padding"
  53. >
  54. <el-tab-pane
  55. name="loaded"
  56. label="已下载"
  57. />
  58. <el-tab-pane
  59. label="下载中"
  60. name="loading"
  61. />
  62. <el-tab-pane
  63. label="异常"
  64. name="error"
  65. />
  66. </el-tabs>
  67. <div class="l-flex__fill l-grid--info mini u-overflow-y--auto">
  68. <asset-card
  69. v-for="asset in list"
  70. :key="asset.keyName"
  71. :asset="asset"
  72. @play="onPlay"
  73. @reload="onReload"
  74. @del="onDel"
  75. />
  76. </div>
  77. <confirm-dialog
  78. ref="assetsDialog"
  79. title="资源列表"
  80. size="lg fixed"
  81. append-to-body
  82. @confirm="onSaveAssets"
  83. >
  84. <div class="l-flex__fill l-flex">
  85. <directory-tree
  86. class="c-sibling-item c-sidebar u-width--md"
  87. @change="onAssetDirectoryChanged"
  88. />
  89. <grid-table
  90. ref="gridTable"
  91. :schema="assetSchema"
  92. >
  93. <grid-table-item v-slot="item">
  94. <media-card
  95. class="o-card"
  96. :asset="item"
  97. @click="onToggleGrid"
  98. >
  99. <el-checkbox
  100. v-model="item.selected"
  101. class="o-card__checkbox"
  102. />
  103. <i
  104. class="o-card__icon el-icon-video-play has-active"
  105. @click.stop="onView(item)"
  106. />
  107. </media-card>
  108. </grid-table-item>
  109. </grid-table>
  110. </div>
  111. </confirm-dialog>
  112. <confirm-dialog
  113. ref="webDialog"
  114. title="网页配置"
  115. @confirm="onSaveWeb"
  116. >
  117. <div class="c-grid-form sm u-align-self--center">
  118. <span class="c-grid-form__label u-required">名称:</span>
  119. <el-input
  120. v-model.trim="web.name"
  121. clearable
  122. />
  123. <span class="c-grid-form__label u-required">网址:</span>
  124. <el-input
  125. v-model.trim="web.keyName"
  126. clearable
  127. />
  128. </div>
  129. </confirm-dialog>
  130. <preview-dialog ref="previewDialog" />
  131. </template>
  132. </div>
  133. </div>
  134. </template>
  135. <script>
  136. import {
  137. State,
  138. AssetType,
  139. AssetTypeInfo,
  140. AssetTag,
  141. AssetTagInfo
  142. } from '@/constant'
  143. import { getAssetsByQuery } from '@/api/asset'
  144. import {
  145. Topic,
  146. MediaType,
  147. MediaStatus,
  148. takeOver
  149. } from './takeover'
  150. import AssetCard from './components/AssetCard'
  151. export default {
  152. name: 'DeviceTakeOver',
  153. components: {
  154. AssetCard
  155. },
  156. props: {
  157. device: {
  158. type: Object,
  159. required: true
  160. },
  161. online: {
  162. type: [Boolean, String],
  163. default: false
  164. }
  165. },
  166. data () {
  167. return {
  168. loading: true,
  169. valid: false,
  170. messages: [],
  171. active: 'loaded',
  172. assets: [],
  173. sources: [],
  174. assetSchema: {
  175. list: this.getAssetsByQuery,
  176. transform: this.transformAsset,
  177. condition: { pageSize: 16, status: State.AVAILABLE, tag: AssetTag.AD, type: AssetType.IMAGE },
  178. filters: [
  179. { key: 'tag', type: 'select', options: [
  180. { value: AssetTag.AD, label: AssetTagInfo[AssetTag.AD] },
  181. { value: AssetTag.PUBLICITY, label: AssetTagInfo[AssetTag.PUBLICITY] },
  182. { value: AssetTag.LOCAL_PUBLICITY, label: AssetTagInfo[AssetTag.LOCAL_PUBLICITY] },
  183. { value: AssetTag.SHIM, label: AssetTagInfo[AssetTag.SHIM] }
  184. ] },
  185. { key: 'type', type: 'select', options: [
  186. { value: AssetType.IMAGE, label: AssetTypeInfo[AssetType.IMAGE] },
  187. { value: AssetType.VIDEO, label: AssetTypeInfo[AssetType.VIDEO] }
  188. ] },
  189. { key: 'originalName', type: 'search', placeholder: '资源名称' }
  190. ]
  191. },
  192. web: {}
  193. }
  194. },
  195. computed: {
  196. list () {
  197. switch (this.active) {
  198. case 'loaded':
  199. return this.assets.filter(({ status }) => status === MediaStatus.LOADED)
  200. case 'loading':
  201. return this.assets.filter(({ status }) => status === MediaStatus.WAITING || status === MediaStatus.LOADING)
  202. case 'error':
  203. return this.assets.filter(({ status }) => status === MediaStatus.ERROR)
  204. default:
  205. return this.assets
  206. }
  207. }
  208. },
  209. watch: {
  210. online: {
  211. handler (val) {
  212. if (val) {
  213. this.createClient()
  214. } else {
  215. this.destroyClient()
  216. }
  217. },
  218. immediate: true
  219. }
  220. },
  221. beforeDestroy () {
  222. this.destroyClient()
  223. },
  224. methods: {
  225. onAssetDirectoryChanged (directory) {
  226. this.$directoryOption = directory
  227. this.$refs.table?.getTable.pageTo(1)
  228. },
  229. getAssetsByQuery (params) {
  230. if (!this.$directoryOption) {
  231. return Promise.resolve({ data: [] })
  232. }
  233. const { root, id, group: { path } } = this.$directoryOption
  234. return getAssetsByQuery({
  235. ...params,
  236. ...(root
  237. ? { org: path }
  238. : { treeId: id, queryRelation: '1' })
  239. })
  240. },
  241. destroyClient () {
  242. this.loading = false
  243. if (this.valid) {
  244. this.$clientProxy.close()
  245. } else {
  246. this.$client?.end(true)
  247. }
  248. },
  249. onMessage (topic, payload, requestPayload) {
  250. switch (topic) {
  251. case Topic.PRELOAD:
  252. this.onReloadReply(requestPayload.assets)
  253. break
  254. case Topic.DEL:
  255. this.onDelReply(requestPayload.assets)
  256. break
  257. case Topic.PULL:
  258. this.onRefresh(payload.assets)
  259. break
  260. case Topic.DOWNLOAD:
  261. this.onDownload(payload)
  262. break
  263. default:
  264. break
  265. }
  266. },
  267. onCreated (proxy) {
  268. this.valid = true
  269. this.$client = null
  270. this.$clientProxy = proxy
  271. this.$clientProxy.pull()
  272. },
  273. onClose () {
  274. this.valid = false
  275. this.loading = false
  276. this.$client = null
  277. this.$clientProxy = null
  278. },
  279. onDebug (message) {
  280. this.messages.unshift({
  281. id: `${Date.now()}_${Math.random().toString().slice(2)}`,
  282. info: message
  283. })
  284. },
  285. createClient () {
  286. this.loading = true
  287. this.$client = takeOver(this.device, {
  288. onMessage: this.onMessage,
  289. onCreated: this.onCreated,
  290. onClose: this.onClose,
  291. debug: this.onDebug
  292. })
  293. },
  294. onRefresh (assets) {
  295. this.loading = false
  296. this.assets = assets
  297. },
  298. onPlay ({ type, name, keyName }) {
  299. this.$clientProxy.play({ type, name, keyName })
  300. },
  301. transformAsset ({ type, originalName, keyName, file, size, md5 }) {
  302. return {
  303. selected: false,
  304. type,
  305. name: originalName,
  306. keyName,
  307. size,
  308. md5,
  309. file
  310. }
  311. },
  312. onAddAsset () {
  313. this.$refs.assetsDialog.show()
  314. },
  315. onToggleGrid (asset) {
  316. asset.selected = !asset.selected
  317. },
  318. onSaveAssets (done) {
  319. const assets = this.$refs.gridTable.getData().filter(({ selected }) => selected).map(({ type, name, keyName, size, md5, file }) => {
  320. return {
  321. type, name, keyName, size, md5,
  322. thumb: file.thumb
  323. }
  324. })
  325. if (assets.length) {
  326. this.sources = this.sources.concat(assets)
  327. this.$clientProxy.preload(assets)
  328. }
  329. done()
  330. },
  331. onView ({ type, keyName }) {
  332. this.$refs.previewDialog.show({ type, url: keyName })
  333. },
  334. onAddWeb () {
  335. this.web = {
  336. name: '',
  337. keyName: ''
  338. }
  339. this.$refs.webDialog.show()
  340. },
  341. onSaveWeb (done) {
  342. const { name, keyName } = this.web
  343. if (!name) {
  344. this.$message({
  345. type: 'warning',
  346. message: '名称不能为空'
  347. })
  348. return
  349. }
  350. if (!keyName) {
  351. this.$message({
  352. type: 'warning',
  353. message: '网址不能为空'
  354. })
  355. return
  356. }
  357. this.$clientProxy.preload([{ type: MediaType.WEB, name, keyName }])
  358. done()
  359. },
  360. onReload (asset) {
  361. const { status, ...data } = asset
  362. this.$clientProxy.preload([data])
  363. },
  364. onReloadReply (assets) {
  365. assets.forEach(asset => {
  366. const targetKeyName = asset.keyName
  367. const index = this.assets.findIndex(({ keyName }) => targetKeyName === keyName)
  368. if (~index) {
  369. this.assets[index].status = MediaStatus.WAITING
  370. } else {
  371. this.assets.push({
  372. status: asset.type === MediaType.WEB ? MediaStatus.LOADED : MediaStatus.WAITING,
  373. ...asset
  374. })
  375. }
  376. })
  377. },
  378. onDel ({ name, keyName }) {
  379. this.onDebug(`删除${name}`)
  380. this.$clientProxy.del([keyName])
  381. },
  382. onDelReply (assets) {
  383. assets.forEach(keyName => {
  384. const index = this.assets.findIndex(asset => asset.keyName === keyName)
  385. if (~index) {
  386. this.assets.splice(index, 1)
  387. }
  388. })
  389. },
  390. onDownload ({ success, asset: { keyName } }) {
  391. const index = this.assets.findIndex(asset => asset.keyName === keyName)
  392. if (~index) {
  393. this.assets[index].status = success ? MediaStatus.LOADED : MediaStatus.ERROR
  394. } else if (!this.loading) {
  395. this.loading = true
  396. this.$clientProxy.pull()
  397. }
  398. }
  399. }
  400. }
  401. </script>
  402. <style lang="scss" scoped>
  403. .c-takeover-debug {
  404. width: 200px;
  405. padding-right: $spacing;
  406. margin-right: $spacing;
  407. color: $black;
  408. font-size: 14px;
  409. line-height: 24px;
  410. word-break: break-word;
  411. border-right: 1px solid $border;
  412. }
  413. .o-card {
  414. &__checkbox {
  415. position: absolute;
  416. top: 10px;
  417. left: 10px;
  418. pointer-events: none;
  419. z-index: 9;
  420. }
  421. &__icon {
  422. position: absolute;
  423. top: 50%;
  424. left: 50%;
  425. padding: 2px;
  426. font-size: 24px;
  427. border-radius: 50%;
  428. background-color: rgba(#000, 0.4);
  429. transform: translate(-50%, -50%);
  430. }
  431. }
  432. </style>