index.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744
  1. <template>
  2. <div class="works con">
  3. <div class="back-top" @click="onClickBackTop" v-show="isShowBackTopBtn">
  4. <i class="iconfont icon-top"></i>
  5. </div>
  6. <div class="tab">
  7. <span>{{ myWorks }} {{ workTotalNum !== undefined ? `(${workTotalNum})` : '' }}</span>
  8. <div class="tab-r">
  9. <div class="filter">
  10. <div :class="{ active: isFilterFocus }" @focusin="onFilterFocus" @focusout="onFilterBlur">
  11. <i class="iconfont iconworks_search search"></i>
  12. <input type="text" :placeholder="search" v-model="searchKey">
  13. <i v-if="searchKey" @click="searchKey = ''" class="iconfont icontoast_red del"></i>
  14. </div>
  15. </div>
  16. </div>
  17. </div>
  18. <div class="mask" v-show="isShowMask"></div>
  19. <!-- 断网时,输入关键字触发网络请求后,骨架图的隐藏会触发v-infinite-scroll,原因未知。进而导致循环触发,因此list为空时要禁用v-infinite-scroll -->
  20. <ul class="w-list" v-if="!(list.length === 0 && !hasMoreData)" v-infinite-scroll="requestMoreData"
  21. :infinite-scroll-disabled="!hasMoreData || isRequestingMoreData || (list.length === 0)" ref="w-list-ref"
  22. @scroll.self="onWorkListScroll">
  23. <li class="add-work" @click="add">
  24. <div class="wrapper">
  25. <div class="add-con">
  26. <div>
  27. <i class="iconfont icon-works_add"></i>
  28. </div>
  29. <span>{{ create }}</span>
  30. </div>
  31. </div>
  32. </li>
  33. <!-- 骨架图 -->
  34. <template v-if="isRequestingMoreData && (list.length === 0)">
  35. <li v-for="index in 19" :key="index">
  36. <div class="wrapper">
  37. <workCardSkeleton></workCardSkeleton>
  38. </div>
  39. </li>
  40. </template>
  41. <li v-for="(item, i) in list" :key="i" :class="{ 'has-more-data': hasMoreData }">
  42. <div class="wrapper">
  43. <div class="li-hover">
  44. <span class="lipreview" @click="handlePreview(item)">{{ preview }}</span>
  45. <ul class="oper">
  46. <li class="comfirmhover" @click="edit(item)"><i class="iconfont icon-works_editor"></i>{{ edittips }}</li>
  47. <li class="comfirmhover" @click="openShare(item)"><i class="iconfont icon-works_share"></i>{{ share }}</li>
  48. <li class="cancelhover" @click="del(item, i)"><i class="iconfont icon-works_delete"></i>{{ deltips }}</li>
  49. </ul>
  50. </div>
  51. <div class="img" @click="handlePreview(item)">
  52. <img class="real" :src="item.icon || $thumb" alt="" />
  53. </div>
  54. <div class="li-info">
  55. <div>
  56. <span class="shenglve tttttt" :title="item.name || no_title">{{ item.name || no_title }}</span>
  57. </div>
  58. <div>
  59. <span>{{ item.createTime.split(' ')[0] }}</span>
  60. <div :title="item.visit">
  61. <i class="iconfont iconworks_look"></i>{{ item.visit > 10000 ? '1w+' : item.visit }}
  62. </div>
  63. </div>
  64. </div>
  65. </div>
  66. </li>
  67. <div class="work-list-loading-wrapper" v-show="isRequestingMoreData && (list.length !== 0)">
  68. <img class="work-list-loading" :src="require('@/assets/images/icons/work-list-loading.gif')" />
  69. </div>
  70. </ul>
  71. <div class="nodata" v-if="list.length == 0 && !hasMoreData && lastestUsedSearchKey">
  72. <img :src="$noresult" alt="" />
  73. <span>{{ no_serch_result }}~</span>
  74. </div>
  75. <div class="nodata" v-if="list.length == 0 && !hasMoreData && !lastestUsedSearchKey">
  76. <img :src="config.empty" alt="" />
  77. <span>{{ no_works }}</span>
  78. <button @click="add" class="upload-btn-in-table">{{ create }}</button>
  79. </div>
  80. <share :show='showShare' :item="shareItem" @close="showShare = false"></share>
  81. <preview v-if="showItem" :name="showItem.name" :show="showPreview"
  82. :ifr="`./show.html?id=${showItem.id}&lang=${$lang}`" :dark="false" @close="showPreview = false" />
  83. <div class="dialog" style="z-index: 1000" v-if="isShowMaterialSelector">
  84. <MaterialSelector
  85. :isDarkTheme="false"
  86. :title="select_material"
  87. :selectableType="['pano', '3D']"
  88. :isMultiSelection="true"
  89. initialMaterialType="pano"
  90. @cancle="isShowMaterialSelector = false"
  91. @submit="handleSubmitFromMaterialSelector"
  92. />
  93. </div>
  94. </div>
  95. </template>
  96. <script>
  97. import share from '../popup/share'
  98. import preview from "@/components/preview";
  99. import workCardSkeleton from "@/components/workCardSkeleton.vue";
  100. import config from "@/config";
  101. import { debounce } from "@/utils/other.js"
  102. import MaterialSelector from "@/components/materialSelector.vue";
  103. import { mapGetters } from "vuex";
  104. import { i18n } from "@/lang"
  105. import { $waiting } from "@/components/shared/loading";
  106. import {
  107. addWorks,
  108. getWorksList,
  109. delWorks,
  110. getPanoInfo,
  111. saveWorks,
  112. } from "@/api";
  113. export default {
  114. components: {
  115. share,
  116. preview,
  117. workCardSkeleton,
  118. MaterialSelector,
  119. },
  120. computed: {
  121. ...mapGetters([
  122. 'info',
  123. ])
  124. },
  125. data() {
  126. return {
  127. myWorks: i18n.t("material.works.my"),
  128. create: i18n.t("material.works.create"),
  129. search: i18n.t("material.works.search"),
  130. preview: i18n.t("material.works.preview"),
  131. edittips: i18n.t("material.works.edit"),
  132. share: i18n.t("material.works.share"),
  133. deltips: i18n.t("material.works.delete"),
  134. no_works: i18n.t("material.works.no_works"),
  135. no_title: i18n.t("gather.no_title"),
  136. no_serch_result: i18n.t("gather.no_serch_result"),
  137. select_material: i18n.t("gather.select_material"),
  138. config,
  139. list: [],
  140. workTotalNum: undefined,
  141. hasMoreData: true,
  142. isRequestingMoreData: false,
  143. searchKey: '',
  144. // 因为searchKey的变化经过debounce、异步请求的延时,才会反映到数据列表的变化上,所以是否显示、显示哪种无数据提示,也要等到数据列表变化后,根据数据列表是否为空,以及引发本次变化的那个searchKey瞬时值来决定。本变量就是用来保存那个瞬时值。
  145. lastestUsedSearchKey: '',
  146. isFilterFocus: false,
  147. showShare: false,
  148. showPreview: false,
  149. showItem: '',
  150. shareItem: '',
  151. isBackingTop: false,
  152. isShowBackTopBtn: false,
  153. isShowMask: false,
  154. isShowMaterialSelector: false,
  155. newWorkId: '',
  156. }
  157. },
  158. mounted() {
  159. this.requestMoreData()
  160. },
  161. watch: {
  162. searchKey: {
  163. handler: function (val) {
  164. if (val.length > 0) {
  165. this.selectedList = [];
  166. }
  167. this.refreshListDebounced()
  168. },
  169. immediate: false,
  170. },
  171. },
  172. methods: {
  173. onFilterFocus() {
  174. this.isFilterFocus = true
  175. },
  176. onFilterBlur() {
  177. this.isFilterFocus = false
  178. },
  179. refreshListDebounced: debounce(function () {
  180. this.list = []
  181. this.isRequestingMoreData = false
  182. this.hasMoreData = true
  183. this.requestMoreData()
  184. }, 500, false),
  185. openShare(data) {
  186. console.log(data);
  187. getPanoInfo(data.id, (data) => {
  188. if (data.scenes.length <= 0) {
  189. return this.$msg.warning(this.$i18n.t("material.works.no_link"));
  190. }
  191. this.showShare = true
  192. this.shareItem = data
  193. })
  194. },
  195. handlePreview(item) {
  196. getPanoInfo(item.id, (data) => {
  197. if (data.scenes.length <= 0) {
  198. return this.$msg.warning(this.$i18n.t("material.works.no_link"));
  199. }
  200. this.showItem = {
  201. ...item,
  202. ...data
  203. }
  204. this.showPreview = true
  205. })
  206. },
  207. add() {
  208. // 新建作品,弹窗让用户给作品选择素材。
  209. $waiting.show();
  210. addWorks(
  211. {},
  212. (res) => {
  213. $waiting.hide();
  214. this.newWorkId = res.data.id
  215. this.isShowMaterialSelector = true
  216. },
  217. )
  218. },
  219. handleSubmitFromMaterialSelector(selected) {
  220. $waiting.show();
  221. // 拿新作品的初始数据
  222. getPanoInfo(
  223. this.newWorkId,
  224. // 拿到了。
  225. (data) => {
  226. // 往里边添加用户选中的素材。
  227. this.$store.commit("SetInfo", data);
  228. for (const item of selected) {
  229. if (item.materialType === 'pano') {
  230. this.info.scenes.push({
  231. icon: item.icon,
  232. sceneCode: item.sceneCode,
  233. sceneTitle: item.name,
  234. category: this.info.catalogs[0].id,
  235. type: "pano",
  236. id: 's_' + this.$randomWord(true, 8, 8)
  237. })
  238. } else if (item.materialType === '3D') {
  239. this.info.scenes.push({
  240. icon: item.thumb,
  241. sceneCode: item.num,
  242. sceneTitle: item.sceneName,
  243. category: this.info.catalogs[0].id,
  244. type: "4dkk",
  245. id: 's_' + this.$randomWord(true, 8, 8)
  246. })
  247. }
  248. }
  249. // 保存新作品
  250. saveWorks(
  251. {
  252. id: this.newWorkId,
  253. password: '',
  254. someData: {
  255. ...this.info,
  256. status: 1,
  257. icon: this.info.scenes[0].icon
  258. },
  259. },
  260. // 保存成功
  261. () => {
  262. $waiting.hide();
  263. // 隐藏素材选择弹窗
  264. this.isShowMaterialSelector = false
  265. // 刷新作品列表
  266. this.list = []
  267. this.isRequestingMoreData = false
  268. this.hasMoreData = true
  269. this.requestMoreData().then(() => {
  270. // 刷新成功
  271. // 弹出提示窗口
  272. this.$confirm({
  273. title: this.$i18n.t('tips_code.tips'),
  274. content: this.$i18n.t("material.works.had_created"),
  275. okText: this.$i18n.t("material.works.goto_preview"),
  276. ok: () => {
  277. this.handlePreview(this.list[0])
  278. this.newWorkId = ''
  279. this.$store.commit("SetInfo", {});
  280. },
  281. ok2Text: this.$i18n.t("material.works.continue_edit"),
  282. ok2: () => {
  283. window.open(`./edit.html?id=${this.newWorkId}&lang=${this.$lang}`)
  284. this.newWorkId = ''
  285. this.$store.commit("SetInfo", {});
  286. },
  287. });
  288. }).catch(() => {
  289. this.$msg.message(this.$i18n.t("material.works.had_created_but_no_link"))
  290. console.error('已成功新建作品,但刷新作品列表失败。')
  291. })
  292. },
  293. // 保存失败,删除新建的作品。
  294. (error) => {
  295. $waiting.hide();
  296. console.error('保存失败:', error);
  297. delWorks(this.newWorkId)
  298. this.newWorkId = ''
  299. this.$store.commit("SetInfo", {});
  300. }
  301. );
  302. },
  303. // 没拿到,删除新建的作品。
  304. (error) => {
  305. console.error('没拿到新建的作品数据:', error);
  306. delWorks(this.newWorkId)
  307. this.newWorkId = ''
  308. }
  309. )
  310. },
  311. edit(item) {
  312. window.open(`./edit.html?id=${item.id}&lang=${this.$lang}`)
  313. },
  314. del(item, index) {
  315. this.$confirm({
  316. title: this.$i18n.t("material.works.delete_work"),
  317. content: this.$i18n.t("material.works.comfirm_delete"),
  318. ok: () => {
  319. $waiting.show();
  320. delWorks(item.id, () => {
  321. this.$msg.success(this.$i18n.t("gather.delete_success"));
  322. this.isRequestingMoreData = true
  323. const lastestUsedSearchKey = this.searchKey
  324. getWorksList(
  325. {
  326. pageNum: this.list.length,
  327. pageSize: 1,
  328. searchKey: this.searchKey
  329. },
  330. (data) => {
  331. $waiting.hide();
  332. this.list.splice(index, 1)
  333. this.list = this.list.concat(data.data.list)
  334. if (this.list.length === data.data.total) {
  335. this.hasMoreData = false
  336. }
  337. this.isRequestingMoreData = false
  338. this.lastestUsedSearchKey = lastestUsedSearchKey
  339. if (!lastestUsedSearchKey) {
  340. this.workTotalNum = data.data.total
  341. }
  342. // TODO: 这是干啥呢?
  343. this.$nextTick(() => {
  344. this.$bus.emit('refreshTips')
  345. })
  346. },
  347. () => {
  348. $waiting.hide();
  349. this.lastestUsedSearchKey = lastestUsedSearchKey
  350. this.isRequestingMoreData = false
  351. }
  352. )
  353. });
  354. },
  355. });
  356. },
  357. requestMoreData() {
  358. this.isRequestingMoreData = true
  359. const lastestUsedSearchKey = this.searchKey
  360. return new Promise((resolve, reject) => {
  361. getWorksList(
  362. {
  363. pageNum: Math.floor(this.list.length / config.PAGE_SIZE) + 1,
  364. pageSize: config.PAGE_SIZE,
  365. searchKey: this.searchKey,
  366. },
  367. (data) => {
  368. this.list = this.list.concat(data.data.list)
  369. if (this.list.length === data.data.total) {
  370. this.hasMoreData = false
  371. }
  372. this.isRequestingMoreData = false
  373. this.lastestUsedSearchKey = lastestUsedSearchKey
  374. if (!lastestUsedSearchKey) {
  375. this.workTotalNum = data.data.total
  376. }
  377. // TODO: 这是干啥呢?
  378. this.$nextTick(() => {
  379. this.$bus.emit('refreshTips')
  380. })
  381. resolve()
  382. },
  383. () => {
  384. this.isRequestingMoreData = false
  385. this.lastestUsedSearchKey = lastestUsedSearchKey
  386. reject()
  387. }
  388. );
  389. })
  390. },
  391. onClickBackTop() {
  392. if (this.isBackingTop) {
  393. return
  394. }
  395. this.isBackingTop = true
  396. const startTime = Date.now()
  397. const totalScroll = this.$refs['w-list-ref'].scrollTop
  398. const fn = () => {
  399. if (this.$refs['w-list-ref'].scrollTop === 0) {
  400. this.isBackingTop = false
  401. return
  402. }
  403. const nowTime = Date.now()
  404. const assumeScrollTop = totalScroll - (nowTime - startTime) * totalScroll / 500
  405. this.$refs['w-list-ref'].scrollTop = assumeScrollTop > 0 ? assumeScrollTop : 0
  406. requestAnimationFrame(fn)
  407. }
  408. requestAnimationFrame(fn)
  409. },
  410. onWorkListScroll(e) {
  411. if (e.target.scrollTop >= 30) {
  412. !this.isShowMask && (this.isShowMask = true)
  413. } else {
  414. this.isShowMask && (this.isShowMask = false)
  415. }
  416. if (e.target.scrollTop >= 600) {
  417. this.isShowBackTopBtn = true
  418. } else {
  419. this.isShowBackTopBtn = false
  420. }
  421. }
  422. }
  423. }
  424. </script>
  425. <style lang="less" scoped>
  426. .works {
  427. width: 100%;
  428. flex-direction: column;
  429. position: relative;
  430. .back-top {
  431. position: absolute;
  432. right: -80px;
  433. bottom: 30px;
  434. width: 60px;
  435. height: 60px;
  436. border-radius: 8px;
  437. background-color: #fff;
  438. z-index: 1;
  439. color: #C8C9CC;
  440. &:hover {
  441. color: #323233;
  442. }
  443. cursor: pointer;
  444. display: flex;
  445. justify-content: center;
  446. align-items: center;
  447. i {
  448. font-size: 20px;
  449. }
  450. }
  451. .tab {
  452. flex: 0 0 auto;
  453. width: 100%;
  454. display: flex;
  455. background: #fff;
  456. justify-content: space-between;
  457. align-items: center;
  458. padding: 20px 30px;
  459. >span {
  460. font-size: 18px;
  461. font-weight: bold;
  462. }
  463. .tab-r {
  464. align-items: center;
  465. display: flex;
  466. .ui-button {
  467. margin-right: 20px;
  468. }
  469. }
  470. }
  471. .mask {
  472. position: absolute;
  473. width: 100%;
  474. top: 200px;
  475. height: 30px;
  476. background: linear-gradient(rgb(239, 242, 244), rgba(255, 255, 255, 0));
  477. z-index: 1;
  478. pointer-events: none;
  479. }
  480. .w-list {
  481. flex: 1 1 auto;
  482. overflow: auto;
  483. margin-top: 22px;
  484. padding-top: 8px;
  485. @gap: 20px;
  486. display: flex;
  487. flex-wrap: wrap;
  488. align-content: flex-start;
  489. // 让宽度和视口等宽,为了保证鼠标列表显示区域以外时列表也能响应滚轮事件。
  490. margin-left: calc((100vw - 100%) / -2);
  491. padding-left: calc((100vw - 100%) / 2);
  492. margin-right: calc((100vw - 100%) / -2);
  493. padding-right: calc((100vw - 100%) / 2);
  494. &::-webkit-scrollbar {
  495. width: 0;
  496. height: 0;
  497. }
  498. >li {
  499. width: calc((100% - @gap * 4) / 5);
  500. height: 322px;
  501. margin-bottom: @gap;
  502. margin-right: @gap;
  503. &:nth-of-type(5n) {
  504. margin-right: 0;
  505. }
  506. // 因为有个“创建作品”card占着空间,每次拿20个数据,每行五个,又不想每次拿到数据后最后一行只有一个card,所以把最后那个card隐藏掉。
  507. &:last-of-type.has-more-data {
  508. display: none;
  509. }
  510. .wrapper {
  511. height: 100%;
  512. background: #fff;
  513. position: relative;
  514. border-radius: 6px;
  515. overflow: hidden;
  516. .li-hover {
  517. display: none;
  518. width: 100%;
  519. height: 240px;
  520. position: absolute;
  521. top: 0;
  522. left: 0;
  523. z-index: 99;
  524. background: rgba(0, 0, 0, 0.6);
  525. .lipreview {
  526. position: absolute;
  527. top: 50%;
  528. left: 50%;
  529. transform: translate(-50%, -50%);
  530. color: #fff;
  531. display: inline-block;
  532. line-height: 40px;
  533. height: 40px;
  534. width: 100px;
  535. text-align: center;
  536. border-radius: 22px;
  537. cursor: pointer;
  538. background-color: transparent;
  539. border: 1px solid #fff;
  540. &:hover {
  541. border: none;
  542. background: #1983F6;
  543. }
  544. }
  545. .oper {
  546. display: flex;
  547. justify-content: space-around;
  548. align-items: center;
  549. position: absolute;
  550. bottom: 10px;
  551. left: 0;
  552. width: 100%;
  553. >li {
  554. color: #fff;
  555. font-size: 13px;
  556. display: flex;
  557. align-items: center;
  558. cursor: pointer;
  559. >i {
  560. font-size: 20px;
  561. margin-right: 4px;
  562. }
  563. }
  564. }
  565. }
  566. .img {
  567. width: 100%;
  568. height: 240px;
  569. position: relative;
  570. overflow: hidden;
  571. cursor: pointer;
  572. .real {
  573. height: 100%;
  574. position: absolute;
  575. top: 0;
  576. left: 50%;
  577. transform: translateX(-50%);
  578. z-index: 0;
  579. transition: all ease 0.3s;
  580. }
  581. }
  582. .li-info {
  583. font-size: 14px;
  584. padding: 10px;
  585. >div {
  586. text-align: left;
  587. &:first-of-type {
  588. >span {
  589. font-weight: bold;
  590. margin-bottom: 10px;
  591. display: inline-block;
  592. text-overflow: ellipsis;
  593. overflow: hidden;
  594. white-space: nowrap;
  595. cursor: pointer;
  596. color: #323233;
  597. font-size: 16px;
  598. }
  599. }
  600. &:last-of-type {
  601. display: flex;
  602. justify-content: space-between;
  603. align-items: center;
  604. >span {
  605. font-size: 14px;
  606. color: #969799;
  607. }
  608. >div {
  609. color: #969799;
  610. i {
  611. margin-right: 6px;
  612. }
  613. }
  614. }
  615. }
  616. }
  617. }
  618. &:hover {
  619. .wrapper {
  620. box-shadow: 0px 2px 12px 0px rgba(50, 50, 51, 0.12);
  621. transform: translateY(-6px);
  622. .li-hover {
  623. display: block;
  624. }
  625. .img {
  626. .real {
  627. height: 108%;
  628. }
  629. }
  630. }
  631. }
  632. }
  633. .add-work {
  634. .wrapper {
  635. .add-con {
  636. position: absolute;
  637. top: 50%;
  638. left: 50%;
  639. transform: translate(-50%, -50%);
  640. text-align: center;
  641. div {
  642. width: 60px;
  643. height: 60px;
  644. border-radius: 50%;
  645. background: linear-gradient(144deg, #00AEFB 0%, #0076F6 100%);
  646. position: relative;
  647. cursor: pointer;
  648. margin: 0 auto;
  649. >i {
  650. font-size: 16px;
  651. position: absolute;
  652. top: 50%;
  653. left: 50%;
  654. transform: translate(-50%, -50%);
  655. color: #fff
  656. }
  657. }
  658. span {
  659. color: #333333;
  660. display: inline-block;
  661. margin-top: 8px;
  662. font-size: 14px;
  663. }
  664. }
  665. }
  666. }
  667. .work-list-loading-wrapper {
  668. width: 100%;
  669. margin-top: 20px;
  670. margin-bottom: 22px;
  671. .work-list-loading {
  672. display: block;
  673. margin: 0 auto;
  674. width: 50px;
  675. height: 8px;
  676. }
  677. }
  678. }
  679. }
  680. </style>
  681. <style lang="less" scoped>
  682. @import '../style.less';
  683. </style>