Bläddra i källkod

feat: 增加相机作品

gemercheung 1 år sedan
förälder
incheckning
089dc81e29

+ 11 - 0
packages/qjkankan-editor/src/api/index.js

@@ -1018,3 +1018,14 @@ export function getNoticeApi(data, ok, no) {
 export function getQrCode(data, ok, no) {
   return http.postJson(`${URL_FILL}/manage/logo/gerQrCode`, data, ok, no);
 }
+
+
+/**
+ * 获取我的作品列表
+ * @param {*} data
+ * @param {*} ok
+ * @param {*} no
+ */
+export function getCamWorksList(data, ok, no) {
+  return http.postJson(`${URL_FILL}/manage/work/sceneWorkList`, data, ok, no);
+}

+ 20 - 15
packages/qjkankan-editor/src/config/menu.js

@@ -1,5 +1,4 @@
-import {i18n} from "@/lang"
-
+import { i18n } from "@/lang";
 
 // 编辑器主菜单
 const PCMenu = [
@@ -27,7 +26,7 @@ const PCMenu = [
     name: "screen",
     hasPreviewArea: true,
     previewAreaExtraLeft: 0,
-    hidden: false
+    hidden: false,
   },
   {
     text: i18n.t("edit_page.hotspot"),
@@ -36,7 +35,7 @@ const PCMenu = [
     name: "hotspot",
     hasPreviewArea: true,
     previewAreaExtraLeft: 0,
-    hidden: false
+    hidden: false,
   },
   {
     text: i18n.t("edit_page.explanation"),
@@ -45,7 +44,7 @@ const PCMenu = [
     name: "explanation",
     hasPreviewArea: true,
     previewAreaExtraLeft: 0,
-    hidden: false
+    hidden: false,
   },
   {
     text: i18n.t("edit_page.mask"),
@@ -54,8 +53,8 @@ const PCMenu = [
     name: "mask",
     hasPreviewArea: true,
     previewAreaExtraLeft: 0,
-    hidden: false
-  }
+    hidden: false,
+  },
 ];
 
 // 管理平台主菜单
@@ -65,37 +64,43 @@ const MATERIALMenu = [
     icon: "icon_base",
     link: "/works",
     name: "works",
-    belong:'works'
+    belong: "works",
+  },
+  {
+    text: "相机作品",
+    icon: "icon_base",
+    link: "/camList",
+    name: "camList",
+    belong: "camList",
   },
   {
     text: "全景图片",
     icon: "icon_base",
     link: "/pano",
     name: "pano",
-    belong:'material'
+    belong: "material",
   },
   {
     text: "图片",
     icon: "iconchangjingdaohang",
     link: "/image",
     name: "image",
-    belong:'material'
+    belong: "material",
   },
   {
     text: "音频",
     icon: "icon_screen",
     link: "/audio",
     name: "audio",
-    belong:'material'
+    belong: "material",
   },
   {
     text: "视频",
     icon: "icon_hotpoint",
     link: "/video",
     name: "video",
-    belong:'material'
-  }
+    belong: "material",
+  },
 ];
 
-
-export {PCMenu,MATERIALMenu}
+export { PCMenu, MATERIALMenu };

+ 46 - 41
packages/qjkankan-editor/src/framework/material/aside.vue

@@ -1,78 +1,83 @@
 <template>
   <div class="aside">
     <ul>
-      <router-link tag="li" :to="item.link" v-for="(item,i) in list" :key="i">
-        <i class="iconfont" :class="item.icon"></i>{{item.name}}
+      <router-link tag="li" :to="item.link" v-for="(item, i) in list" :key="i">
+        <i class="iconfont" :class="item.icon"></i>{{ item.name }}
       </router-link>
     </ul>
   </div>
 </template>
 
 <script>
-import {i18n} from "@/lang"
+import { i18n } from "@/lang";
 
 export default {
-  data(){
+  data() {
     return {
-
-      list:[{
-        name:i18n.t("gather.panorama"),
-        id:'pano',
-        link:{path:'/pano'},
-        icon:'icon-material_panoramic_picture'
-      },{
-        name:i18n.t("gather.image"),
-        id:'image',
-        link:{path:'/image'},
-        icon:'icon-material_image'
-      },{
-        name:i18n.t("gather.audio"),
-        id:'audio',
-        link:{path:'/audio'},
-        icon:'icon-material_music'
-      },{
-        name:i18n.t("gather.video"),
-        id:'video',
-        link:{path:'/video'},
-        icon:'icon-material_video'
-      }]
-    }
+      list: [
+        {
+          name: i18n.t("gather.panorama"),
+          id: "pano",
+          link: { path: "/pano" },
+          icon: "icon-material_panoramic_picture",
+        },
+        {
+          name: i18n.t("gather.image"),
+          id: "image",
+          link: { path: "/image" },
+          icon: "icon-material_image",
+        },
+        {
+          name: i18n.t("gather.audio"),
+          id: "audio",
+          link: { path: "/audio" },
+          icon: "icon-material_music",
+        },
+        {
+          name: i18n.t("gather.video"),
+          id: "video",
+          link: { path: "/video" },
+          icon: "icon-material_video",
+        },
+      ],
+    };
   },
-  methods:{
-    handleItem(data){
-      this.active = data
-      this.$router.push(data.link)
-    }
-  }
-}
+  methods: {
+    handleItem(data) {
+      this.active = data;
+      this.$router.push(data.link);
+    },
+  },
+};
 </script>
 
 <style lang="less" scoped>
-.aside{
+.aside {
   color: #969799;
   background: #fff;
   width: 200px;
   margin-right: 30px;
   flex-shrink: 0;
   max-height: 232px;
-  >ul{
+  > ul {
     padding: 20px 0;
-    >li{
+    > li {
       padding-left: 50px;
       cursor: pointer;
       line-height: 48px;
       height: 48px;
       color: #969799;
       font-size: 16px;
-      &:hover, &.router-link-exact-active,
+      &:hover,
+      &.router-link-exact-active,
       &.router-link-active {
         color: #323233;
-        background: #F7F8FA;
+        background: #f7f8fa;
       }
-      .iconfont{
+      .iconfont {
         margin-right: 10px;
       }
     }
   }
 }
-</style>
+</style>

+ 73 - 45
packages/qjkankan-editor/src/framework/material/header.vue

@@ -2,23 +2,44 @@
   <div class="header">
     <div class="con">
       <a :href="homeLink" class="logo">
-        <img :src="require(`@/assets/images/icons/logo_black_${$lang}.svg`)" alt="" />
+        <img
+          :src="require(`@/assets/images/icons/logo_black_${$lang}.svg`)"
+          alt=""
+        />
       </a>
       <ul class="tab">
-        <li @click="handleItem(item)" :class="{ active: active.id == item.id }" v-for="(item, i) in tab" :key="i">
+        <li
+          @click="handleItem(item)"
+          :class="{ active: active.id == item.id }"
+          v-for="(item, i) in tab"
+          :key="i"
+        >
           {{ item.name }}
         </li>
       </ul>
 
       <div class="language-w">
         <div class="list">
-          <a class="header-item" :class="{ 'is-hover': showLangList }" @touchstart="showLangList = !showLangList">
-            <p class="guoqi" :style="{ 'background-image': `url(${languageObj.img})` }">
+          <a
+            class="header-item"
+            :class="{ 'is-hover': showLangList }"
+            @touchstart="showLangList = !showLangList"
+          >
+            <p
+              class="guoqi"
+              :style="{ 'background-image': `url(${languageObj.img})` }"
+            >
               {{ languageObj.name }}
             </p>
             <ul class="child-list">
-              <li v-for="item in languageList" :key="item.name" :style="{ 'background-image': `url(${item.img})` }"
-                @click="changeLanguage(item.value)">{{ item.name }}</li>
+              <li
+                v-for="item in languageList"
+                :key="item.name"
+                :style="{ 'background-image': `url(${item.img})` }"
+                @click="changeLanguage(item.value)"
+              >
+                {{ item.name }}
+              </li>
             </ul>
           </a>
         </div>
@@ -32,7 +53,7 @@
 
 <script>
 import UserInfo from "@/components/userInfo.vue";
-import { i18n } from "@/lang"
+import { i18n } from "@/lang";
 
 export default {
   components: {
@@ -41,18 +62,18 @@ export default {
   data() {
     return {
       homeLink: process.env.VUE_APP_PROXY_URL_ROOT,
-      langauge: localStorage.getItem('language'),
+      langauge: localStorage.getItem("language"),
       languageList: [
         {
-          name: '简体中文',
-          img: require('@/assets/images/icons/china@2x.png'),
-          value: 'zh'
+          name: "简体中文",
+          img: require("@/assets/images/icons/china@2x.png"),
+          value: "zh",
         },
         {
-          name: 'English',
-          img: require('@/assets/images/icons/USA@2x.jpg'),
-          value: 'en'
-        }
+          name: "English",
+          img: require("@/assets/images/icons/USA@2x.jpg"),
+          value: "en",
+        },
       ],
       showLangList: false,
       active: {},
@@ -61,45 +82,52 @@ export default {
           name: i18n.t("gather.my_works"),
           id: "works",
           path: {
-            path: '/works'
-          }
+            path: "/works",
+          },
         },
         {
           name: i18n.t("gather.my_material"),
           id: "material",
           path: {
-            path: '/pano'
-          }
-        }
+            path: "/pano",
+          },
+        },
+        {
+          name: "相机作品",
+          id: "camList",
+          path: {
+            path: "/camList",
+          },
+        },
       ],
     };
   },
   computed: {
     languageObj() {
       console.log(i18n.locale);
-      return this.languageList.find(item => item.value === i18n.locale)
+      return this.languageList.find((item) => item.value === i18n.locale);
     },
   },
   watch: {
-    '$route.meta': {
+    "$route.meta": {
       deep: true,
       handler: function (newVal) {
-        this.active = this.tab.find(item => {
-          return item.id == newVal.belong
-        })
-      }
-    }
+        this.active = this.tab.find((item) => {
+          return item.id == newVal.belong;
+        });
+      },
+    },
   },
   methods: {
     changeLanguage(lang) {
-      let arr = location.href.split('#/')
-      location.href = `material.html?lang=${lang}#/${arr[1]}`
-      localStorage.language = lang
+      let arr = location.href.split("#/");
+      location.href = `material.html?lang=${lang}#/${arr[1]}`;
+      localStorage.language = lang;
     },
     handleItem(item) {
-      this.$router.push(item.path)
-    }
-  }
+      this.$router.push(item.path);
+    },
+  },
 };
 </script>
 
@@ -127,7 +155,7 @@ export default {
       font-weight: bold;
       height: 100%;
 
-      >img {
+      > img {
         height: 100%;
         vertical-align: middle;
       }
@@ -138,7 +166,7 @@ export default {
       align-items: center;
       margin-left: 116px;
 
-      >li {
+      > li {
         cursor: pointer;
         margin-right: 50px;
         text-align: left;
@@ -148,14 +176,14 @@ export default {
         &.active {
           font-size: 16px;
           font-weight: bold;
-          color: #0076F6;
+          color: #0076f6;
           position: relative;
 
           &::before {
-            content: '';
+            content: "";
             width: 20px;
             height: 2px;
-            background: #0076F6;
+            background: #0076f6;
             display: inline-block;
             position: absolute;
             left: 50%;
@@ -168,7 +196,7 @@ export default {
 
     .language-w {
       margin-left: auto;
-      .list{
+      .list {
         height: 100%;
         display: flex;
       }
@@ -191,7 +219,8 @@ export default {
           font-size: 14px;
           font-weight: normal;
           cursor: pointer;
-          background: url(~@/assets/images/icons/china.png) no-repeat left center;
+          background: url(~@/assets/images/icons/china.png) no-repeat left
+            center;
           background-size: 20px 14px;
           padding: 10px 0 10px 28px;
 
@@ -199,7 +228,6 @@ export default {
             font-size: 10px;
             color: rgb(144, 144, 144);
           }
-
         }
 
         &:hover p .iconfont::before {
@@ -228,7 +256,7 @@ export default {
           margin-left: -2px;
           width: 0;
           height: 0px;
-          content: '';
+          content: "";
           background: #fff;
           border-style: solid;
           border-width: 4px;
@@ -253,16 +281,16 @@ export default {
           font-size: 14px;
           font-weight: normal;
           padding: 0 13px 0 40px;
-          background: url(~@/assets/images/icons/china.png) no-repeat 13px center;
+          background: url(~@/assets/images/icons/china.png) no-repeat 13px
+            center;
           background-size: 20px 14px;
 
           &:hover {
-            background-color: #EBEBEB;
+            background-color: #ebebeb;
             color: #202020;
           }
         }
       }
-
     }
 
     .user-info {

+ 26 - 22
packages/qjkankan-editor/src/framework/material/index.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="material">
     <app-header></app-header>
-    <router-view class="body" v-if="$route.name=='works'"/>
+    <router-view class="body" v-if="isFilterAside" />
     <div class="body" v-else>
       <app-aside></app-aside>
       <app-main></app-main>
@@ -15,38 +15,43 @@ import AppHeader from "./header.vue";
 import AppMain from "./Main.vue";
 
 export default {
-  components:{
+  components: {
     AppAside,
     AppHeader,
-    AppMain
+    AppMain,
+  },
+  computed: {
+    isFilterAside: function () {
+      return ["works", "camList"].includes(this.$route.name);
+    },
   },
   mounted() {
-    this.$nextTick(()=>{
-      this.$bus.on('refreshTips',(data)=>{
-      setTimeout(() => {
-        let lastinstances = $.tooltipster.instancesLatest();
-        $.each(lastinstances, function(i, instance){
-          instance.destroy()
+    this.$nextTick(() => {
+      this.$bus.on("refreshTips", (data) => {
+        setTimeout(() => {
+          let lastinstances = $.tooltipster.instancesLatest();
+          $.each(lastinstances, function (i, instance) {
+            instance.destroy();
+          });
+
+          $(".tttttt").tooltipster({
+            delay: 300,
+            ...data,
+          });
         });
-        
-        $('.tttttt').tooltipster({
-          delay: 300,
-          ...data
-        }); 
       });
-    })
-    })
+    });
   },
-}
+};
 </script>
 
 <style lang="less" scoped>
-.material{
+.material {
   overflow: hidden;
   position: relative;
-  background: #EFF2F4;
+  background: #eff2f4;
   height: 100vh;
-  .body{
+  .body {
     max-width: 1280px;
     margin: 0 auto;
     padding-bottom: 30px;
@@ -55,5 +60,4 @@ export default {
     height: 100%;
   }
 }
-
-</style>
+</style>

+ 47 - 57
packages/qjkankan-editor/src/router/editorRouter.js

@@ -1,24 +1,23 @@
-import Vue from 'vue'
-import Router from 'vue-router'
+import Vue from "vue";
+import Router from "vue-router";
 import { PCMenu } from "../config/menu.js";
-import { checkWork, checkLogin, getPanoInfo } from '@/api'
-import store from '../Store'
+import { checkWork, checkLogin, getPanoInfo } from "@/api";
+import store from "../Store";
 
-import {i18n} from '@/lang'
+import { i18n } from "@/lang";
 
+let vue = new Vue();
 
-let vue = new Vue()
+import { LoginDetector, OnlineDetector } from "@/utils/starter";
+Vue.use(Router);
 
-import { LoginDetector,OnlineDetector } from "@/utils/starter";
-Vue.use(Router)
-
-const originalPush = Router.prototype.push
-Router.prototype.push = function push (location) {
-  return originalPush.call(this, location).catch(err => err)
-}
+const originalPush = Router.prototype.push;
+Router.prototype.push = function push(location) {
+  return originalPush.call(this, location).catch((err) => err);
+};
 
 let routes = [];
-PCMenu.forEach(item => {
+PCMenu.forEach((item) => {
   routes.push({
     name: item.name,
     path: `${item.link}`,
@@ -27,78 +26,69 @@ PCMenu.forEach(item => {
       hasPreviewArea: item.hasPreviewArea,
       previewAreaExtraLeft: item.previewAreaExtraLeft,
     },
-    component: () => import(`../views/${item.name}/index.vue`)
+    component: () => import(`../views/${item.name}/index.vue`),
   });
 });
 
-
 LoginDetector.register(
-  detector => new Promise(resolve => detector.resolve(resolve))
+  (detector) => new Promise((resolve) => detector.resolve(resolve))
 );
 
 OnlineDetector.register(
-  detector => new Promise(resolve => detector.resolve(resolve))
+  (detector) => new Promise((resolve) => detector.resolve(resolve))
 );
 
 //检验是不是该用户作品
 
-
-checkWork().then(res=>{
+checkWork().then((res) => {
   if (res.data) {
-    checkLogin().then(response => {
+    checkLogin().then((response) => {
       if (response.code == 3005) {
-        store.commit('UpdateIsShowState', false)
-        vue.$bus.emit('canLoad',false) 
-        return vue.$alert({content: '当前无操作权限'});
-      } else{
-        vue.$bus.emit('canLoad',true) 
+        store.commit("UpdateIsShowState", false);
+        vue.$bus.emit("canLoad", false);
+        return vue.$alert({ content: "当前无操作权限" });
+      } else {
+        vue.$bus.emit("canLoad", true);
         LoginDetector.valid();
       }
     });
-  } 
-  else{
-    return vue.$alert({content: '该作品已被删除'});
+  } else {
+    return vue.$alert({ content: "该作品已被删除" });
   }
-  
-})
+});
 
 getPanoInfo().then(() => {
-  store.commit('UpdateIsShowState', true)
+  store.commit("UpdateIsShowState", true);
   // if(response&&response.status == 1){
   // }
   OnlineDetector.valid();
 });
 
-
 const router = new Router({
-  routes: routes
-})
-
+  routes: routes,
+});
 
 router.beforeEach(async (to, from, next) => {
-  
-    await LoginDetector.listener();
-    await OnlineDetector.listener();
-    if (from.name == 'hotspot') {
-      if (store.getters.isEditing) {
-        vue.$confirm({
-          content: i18n.t('hotspot.close_dialog'),
-          ok: () => {
-            vue.$bus.emit('delhotspot') 
-            return next()
-          }
-        });
-        return
-      }
+  await LoginDetector.listener();
+  await OnlineDetector.listener();
+  if (from.name == "hotspot") {
+    if (store.getters.isEditing) {
+      vue.$confirm({
+        content: i18n.t("hotspot.close_dialog"),
+        ok: () => {
+          vue.$bus.emit("delhotspot");
+          return next();
+        },
+      });
+      return;
     }
+  }
 
+  if (to.path == "/") {
+    return next({ path: "/base" });
+  }
 
-    if (to.path == '/') {
-        return next({path: "/base" })
-    }
-    
-    next()
-})
-
+  next();
+});
 
 export default router;

+ 16 - 20
packages/qjkankan-editor/src/router/material.js

@@ -1,39 +1,35 @@
-import Vue from 'vue'
-import Router from 'vue-router'
+import Vue from "vue";
+import Router from "vue-router";
 import { MATERIALMenu } from "../config/menu";
 
-Vue.use(Router)
+Vue.use(Router);
 
-const originalPush = Router.prototype.push
-Router.prototype.push = function push (location) {
-  return originalPush.call(this, location).catch(err => err)
-}
+const originalPush = Router.prototype.push;
+Router.prototype.push = function push(location) {
+  return originalPush.call(this, location).catch((err) => err);
+};
 
 let routes = [];
-MATERIALMenu.forEach(item => {
+MATERIALMenu.forEach((item) => {
   routes.push({
     name: item.name,
     path: `${item.link}`,
     meta: {
-      belong: item.belong
+      belong: item.belong,
     },
-    component: () => import(`../views/material/${item.name}/index.vue`)
+    component: () => import(`../views/material/${item.name}/index.vue`),
   });
 });
 
 const router = new Router({
-  routes: routes
-})
-
+  routes: routes,
+});
 
 router.beforeEach(async (to, from, next) => {
-  if (to.path == '/') {
-      return next({path: "/works" })
+  if (to.path == "/") {
+    return next({ path: "/works" });
   }
-  next()
-})
-
-
-
+  next();
+});
 
 export default router;

+ 825 - 0
packages/qjkankan-editor/src/views/material/camList/index.vue

@@ -0,0 +1,825 @@
+<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,
+  getCamWorksList,
+  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) {
+      window.open(`./edit.html?id=${item.id}&lang=${this.$lang}`);
+    },
+    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>
+.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>