index.vue 20 KB

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