index.vue 21 KB

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