Browse Source

feat: 分类

gemercheung 1 year ago
parent
commit
6dc6e559da

+ 3 - 1
packages/qjkankan-editor/package.json

@@ -26,12 +26,14 @@
     "html2canvas": "^1.4.1",
     "is-chinese-phone-number": "^0.1.9",
     "libphonenumber-js": "^1.10.19",
+    "lodash": "^4.17.21",
     "log-beautify": "^1.2.0",
     "photoswipe": "^4.1.3",
     "quill": "^1.3.7",
     "swiper": "^5.3.8",
     "v-viewer": "^1.5.1",
     "video.js": "^7.11.8",
+    "viewerjs": "^1.11.6",
     "vue": "^2.6.12",
     "vue-awesome-swiper": "^4.1.1",
     "vue-cropperjs": "^4.2.0",
@@ -58,4 +60,4 @@
     "less-loader": "^5.0.0",
     "vue-template-compiler": "^2.6.12"
   }
-}
+}

+ 2 - 4
packages/qjkankan-editor/src/framework/EditorHead.vue

@@ -2,12 +2,10 @@
   <header class="app-head" app-border dir-bottom>
     <a
       class="app-head-back"
-      :href="`./material.html?lang=${$lang}#/${
-        routerForm === 'works' ? 'works' : 'camList'
-      }`"
+      :href="`./material.html?lang=${$lang}&from=${routerForm}#/works`"
     >
       <i class="iconfont icon-editor_return"></i>
-      {{ routerForm === "works" ? back_myworks : "返回相机作品" }}
+      {{ back_myworks }}
     </a>
     <span class="app-head-title">{{ info.name }}</span>
     <div

+ 0 - 7
packages/qjkankan-editor/src/framework/material/header.vue

@@ -92,13 +92,6 @@ export default {
             path: "/pano",
           },
         },
-        {
-          name: "相机作品",
-          id: "camList",
-          path: {
-            path: "/camList",
-          },
-        },
       ],
     };
   },

+ 790 - 0
packages/qjkankan-editor/src/views/material/works/cam.vue

@@ -0,0 +1,790 @@
+<template>
+  <div
+    class="scroll-container"
+    ref="w-list-ref"
+    @scroll.self="onWorkListScroll"
+  >
+    <div class="back-top" @click="onClickBackTop" v-show="isShowBackTopBtn">
+      <i class="iconfont icon-top"></i>
+    </div>
+    <div class="mask" v-show="isShowMask"></div>
+    <!-- 断网时,输入关键字触发网络请求后,骨架图的隐藏会触发v-infinite-scroll,原因未知。进而导致循环触发,因此list为空时要禁用v-infinite-scroll -->
+    <ul
+      class="w-list"
+      v-if="!(list.length === 0 && !hasMoreData)"
+      v-infinite-scroll="requestMoreData"
+      :infinite-scroll-disabled="
+        !hasMoreData || isRequestingMoreData || list.length === 0
+      "
+    >
+      <!-- <li class="add-work" @click="add">
+        <div class="wrapper">
+          <div class="add-con">
+            <div>
+              <i class="iconfont icon-works_add"></i>
+            </div>
+            <span>{{ create }}</span>
+          </div>
+        </div>
+      </li> -->
+      <!-- 骨架图 -->
+      <template v-if="isRequestingMoreData && list.length === 0">
+        <li v-for="index in 19" :key="index">
+          <div class="wrapper">
+            <workCardSkeleton></workCardSkeleton>
+          </div>
+        </li>
+      </template>
+      <li
+        v-for="(item, i) in list"
+        :key="i"
+        :class="{ 'has-more-data': hasMoreData }"
+      >
+        <div class="wrapper">
+          <div class="li-hover">
+            <span class="lipreview" @click="handlePreview(item)">{{
+              preview
+            }}</span>
+            <ul class="oper">
+              <li class="comfirmhover" @click="edit(item)">
+                <i class="iconfont icon-works_editor"></i>{{ edittips }}
+              </li>
+              <li class="comfirmhover" @click="openShare(item)">
+                <i class="iconfont icon-works_share"></i>{{ share }}
+              </li>
+              <li class="cancelhover" @click="del(item, i)">
+                <i class="iconfont icon-works_delete"></i>{{ deltips }}
+              </li>
+            </ul>
+          </div>
+          <div class="img" @click="handlePreview(item)">
+            <img class="real" :src="item.icon || $thumb" alt="" />
+          </div>
+          <div class="li-info">
+            <div>
+              <span class="shenglve tttttt" :title="item.name || no_title">{{
+                item.name || no_title
+              }}</span>
+            </div>
+            <div>
+              <span>{{ item.createTime.split(" ")[0] }}</span>
+              <div :title="item.visit">
+                <i class="iconfont icon-works_look"></i
+                >{{ item.visit > 10000 ? "1w+" : item.visit }}
+              </div>
+            </div>
+          </div>
+        </div>
+      </li>
+      <div
+        class="work-list-loading-wrapper"
+        v-show="isRequestingMoreData && list.length !== 0"
+      >
+        <img
+          class="work-list-loading"
+          :src="require('@/assets/images/icons/work-list-loading.gif')"
+        />
+      </div>
+    </ul>
+    <div
+      class="nodata"
+      v-if="list.length == 0 && !hasMoreData && lastestUsedSearchKey"
+    >
+      <img :src="$noresult" alt="" />
+      <span>{{ no_search_result }}~</span>
+    </div>
+    <div
+      class="nodata"
+      v-if="list.length == 0 && !hasMoreData && !lastestUsedSearchKey"
+    >
+      <img :src="config.empty" alt="" />
+      <span>{{ no_works }}</span>
+      <button @click="add" class="upload-btn-in-table">{{ create }}</button>
+    </div>
+
+    <share
+      :show="showShare"
+      :item="shareItem"
+      @close="showShare = false"
+    ></share>
+
+    <preview
+      v-if="showItem"
+      :name="showItem.name"
+      :show="showPreview"
+      :ifr="`./show.html?id=${showItem.id}&lang=${$lang}`"
+      :dark="false"
+      @close="showPreview = false"
+    />
+
+    <div class="dialog" style="z-index: 1000" v-if="isShowMaterialSelector">
+      <MaterialSelector
+        :isDarkTheme="false"
+        :title="select_material"
+        :selectableType="['pano', '3D']"
+        :isMultiSelection="true"
+        initialMaterialType="pano"
+        @cancel="isShowMaterialSelector = false"
+        @submit="handleSubmitFromMaterialSelector"
+      />
+    </div>
+  </div>
+</template>
+
+<script>
+import share from "../popup/share";
+import preview from "@/components/preview";
+import workCardSkeleton from "@/components/workCardSkeleton.vue";
+import config from "@/config";
+import { debounce } from "@/utils/other.js";
+import MaterialSelector from "@/components/materialSelector.vue";
+import { mapGetters } from "vuex";
+import { i18n } from "@/lang";
+import { $waiting } from "@/components/shared/loading";
+
+import {
+  addWorks,
+  getCamWorksList,
+  delWorks,
+  getPanoInfo,
+  saveWorks,
+} from "@/api";
+
+export default {
+  components: {
+    share,
+    preview,
+    workCardSkeleton,
+    MaterialSelector,
+  },
+  computed: {
+    ...mapGetters(["info"]),
+  },
+  props: {
+    searchKey: String,
+  },
+  data() {
+    return {
+      myWorks: i18n.t("material.works.my"),
+      create: i18n.t("material.works.create"),
+      search: i18n.t("material.works.search"),
+      preview: i18n.t("material.works.preview"),
+      edittips: i18n.t("material.works.edit"),
+      share: i18n.t("material.works.share"),
+      deltips: i18n.t("material.works.delete"),
+      no_works: i18n.t("material.works.no_works"),
+      no_title: i18n.t("gather.no_title"),
+      no_search_result: i18n.t("gather.no_search_result"),
+      select_material: i18n.t("gather.select_material"),
+
+      config,
+      list: [],
+      workTotalNum: undefined,
+      hasMoreData: true,
+      isRequestingMoreData: false,
+
+      lastestUsedSearchKey: "",
+      isFilterFocus: false,
+
+      showShare: false,
+      showPreview: false,
+      showItem: "",
+      shareItem: "",
+      isBackingTop: false,
+      isShowBackTopBtn: false,
+      isShowMask: false,
+      isShowMaterialSelector: false,
+      newWorkId: "",
+    };
+  },
+  mounted() {
+    this.requestMoreData();
+  },
+  watch: {
+    searchKey: {
+      handler: function (val) {
+        if (val.length > 0) {
+          this.selectedList = [];
+        }
+        this.refreshListDebounced();
+      },
+      immediate: false,
+    },
+    workTotalNum: {
+      handler: function (val) {
+        if (val) {
+          this.$emit("updateNum", val);
+        }
+      },
+      immediate: true,
+    },
+  },
+  methods: {
+    onFilterFocus() {
+      this.isFilterFocus = true;
+    },
+    onFilterBlur() {
+      this.isFilterFocus = false;
+    },
+    refreshListDebounced: debounce(
+      function () {
+        this.list = [];
+        this.isRequestingMoreData = false;
+        this.hasMoreData = true;
+        this.requestMoreData();
+      },
+      500,
+      false
+    ),
+    openShare(data) {
+      console.log(data);
+      getPanoInfo(data.id, (data) => {
+        if (data.scenes.length <= 0) {
+          return this.$msg.warning(this.$i18n.t("material.works.no_link"));
+        }
+        this.showShare = true;
+        this.shareItem = data;
+      });
+    },
+
+    handlePreview(item) {
+      getPanoInfo(item.id, (data) => {
+        if (data.scenes.length <= 0) {
+          return this.$msg.warning(this.$i18n.t("material.works.no_link"));
+        }
+        this.showItem = {
+          ...item,
+          ...data,
+        };
+        this.showPreview = true;
+      });
+    },
+    add() {
+      // 新建作品,弹窗让用户给作品选择素材。
+      $waiting.show();
+      addWorks({}, (res) => {
+        $waiting.hide();
+        this.newWorkId = res.data.id;
+        this.isShowMaterialSelector = true;
+      });
+    },
+    handleSubmitFromMaterialSelector(selected) {
+      $waiting.show();
+      // 拿新作品的初始数据
+      getPanoInfo(
+        this.newWorkId,
+        // 拿到了。
+        (data) => {
+          // 往里边添加用户选中的素材。
+          this.$store.commit("SetInfo", data);
+          console.log("selected", selected);
+          for (const [key, item] of Object.entries(selected)) {
+            if (item.materialType === "pano") {
+              let newScene = {
+                icon: item.icon,
+                sceneCode: item.sceneCode,
+                sceneTitle: item.name,
+                category: this.info.catalogs[0].id,
+                type: "pano",
+                id: "s_" + this.$randomWord(true, 8, 8),
+              };
+
+              console.log("key", key);
+              if (Number(key) === 0) {
+                //新建时开天空mask
+                newScene = Object.assign(newScene, this.info.scenes[0]);
+                newScene.customMask.sky.isShow = true;
+                this.info.scenes[0] = newScene;
+              } else {
+                newScene = Object.assign(newScene, {
+                  customMask: this.info.scenes[0].customMask,
+                  initVisual: this.info.scenes[0].initVisual,
+                });
+                newScene.customMask.sky.isShow = true;
+                this.info.scenes.push(newScene);
+              }
+            } else if (item.materialType === "3D") {
+              let newScene = {
+                icon: item.thumb,
+                sceneCode: item.num,
+                sceneTitle: item.sceneName,
+                category: this.info.catalogs[0].id,
+                type: "4dkk",
+                id: "s_" + this.$randomWord(true, 8, 8),
+              };
+              if (Number(key) === 0) {
+                this.info.scenes[0] = null;
+                this.info.scenes[0] = newScene;
+              } else {
+                this.info.scenes.push(newScene);
+              }
+            }
+          }
+
+          // 保存新作品
+          saveWorks(
+            {
+              id: this.newWorkId,
+              password: "",
+              someData: {
+                ...this.info,
+                status: 1,
+                icon: this.info.scenes[0].icon,
+              },
+            },
+            // 保存成功
+            () => {
+              $waiting.hide();
+              // 隐藏素材选择弹窗
+              this.isShowMaterialSelector = false;
+
+              // 刷新作品列表
+              this.list = [];
+              this.isRequestingMoreData = false;
+              this.hasMoreData = true;
+              this.requestMoreData()
+                .then(() => {
+                  // 刷新成功
+
+                  // 弹出提示窗口
+                  this.$confirm({
+                    title: this.$i18n.t("tips_code.tips"),
+                    content: this.$i18n.t("material.works.had_created"),
+                    okText: this.$i18n.t("material.works.goto_preview"),
+                    ok: () => {
+                      this.handlePreview(this.list[0]);
+                      this.newWorkId = "";
+                      this.$store.commit("SetInfo", {});
+                    },
+                    ok2Text: this.$i18n.t("material.works.continue_edit"),
+                    ok2: () => {
+                      window.open(
+                        `./edit.html?id=${this.newWorkId}&lang=${this.$lang}`
+                      );
+                      this.newWorkId = "";
+                      this.$store.commit("SetInfo", {});
+                    },
+                  });
+                })
+                .catch(() => {
+                  this.$msg.message(
+                    this.$i18n.t("material.works.had_created_but_no_link")
+                  );
+                  console.error("已成功新建作品,但刷新作品列表失败。");
+                });
+            },
+            // 保存失败,删除新建的作品。
+            (error) => {
+              $waiting.hide();
+              console.error("保存失败:", error);
+              delWorks(this.newWorkId);
+              this.newWorkId = "";
+              this.$store.commit("SetInfo", {});
+            }
+          );
+        },
+        // 没拿到,删除新建的作品。
+        (error) => {
+          console.error("没拿到新建的作品数据:", error);
+          delWorks(this.newWorkId);
+          this.newWorkId = "";
+        }
+      );
+    },
+
+    edit(item) {
+      const from = this.$route.name;
+      window.open(`./edit.html?id=${item.id}&lang=${this.$lang}&from=cam`);
+    },
+
+    del(item, index) {
+      this.$confirm({
+        title: this.$i18n.t("material.works.delete_work"),
+        content: this.$i18n.t("material.works.comfirm_delete"),
+        ok: () => {
+          $waiting.show();
+
+          delWorks(item.id, () => {
+            this.$msg.success(this.$i18n.t("gather.delete_success"));
+            this.isRequestingMoreData = true;
+            const lastestUsedSearchKey = this.searchKey;
+            getCamWorksList(
+              {
+                pageNum: this.list.length,
+                pageSize: 1,
+                searchKey: this.searchKey,
+              },
+              (data) => {
+                $waiting.hide();
+                this.list.splice(index, 1);
+                this.list = this.list.concat(data.data.list);
+                if (this.list.length === data.data.total) {
+                  this.hasMoreData = false;
+                }
+                this.isRequestingMoreData = false;
+                this.lastestUsedSearchKey = lastestUsedSearchKey;
+                if (!lastestUsedSearchKey) {
+                  this.workTotalNum = data.data.total;
+                }
+                // TODO: 这是干啥呢?
+                this.$nextTick(() => {
+                  this.$bus.emit("refreshTips");
+                });
+              },
+              () => {
+                $waiting.hide();
+                this.lastestUsedSearchKey = lastestUsedSearchKey;
+                this.isRequestingMoreData = false;
+              }
+            );
+          });
+        },
+      });
+    },
+
+    requestMoreData() {
+      this.isRequestingMoreData = true;
+
+      const lastestUsedSearchKey = this.searchKey;
+      return new Promise((resolve, reject) => {
+        getCamWorksList(
+          {
+            pageNum: Math.floor(this.list.length / config.PAGE_SIZE) + 1,
+            pageSize: config.PAGE_SIZE,
+            searchKey: this.searchKey,
+          },
+          (data) => {
+            this.list = this.list.concat(data.data.list);
+            if (this.list.length === data.data.total) {
+              this.hasMoreData = false;
+            }
+            this.isRequestingMoreData = false;
+            this.lastestUsedSearchKey = lastestUsedSearchKey;
+            if (!lastestUsedSearchKey) {
+              this.workTotalNum = data.data.total;
+            }
+            // TODO: 这是干啥呢?
+            this.$nextTick(() => {
+              this.$bus.emit("refreshTips");
+            });
+            resolve();
+          },
+          () => {
+            this.isRequestingMoreData = false;
+            this.lastestUsedSearchKey = lastestUsedSearchKey;
+            reject();
+          }
+        );
+      });
+    },
+
+    onClickBackTop() {
+      if (this.isBackingTop) {
+        return;
+      }
+      this.isBackingTop = true;
+
+      const startTime = Date.now();
+      const totalScroll = this.$refs["w-list-ref"].scrollTop;
+      const fn = () => {
+        if (this.$refs["w-list-ref"].scrollTop === 0) {
+          this.isBackingTop = false;
+          return;
+        }
+
+        const nowTime = Date.now();
+        const assumeScrollTop =
+          totalScroll - ((nowTime - startTime) * totalScroll) / 500;
+        this.$refs["w-list-ref"].scrollTop =
+          assumeScrollTop > 0 ? assumeScrollTop : 0;
+        requestAnimationFrame(fn);
+      };
+      requestAnimationFrame(fn);
+    },
+    onWorkListScroll(e) {
+      if (e.target.scrollTop >= 30) {
+        !this.isShowMask && (this.isShowMask = true);
+      } else {
+        this.isShowMask && (this.isShowMask = false);
+      }
+
+      if (e.target.scrollTop >= 600) {
+        this.isShowBackTopBtn = true;
+      } else {
+        this.isShowBackTopBtn = false;
+      }
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.scroll-container {
+  flex: 1 1 auto;
+  overflow: auto;
+  margin-left: calc((100vw - 100%) / -2);
+  padding-left: calc((100vw - 100%) / 2);
+  margin-right: calc((100vw - 100%) / -2);
+  padding-right: calc((100vw - 100%) / 2);
+  &::-webkit-scrollbar {
+    width: 0;
+    height: 0;
+  }
+  .back-top {
+    position: absolute;
+    right: -80px;
+    bottom: 30px;
+    width: 60px;
+    height: 60px;
+    border-radius: 8px;
+    background-color: #fff;
+    z-index: 1;
+    color: #c8c9cc;
+
+    &:hover {
+      color: #323233;
+    }
+
+    cursor: pointer;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+
+    i {
+      font-size: 20px;
+    }
+  }
+
+  .mask {
+    position: absolute;
+    width: 100%;
+    top: 180px;
+    height: 30px;
+    background: linear-gradient(rgb(239, 242, 244), rgba(255, 255, 255, 0));
+    z-index: 1;
+    pointer-events: none;
+  }
+
+  .w-list {
+    margin-top: 22px;
+    padding-top: 8px;
+    width: 100%;
+    @gap: 20px;
+    display: flex;
+    flex-wrap: wrap;
+    align-content: flex-start;
+
+    > li {
+      width: calc((100% - @gap * 4) / 5);
+      height: 322px;
+      margin-bottom: @gap;
+      margin-right: @gap;
+
+      &:nth-of-type(5n) {
+        margin-right: 0;
+      }
+
+      // 因为有个“创建作品”card占着空间,每次拿20个数据,每行五个,又不想每次拿到数据后最后一行只有一个card,所以把最后那个card隐藏掉。
+      &:last-of-type.has-more-data {
+        display: none;
+      }
+
+      .wrapper {
+        height: 100%;
+        background: #fff;
+        position: relative;
+        border-radius: 6px;
+        overflow: hidden;
+
+        .li-hover {
+          display: none;
+          width: 100%;
+          height: 240px;
+          position: absolute;
+          top: 0;
+          left: 0;
+          z-index: 99;
+          background: rgba(0, 0, 0, 0.6);
+
+          .lipreview {
+            position: absolute;
+            top: 50%;
+            left: 50%;
+            transform: translate(-50%, -50%);
+            color: #fff;
+            display: inline-block;
+            line-height: 40px;
+            height: 40px;
+            width: 100px;
+            text-align: center;
+            border-radius: 22px;
+            cursor: pointer;
+            background-color: transparent;
+            border: 1px solid #fff;
+
+            &:hover {
+              border: none;
+              background: #1983f6;
+            }
+          }
+
+          .oper {
+            display: flex;
+            justify-content: space-around;
+            align-items: center;
+            position: absolute;
+            bottom: 10px;
+            left: 0;
+            width: 100%;
+
+            > li {
+              color: #fff;
+              font-size: 13px;
+              display: flex;
+              align-items: center;
+              cursor: pointer;
+
+              > i {
+                font-size: 20px;
+                margin-right: 4px;
+              }
+            }
+          }
+        }
+
+        .img {
+          width: 100%;
+          height: 240px;
+          position: relative;
+          overflow: hidden;
+          cursor: pointer;
+
+          .real {
+            height: 100%;
+            position: absolute;
+            top: 0;
+            left: 50%;
+            transform: translateX(-50%);
+            z-index: 0;
+            transition: all ease 0.3s;
+          }
+        }
+
+        .li-info {
+          font-size: 14px;
+          padding: 10px;
+
+          > div {
+            text-align: left;
+
+            &:first-of-type {
+              > span {
+                font-weight: bold;
+                margin-bottom: 10px;
+                display: inline-block;
+                text-overflow: ellipsis;
+                overflow: hidden;
+                white-space: nowrap;
+                cursor: pointer;
+                color: #323233;
+                font-size: 16px;
+              }
+            }
+
+            &:last-of-type {
+              display: flex;
+              justify-content: space-between;
+              align-items: center;
+
+              > span {
+                font-size: 14px;
+                color: #969799;
+              }
+
+              > div {
+                color: #969799;
+
+                i {
+                  margin-right: 6px;
+                }
+              }
+            }
+          }
+        }
+      }
+
+      &:hover {
+        .wrapper {
+          box-shadow: 0px 2px 12px 0px rgba(50, 50, 51, 0.12);
+          transform: translateY(-6px);
+
+          .li-hover {
+            display: block;
+          }
+
+          .img {
+            .real {
+              height: 108%;
+            }
+          }
+        }
+      }
+    }
+
+    .add-work {
+      .wrapper {
+        .add-con {
+          position: absolute;
+          top: 50%;
+          left: 50%;
+          transform: translate(-50%, -50%);
+          text-align: center;
+
+          div {
+            width: 60px;
+            height: 60px;
+            border-radius: 50%;
+            background: linear-gradient(144deg, #00aefb 0%, #0076f6 100%);
+            position: relative;
+            cursor: pointer;
+            margin: 0 auto;
+
+            > i {
+              font-size: 16px;
+              position: absolute;
+              top: 50%;
+              left: 50%;
+              transform: translate(-50%, -50%);
+              color: #fff;
+            }
+          }
+
+          span {
+            color: #333333;
+            display: inline-block;
+            margin-top: 8px;
+            font-size: 14px;
+          }
+        }
+      }
+    }
+
+    .work-list-loading-wrapper {
+      width: 100%;
+      margin-top: 20px;
+      margin-bottom: 22px;
+
+      .work-list-loading {
+        display: block;
+        margin: 0 auto;
+        width: 50px;
+        height: 8px;
+      }
+    }
+  }
+}
+</style>
+
+<style lang="less" scoped>
+@import "../style.less";
+</style>

+ 64 - 478
packages/qjkankan-editor/src/views/material/works/index.vue

@@ -1,13 +1,16 @@
 <template>
   <div class="works con">
-    <div class="back-top" @click="onClickBackTop" v-show="isShowBackTopBtn">
-      <i class="iconfont icon-top"></i>
-    </div>
     <div class="tab">
-      <span
-        >{{ myWorks }}
-        {{ workTotalNum !== undefined ? `(${workTotalNum})` : "" }}</span
-      >
+      <div class="tab-l">
+        <span @click="changeTab('list')" :class="{ active: isActive('list') }">
+          在线制作
+          {{ work1TotalNum ? `(${work1TotalNum})` : "" }}
+        </span>
+        <span @click="changeTab('cam')" :class="{ active: isActive('cam') }">
+          相机生成
+          {{ work2TotalNum ? `(${work2TotalNum})` : "" }}
+        </span>
+      </div>
       <div class="tab-r">
         <div class="filter">
           <div
@@ -26,500 +29,73 @@
         </div>
       </div>
     </div>
-    <div class="mask" v-show="isShowMask"></div>
-    <!-- 断网时,输入关键字触发网络请求后,骨架图的隐藏会触发v-infinite-scroll,原因未知。进而导致循环触发,因此list为空时要禁用v-infinite-scroll -->
-    <ul
-      class="w-list"
-      v-if="!(list.length === 0 && !hasMoreData)"
-      v-infinite-scroll="requestMoreData"
-      :infinite-scroll-disabled="
-        !hasMoreData || isRequestingMoreData || list.length === 0
-      "
-      ref="w-list-ref"
-      @scroll.self="onWorkListScroll"
-    >
-      <li class="add-work" @click="add">
-        <div class="wrapper">
-          <div class="add-con">
-            <div>
-              <i class="iconfont icon-works_add"></i>
-            </div>
-            <span>{{ create }}</span>
-          </div>
-        </div>
-      </li>
-      <!-- 骨架图 -->
-      <template v-if="isRequestingMoreData && list.length === 0">
-        <li v-for="index in 19" :key="index">
-          <div class="wrapper">
-            <workCardSkeleton></workCardSkeleton>
-          </div>
-        </li>
-      </template>
-      <li
-        v-for="(item, i) in list"
-        :key="i"
-        :class="{ 'has-more-data': hasMoreData }"
-      >
-        <div class="wrapper">
-          <div class="li-hover">
-            <span class="lipreview" @click="handlePreview(item)">{{
-              preview
-            }}</span>
-            <ul class="oper">
-              <li class="comfirmhover" @click="edit(item)">
-                <i class="iconfont icon-works_editor"></i>{{ edittips }}
-              </li>
-              <li class="comfirmhover" @click="openShare(item)">
-                <i class="iconfont icon-works_share"></i>{{ share }}
-              </li>
-              <li class="cancelhover" @click="del(item, i)">
-                <i class="iconfont icon-works_delete"></i>{{ deltips }}
-              </li>
-            </ul>
-          </div>
-          <div class="img" @click="handlePreview(item)">
-            <img class="real" :src="item.icon || $thumb" alt="" />
-          </div>
-          <div class="li-info">
-            <div>
-              <span class="shenglve tttttt" :title="item.name || no_title">{{
-                item.name || no_title
-              }}</span>
-            </div>
-            <div>
-              <span>{{ item.createTime.split(" ")[0] }}</span>
-              <div :title="item.visit">
-                <i class="iconfont icon-works_look"></i
-                >{{ item.visit > 10000 ? "1w+" : item.visit }}
-              </div>
-            </div>
-          </div>
-        </div>
-      </li>
-      <div
-        class="work-list-loading-wrapper"
-        v-show="isRequestingMoreData && list.length !== 0"
-      >
-        <img
-          class="work-list-loading"
-          :src="require('@/assets/images/icons/work-list-loading.gif')"
-        />
-      </div>
-    </ul>
-    <div
-      class="nodata"
-      v-if="list.length == 0 && !hasMoreData && lastestUsedSearchKey"
-    >
-      <img :src="$noresult" alt="" />
-      <span>{{ no_search_result }}~</span>
-    </div>
-    <div
-      class="nodata"
-      v-if="list.length == 0 && !hasMoreData && !lastestUsedSearchKey"
-    >
-      <img :src="config.empty" alt="" />
-      <span>{{ no_works }}</span>
-      <button @click="add" class="upload-btn-in-table">{{ create }}</button>
-    </div>
-    <share
-      :show="showShare"
-      :item="shareItem"
-      @close="showShare = false"
-    ></share>
-    <preview
-      v-if="showItem"
-      :name="showItem.name"
-      :show="showPreview"
-      :ifr="`./show.html?id=${showItem.id}&lang=${$lang}`"
-      :dark="false"
-      @close="showPreview = false"
-    />
-    <div class="dialog" style="z-index: 1000" v-if="isShowMaterialSelector">
-      <MaterialSelector
-        :isDarkTheme="false"
-        :title="select_material"
-        :selectableType="['pano', '3D']"
-        :isMultiSelection="true"
-        initialMaterialType="pano"
-        @cancel="isShowMaterialSelector = false"
-        @submit="handleSubmitFromMaterialSelector"
-      />
-    </div>
+
+    <template v-if="isActive('list')">
+      <list @updateNum="handleWorkNum($event, 1)" :searchKey="searchKey"></list>
+    </template>
+    <template v-if="isActive('cam')">
+      <CamList
+        @updateNum="handleWorkNum($event, 2)"
+        :searchKey="searchKey"
+      ></CamList>
+    </template>
   </div>
 </template>
 
 <script>
-import share from "../popup/share";
-import preview from "@/components/preview";
-import workCardSkeleton from "@/components/workCardSkeleton.vue";
-import config from "@/config";
-import { debounce } from "@/utils/other.js";
-import MaterialSelector from "@/components/materialSelector.vue";
 import { mapGetters } from "vuex";
 import { i18n } from "@/lang";
-import { $waiting } from "@/components/shared/loading";
 
-import {
-  addWorks,
-  getWorksList,
-  delWorks,
-  getPanoInfo,
-  saveWorks,
-} from "@/api";
+import List from "./list";
+import CamList from "./cam";
+import browser from "@/utils/browser";
+
+const allTabType = ["list", "cam"];
 
 export default {
   components: {
-    share,
-    preview,
-    workCardSkeleton,
-    MaterialSelector,
+    List,
+    CamList,
   },
   computed: {
     ...mapGetters(["info"]),
+    isActive: function () {
+      return (key) => {
+        return this.currentTab === key;
+      };
+    },
   },
   data() {
     return {
-      myWorks: i18n.t("material.works.my"),
-      create: i18n.t("material.works.create"),
-      search: i18n.t("material.works.search"),
-      preview: i18n.t("material.works.preview"),
-      edittips: i18n.t("material.works.edit"),
-      share: i18n.t("material.works.share"),
-      deltips: i18n.t("material.works.delete"),
-      no_works: i18n.t("material.works.no_works"),
-      no_title: i18n.t("gather.no_title"),
-      no_search_result: i18n.t("gather.no_search_result"),
-      select_material: i18n.t("gather.select_material"),
-
-      config,
-      list: [],
-      workTotalNum: undefined,
-      hasMoreData: true,
-      isRequestingMoreData: false,
-
       searchKey: "",
-      // 因为searchKey的变化经过debounce、异步请求的延时,才会反映到数据列表的变化上,所以是否显示、显示哪种无数据提示,也要等到数据列表变化后,根据数据列表是否为空,以及引发本次变化的那个searchKey瞬时值来决定。本变量就是用来保存那个瞬时值。
-      lastestUsedSearchKey: "",
+      work1TotalNum: "",
+      work2TotalNum: "",
       isFilterFocus: false,
-
-      showShare: false,
-      showPreview: false,
-      showItem: "",
-      shareItem: "",
-
-      isBackingTop: false,
-      isShowBackTopBtn: false,
-
-      isShowMask: false,
-
-      isShowMaterialSelector: false,
-      newWorkId: "",
+      search: i18n.t("material.works.search"),
+      searchKey: "",
+      currentTab: "list",
     };
   },
+
   mounted() {
-    this.requestMoreData();
-  },
-  watch: {
-    searchKey: {
-      handler: function (val) {
-        if (val.length > 0) {
-          this.selectedList = [];
-        }
-        this.refreshListDebounced();
-      },
-      immediate: false,
-    },
+    const from = browser.urlQueryValue("from");
+    this.changeTab(from);
   },
   methods: {
-    onFilterFocus() {
-      this.isFilterFocus = true;
-    },
-    onFilterBlur() {
-      this.isFilterFocus = false;
-    },
-    refreshListDebounced: debounce(
-      function () {
-        this.list = [];
-        this.isRequestingMoreData = false;
-        this.hasMoreData = true;
-        this.requestMoreData();
-      },
-      500,
-      false
-    ),
-    openShare(data) {
-      console.log(data);
-      getPanoInfo(data.id, (data) => {
-        if (data.scenes.length <= 0) {
-          return this.$msg.warning(this.$i18n.t("material.works.no_link"));
-        }
-        this.showShare = true;
-        this.shareItem = data;
-      });
-    },
-
-    handlePreview(item) {
-      getPanoInfo(item.id, (data) => {
-        if (data.scenes.length <= 0) {
-          return this.$msg.warning(this.$i18n.t("material.works.no_link"));
-        }
-        this.showItem = {
-          ...item,
-          ...data,
-        };
-        this.showPreview = true;
-      });
-    },
-    add() {
-      // 新建作品,弹窗让用户给作品选择素材。
-      $waiting.show();
-      addWorks({}, (res) => {
-        $waiting.hide();
-        this.newWorkId = res.data.id;
-        this.isShowMaterialSelector = true;
-      });
-    },
-    handleSubmitFromMaterialSelector(selected) {
-      $waiting.show();
-      // 拿新作品的初始数据
-      getPanoInfo(
-        this.newWorkId,
-        // 拿到了。
-        (data) => {
-          // 往里边添加用户选中的素材。
-          this.$store.commit("SetInfo", data);
-          console.log("selected", selected);
-          for (const [key, item] of Object.entries(selected)) {
-            if (item.materialType === "pano") {
-              let newScene = {
-                icon: item.icon,
-                sceneCode: item.sceneCode,
-                sceneTitle: item.name,
-                category: this.info.catalogs[0].id,
-                type: "pano",
-                id: "s_" + this.$randomWord(true, 8, 8),
-              };
-
-              console.log("key", key);
-              if (Number(key) === 0) {
-                //新建时开天空mask
-                newScene = Object.assign(newScene, this.info.scenes[0]);
-                newScene.customMask.sky.isShow = true;
-                this.info.scenes[0] = newScene;
-              } else {
-                newScene = Object.assign(newScene, {
-                  customMask: this.info.scenes[0].customMask,
-                  initVisual: this.info.scenes[0].initVisual
-                });
-                newScene.customMask.sky.isShow = true;
-                this.info.scenes.push(newScene);
-              }
-            } else if (item.materialType === "3D") {
-              let newScene = {
-                icon: item.thumb,
-                sceneCode: item.num,
-                sceneTitle: item.sceneName,
-                category: this.info.catalogs[0].id,
-                type: "4dkk",
-                id: "s_" + this.$randomWord(true, 8, 8),
-              };
-              if (Number(key) === 0) {
-                this.info.scenes[0] = null;
-                this.info.scenes[0] = newScene;
-              } else {
-                this.info.scenes.push(newScene);
-              }
-            }
-          }
-
-          // 保存新作品
-          saveWorks(
-            {
-              id: this.newWorkId,
-              password: "",
-              someData: {
-                ...this.info,
-                status: 1,
-                icon: this.info.scenes[0].icon,
-              },
-            },
-            // 保存成功
-            () => {
-              $waiting.hide();
-              // 隐藏素材选择弹窗
-              this.isShowMaterialSelector = false;
-
-              // 刷新作品列表
-              this.list = [];
-              this.isRequestingMoreData = false;
-              this.hasMoreData = true;
-              this.requestMoreData()
-                .then(() => {
-                  // 刷新成功
-
-                  // 弹出提示窗口
-                  this.$confirm({
-                    title: this.$i18n.t("tips_code.tips"),
-                    content: this.$i18n.t("material.works.had_created"),
-                    okText: this.$i18n.t("material.works.goto_preview"),
-                    ok: () => {
-                      this.handlePreview(this.list[0]);
-                      this.newWorkId = "";
-                      this.$store.commit("SetInfo", {});
-                    },
-                    ok2Text: this.$i18n.t("material.works.continue_edit"),
-                    ok2: () => {
-                      window.open(
-                        `./edit.html?id=${this.newWorkId}&lang=${this.$lang}`
-                      );
-                      this.newWorkId = "";
-                      this.$store.commit("SetInfo", {});
-                    },
-                  });
-                })
-                .catch(() => {
-                  this.$msg.message(
-                    this.$i18n.t("material.works.had_created_but_no_link")
-                  );
-                  console.error("已成功新建作品,但刷新作品列表失败。");
-                });
-            },
-            // 保存失败,删除新建的作品。
-            (error) => {
-              $waiting.hide();
-              console.error("保存失败:", error);
-              delWorks(this.newWorkId);
-              this.newWorkId = "";
-              this.$store.commit("SetInfo", {});
-            }
-          );
-        },
-        // 没拿到,删除新建的作品。
-        (error) => {
-          console.error("没拿到新建的作品数据:", error);
-          delWorks(this.newWorkId);
-          this.newWorkId = "";
-        }
-      );
-    },
-    edit(item) {
-      const from = this.$route.name;
-      window.open(`./edit.html?id=${item.id}&lang=${this.$lang}&from=${from}`);
-    },
-   
-    del(item, index) {
-      this.$confirm({
-        title: this.$i18n.t("material.works.delete_work"),
-        content: this.$i18n.t("material.works.comfirm_delete"),
-        ok: () => {
-          $waiting.show();
-
-          delWorks(item.id, () => {
-            this.$msg.success(this.$i18n.t("gather.delete_success"));
-            this.isRequestingMoreData = true;
-            const lastestUsedSearchKey = this.searchKey;
-            getWorksList(
-              {
-                pageNum: this.list.length,
-                pageSize: 1,
-                searchKey: this.searchKey,
-              },
-              (data) => {
-                $waiting.hide();
-                this.list.splice(index, 1);
-                this.list = this.list.concat(data.data.list);
-                if (this.list.length === data.data.total) {
-                  this.hasMoreData = false;
-                }
-                this.isRequestingMoreData = false;
-                this.lastestUsedSearchKey = lastestUsedSearchKey;
-                if (!lastestUsedSearchKey) {
-                  this.workTotalNum = data.data.total;
-                }
-                // TODO: 这是干啥呢?
-                this.$nextTick(() => {
-                  this.$bus.emit("refreshTips");
-                });
-              },
-              () => {
-                $waiting.hide();
-                this.lastestUsedSearchKey = lastestUsedSearchKey;
-                this.isRequestingMoreData = false;
-              }
-            );
-          });
-        },
-      });
-    },
-    requestMoreData() {
-      this.isRequestingMoreData = true;
-      const lastestUsedSearchKey = this.searchKey;
-      return new Promise((resolve, reject) => {
-        getWorksList(
-          {
-            pageNum: Math.floor(this.list.length / config.PAGE_SIZE) + 1,
-            pageSize: config.PAGE_SIZE,
-            searchKey: this.searchKey,
-          },
-          (data) => {
-            this.list = this.list.concat(data.data.list);
-            if (this.list.length === data.data.total) {
-              this.hasMoreData = false;
-            }
-            this.isRequestingMoreData = false;
-            this.lastestUsedSearchKey = lastestUsedSearchKey;
-            if (!lastestUsedSearchKey) {
-              this.workTotalNum = data.data.total;
-            }
-            // TODO: 这是干啥呢?
-            this.$nextTick(() => {
-              this.$bus.emit("refreshTips");
-            });
-            resolve();
-          },
-          () => {
-            this.isRequestingMoreData = false;
-            this.lastestUsedSearchKey = lastestUsedSearchKey;
-            reject();
-          }
-        );
-      });
-    },
-    onClickBackTop() {
-      if (this.isBackingTop) {
-        return;
+    onFilterBlur() {},
+    onFilterFocus() {},
+    changeTab(type) {
+      if (allTabType.includes(type)) {
+        this.currentTab = type;
       }
-      this.isBackingTop = true;
-
-      const startTime = Date.now();
-      const totalScroll = this.$refs["w-list-ref"].scrollTop;
-      const fn = () => {
-        if (this.$refs["w-list-ref"].scrollTop === 0) {
-          this.isBackingTop = false;
-          return;
-        }
-
-        const nowTime = Date.now();
-        const assumeScrollTop =
-          totalScroll - ((nowTime - startTime) * totalScroll) / 500;
-        this.$refs["w-list-ref"].scrollTop =
-          assumeScrollTop > 0 ? assumeScrollTop : 0;
-        requestAnimationFrame(fn);
-      };
-      requestAnimationFrame(fn);
     },
-    onWorkListScroll(e) {
-      if (e.target.scrollTop >= 30) {
-        !this.isShowMask && (this.isShowMask = true);
-      } else {
-        this.isShowMask && (this.isShowMask = false);
+    handleWorkNum(val, type) {
+      console.log("handleWorkNum", val, type);
+      if (type === 1) {
+        this.work1TotalNum = val;
       }
-
-      if (e.target.scrollTop >= 600) {
-        this.isShowBackTopBtn = true;
-      } else {
-        this.isShowBackTopBtn = false;
+      if (type === 2) {
+        this.work2TotalNum = val;
       }
     },
   },
@@ -566,9 +142,19 @@ export default {
     align-items: center;
     padding: 20px 30px;
 
-    > span {
-      font-size: 18px;
-      font-weight: bold;
+    .tab-l {
+      > span {
+        font-size: 18px;
+        font-weight: bold;
+        margin-right: 30px;
+        cursor: pointer;
+        &:hover {
+          color: #0076f6;
+        }
+        &.active {
+          color: #0076f6;
+        }
+      }
     }
 
     .tab-r {

+ 830 - 0
packages/qjkankan-editor/src/views/material/works/list copy.vue

@@ -0,0 +1,830 @@
+<template>
+  <div class="works con">
+    <div class="back-top" @click="onClickBackTop" v-show="isShowBackTopBtn">
+      <i class="iconfont icon-top"></i>
+    </div>
+    <div class="tab">
+      <span
+        >{{ myWorks }}
+        {{ workTotalNum !== undefined ? `(${workTotalNum})` : "" }}</span
+      >
+      <div class="tab-r">
+        <div class="filter">
+          <div
+            :class="{ active: isFilterFocus }"
+            @focusin="onFilterFocus"
+            @focusout="onFilterBlur"
+          >
+            <i class="iconfont iconworks_search search"></i>
+            <input type="text" :placeholder="search" v-model="searchKey" />
+            <i
+              v-if="searchKey"
+              @click="searchKey = ''"
+              class="iconfont icon-toast_red del"
+            ></i>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="mask" v-show="isShowMask"></div>
+    <!-- 断网时,输入关键字触发网络请求后,骨架图的隐藏会触发v-infinite-scroll,原因未知。进而导致循环触发,因此list为空时要禁用v-infinite-scroll -->
+    <ul
+      class="w-list"
+      v-if="!(list.length === 0 && !hasMoreData)"
+      v-infinite-scroll="requestMoreData"
+      :infinite-scroll-disabled="
+        !hasMoreData || isRequestingMoreData || list.length === 0
+      "
+      ref="w-list-ref"
+      @scroll.self="onWorkListScroll"
+    >
+      <li class="add-work" @click="add">
+        <div class="wrapper">
+          <div class="add-con">
+            <div>
+              <i class="iconfont icon-works_add"></i>
+            </div>
+            <span>{{ create }}</span>
+          </div>
+        </div>
+      </li>
+      <!-- 骨架图 -->
+      <template v-if="isRequestingMoreData && list.length === 0">
+        <li v-for="index in 19" :key="index">
+          <div class="wrapper">
+            <workCardSkeleton></workCardSkeleton>
+          </div>
+        </li>
+      </template>
+      <li
+        v-for="(item, i) in list"
+        :key="i"
+        :class="{ 'has-more-data': hasMoreData }"
+      >
+        <div class="wrapper">
+          <div class="li-hover">
+            <span class="lipreview" @click="handlePreview(item)">{{
+              preview
+            }}</span>
+            <ul class="oper">
+              <li class="comfirmhover" @click="edit(item)">
+                <i class="iconfont icon-works_editor"></i>{{ edittips }}
+              </li>
+              <li class="comfirmhover" @click="openShare(item)">
+                <i class="iconfont icon-works_share"></i>{{ share }}
+              </li>
+              <li class="cancelhover" @click="del(item, i)">
+                <i class="iconfont icon-works_delete"></i>{{ deltips }}
+              </li>
+            </ul>
+          </div>
+          <div class="img" @click="handlePreview(item)">
+            <img class="real" :src="item.icon || $thumb" alt="" />
+          </div>
+          <div class="li-info">
+            <div>
+              <span class="shenglve tttttt" :title="item.name || no_title">{{
+                item.name || no_title
+              }}</span>
+            </div>
+            <div>
+              <span>{{ item.createTime.split(" ")[0] }}</span>
+              <div :title="item.visit">
+                <i class="iconfont icon-works_look"></i
+                >{{ item.visit > 10000 ? "1w+" : item.visit }}
+              </div>
+            </div>
+          </div>
+        </div>
+      </li>
+      <div
+        class="work-list-loading-wrapper"
+        v-show="isRequestingMoreData && list.length !== 0"
+      >
+        <img
+          class="work-list-loading"
+          :src="require('@/assets/images/icons/work-list-loading.gif')"
+        />
+      </div>
+    </ul>
+    <div
+      class="nodata"
+      v-if="list.length == 0 && !hasMoreData && lastestUsedSearchKey"
+    >
+      <img :src="$noresult" alt="" />
+      <span>{{ no_search_result }}~</span>
+    </div>
+    <div
+      class="nodata"
+      v-if="list.length == 0 && !hasMoreData && !lastestUsedSearchKey"
+    >
+      <img :src="config.empty" alt="" />
+      <span>{{ no_works }}</span>
+      <button @click="add" class="upload-btn-in-table">{{ create }}</button>
+    </div>
+    <share
+      :show="showShare"
+      :item="shareItem"
+      @close="showShare = false"
+    ></share>
+    <preview
+      v-if="showItem"
+      :name="showItem.name"
+      :show="showPreview"
+      :ifr="`./show.html?id=${showItem.id}&lang=${$lang}`"
+      :dark="false"
+      @close="showPreview = false"
+    />
+    <div class="dialog" style="z-index: 1000" v-if="isShowMaterialSelector">
+      <MaterialSelector
+        :isDarkTheme="false"
+        :title="select_material"
+        :selectableType="['pano', '3D']"
+        :isMultiSelection="true"
+        initialMaterialType="pano"
+        @cancel="isShowMaterialSelector = false"
+        @submit="handleSubmitFromMaterialSelector"
+      />
+    </div>
+  </div>
+</template>
+
+<script>
+import share from "../popup/share";
+import preview from "@/components/preview";
+import workCardSkeleton from "@/components/workCardSkeleton.vue";
+import config from "@/config";
+import { debounce } from "@/utils/other.js";
+import MaterialSelector from "@/components/materialSelector.vue";
+import { mapGetters } from "vuex";
+import { i18n } from "@/lang";
+import { $waiting } from "@/components/shared/loading";
+
+import {
+  addWorks,
+  getWorksList,
+  delWorks,
+  getPanoInfo,
+  saveWorks,
+} from "@/api";
+
+export default {
+  components: {
+    share,
+    preview,
+    workCardSkeleton,
+    MaterialSelector,
+  },
+  computed: {
+    ...mapGetters(["info"]),
+  },
+  data() {
+    return {
+      myWorks: i18n.t("material.works.my"),
+      create: i18n.t("material.works.create"),
+      search: i18n.t("material.works.search"),
+      preview: i18n.t("material.works.preview"),
+      edittips: i18n.t("material.works.edit"),
+      share: i18n.t("material.works.share"),
+      deltips: i18n.t("material.works.delete"),
+      no_works: i18n.t("material.works.no_works"),
+      no_title: i18n.t("gather.no_title"),
+      no_search_result: i18n.t("gather.no_search_result"),
+      select_material: i18n.t("gather.select_material"),
+
+      config,
+      list: [],
+      workTotalNum: undefined,
+      hasMoreData: true,
+      isRequestingMoreData: false,
+
+      searchKey: "",
+      // 因为searchKey的变化经过debounce、异步请求的延时,才会反映到数据列表的变化上,所以是否显示、显示哪种无数据提示,也要等到数据列表变化后,根据数据列表是否为空,以及引发本次变化的那个searchKey瞬时值来决定。本变量就是用来保存那个瞬时值。
+      lastestUsedSearchKey: "",
+      isFilterFocus: false,
+
+      showShare: false,
+      showPreview: false,
+      showItem: "",
+      shareItem: "",
+
+      isBackingTop: false,
+      isShowBackTopBtn: false,
+
+      isShowMask: false,
+
+      isShowMaterialSelector: false,
+      newWorkId: "",
+    };
+  },
+  mounted() {
+    this.requestMoreData();
+  },
+  watch: {
+    searchKey: {
+      handler: function (val) {
+        if (val.length > 0) {
+          this.selectedList = [];
+        }
+        this.refreshListDebounced();
+      },
+      immediate: false,
+    },
+  },
+  methods: {
+    onFilterFocus() {
+      this.isFilterFocus = true;
+    },
+    onFilterBlur() {
+      this.isFilterFocus = false;
+    },
+    refreshListDebounced: debounce(
+      function () {
+        this.list = [];
+        this.isRequestingMoreData = false;
+        this.hasMoreData = true;
+        this.requestMoreData();
+      },
+      500,
+      false
+    ),
+    openShare(data) {
+      console.log(data);
+      getPanoInfo(data.id, (data) => {
+        if (data.scenes.length <= 0) {
+          return this.$msg.warning(this.$i18n.t("material.works.no_link"));
+        }
+        this.showShare = true;
+        this.shareItem = data;
+      });
+    },
+
+    handlePreview(item) {
+      getPanoInfo(item.id, (data) => {
+        if (data.scenes.length <= 0) {
+          return this.$msg.warning(this.$i18n.t("material.works.no_link"));
+        }
+        this.showItem = {
+          ...item,
+          ...data,
+        };
+        this.showPreview = true;
+      });
+    },
+    add() {
+      // 新建作品,弹窗让用户给作品选择素材。
+      $waiting.show();
+      addWorks({}, (res) => {
+        $waiting.hide();
+        this.newWorkId = res.data.id;
+        this.isShowMaterialSelector = true;
+      });
+    },
+    handleSubmitFromMaterialSelector(selected) {
+      $waiting.show();
+      // 拿新作品的初始数据
+      getPanoInfo(
+        this.newWorkId,
+        // 拿到了。
+        (data) => {
+          // 往里边添加用户选中的素材。
+          this.$store.commit("SetInfo", data);
+          console.log("selected", selected);
+          for (const [key, item] of Object.entries(selected)) {
+            if (item.materialType === "pano") {
+              let newScene = {
+                icon: item.icon,
+                sceneCode: item.sceneCode,
+                sceneTitle: item.name,
+                category: this.info.catalogs[0].id,
+                type: "pano",
+                id: "s_" + this.$randomWord(true, 8, 8),
+              };
+
+              console.log("key", key);
+              if (Number(key) === 0) {
+                //新建时开天空mask
+                newScene = Object.assign(newScene, this.info.scenes[0]);
+                newScene.customMask.sky.isShow = true;
+                this.info.scenes[0] = newScene;
+              } else {
+                newScene = Object.assign(newScene, {
+                  customMask: this.info.scenes[0].customMask,
+                  initVisual: this.info.scenes[0].initVisual,
+                });
+                newScene.customMask.sky.isShow = true;
+                this.info.scenes.push(newScene);
+              }
+            } else if (item.materialType === "3D") {
+              let newScene = {
+                icon: item.thumb,
+                sceneCode: item.num,
+                sceneTitle: item.sceneName,
+                category: this.info.catalogs[0].id,
+                type: "4dkk",
+                id: "s_" + this.$randomWord(true, 8, 8),
+              };
+              if (Number(key) === 0) {
+                this.info.scenes[0] = null;
+                this.info.scenes[0] = newScene;
+              } else {
+                this.info.scenes.push(newScene);
+              }
+            }
+          }
+
+          // 保存新作品
+          saveWorks(
+            {
+              id: this.newWorkId,
+              password: "",
+              someData: {
+                ...this.info,
+                status: 1,
+                icon: this.info.scenes[0].icon,
+              },
+            },
+            // 保存成功
+            () => {
+              $waiting.hide();
+              // 隐藏素材选择弹窗
+              this.isShowMaterialSelector = false;
+
+              // 刷新作品列表
+              this.list = [];
+              this.isRequestingMoreData = false;
+              this.hasMoreData = true;
+              this.requestMoreData()
+                .then(() => {
+                  // 刷新成功
+
+                  // 弹出提示窗口
+                  this.$confirm({
+                    title: this.$i18n.t("tips_code.tips"),
+                    content: this.$i18n.t("material.works.had_created"),
+                    okText: this.$i18n.t("material.works.goto_preview"),
+                    ok: () => {
+                      this.handlePreview(this.list[0]);
+                      this.newWorkId = "";
+                      this.$store.commit("SetInfo", {});
+                    },
+                    ok2Text: this.$i18n.t("material.works.continue_edit"),
+                    ok2: () => {
+                      window.open(
+                        `./edit.html?id=${this.newWorkId}&lang=${this.$lang}`
+                      );
+                      this.newWorkId = "";
+                      this.$store.commit("SetInfo", {});
+                    },
+                  });
+                })
+                .catch(() => {
+                  this.$msg.message(
+                    this.$i18n.t("material.works.had_created_but_no_link")
+                  );
+                  console.error("已成功新建作品,但刷新作品列表失败。");
+                });
+            },
+            // 保存失败,删除新建的作品。
+            (error) => {
+              $waiting.hide();
+              console.error("保存失败:", error);
+              delWorks(this.newWorkId);
+              this.newWorkId = "";
+              this.$store.commit("SetInfo", {});
+            }
+          );
+        },
+        // 没拿到,删除新建的作品。
+        (error) => {
+          console.error("没拿到新建的作品数据:", error);
+          delWorks(this.newWorkId);
+          this.newWorkId = "";
+        }
+      );
+    },
+    
+    edit(item) {
+      const from = this.$route.name;
+      window.open(`./edit.html?id=${item.id}&lang=${this.$lang}&from=${from}`);
+    },
+
+    del(item, index) {
+      this.$confirm({
+        title: this.$i18n.t("material.works.delete_work"),
+        content: this.$i18n.t("material.works.comfirm_delete"),
+        ok: () => {
+          $waiting.show();
+
+          delWorks(item.id, () => {
+            this.$msg.success(this.$i18n.t("gather.delete_success"));
+            this.isRequestingMoreData = true;
+            const lastestUsedSearchKey = this.searchKey;
+            getWorksList(
+              {
+                pageNum: this.list.length,
+                pageSize: 1,
+                searchKey: this.searchKey,
+              },
+              (data) => {
+                $waiting.hide();
+                this.list.splice(index, 1);
+                this.list = this.list.concat(data.data.list);
+                if (this.list.length === data.data.total) {
+                  this.hasMoreData = false;
+                }
+                this.isRequestingMoreData = false;
+                this.lastestUsedSearchKey = lastestUsedSearchKey;
+                if (!lastestUsedSearchKey) {
+                  this.workTotalNum = data.data.total;
+                }
+                // TODO: 这是干啥呢?
+                this.$nextTick(() => {
+                  this.$bus.emit("refreshTips");
+                });
+              },
+              () => {
+                $waiting.hide();
+                this.lastestUsedSearchKey = lastestUsedSearchKey;
+                this.isRequestingMoreData = false;
+              }
+            );
+          });
+        },
+      });
+    },
+
+    requestMoreData() {
+      this.isRequestingMoreData = true;
+      const lastestUsedSearchKey = this.searchKey;
+      return new Promise((resolve, reject) => {
+        getWorksList(
+          {
+            pageNum: Math.floor(this.list.length / config.PAGE_SIZE) + 1,
+            pageSize: config.PAGE_SIZE,
+            searchKey: this.searchKey,
+          },
+          (data) => {
+            this.list = this.list.concat(data.data.list);
+            if (this.list.length === data.data.total) {
+              this.hasMoreData = false;
+            }
+            this.isRequestingMoreData = false;
+            this.lastestUsedSearchKey = lastestUsedSearchKey;
+            if (!lastestUsedSearchKey) {
+              this.workTotalNum = data.data.total;
+            }
+            // TODO: 这是干啥呢?
+            this.$nextTick(() => {
+              this.$bus.emit("refreshTips");
+            });
+            resolve();
+          },
+          () => {
+            this.isRequestingMoreData = false;
+            this.lastestUsedSearchKey = lastestUsedSearchKey;
+            reject();
+          }
+        );
+      });
+    },
+
+    onClickBackTop() {
+      if (this.isBackingTop) {
+        return;
+      }
+      this.isBackingTop = true;
+
+      const startTime = Date.now();
+      const totalScroll = this.$refs["w-list-ref"].scrollTop;
+      const fn = () => {
+        if (this.$refs["w-list-ref"].scrollTop === 0) {
+          this.isBackingTop = false;
+          return;
+        }
+
+        const nowTime = Date.now();
+        const assumeScrollTop =
+          totalScroll - ((nowTime - startTime) * totalScroll) / 500;
+        this.$refs["w-list-ref"].scrollTop =
+          assumeScrollTop > 0 ? assumeScrollTop : 0;
+        requestAnimationFrame(fn);
+      };
+      requestAnimationFrame(fn);
+    },
+    onWorkListScroll(e) {
+      if (e.target.scrollTop >= 30) {
+        !this.isShowMask && (this.isShowMask = true);
+      } else {
+        this.isShowMask && (this.isShowMask = false);
+      }
+
+      if (e.target.scrollTop >= 600) {
+        this.isShowBackTopBtn = true;
+      } else {
+        this.isShowBackTopBtn = false;
+      }
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.works {
+  width: 100%;
+  flex-direction: column;
+  position: relative;
+
+  .back-top {
+    position: absolute;
+    right: -80px;
+    bottom: 30px;
+    width: 60px;
+    height: 60px;
+    border-radius: 8px;
+    background-color: #fff;
+    z-index: 1;
+    color: #c8c9cc;
+
+    &:hover {
+      color: #323233;
+    }
+
+    cursor: pointer;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+
+    i {
+      font-size: 20px;
+    }
+  }
+
+  .tab {
+    flex: 0 0 auto;
+    width: 100%;
+    display: flex;
+    background: #fff;
+    justify-content: space-between;
+    align-items: center;
+    padding: 20px 30px;
+
+    > span {
+      font-size: 18px;
+      font-weight: bold;
+    }
+
+    .tab-r {
+      align-items: center;
+      display: flex;
+
+      .ui-button {
+        margin-right: 20px;
+      }
+    }
+  }
+
+  .mask {
+    position: absolute;
+    width: 100%;
+    top: 200px;
+    height: 30px;
+    background: linear-gradient(rgb(239, 242, 244), rgba(255, 255, 255, 0));
+    z-index: 1;
+    pointer-events: none;
+  }
+
+  .w-list {
+    flex: 1 1 auto;
+    overflow: auto;
+    margin-top: 22px;
+    padding-top: 8px;
+    @gap: 20px;
+    display: flex;
+    flex-wrap: wrap;
+    align-content: flex-start;
+    // 让宽度和视口等宽,为了保证鼠标列表显示区域以外时列表也能响应滚轮事件。
+    margin-left: calc((100vw - 100%) / -2);
+    padding-left: calc((100vw - 100%) / 2);
+    margin-right: calc((100vw - 100%) / -2);
+    padding-right: calc((100vw - 100%) / 2);
+
+    &::-webkit-scrollbar {
+      width: 0;
+      height: 0;
+    }
+
+    > li {
+      width: calc((100% - @gap * 4) / 5);
+      height: 322px;
+      margin-bottom: @gap;
+      margin-right: @gap;
+
+      &:nth-of-type(5n) {
+        margin-right: 0;
+      }
+
+      // 因为有个“创建作品”card占着空间,每次拿20个数据,每行五个,又不想每次拿到数据后最后一行只有一个card,所以把最后那个card隐藏掉。
+      &:last-of-type.has-more-data {
+        display: none;
+      }
+
+      .wrapper {
+        height: 100%;
+        background: #fff;
+        position: relative;
+        border-radius: 6px;
+        overflow: hidden;
+
+        .li-hover {
+          display: none;
+          width: 100%;
+          height: 240px;
+          position: absolute;
+          top: 0;
+          left: 0;
+          z-index: 99;
+          background: rgba(0, 0, 0, 0.6);
+
+          .lipreview {
+            position: absolute;
+            top: 50%;
+            left: 50%;
+            transform: translate(-50%, -50%);
+            color: #fff;
+            display: inline-block;
+            line-height: 40px;
+            height: 40px;
+            width: 100px;
+            text-align: center;
+            border-radius: 22px;
+            cursor: pointer;
+            background-color: transparent;
+            border: 1px solid #fff;
+
+            &:hover {
+              border: none;
+              background: #1983f6;
+            }
+          }
+
+          .oper {
+            display: flex;
+            justify-content: space-around;
+            align-items: center;
+            position: absolute;
+            bottom: 10px;
+            left: 0;
+            width: 100%;
+
+            > li {
+              color: #fff;
+              font-size: 13px;
+              display: flex;
+              align-items: center;
+              cursor: pointer;
+
+              > i {
+                font-size: 20px;
+                margin-right: 4px;
+              }
+            }
+          }
+        }
+
+        .img {
+          width: 100%;
+          height: 240px;
+          position: relative;
+          overflow: hidden;
+          cursor: pointer;
+
+          .real {
+            height: 100%;
+            position: absolute;
+            top: 0;
+            left: 50%;
+            transform: translateX(-50%);
+            z-index: 0;
+            transition: all ease 0.3s;
+          }
+        }
+
+        .li-info {
+          font-size: 14px;
+          padding: 10px;
+
+          > div {
+            text-align: left;
+
+            &:first-of-type {
+              > span {
+                font-weight: bold;
+                margin-bottom: 10px;
+                display: inline-block;
+                text-overflow: ellipsis;
+                overflow: hidden;
+                white-space: nowrap;
+                cursor: pointer;
+                color: #323233;
+                font-size: 16px;
+              }
+            }
+
+            &:last-of-type {
+              display: flex;
+              justify-content: space-between;
+              align-items: center;
+
+              > span {
+                font-size: 14px;
+                color: #969799;
+              }
+
+              > div {
+                color: #969799;
+
+                i {
+                  margin-right: 6px;
+                }
+              }
+            }
+          }
+        }
+      }
+
+      &:hover {
+        .wrapper {
+          box-shadow: 0px 2px 12px 0px rgba(50, 50, 51, 0.12);
+          transform: translateY(-6px);
+
+          .li-hover {
+            display: block;
+          }
+
+          .img {
+            .real {
+              height: 108%;
+            }
+          }
+        }
+      }
+    }
+
+    .add-work {
+      .wrapper {
+        .add-con {
+          position: absolute;
+          top: 50%;
+          left: 50%;
+          transform: translate(-50%, -50%);
+          text-align: center;
+
+          div {
+            width: 60px;
+            height: 60px;
+            border-radius: 50%;
+            background: linear-gradient(144deg, #00aefb 0%, #0076f6 100%);
+            position: relative;
+            cursor: pointer;
+            margin: 0 auto;
+
+            > i {
+              font-size: 16px;
+              position: absolute;
+              top: 50%;
+              left: 50%;
+              transform: translate(-50%, -50%);
+              color: #fff;
+            }
+          }
+
+          span {
+            color: #333333;
+            display: inline-block;
+            margin-top: 8px;
+            font-size: 14px;
+          }
+        }
+      }
+    }
+
+    .work-list-loading-wrapper {
+      width: 100%;
+      margin-top: 20px;
+      margin-bottom: 22px;
+
+      .work-list-loading {
+        display: block;
+        margin: 0 auto;
+        width: 50px;
+        height: 8px;
+      }
+    }
+  }
+}
+</style>
+
+<style lang="less" scoped>
+@import "../style.less";
+</style>

+ 790 - 0
packages/qjkankan-editor/src/views/material/works/list.vue

@@ -0,0 +1,790 @@
+<template>
+  <div
+    class="scroll-container"
+    ref="w-list-ref"
+    @scroll.self="onWorkListScroll"
+  >
+    <div class="back-top" @click="onClickBackTop" v-show="isShowBackTopBtn">
+      <i class="iconfont icon-top"></i>
+    </div>
+    <div class="mask" v-show="isShowMask"></div>
+    <!-- 断网时,输入关键字触发网络请求后,骨架图的隐藏会触发v-infinite-scroll,原因未知。进而导致循环触发,因此list为空时要禁用v-infinite-scroll -->
+    <ul
+      class="w-list"
+      v-if="!(list.length === 0 && !hasMoreData)"
+      v-infinite-scroll="requestMoreData"
+      :infinite-scroll-disabled="
+        !hasMoreData || isRequestingMoreData || list.length === 0
+      "
+    >
+      <li class="add-work" @click="add">
+        <div class="wrapper">
+          <div class="add-con">
+            <div>
+              <i class="iconfont icon-works_add"></i>
+            </div>
+            <span>{{ create }}</span>
+          </div>
+        </div>
+      </li>
+      <!-- 骨架图 -->
+      <template v-if="isRequestingMoreData && list.length === 0">
+        <li v-for="index in 19" :key="index">
+          <div class="wrapper">
+            <workCardSkeleton></workCardSkeleton>
+          </div>
+        </li>
+      </template>
+      <li
+        v-for="(item, i) in list"
+        :key="i"
+        :class="{ 'has-more-data': hasMoreData }"
+      >
+        <div class="wrapper">
+          <div class="li-hover">
+            <span class="lipreview" @click="handlePreview(item)">{{
+              preview
+            }}</span>
+            <ul class="oper">
+              <li class="comfirmhover" @click="edit(item)">
+                <i class="iconfont icon-works_editor"></i>{{ edittips }}
+              </li>
+              <li class="comfirmhover" @click="openShare(item)">
+                <i class="iconfont icon-works_share"></i>{{ share }}
+              </li>
+              <li class="cancelhover" @click="del(item, i)">
+                <i class="iconfont icon-works_delete"></i>{{ deltips }}
+              </li>
+            </ul>
+          </div>
+          <div class="img" @click="handlePreview(item)">
+            <img class="real" :src="item.icon || $thumb" alt="" />
+          </div>
+          <div class="li-info">
+            <div>
+              <span class="shenglve tttttt" :title="item.name || no_title">{{
+                item.name || no_title
+              }}</span>
+            </div>
+            <div>
+              <span>{{ item.createTime.split(" ")[0] }}</span>
+              <div :title="item.visit">
+                <i class="iconfont icon-works_look"></i
+                >{{ item.visit > 10000 ? "1w+" : item.visit }}
+              </div>
+            </div>
+          </div>
+        </div>
+      </li>
+      <div
+        class="work-list-loading-wrapper"
+        v-show="isRequestingMoreData && list.length !== 0"
+      >
+        <img
+          class="work-list-loading"
+          :src="require('@/assets/images/icons/work-list-loading.gif')"
+        />
+      </div>
+    </ul>
+    <div
+      class="nodata"
+      v-if="list.length == 0 && !hasMoreData && lastestUsedSearchKey"
+    >
+      <img :src="$noresult" alt="" />
+      <span>{{ no_search_result }}~</span>
+    </div>
+    <div
+      class="nodata"
+      v-if="list.length == 0 && !hasMoreData && !lastestUsedSearchKey"
+    >
+      <img :src="config.empty" alt="" />
+      <span>{{ no_works }}</span>
+      <button @click="add" class="upload-btn-in-table">{{ create }}</button>
+    </div>
+
+    <share
+      :show="showShare"
+      :item="shareItem"
+      @close="showShare = false"
+    ></share>
+
+    <preview
+      v-if="showItem"
+      :name="showItem.name"
+      :show="showPreview"
+      :ifr="`./show.html?id=${showItem.id}&lang=${$lang}`"
+      :dark="false"
+      @close="showPreview = false"
+    />
+
+    <div class="dialog" style="z-index: 1000" v-if="isShowMaterialSelector">
+      <MaterialSelector
+        :isDarkTheme="false"
+        :title="select_material"
+        :selectableType="['pano', '3D']"
+        :isMultiSelection="true"
+        initialMaterialType="pano"
+        @cancel="isShowMaterialSelector = false"
+        @submit="handleSubmitFromMaterialSelector"
+      />
+    </div>
+  </div>
+</template>
+
+<script>
+import share from "../popup/share";
+import preview from "@/components/preview";
+import workCardSkeleton from "@/components/workCardSkeleton.vue";
+import config from "@/config";
+import { debounce } from "@/utils/other.js";
+import MaterialSelector from "@/components/materialSelector.vue";
+import { mapGetters } from "vuex";
+import { i18n } from "@/lang";
+import { $waiting } from "@/components/shared/loading";
+
+import {
+  addWorks,
+  getWorksList,
+  delWorks,
+  getPanoInfo,
+  saveWorks,
+} from "@/api";
+
+export default {
+  components: {
+    share,
+    preview,
+    workCardSkeleton,
+    MaterialSelector,
+  },
+  computed: {
+    ...mapGetters(["info"]),
+  },
+  props: {
+    searchKey: String,
+  },
+  data() {
+    return {
+      myWorks: i18n.t("material.works.my"),
+      create: i18n.t("material.works.create"),
+      preview: i18n.t("material.works.preview"),
+      edittips: i18n.t("material.works.edit"),
+      share: i18n.t("material.works.share"),
+      deltips: i18n.t("material.works.delete"),
+      no_works: i18n.t("material.works.no_works"),
+      no_title: i18n.t("gather.no_title"),
+      no_search_result: i18n.t("gather.no_search_result"),
+      select_material: i18n.t("gather.select_material"),
+
+      config,
+      list: [],
+      workTotalNum: undefined,
+      hasMoreData: true,
+      isRequestingMoreData: false,
+
+      // searchKey: "",
+      // 因为searchKey的变化经过debounce、异步请求的延时,才会反映到数据列表的变化上,所以是否显示、显示哪种无数据提示,也要等到数据列表变化后,根据数据列表是否为空,以及引发本次变化的那个searchKey瞬时值来决定。本变量就是用来保存那个瞬时值。
+      lastestUsedSearchKey: "",
+      isFilterFocus: false,
+
+      showShare: false,
+      showPreview: false,
+      showItem: "",
+      shareItem: "",
+      isBackingTop: false,
+      isShowBackTopBtn: false,
+      isShowMask: false,
+      isShowMaterialSelector: false,
+      newWorkId: "",
+    };
+  },
+  mounted() {
+    this.requestMoreData();
+  },
+  watch: {
+    searchKey: {
+      handler: function (val) {
+        if (val.length > 0) {
+          this.selectedList = [];
+        }
+        this.refreshListDebounced();
+      },
+      immediate: false,
+    },
+    workTotalNum: {
+      handler: function (val) {
+        if (val) {
+          this.$emit("updateNum", val);
+        }
+      },
+      immediate: true,
+    },
+  },
+  methods: {
+    onFilterFocus() {
+      this.isFilterFocus = true;
+    },
+    onFilterBlur() {
+      this.isFilterFocus = false;
+    },
+    refreshListDebounced: debounce(
+      function () {
+        this.list = [];
+        this.isRequestingMoreData = false;
+        this.hasMoreData = true;
+        this.requestMoreData();
+      },
+      500,
+      false
+    ),
+    openShare(data) {
+      console.log(data);
+      getPanoInfo(data.id, (data) => {
+        if (data.scenes.length <= 0) {
+          return this.$msg.warning(this.$i18n.t("material.works.no_link"));
+        }
+        this.showShare = true;
+        this.shareItem = data;
+      });
+    },
+
+    handlePreview(item) {
+      getPanoInfo(item.id, (data) => {
+        if (data.scenes.length <= 0) {
+          return this.$msg.warning(this.$i18n.t("material.works.no_link"));
+        }
+        this.showItem = {
+          ...item,
+          ...data,
+        };
+        this.showPreview = true;
+      });
+    },
+    add() {
+      // 新建作品,弹窗让用户给作品选择素材。
+      $waiting.show();
+      addWorks({}, (res) => {
+        $waiting.hide();
+        this.newWorkId = res.data.id;
+        this.isShowMaterialSelector = true;
+      });
+    },
+    handleSubmitFromMaterialSelector(selected) {
+      $waiting.show();
+      // 拿新作品的初始数据
+      getPanoInfo(
+        this.newWorkId,
+        // 拿到了。
+        (data) => {
+          // 往里边添加用户选中的素材。
+          this.$store.commit("SetInfo", data);
+          console.log("selected", selected);
+          for (const [key, item] of Object.entries(selected)) {
+            if (item.materialType === "pano") {
+              let newScene = {
+                icon: item.icon,
+                sceneCode: item.sceneCode,
+                sceneTitle: item.name,
+                category: this.info.catalogs[0].id,
+                type: "pano",
+                id: "s_" + this.$randomWord(true, 8, 8),
+              };
+
+              console.log("key", key);
+              if (Number(key) === 0) {
+                //新建时开天空mask
+                newScene = Object.assign(newScene, this.info.scenes[0]);
+                newScene.customMask.sky.isShow = true;
+                this.info.scenes[0] = newScene;
+              } else {
+                newScene = Object.assign(newScene, {
+                  customMask: this.info.scenes[0].customMask,
+                  initVisual: this.info.scenes[0].initVisual,
+                });
+                newScene.customMask.sky.isShow = true;
+                this.info.scenes.push(newScene);
+              }
+            } else if (item.materialType === "3D") {
+              let newScene = {
+                icon: item.thumb,
+                sceneCode: item.num,
+                sceneTitle: item.sceneName,
+                category: this.info.catalogs[0].id,
+                type: "4dkk",
+                id: "s_" + this.$randomWord(true, 8, 8),
+              };
+              if (Number(key) === 0) {
+                this.info.scenes[0] = null;
+                this.info.scenes[0] = newScene;
+              } else {
+                this.info.scenes.push(newScene);
+              }
+            }
+          }
+
+          // 保存新作品
+          saveWorks(
+            {
+              id: this.newWorkId,
+              password: "",
+              someData: {
+                ...this.info,
+                status: 1,
+                icon: this.info.scenes[0].icon,
+              },
+            },
+            // 保存成功
+            () => {
+              $waiting.hide();
+              // 隐藏素材选择弹窗
+              this.isShowMaterialSelector = false;
+
+              // 刷新作品列表
+              this.list = [];
+              this.isRequestingMoreData = false;
+              this.hasMoreData = true;
+              this.requestMoreData()
+                .then(() => {
+                  // 刷新成功
+
+                  // 弹出提示窗口
+                  this.$confirm({
+                    title: this.$i18n.t("tips_code.tips"),
+                    content: this.$i18n.t("material.works.had_created"),
+                    okText: this.$i18n.t("material.works.goto_preview"),
+                    ok: () => {
+                      this.handlePreview(this.list[0]);
+                      this.newWorkId = "";
+                      this.$store.commit("SetInfo", {});
+                    },
+                    ok2Text: this.$i18n.t("material.works.continue_edit"),
+                    ok2: () => {
+                      window.open(
+                        `./edit.html?id=${this.newWorkId}&lang=${this.$lang}`
+                      );
+                      this.newWorkId = "";
+                      this.$store.commit("SetInfo", {});
+                    },
+                  });
+                })
+                .catch(() => {
+                  this.$msg.message(
+                    this.$i18n.t("material.works.had_created_but_no_link")
+                  );
+                  console.error("已成功新建作品,但刷新作品列表失败。");
+                });
+            },
+            // 保存失败,删除新建的作品。
+            (error) => {
+              $waiting.hide();
+              console.error("保存失败:", error);
+              delWorks(this.newWorkId);
+              this.newWorkId = "";
+              this.$store.commit("SetInfo", {});
+            }
+          );
+        },
+        // 没拿到,删除新建的作品。
+        (error) => {
+          console.error("没拿到新建的作品数据:", error);
+          delWorks(this.newWorkId);
+          this.newWorkId = "";
+        }
+      );
+    },
+
+    edit(item) {
+      window.open(`./edit.html?id=${item.id}&lang=${this.$lang}&from=list`);
+    },
+
+    del(item, index) {
+      this.$confirm({
+        title: this.$i18n.t("material.works.delete_work"),
+        content: this.$i18n.t("material.works.comfirm_delete"),
+        ok: () => {
+          $waiting.show();
+
+          delWorks(item.id, () => {
+            this.$msg.success(this.$i18n.t("gather.delete_success"));
+            this.isRequestingMoreData = true;
+            const lastestUsedSearchKey = this.searchKey;
+            getWorksList(
+              {
+                pageNum: this.list.length,
+                pageSize: 1,
+                searchKey: this.searchKey,
+              },
+              (data) => {
+                $waiting.hide();
+                this.list.splice(index, 1);
+                this.list = this.list.concat(data.data.list);
+                if (this.list.length === data.data.total) {
+                  this.hasMoreData = false;
+                }
+                this.isRequestingMoreData = false;
+                this.lastestUsedSearchKey = lastestUsedSearchKey;
+                if (!lastestUsedSearchKey) {
+                  this.workTotalNum = data.data.total;
+                }
+                // TODO: 这是干啥呢?
+                this.$nextTick(() => {
+                  this.$bus.emit("refreshTips");
+                });
+              },
+              () => {
+                $waiting.hide();
+                this.lastestUsedSearchKey = lastestUsedSearchKey;
+                this.isRequestingMoreData = false;
+              }
+            );
+          });
+        },
+      });
+    },
+
+    requestMoreData() {
+      this.isRequestingMoreData = true;
+
+      const lastestUsedSearchKey = this.searchKey;
+      return new Promise((resolve, reject) => {
+        getWorksList(
+          {
+            pageNum: Math.floor(this.list.length / config.PAGE_SIZE) + 1,
+            pageSize: config.PAGE_SIZE,
+            searchKey: this.searchKey,
+          },
+          (data) => {
+            this.list = this.list.concat(data.data.list);
+            if (this.list.length === data.data.total) {
+              this.hasMoreData = false;
+            }
+            this.isRequestingMoreData = false;
+            this.lastestUsedSearchKey = lastestUsedSearchKey;
+            if (!lastestUsedSearchKey) {
+              this.workTotalNum = data.data.total;
+            }
+            // TODO: 这是干啥呢?
+            this.$nextTick(() => {
+              this.$bus.emit("refreshTips");
+            });
+            resolve();
+          },
+          () => {
+            this.isRequestingMoreData = false;
+            this.lastestUsedSearchKey = lastestUsedSearchKey;
+            reject();
+          }
+        );
+      });
+    },
+
+    onClickBackTop() {
+      if (this.isBackingTop) {
+        return;
+      }
+      this.isBackingTop = true;
+
+      const startTime = Date.now();
+      const totalScroll = this.$refs["w-list-ref"].scrollTop;
+      const fn = () => {
+        if (this.$refs["w-list-ref"].scrollTop === 0) {
+          this.isBackingTop = false;
+          return;
+        }
+
+        const nowTime = Date.now();
+        const assumeScrollTop =
+          totalScroll - ((nowTime - startTime) * totalScroll) / 500;
+        this.$refs["w-list-ref"].scrollTop =
+          assumeScrollTop > 0 ? assumeScrollTop : 0;
+        requestAnimationFrame(fn);
+      };
+      requestAnimationFrame(fn);
+    },
+    onWorkListScroll(e) {
+      if (e.target.scrollTop >= 30) {
+        !this.isShowMask && (this.isShowMask = true);
+      } else {
+        this.isShowMask && (this.isShowMask = false);
+      }
+
+      if (e.target.scrollTop >= 600) {
+        this.isShowBackTopBtn = true;
+      } else {
+        this.isShowBackTopBtn = false;
+      }
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.scroll-container {
+  flex: 1 1 auto;
+  overflow: auto;
+  margin-left: calc((100vw - 100%) / -2);
+  padding-left: calc((100vw - 100%) / 2);
+  margin-right: calc((100vw - 100%) / -2);
+  padding-right: calc((100vw - 100%) / 2);
+  &::-webkit-scrollbar {
+    width: 0;
+    height: 0;
+  }
+  .back-top {
+    position: absolute;
+    right: -80px;
+    bottom: 30px;
+    width: 60px;
+    height: 60px;
+    border-radius: 8px;
+    background-color: #fff;
+    z-index: 1;
+    color: #c8c9cc;
+
+    &:hover {
+      color: #323233;
+    }
+
+    cursor: pointer;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+
+    i {
+      font-size: 20px;
+    }
+  }
+
+  .mask {
+    position: absolute;
+    width: 100%;
+    top: 180px;
+    height: 30px;
+    background: linear-gradient(rgb(239, 242, 244), rgba(255, 255, 255, 0));
+    z-index: 1;
+    pointer-events: none;
+  }
+
+  .w-list {
+    margin-top: 22px;
+    padding-top: 8px;
+    width: 100%;
+    @gap: 20px;
+    display: flex;
+    flex-wrap: wrap;
+    align-content: flex-start;
+
+    > li {
+      width: calc((100% - @gap * 4) / 5);
+      height: 322px;
+      margin-bottom: @gap;
+      margin-right: @gap;
+
+      &:nth-of-type(5n) {
+        margin-right: 0;
+      }
+
+      // 因为有个“创建作品”card占着空间,每次拿20个数据,每行五个,又不想每次拿到数据后最后一行只有一个card,所以把最后那个card隐藏掉。
+      &:last-of-type.has-more-data {
+        display: none;
+      }
+
+      .wrapper {
+        height: 100%;
+        background: #fff;
+        position: relative;
+        border-radius: 6px;
+        overflow: hidden;
+
+        .li-hover {
+          display: none;
+          width: 100%;
+          height: 240px;
+          position: absolute;
+          top: 0;
+          left: 0;
+          z-index: 99;
+          background: rgba(0, 0, 0, 0.6);
+
+          .lipreview {
+            position: absolute;
+            top: 50%;
+            left: 50%;
+            transform: translate(-50%, -50%);
+            color: #fff;
+            display: inline-block;
+            line-height: 40px;
+            height: 40px;
+            width: 100px;
+            text-align: center;
+            border-radius: 22px;
+            cursor: pointer;
+            background-color: transparent;
+            border: 1px solid #fff;
+
+            &:hover {
+              border: none;
+              background: #1983f6;
+            }
+          }
+
+          .oper {
+            display: flex;
+            justify-content: space-around;
+            align-items: center;
+            position: absolute;
+            bottom: 10px;
+            left: 0;
+            width: 100%;
+
+            > li {
+              color: #fff;
+              font-size: 13px;
+              display: flex;
+              align-items: center;
+              cursor: pointer;
+
+              > i {
+                font-size: 20px;
+                margin-right: 4px;
+              }
+            }
+          }
+        }
+
+        .img {
+          width: 100%;
+          height: 240px;
+          position: relative;
+          overflow: hidden;
+          cursor: pointer;
+
+          .real {
+            height: 100%;
+            position: absolute;
+            top: 0;
+            left: 50%;
+            transform: translateX(-50%);
+            z-index: 0;
+            transition: all ease 0.3s;
+          }
+        }
+
+        .li-info {
+          font-size: 14px;
+          padding: 10px;
+
+          > div {
+            text-align: left;
+
+            &:first-of-type {
+              > span {
+                font-weight: bold;
+                margin-bottom: 10px;
+                display: inline-block;
+                text-overflow: ellipsis;
+                overflow: hidden;
+                white-space: nowrap;
+                cursor: pointer;
+                color: #323233;
+                font-size: 16px;
+              }
+            }
+
+            &:last-of-type {
+              display: flex;
+              justify-content: space-between;
+              align-items: center;
+
+              > span {
+                font-size: 14px;
+                color: #969799;
+              }
+
+              > div {
+                color: #969799;
+
+                i {
+                  margin-right: 6px;
+                }
+              }
+            }
+          }
+        }
+      }
+
+      &:hover {
+        .wrapper {
+          box-shadow: 0px 2px 12px 0px rgba(50, 50, 51, 0.12);
+          transform: translateY(-6px);
+
+          .li-hover {
+            display: block;
+          }
+
+          .img {
+            .real {
+              height: 108%;
+            }
+          }
+        }
+      }
+    }
+
+    .add-work {
+      .wrapper {
+        .add-con {
+          position: absolute;
+          top: 50%;
+          left: 50%;
+          transform: translate(-50%, -50%);
+          text-align: center;
+
+          div {
+            width: 60px;
+            height: 60px;
+            border-radius: 50%;
+            background: linear-gradient(144deg, #00aefb 0%, #0076f6 100%);
+            position: relative;
+            cursor: pointer;
+            margin: 0 auto;
+
+            > i {
+              font-size: 16px;
+              position: absolute;
+              top: 50%;
+              left: 50%;
+              transform: translate(-50%, -50%);
+              color: #fff;
+            }
+          }
+
+          span {
+            color: #333333;
+            display: inline-block;
+            margin-top: 8px;
+            font-size: 14px;
+          }
+        }
+      }
+    }
+
+    .work-list-loading-wrapper {
+      width: 100%;
+      margin-top: 20px;
+      margin-bottom: 22px;
+
+      .work-list-loading {
+        display: block;
+        margin: 0 auto;
+        width: 50px;
+        height: 8px;
+      }
+    }
+  }
+}
+</style>
+
+<style lang="less" scoped>
+@import "../style.less";
+</style>

+ 4 - 1
packages/qjkankan-view/package.json

@@ -51,5 +51,8 @@
     "sass": "^1.26.5",
     "sass-loader": "^8.0.2",
     "webpackbar": "^5.0.2"
+  },
+  "browser": {
+    "process": false
   }
-}
+}

+ 64 - 83
pnpm-lock.yaml

@@ -66,6 +66,9 @@ importers:
       libphonenumber-js:
         specifier: ^1.10.19
         version: 1.10.59
+      lodash:
+        specifier: ^4.17.21
+        version: 4.17.21
       log-beautify:
         specifier: ^1.2.0
         version: 1.2.0
@@ -84,6 +87,9 @@ importers:
       video.js:
         specifier: ^7.11.8
         version: 7.21.5
+      viewerjs:
+        specifier: ^1.11.6
+        version: 1.11.6
       vue:
         specifier: ^2.6.12
         version: 2.7.16
@@ -104,7 +110,7 @@ importers:
         version: 1.2.0
       vue-photo-preview:
         specifier: ^1.1.3
-        version: 1.1.3
+        version: 1.1.3(lodash@4.17.21)
       vue-router:
         specifier: ^3.4.9
         version: 3.6.5(vue@2.7.16)
@@ -117,6 +123,9 @@ importers:
       vue-toastification:
         specifier: ^1.7.14
         version: 1.7.14(vue@2.7.16)
+      vue2-perfect-scrollbar:
+        specifier: ^1.5.56
+        version: 1.5.56(postcss@8.4.38)
       vuex:
         specifier: ^3.6.0
         version: 3.6.2(vue@2.7.16)
@@ -132,7 +141,7 @@ importers:
         version: 4.5.19(@vue/cli-service@4.5.19)
       '@vue/cli-service':
         specifier: ^4.5.9
-        version: 4.5.19(babel-core@6.26.3)(less-loader@5.0.0)(vue-template-compiler@2.7.16)(vue@2.7.16)
+        version: 4.5.19(babel-core@6.26.3)(less-loader@5.0.0)(lodash@4.17.21)(vue-template-compiler@2.7.16)(vue@2.7.16)
       babel-eslint:
         specifier: ^10.1.0
         version: 10.1.0(eslint@6.8.0)
@@ -1958,7 +1967,6 @@ packages:
   /@trysound/sax@0.2.0:
     resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
     engines: {node: '>=10.13.0'}
-    dev: true
 
   /@types/body-parser@1.19.5:
     resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==}
@@ -2505,7 +2513,7 @@ packages:
     dependencies:
       '@babel/core': 7.24.3
       '@vue/babel-preset-app': 4.5.19(@babel/core@7.24.3)(core-js@3.36.1)(vue@2.7.16)
-      '@vue/cli-service': 4.5.19(babel-core@6.26.3)(less-loader@5.0.0)(vue-template-compiler@2.7.16)(vue@2.7.16)
+      '@vue/cli-service': 4.5.19(babel-core@6.26.3)(less-loader@5.0.0)(lodash@4.17.21)(vue-template-compiler@2.7.16)(vue@2.7.16)
       '@vue/cli-shared-utils': 4.5.19
       babel-loader: 8.3.0(@babel/core@7.24.3)(webpack@4.47.0)
       cache-loader: 4.1.0(webpack@4.47.0)
@@ -2548,7 +2556,7 @@ packages:
       '@vue/cli-service': ^3.0.0 || ^4.0.0-0
       eslint: '>= 1.6.0 < 7.0.0'
     dependencies:
-      '@vue/cli-service': 4.5.19(babel-core@6.26.3)(less-loader@5.0.0)(vue-template-compiler@2.7.16)(vue@2.7.16)
+      '@vue/cli-service': 4.5.19(babel-core@6.26.3)(less-loader@5.0.0)(lodash@4.17.21)(vue-template-compiler@2.7.16)(vue@2.7.16)
       '@vue/cli-shared-utils': 4.5.19
       eslint: 6.8.0
       eslint-loader: 2.2.1(eslint@6.8.0)(webpack@4.47.0)
@@ -2588,7 +2596,7 @@ packages:
     peerDependencies:
       '@vue/cli-service': ^3.0.0 || ^4.0.0-0
     dependencies:
-      '@vue/cli-service': 4.5.19(babel-core@6.26.3)(less-loader@5.0.0)(vue-template-compiler@2.7.16)(vue@2.7.16)
+      '@vue/cli-service': 4.5.19(babel-core@6.26.3)(less-loader@5.0.0)(lodash@4.17.21)(vue-template-compiler@2.7.16)(vue@2.7.16)
       '@vue/cli-shared-utils': 4.5.19
     dev: true
 
@@ -2608,7 +2616,7 @@ packages:
     peerDependencies:
       '@vue/cli-service': ^3.0.0 || ^4.0.0-0
     dependencies:
-      '@vue/cli-service': 4.5.19(babel-core@6.26.3)(less-loader@5.0.0)(vue-template-compiler@2.7.16)(vue@2.7.16)
+      '@vue/cli-service': 4.5.19(babel-core@6.26.3)(less-loader@5.0.0)(lodash@4.17.21)(vue-template-compiler@2.7.16)(vue@2.7.16)
     dev: true
 
   /@vue/cli-plugin-vuex@5.0.8(@vue/cli-service@5.0.8):
@@ -2619,7 +2627,7 @@ packages:
       '@vue/cli-service': 5.0.8(prettier@2.8.8)(sass-loader@8.0.2)(vue@3.4.21)
     dev: true
 
-  /@vue/cli-service@4.5.19(babel-core@6.26.3)(less-loader@5.0.0)(vue-template-compiler@2.7.16)(vue@2.7.16):
+  /@vue/cli-service@4.5.19(babel-core@6.26.3)(less-loader@5.0.0)(lodash@4.17.21)(vue-template-compiler@2.7.16)(vue@2.7.16):
     resolution: {integrity: sha512-+Wpvj8fMTCt9ZPOLu5YaLkFCQmB4MrZ26aRmhhKiCQ/4PMoL6mLezfqdt6c+m2htM+1WV5RunRo+0WHl2DfwZA==}
     engines: {node: '>=8'}
     hasBin: true
@@ -2657,7 +2665,7 @@ packages:
       '@vue/cli-plugin-router': 4.5.19(@vue/cli-service@4.5.19)
       '@vue/cli-plugin-vuex': 4.5.19(@vue/cli-service@4.5.19)
       '@vue/cli-shared-utils': 4.5.19
-      '@vue/component-compiler-utils': 3.3.0(babel-core@6.26.3)
+      '@vue/component-compiler-utils': 3.3.0(babel-core@6.26.3)(lodash@4.17.21)
       '@vue/preload-webpack-plugin': 1.1.2(html-webpack-plugin@3.2.0)(webpack@4.47.0)
       '@vue/web-component-wrapper': 1.3.0
       acorn: 7.4.1
@@ -2696,7 +2704,7 @@ packages:
       terser-webpack-plugin: 1.4.5(webpack@4.47.0)
       thread-loader: 2.1.3(webpack@4.47.0)
       url-loader: 2.3.0(file-loader@4.3.0)(webpack@4.47.0)
-      vue-loader: 15.11.1(babel-core@6.26.3)(cache-loader@4.1.0)(css-loader@3.6.0)(vue-template-compiler@2.7.16)(webpack@4.47.0)
+      vue-loader: 15.11.1(babel-core@6.26.3)(cache-loader@4.1.0)(css-loader@3.6.0)(lodash@4.17.21)(vue-template-compiler@2.7.16)(webpack@4.47.0)
       vue-style-loader: 4.1.3
       vue-template-compiler: 2.7.16
       webpack: 4.47.0
@@ -2809,7 +2817,7 @@ packages:
       '@vue/cli-plugin-router': 5.0.8(@vue/cli-service@5.0.8)
       '@vue/cli-plugin-vuex': 5.0.8(@vue/cli-service@5.0.8)
       '@vue/cli-shared-utils': 5.0.8
-      '@vue/component-compiler-utils': 3.3.0(babel-core@6.26.3)
+      '@vue/component-compiler-utils': 3.3.0(babel-core@6.26.3)(lodash@4.17.21)
       '@vue/vue-loader-v15': /vue-loader@15.11.1(@vue/compiler-sfc@3.4.21)(css-loader@6.10.0)(webpack@5.91.0)
       '@vue/web-component-wrapper': 1.3.0
       acorn: 8.11.3
@@ -2967,7 +2975,7 @@ packages:
       '@vue/cli-plugin-router': 5.0.8(@vue/cli-service@5.0.8)
       '@vue/cli-plugin-vuex': 5.0.8(@vue/cli-service@5.0.8)
       '@vue/cli-shared-utils': 5.0.8
-      '@vue/component-compiler-utils': 3.3.0(babel-core@6.26.3)
+      '@vue/component-compiler-utils': 3.3.0(babel-core@6.26.3)(lodash@4.17.21)
       '@vue/vue-loader-v15': /vue-loader@15.11.1(css-loader@6.10.0)(prettier@2.8.8)(webpack@5.91.0)
       '@vue/web-component-wrapper': 1.3.0
       acorn: 8.11.3
@@ -3165,10 +3173,10 @@ packages:
       '@vue/compiler-dom': 3.4.21
       '@vue/shared': 3.4.21
 
-  /@vue/component-compiler-utils@3.3.0(babel-core@6.26.3):
+  /@vue/component-compiler-utils@3.3.0(babel-core@6.26.3)(lodash@4.17.21):
     resolution: {integrity: sha512-97sfH2mYNU+2PzGrmK2haqffDpVASuib9/w2/noxiFi31Z54hW+q3izKQXXQZSNhtiUpAI36uSuYepeBe4wpHQ==}
     dependencies:
-      consolidate: 0.15.1(babel-core@6.26.3)
+      consolidate: 0.15.1(babel-core@6.26.3)(lodash@4.17.21)
       hash-sum: 1.0.2
       lru-cache: 4.1.5
       merge-source-map: 1.1.0
@@ -5023,7 +5031,6 @@ packages:
 
   /boolbase@1.0.0:
     resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
-    dev: true
 
   /brace-expansion@1.1.11:
     resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
@@ -5190,7 +5197,6 @@ packages:
       electron-to-chromium: 1.4.722
       node-releases: 2.0.14
       update-browserslist-db: 1.0.13(browserslist@4.23.0)
-    dev: true
 
   /buffer-from@1.1.2:
     resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
@@ -5393,7 +5399,6 @@ packages:
       caniuse-lite: 1.0.30001603
       lodash.memoize: 4.1.2
       lodash.uniq: 4.5.0
-    dev: true
 
   /caniuse-db@1.0.30001603:
     resolution: {integrity: sha512-z/6jwCdUEdkqER/0vMhFTKTIEhJQ+BNH4Dyu46UPeDsDMWmPiaq5K9Jq3uGD3Vz7ikWZ5kITihwTewlCozQX2g==}
@@ -5816,7 +5821,6 @@ packages:
 
   /colord@2.9.3:
     resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
-    dev: true
 
   /colorette@2.0.20:
     resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
@@ -5946,7 +5950,7 @@ packages:
   /console-browserify@1.2.0:
     resolution: {integrity: sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==}
 
-  /consolidate@0.14.5(babel-core@6.26.3):
+  /consolidate@0.14.5(babel-core@6.26.3)(lodash@4.17.21):
     resolution: {integrity: sha512-PZFskfj64QnpKVK9cPdY36pyWEhZNM+srRVqtwMiVTlnViSoZcvX35PpBhhUcyLTHXYvz7pZRmxvsqwzJqg9kA==}
     deprecated: Please upgrade to consolidate v1.0.0+ as it has been modernized with several long-awaited fixes implemented. Maintenance is supported by Forward Email at https://forwardemail.net ; follow/watch https://github.com/ladjs/consolidate for updates and release changelog
     peerDependencies:
@@ -6113,9 +6117,10 @@ packages:
     dependencies:
       babel-core: 6.26.3
       bluebird: 3.7.2
+      lodash: 4.17.21
     dev: false
 
-  /consolidate@0.15.1(babel-core@6.26.3):
+  /consolidate@0.15.1(babel-core@6.26.3)(lodash@4.17.21):
     resolution: {integrity: sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==}
     engines: {node: '>= 0.10.0'}
     deprecated: Please upgrade to consolidate v1.0.0+ as it has been modernized with several long-awaited fixes implemented. Maintenance is supported by Forward Email at https://forwardemail.net ; follow/watch https://github.com/ladjs/consolidate for updates and release changelog
@@ -6283,6 +6288,7 @@ packages:
     dependencies:
       babel-core: 6.26.3
       bluebird: 3.7.2
+      lodash: 4.17.21
     dev: true
 
   /constants-browserify@1.0.0:
@@ -6538,7 +6544,6 @@ packages:
       postcss: ^8.0.9
     dependencies:
       postcss: 8.4.38
-    dev: true
 
   /css-line-break@2.1.0:
     resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==}
@@ -6660,7 +6665,6 @@ packages:
       domhandler: 4.3.1
       domutils: 2.8.0
       nth-check: 2.1.1
-    dev: true
 
   /css-selector-tokenizer@0.7.3:
     resolution: {integrity: sha512-jWQv3oCEL5kMErj4wRnK/OPoBi0D+P1FR2cDCKYPaMeD2eW3/mttav8HT4hT1CKopiJI/psEULjkClhvJo4Lvg==}
@@ -6683,7 +6687,6 @@ packages:
     dependencies:
       mdn-data: 2.0.14
       source-map: 0.6.1
-    dev: true
 
   /css-what@3.4.2:
     resolution: {integrity: sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==}
@@ -6693,7 +6696,6 @@ packages:
   /css-what@6.1.0:
     resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
     engines: {node: '>= 6'}
-    dev: true
 
   /css@2.2.4:
     resolution: {integrity: sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==}
@@ -6781,7 +6783,6 @@ packages:
       postcss-reduce-transforms: 5.1.0(postcss@8.4.38)
       postcss-svgo: 5.1.0(postcss@8.4.38)
       postcss-unique-selectors: 5.1.1(postcss@8.4.38)
-    dev: true
 
   /cssnano-util-get-arguments@4.0.0:
     resolution: {integrity: sha512-6RIcwmV3/cBMG8Aj5gucQRsJb4vv4I4rn6YjPbVWd5+Pn/fuG+YseGvXGk00XLkoZkaj31QOD7vMUpNPC4FIuw==}
@@ -6812,7 +6813,6 @@ packages:
       postcss: ^8.2.15
     dependencies:
       postcss: 8.4.38
-    dev: true
 
   /cssnano@3.10.0:
     resolution: {integrity: sha512-0o0IMQE0Ezo4b41Yrm8U6Rp9/Ag81vNXY1gZMnT1XhO4DpjEf2utKERqWJbOoz3g1Wdc1d3QSta/cIuJ1wSTEg==}
@@ -6871,7 +6871,6 @@ packages:
       lilconfig: 2.1.0
       postcss: 8.4.38
       yaml: 1.10.2
-    dev: true
 
   /csso@2.3.2:
     resolution: {integrity: sha512-FmCI/hmqDeHHLaIQckMhMZneS84yzUZdrWDAvJVVxOwcKE1P1LF9FGmzr1ktIQSxOw6fl3PaQsmfg+GN+VvR3w==}
@@ -6887,7 +6886,6 @@ packages:
     engines: {node: '>=8.0.0'}
     dependencies:
       css-tree: 1.1.3
-    dev: true
 
   /csstype@3.1.3:
     resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
@@ -7522,7 +7520,6 @@ packages:
       domelementtype: 2.3.0
       domhandler: 4.3.1
       entities: 2.2.0
-    dev: true
 
   /dom-walk@0.1.2:
     resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==}
@@ -7550,14 +7547,12 @@ packages:
 
   /domelementtype@2.3.0:
     resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
-    dev: true
 
   /domhandler@4.3.1:
     resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==}
     engines: {node: '>= 4'}
     dependencies:
       domelementtype: 2.3.0
-    dev: true
 
   /domutils@1.7.0:
     resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==}
@@ -7572,7 +7567,6 @@ packages:
       dom-serializer: 1.4.1
       domelementtype: 2.3.0
       domhandler: 4.3.1
-    dev: true
 
   /dot-case@3.0.4:
     resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
@@ -7730,7 +7724,6 @@ packages:
 
   /entities@2.2.0:
     resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==}
-    dev: true
 
   /entities@4.5.0:
     resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
@@ -7906,7 +7899,6 @@ packages:
   /escalade@3.1.2:
     resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==}
     engines: {node: '>=6'}
-    dev: true
 
   /escape-html@1.0.3:
     resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
@@ -10727,7 +10719,6 @@ packages:
   /lilconfig@2.1.0:
     resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
     engines: {node: '>=10'}
-    dev: true
 
   /lines-and-columns@1.2.4:
     resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
@@ -11165,7 +11156,6 @@ packages:
 
   /mdn-data@2.0.14:
     resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==}
-    dev: true
 
   /mdn-data@2.0.4:
     resolution: {integrity: sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==}
@@ -11728,7 +11718,6 @@ packages:
 
   /node-releases@2.0.14:
     resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==}
-    dev: true
 
   /normalize-package-data@2.5.0:
     resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
@@ -11774,7 +11763,6 @@ packages:
   /normalize-url@6.1.0:
     resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==}
     engines: {node: '>=10'}
-    dev: true
 
   /normalize-wheel@1.0.1:
     resolution: {integrity: sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==}
@@ -11819,7 +11807,6 @@ packages:
     resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
     dependencies:
       boolbase: 1.0.0
-    dev: true
 
   /num2fraction@1.2.2:
     resolution: {integrity: sha512-Y1wZESM7VUThYY+4W+X4ySH2maqcA+p7UR+w8VWNWVAd6lwuXXWz/w/Cz43J/dI2I+PS6wD5N+bJUF+gjWvIqg==}
@@ -12316,6 +12303,10 @@ packages:
       safe-buffer: 5.2.1
       sha.js: 2.4.11
 
+  /perfect-scrollbar@1.5.5:
+    resolution: {integrity: sha512-dzalfutyP3e/FOpdlhVryN4AJ5XDVauVWxybSkLZmakFE2sS3y3pc4JnSprw8tGmHvkaG5Edr5T7LBTZ+WWU2g==}
+    dev: false
+
   /performance-now@2.1.0:
     resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
     dev: true
@@ -12337,7 +12328,6 @@ packages:
 
   /picocolors@0.2.1:
     resolution: {integrity: sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==}
-    dev: true
 
   /picocolors@1.0.0:
     resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
@@ -12488,7 +12478,6 @@ packages:
       postcss: 8.4.38
       postcss-selector-parser: 6.0.16
       postcss-value-parser: 4.2.0
-    dev: true
 
   /postcss-colormin@2.2.2:
     resolution: {integrity: sha512-XXitQe+jNNPf+vxvQXIQ1+pvdQKWKgkx8zlJNltcMEmLma1ypDRDQwlLt+6cP26fBreihNhZxohh1rcgCH2W5w==}
@@ -12520,7 +12509,6 @@ packages:
       colord: 2.9.3
       postcss: 8.4.38
       postcss-value-parser: 4.2.0
-    dev: true
 
   /postcss-convert-values@2.6.1:
     resolution: {integrity: sha512-SE7mf25D3ORUEXpu3WUqQqy0nCbMuM5BEny+ULE/FXdS/0UMA58OdzwvzuHJRpIFlk1uojt16JhaEogtP6W2oA==}
@@ -12546,7 +12534,6 @@ packages:
       browserslist: 4.23.0
       postcss: 8.4.38
       postcss-value-parser: 4.2.0
-    dev: true
 
   /postcss-discard-comments@2.0.4:
     resolution: {integrity: sha512-yGbyBDo5FxsImE90LD8C87vgnNlweQkODMkUZlDVM/CBgLr9C5RasLGJxxh9GjVOBeG8NcCMatoqI1pXg8JNXg==}
@@ -12568,7 +12555,6 @@ packages:
       postcss: ^8.2.15
     dependencies:
       postcss: 8.4.38
-    dev: true
 
   /postcss-discard-duplicates@2.1.0:
     resolution: {integrity: sha512-+lk5W1uqO8qIUTET+UETgj9GWykLC3LOldr7EehmymV0Wu36kyoHimC4cILrAAYpHQ+fr4ypKcWcVNaGzm0reA==}
@@ -12590,7 +12576,6 @@ packages:
       postcss: ^8.2.15
     dependencies:
       postcss: 8.4.38
-    dev: true
 
   /postcss-discard-empty@2.1.0:
     resolution: {integrity: sha512-IBFoyrwk52dhF+5z/ZAbzq5Jy7Wq0aLUsOn69JNS+7YeuyHaNzJwBIYE0QlUH/p5d3L+OON72Fsexyb7OK/3og==}
@@ -12612,7 +12597,6 @@ packages:
       postcss: ^8.2.15
     dependencies:
       postcss: 8.4.38
-    dev: true
 
   /postcss-discard-overridden@0.1.1:
     resolution: {integrity: sha512-IyKoDL8QNObOiUc6eBw8kMxBHCfxUaERYTUe2QF8k7j/xiirayDzzkmlR6lMQjrAM1p1DDRTvWrS7Aa8lp6/uA==}
@@ -12634,7 +12618,6 @@ packages:
       postcss: ^8.2.15
     dependencies:
       postcss: 8.4.38
-    dev: true
 
   /postcss-discard-unused@2.2.3:
     resolution: {integrity: sha512-nCbFNfqYAbKCw9J6PSJubpN9asnrwVLkRDFc4KCwyUEdOtM5XDE/eTW3OpqHrYY1L4fZxgan7LLRAAYYBzwzrg==}
@@ -12649,6 +12632,16 @@ packages:
       postcss: 5.2.18
     dev: false
 
+  /postcss-import@12.0.1:
+    resolution: {integrity: sha512-3Gti33dmCjyKBgimqGxL3vcV8w9+bsHwO5UrBawp796+jdardbcFl4RP5w/76BwNL7aGzpKstIfF9I+kdE8pTw==}
+    engines: {node: '>=6.0.0'}
+    dependencies:
+      postcss: 7.0.39
+      postcss-value-parser: 3.3.1
+      read-cache: 1.0.0
+      resolve: 1.22.8
+    dev: false
+
   /postcss-load-config@1.2.0:
     resolution: {integrity: sha512-3fpCfnXo9Qd/O/q/XL4cJUhRsqjVD2V1Vhy3wOEcLE5kz0TGtdDXJSoiTdH4e847KphbEac4+EZSH4qLRYIgLw==}
     engines: {node: '>=0.12'}
@@ -12740,7 +12733,6 @@ packages:
       postcss: 8.4.38
       postcss-value-parser: 4.2.0
       stylehacks: 5.1.1(postcss@8.4.38)
-    dev: true
 
   /postcss-merge-rules@2.1.2:
     resolution: {integrity: sha512-Wgg2FS6W3AYBl+5L9poL6ZUISi5YzL+sDCJfM7zNw/Q1qsyVQXXZ2cbVui6mu2cYJpt1hOKCGj1xA4mq/obz/Q==}
@@ -12775,7 +12767,6 @@ packages:
       cssnano-utils: 3.1.0(postcss@8.4.38)
       postcss: 8.4.38
       postcss-selector-parser: 6.0.16
-    dev: true
 
   /postcss-message-helpers@2.0.0:
     resolution: {integrity: sha512-tPLZzVAiIJp46TBbpXtrUAKqedXSyW5xDEo1sikrfEfnTs+49SBZR/xDdqCiJvSSbtr615xDsaMF3RrxS2jZlA==}
@@ -12805,7 +12796,6 @@ packages:
     dependencies:
       postcss: 8.4.38
       postcss-value-parser: 4.2.0
-    dev: true
 
   /postcss-minify-gradients@1.0.5:
     resolution: {integrity: sha512-DZhT0OE+RbVqVyGsTIKx84rU/5cury1jmwPa19bViqYPQu499ZU831yMzzsyC8EhiZVd73+h5Z9xb/DdaBpw7Q==}
@@ -12834,7 +12824,6 @@ packages:
       cssnano-utils: 3.1.0(postcss@8.4.38)
       postcss: 8.4.38
       postcss-value-parser: 4.2.0
-    dev: true
 
   /postcss-minify-params@1.2.2:
     resolution: {integrity: sha512-hhJdMVgP8vasrHbkKAk+ab28vEmPYgyuDzRl31V3BEB3QOR3L5TTIVEWLDNnZZ3+fiTi9d6Ker8GM8S1h8p2Ow==}
@@ -12867,7 +12856,6 @@ packages:
       cssnano-utils: 3.1.0(postcss@8.4.38)
       postcss: 8.4.38
       postcss-value-parser: 4.2.0
-    dev: true
 
   /postcss-minify-selectors@2.1.1:
     resolution: {integrity: sha512-e13vxPBSo3ZaPne43KVgM+UETkx3Bs4/Qvm6yXI9HQpQp4nyb7HZ0gKpkF+Wn2x+/dbQ+swNpCdZSbMOT7+TIA==}
@@ -12896,7 +12884,6 @@ packages:
     dependencies:
       postcss: 8.4.38
       postcss-selector-parser: 6.0.16
-    dev: true
 
   /postcss-modules-extract-imports@1.2.1:
     resolution: {integrity: sha512-6jt9XZwUhwmRUhb/CkyJY020PYaPJsCyt3UjbaWo6XEbH/94Hmv6MP7fG2C5NDU/BcHzyGYxNtHvM+LTf9HrYw==}
@@ -13018,7 +13005,6 @@ packages:
       postcss: ^8.2.15
     dependencies:
       postcss: 8.4.38
-    dev: true
 
   /postcss-normalize-display-values@4.0.2:
     resolution: {integrity: sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==}
@@ -13037,7 +13023,6 @@ packages:
     dependencies:
       postcss: 8.4.38
       postcss-value-parser: 4.2.0
-    dev: true
 
   /postcss-normalize-positions@4.0.2:
     resolution: {integrity: sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==}
@@ -13057,7 +13042,6 @@ packages:
     dependencies:
       postcss: 8.4.38
       postcss-value-parser: 4.2.0
-    dev: true
 
   /postcss-normalize-repeat-style@4.0.2:
     resolution: {integrity: sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==}
@@ -13077,7 +13061,6 @@ packages:
     dependencies:
       postcss: 8.4.38
       postcss-value-parser: 4.2.0
-    dev: true
 
   /postcss-normalize-string@4.0.2:
     resolution: {integrity: sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==}
@@ -13096,7 +13079,6 @@ packages:
     dependencies:
       postcss: 8.4.38
       postcss-value-parser: 4.2.0
-    dev: true
 
   /postcss-normalize-timing-functions@4.0.2:
     resolution: {integrity: sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==}
@@ -13115,7 +13097,6 @@ packages:
     dependencies:
       postcss: 8.4.38
       postcss-value-parser: 4.2.0
-    dev: true
 
   /postcss-normalize-unicode@4.0.1:
     resolution: {integrity: sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==}
@@ -13135,7 +13116,6 @@ packages:
       browserslist: 4.23.0
       postcss: 8.4.38
       postcss-value-parser: 4.2.0
-    dev: true
 
   /postcss-normalize-url@3.0.8:
     resolution: {integrity: sha512-WqtWG6GV2nELsQEFES0RzfL2ebVwmGl/M8VmMbshKto/UClBo+mznX8Zi4/hkThdqx7ijwv+O8HWPdpK7nH/Ig==}
@@ -13165,7 +13145,6 @@ packages:
       normalize-url: 6.1.0
       postcss: 8.4.38
       postcss-value-parser: 4.2.0
-    dev: true
 
   /postcss-normalize-whitespace@4.0.2:
     resolution: {integrity: sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==}
@@ -13183,7 +13162,6 @@ packages:
     dependencies:
       postcss: 8.4.38
       postcss-value-parser: 4.2.0
-    dev: true
 
   /postcss-ordered-values@2.2.3:
     resolution: {integrity: sha512-5RB1IUZhkxDCfa5fx/ogp/A82mtq+r7USqS+7zt0e428HJ7+BHCxyeY39ClmkkUtxdOd3mk8gD6d9bjH2BECMg==}
@@ -13210,7 +13188,6 @@ packages:
       cssnano-utils: 3.1.0(postcss@8.4.38)
       postcss: 8.4.38
       postcss-value-parser: 4.2.0
-    dev: true
 
   /postcss-reduce-idents@2.4.0:
     resolution: {integrity: sha512-0+Ow9e8JLtffjumJJFPqvN4qAvokVbdQPnijUDSOX8tfTwrILLP4ETvrZcXZxAtpFLh/U0c+q8oRMJLr1Kiu4w==}
@@ -13244,7 +13221,6 @@ packages:
       browserslist: 4.23.0
       caniuse-api: 3.0.0
       postcss: 8.4.38
-    dev: true
 
   /postcss-reduce-transforms@1.0.4:
     resolution: {integrity: sha512-lGgRqnSuAR5i5uUg1TA33r9UngfTadWxOyL2qx1KuPoCQzfmtaHjp9PuwX7yVyRxG3BWBzeFUaS5uV9eVgnEgQ==}
@@ -13272,7 +13248,6 @@ packages:
     dependencies:
       postcss: 8.4.38
       postcss-value-parser: 4.2.0
-    dev: true
 
   /postcss-safe-parser@1.0.7:
     resolution: {integrity: sha512-LHsxELCg/Rzv2uFQN/4+pW3g2VLncnUSlUf8WtyQ2Af3PdtzQh0zWseKWrb0l8h2jLNA8bCOabBU9Fuz3+maKg==}
@@ -13303,7 +13278,6 @@ packages:
     dependencies:
       cssesc: 3.0.0
       util-deprecate: 1.0.2
-    dev: true
 
   /postcss-svgo@2.1.6:
     resolution: {integrity: sha512-y5AdQdgBoF4rbpdbeWAJuxE953g/ylRfVNp6mvAi61VCN/Y25Tu9p5mh3CyI42WbTRIiwR9a1GdFtmDnNPeskQ==}
@@ -13332,7 +13306,6 @@ packages:
       postcss: 8.4.38
       postcss-value-parser: 4.2.0
       svgo: 2.8.0
-    dev: true
 
   /postcss-unique-selectors@2.0.2:
     resolution: {integrity: sha512-WZX8r1M0+IyljoJOJleg3kYm10hxNYF9scqAT7v/xeSX1IdehutOM85SNO0gP9K+bgs86XERr7Ud5u3ch4+D8g==}
@@ -13359,14 +13332,12 @@ packages:
     dependencies:
       postcss: 8.4.38
       postcss-selector-parser: 6.0.16
-    dev: true
 
   /postcss-value-parser@3.3.1:
     resolution: {integrity: sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==}
 
   /postcss-value-parser@4.2.0:
     resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
-    dev: true
 
   /postcss-zindex@2.2.0:
     resolution: {integrity: sha512-uhRZ2hRgj0lorxm9cr62B01YzpUe63h0RXMXQ4gWW3oa2rpJh+FJAiEAytaFCPU/VgaBS+uW2SJ1XKyDNz1h4w==}
@@ -13401,7 +13372,6 @@ packages:
     dependencies:
       picocolors: 0.2.1
       source-map: 0.6.1
-    dev: true
 
   /postcss@8.4.38:
     resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==}
@@ -13661,6 +13631,12 @@ packages:
       iconv-lite: 0.4.24
       unpipe: 1.0.0
 
+  /read-cache@1.0.0:
+    resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
+    dependencies:
+      pify: 2.3.0
+    dev: false
+
   /read-pkg-up@1.0.1:
     resolution: {integrity: sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==}
     engines: {node: '>=0.10.0'}
@@ -14950,7 +14926,6 @@ packages:
   /stable@0.1.8:
     resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==}
     deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility'
-    dev: true
 
   /stackframe@1.3.4:
     resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==}
@@ -15186,7 +15161,6 @@ packages:
       browserslist: 4.23.0
       postcss: 8.4.38
       postcss-selector-parser: 6.0.16
-    dev: true
 
   /supports-color@2.0.0:
     resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==}
@@ -15286,7 +15260,6 @@ packages:
       csso: 4.2.0
       picocolors: 1.0.0
       stable: 0.1.8
-    dev: true
 
   /swiper@5.4.5:
     resolution: {integrity: sha512-7QjA0XpdOmiMoClfaZ2lYN6ICHcMm72LXiY+NF4fQLFidigameaofvpjEEiTQuw3xm5eksG5hzkaRsjQX57vtA==}
@@ -15837,7 +15810,6 @@ packages:
       browserslist: 4.23.0
       escalade: 3.1.2
       picocolors: 1.0.0
-    dev: true
 
   /upper-case@1.1.3:
     resolution: {integrity: sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==}
@@ -16131,7 +16103,7 @@ packages:
       '@vue/devtools-api': 6.6.1
       vue: 3.4.21
 
-  /vue-loader@13.7.3(babel-core@6.26.3)(css-loader@0.28.11)(vue-template-compiler@2.7.16)(webpack@3.12.0):
+  /vue-loader@13.7.3(babel-core@6.26.3)(css-loader@0.28.11)(lodash@4.17.21)(vue-template-compiler@2.7.16)(webpack@3.12.0):
     resolution: {integrity: sha512-ACCwbfeC6HjY2pnDii+Zer+MZ6sdOtwvLmDXRK/BoD3WNR551V22R6KEagwHoTRJ0ZlIhpCBkptpCU6+Ri/05w==}
     peerDependencies:
       '@vue/compiler-sfc': ^3.0.8
@@ -16142,7 +16114,7 @@ packages:
       '@vue/compiler-sfc':
         optional: true
     dependencies:
-      consolidate: 0.14.5(babel-core@6.26.3)
+      consolidate: 0.14.5(babel-core@6.26.3)(lodash@4.17.21)
       css-loader: 0.28.11
       hash-sum: 1.0.2
       loader-utils: 1.4.2
@@ -16234,7 +16206,7 @@ packages:
         optional: true
     dependencies:
       '@vue/compiler-sfc': 3.4.21
-      '@vue/component-compiler-utils': 3.3.0(babel-core@6.26.3)
+      '@vue/component-compiler-utils': 3.3.0(babel-core@6.26.3)(lodash@4.17.21)
       css-loader: 6.10.0(webpack@5.91.0)
       hash-sum: 1.0.2
       loader-utils: 1.4.2
@@ -16297,7 +16269,7 @@ packages:
       - whiskers
     dev: true
 
-  /vue-loader@15.11.1(babel-core@6.26.3)(cache-loader@4.1.0)(css-loader@3.6.0)(vue-template-compiler@2.7.16)(webpack@4.47.0):
+  /vue-loader@15.11.1(babel-core@6.26.3)(cache-loader@4.1.0)(css-loader@3.6.0)(lodash@4.17.21)(vue-template-compiler@2.7.16)(webpack@4.47.0):
     resolution: {integrity: sha512-0iw4VchYLePqJfJu9s62ACWUXeSqM30SQqlIftbYWM3C+jpPcEHKSPUZBLjSF9au4HTHQ/naF6OGnO3Q/qGR3Q==}
     peerDependencies:
       '@vue/compiler-sfc': ^3.0.8
@@ -16316,7 +16288,7 @@ packages:
       vue-template-compiler:
         optional: true
     dependencies:
-      '@vue/component-compiler-utils': 3.3.0(babel-core@6.26.3)
+      '@vue/component-compiler-utils': 3.3.0(babel-core@6.26.3)(lodash@4.17.21)
       cache-loader: 4.1.0(webpack@4.47.0)
       css-loader: 3.6.0(webpack@4.47.0)
       hash-sum: 1.0.2
@@ -16400,7 +16372,7 @@ packages:
       vue-template-compiler:
         optional: true
     dependencies:
-      '@vue/component-compiler-utils': 3.3.0(babel-core@6.26.3)
+      '@vue/component-compiler-utils': 3.3.0(babel-core@6.26.3)(lodash@4.17.21)
       css-loader: 6.10.0(webpack@5.91.0)
       hash-sum: 1.0.2
       loader-utils: 1.4.2
@@ -16521,7 +16493,7 @@ packages:
       - typescript
     dev: false
 
-  /vue-photo-preview@1.1.3:
+  /vue-photo-preview@1.1.3(lodash@4.17.21):
     resolution: {integrity: sha512-L9JTQh62rYqLHNCdpy7zQdqVisks9dvkmUpM8+7kIKG7l+KkbRLzKWK7lCjCKc6OQT42o6/ngK6B13niXunS9Q==}
     dependencies:
       autoprefixer-loader: 3.2.0
@@ -16536,7 +16508,7 @@ packages:
       resolve-url-loader: 2.3.2
       style-loader: 0.19.1
       vue: 2.7.16
-      vue-loader: 13.7.3(babel-core@6.26.3)(css-loader@0.28.11)(vue-template-compiler@2.7.16)(webpack@3.12.0)
+      vue-loader: 13.7.3(babel-core@6.26.3)(css-loader@0.28.11)(lodash@4.17.21)(vue-template-compiler@2.7.16)(webpack@3.12.0)
       vue-template-compiler: 2.7.16
       webpack: 3.12.0
       webpack-dev-server: 2.11.5(webpack@3.12.0)
@@ -16682,6 +16654,16 @@ packages:
       vue: 3.4.21
     dev: false
 
+  /vue2-perfect-scrollbar@1.5.56(postcss@8.4.38):
+    resolution: {integrity: sha512-0ciZFj8kfMnsVkEi9BYf16HoybdN8bju8zj4Okwlrg9+rJp6i/PYXh+ZWsdeQn6jLDMi6CRSNEsaTsLPStIVHQ==}
+    dependencies:
+      cssnano: 5.1.15(postcss@8.4.38)
+      perfect-scrollbar: 1.5.5
+      postcss-import: 12.0.1
+    transitivePeerDependencies:
+      - postcss
+    dev: false
+
   /vue3-lazyload@0.3.8(vue@3.4.21):
     resolution: {integrity: sha512-UiJHRT7mzry102WbhtrRgJh+f8Z8u4Z+H1RU4dvPmQeq7wFSDFxZB9iJOWGihH2FscXN/8rMGLDOQJAmjwqpCg==}
     peerDependencies:
@@ -17448,7 +17430,6 @@ packages:
   /yaml@1.10.2:
     resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
     engines: {node: '>= 6'}
-    dev: true
 
   /yargs-parser@13.1.2:
     resolution: {integrity: sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==}