wangfumin il y a 1 mois
Parent
commit
5457483f6b

+ 1 - 0
mobile/package.json

@@ -35,6 +35,7 @@
     "eslint-plugin-vue": "~10.3.0",
     "globals": "^16.3.0",
     "jsdom": "^26.1.0",
+    "postcss-pxtorem": "^6.1.0",
     "prettier": "3.6.2",
     "vite": "^7.0.6",
     "vite-plugin-vue-devtools": "^8.0.0",

+ 12 - 0
mobile/pnpm-lock.yaml

@@ -63,6 +63,9 @@ importers:
       jsdom:
         specifier: ^26.1.0
         version: 26.1.0
+      postcss-pxtorem:
+        specifier: ^6.1.0
+        version: 6.1.0(postcss@8.5.6)
       prettier:
         specifier: 3.6.2
         version: 3.6.2
@@ -1715,6 +1718,11 @@ packages:
     resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
     engines: {node: '>=12'}
 
+  postcss-pxtorem@6.1.0:
+    resolution: {integrity: sha512-ROODSNci9ADal3zUcPHOF/K83TiCgNSPXQFSbwyPHNV8ioHIE4SaC+FPOufd8jsr5jV2uIz29v1Uqy1c4ov42g==}
+    peerDependencies:
+      postcss: ^8.0.0
+
   postcss-selector-parser@6.1.2:
     resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
     engines: {node: '>=4'}
@@ -3975,6 +3983,10 @@ snapshots:
 
   picomatch@4.0.3: {}
 
+  postcss-pxtorem@6.1.0(postcss@8.5.6):
+    dependencies:
+      postcss: 8.5.6
+
   postcss-selector-parser@6.1.2:
     dependencies:
       cssesc: 3.0.0

+ 5 - 7
mobile/src/api/index.js

@@ -5,20 +5,16 @@ const hhbangBookApi = {
   // 首页获取推荐列表
   getRecommendListApi(params = {}) {
     return request({
-      url: '/hyb/artArtworks/index/page',
+      url: '/hyb/artArtworks/index/list',
       method: 'get',
-      params: {
-        pageNo: Math.floor(Math.random() * 10) + 1, // 随机页码
-        pageSize: 60,
-        ...params
-      }
+      params: {}
     })
   },
 
   // 获取文物列表 - 用于收藏页面和详情页上下页切换
   getArtifactListApi(params = {}) {
     return request({
-      url: '/hyb/artArtworks/list',
+      url: '/hyb/artArtworks/page',
       method: 'get',
       params: {
         agetype: params.era || '', // 年代
@@ -26,6 +22,8 @@ const hhbangBookApi = {
         grade: params.level || '', // 级别
         searchText: params.keyword || '', // 关键词
         texture: params.material || '', // 材质
+        pageNo: params.pageNo || 1,
+        pageSize: params.pageSize || 50,
         ...params
       },
       //序列化

BIN
mobile/src/assets/img/btn_stoke_03.png


BIN
mobile/src/assets/img/icon_3d.png


BIN
mobile/src/assets/img/icon_restart.png


+ 15 - 7
mobile/src/assets/styles/cut-corner.scss

@@ -1,24 +1,29 @@
 // 公共的切角样式
 .cut-corner-select,
 .cut-corner-input {
-  .el-input__wrapper,
-  .el-select__wrapper {
+  .el-input__wrapper, .el-select__wrapper {
     position: relative;
     border: none;
     border-radius: 0;
     padding: 2px 8px;
     box-shadow: none;
+    line-height: 1.0667rem;
     .el-select__selection{
-      height: 40px;
+      height: 1.0667rem;
     }
     .el-input__inner,
     .el-select__selected-label {
-      height: 40px;
+      height: 1.0667rem;
       background: transparent;
       border: none;
       color: #333;
     }
   }
+  .el-select__wrapper{
+    padding: 0.08rem 0.16rem;
+    font-size: 0.5rem;
+    color: #B49D7E;
+  }
   .el-input__wrapper{
     background-image: url('@/assets/img/btn_stoke_01.png');
     background-size: 100% 100%;
@@ -26,7 +31,7 @@
     background-position: center;
   }
   .el-select__wrapper{
-    background-image: url('@/assets/img/btn_stoke_01.png');
+    background-image: url('@/assets/img/btn_stoke_03.png');
     background-size: 100% 100%;
     background-repeat: no-repeat;
     background-position: center;
@@ -35,10 +40,13 @@
 
 // el-select 宽度设置
 .cut-corner-select {
-  width: 140px;
+  width: 16.2%;
+}
+.cut-corner-select-dim {
+  width: 21%;
 }
 
 // 搜索关键词输入框宽度
 .search-keyword-input {
-  width: 350px;
+  width: 35.2rem;
 }

+ 26 - 26
mobile/src/components/Tabbar/index.vue

@@ -45,18 +45,18 @@ const LIST = [
     activeIcon: BookActiveIcon,
     routeName: "collectpage",
   },
-  {
-    label: "公告资讯",
-    icon: noticeIcon,
-    activeIcon: noticeActiveIcon,
-    routeName: "contentPage",
-  },
-  {
-    label: "文物线索联系我们",
-    icon: recruitIcon,
-    activeIcon: recruitActiveIcon,
-    routeName: "recruitPage",
-  },
+  // {
+  //   label: "公告资讯",
+  //   icon: noticeIcon,
+  //   activeIcon: noticeActiveIcon,
+  //   routeName: "contentPage",
+  // },
+  // {
+  //   label: "文物线索联系我们",
+  //   icon: recruitIcon,
+  //   activeIcon: recruitActiveIcon,
+  //   routeName: "recruitPage",
+  // },
 ];
 
 const route = useRoute();
@@ -80,16 +80,16 @@ const handleClick = (item) => {
 <style lang="scss" scoped>
 .tabbar {
   position: fixed;
-  left: 30px;
-  right: 30px;
-  bottom: 20px;
+  left: 0.8rem;
+  right: 0.8rem;
+  bottom: 0.5333rem;
   display: flex;
   align-items: center;
-  justify-content: space-between;
-  padding: 6px 30px;
-  border-radius: 100px;
+  justify-content: space-around;
+  padding: 0.16rem 0.8rem;
+  border-radius: 2.6667rem;
   background: rgba(255, 255, 255, 0.8);
-  box-shadow: 0px 0 72px 0px rgba(102, 86, 65, 0.2);
+  box-shadow: 0 0 1.92rem 0 rgba(102, 86, 65, 0.2);
   transition: bottom ease-in-out 0.2s;
   z-index: 999;
 
@@ -97,19 +97,19 @@ const handleClick = (item) => {
     bottom: -50%;
   }
   &-item {
-    height: 50px;
+    height: 1.3333rem;
     display: flex;
     flex-direction: column;
     align-items: center;
-    font-size: 12px;
+    font-size: 0.32rem;
     color: #c8c4be;
 
     &.active {
       color: #a99271;
     }
     &-icon {
-      width: 30px;
-      height: 30px;
+      width: 0.8rem;
+      height: 0.8rem;
       img{
         width: 100%;
         height: 100%;
@@ -117,10 +117,10 @@ const handleClick = (item) => {
     }
     &-label{
       position: absolute;
-      top: 34px;
-      width: 50px;
+      top: 0.9067rem;
+      width: 1.3333rem;
       text-align: center;
-      line-height: 12px;
+      line-height: 0.32rem;
     }
   }
 }

+ 10 - 0
mobile/src/main.js

@@ -5,6 +5,16 @@ import router from './router'
 import store from './store'
 import api from './api'
 
+// 动态设置根字体大小:1rem = 视口宽度 / 10
+// 结合 postcss-pxtorem 的 rootValue: 216(按 2160 设计稿 / 10)
+const setRem = () => {
+  const width = document.documentElement.clientWidth
+  document.documentElement.style.fontSize = (width / 10) + 'px'
+}
+setRem()
+window.addEventListener('resize', setRem)
+window.addEventListener('orientationchange', setRem)
+
 const app = createApp(App)
 import ElementPlus from 'element-plus'
 import 'element-plus/dist/index.css'

+ 4 - 7
mobile/src/utils/index.js

@@ -25,7 +25,6 @@ export function addWatermarkToCollectionImage(imageUrl, options = {}) {
           // console.log(`图片加载完成 ${imagesLoaded}/2`);
           if (imagesLoaded === 2) {
               try {
-                  console.log('开始绘制水印');
                   // 两张图片都加载完成
                   const canvas = document.createElement('canvas');
                   const ctx = canvas.getContext('2d');
@@ -88,8 +87,7 @@ export function addWatermarkToCollectionImage(imageUrl, options = {}) {
                   // console.log('水印处理完成,生成的URL长度:', watermarkedImageUrl.length);
                   resolve(watermarkedImageUrl);
               } catch (error) {
-                  console.error('Canvas绘制错误:', error);
-                  reject(new Error('水印处理失败: ' + error.message));
+                  reject(new Error('图片加载失败'));
               }
           }
       }
@@ -97,13 +95,12 @@ export function addWatermarkToCollectionImage(imageUrl, options = {}) {
       image.onload = onImageLoad;
       watermark.onload = onImageLoad;
       
+      // 主图或水印加载失败时,直接返回原链接,避免继续添加水印造成反复报错
       image.onerror = () => {
-        //   console.error('主图片加载失败:', imageUrl);
-          reject(new Error(`主图片加载失败: ${imageUrl}`));
+          resolve(imageUrl);
       };
       watermark.onerror = () => {
-        //   console.error('水印图片加载失败');
-          reject(new Error('水印图片加载失败'));
+          resolve(imageUrl);
       };
       
       // 开始加载图片

+ 29 - 23
mobile/src/views/Home/components/SearchPane.vue

@@ -49,54 +49,60 @@ const handleSearch = () => {
 <style lang="scss" scoped>
 .search-pane {
   .limit-line{
-    height: 24px;
+    height: 0.64rem;
     width: auto;
     background: url("@/assets/img/btn_more.png") no-repeat center / contain;
   }
   :deep(.el-input__wrapper){
     background: rgba(255, 255, 255, 0.5);
-    border: 2px solid #D3BFA2;
-    border-radius: 100px;
+    border: 0.0533rem solid #D3BFA2;
+    border-radius: 2.6667rem;
     box-shadow: none;
-    height: 40px;
-    padding-right: 4px;
+    height: 1.0667rem;
+    padding-right: 0.1067rem;
   }
   :deep(.el-input__inner){
     background: transparent;
     border: none;
     outline: none;
-    height: 40px;
+    height: 1.0667rem;
+    font-size: 0.5rem;
+    padding-left: 0.3333rem;
   }
   :deep(.search-input__icon) {
-    width: 38px;
-    height: 38px;
+    width: 1.0133rem;
+    height: 1.0133rem;
     display: flex;
     justify-content: center;
     align-items: center;
     background-color: #D3BFA2!important;
     border-radius: 50%;
     background: none;
+    img{
+      width: 0.6133rem;
+      height: 0.6133rem;
+    }
   }
   &-main {
     position: absolute;
     top: 25%;
     left: 0;
     right: 0;
-    padding: 0 30px;
+    padding: 0 0.8rem;
     display: flex;
     flex-direction: column;
     align-items: center;
     z-index: 99;
 
     .logo {
-      width: calc(100% - 80px);
-      margin-bottom: 20px;
+      width: calc(100% - 2.1333rem);
+      margin-bottom: 0.5333rem;
     }
   }
   &-more {
     width: 100%;
-    height: 45px;
-    font-size: 23px;
+    height: 1.2rem;
+    font-size: 0.6133rem;
     color: var(--van-primary-color);
     background: url("../images/img_tip@3x.png") no-repeat center / contain;
     box-sizing: border-box;
@@ -104,35 +110,35 @@ const handleSearch = () => {
   &-tips {
     position: absolute;
     left: 50%;
-    bottom: calc(var(--tabbar-height) + 80px);
+    bottom: calc(var(--tabbar-height) + 2.1333rem);
     display: flex;
     align-items: center;
     justify-content: center;
-    gap: 17px;
+    gap: 0.4533rem;
     color: #a99271;
     transform: translateX(-50%);
     z-index: 99;
 
     img {
-      width: 60px;
+      width: 1.6rem;
     }
   }
   .search-input {
-    margin: 30px 0 0px;
+    margin: 0.8rem 0 0;
   }
   .limit-line {
     text-align: center;
-    font-size: 12px;
+    font-size: 0.32rem;
     color: #D3BFA2;
-    margin-top: 10px;
-    line-height: 24px;
+    margin-top: 0.2667rem;
+    line-height: 0.64rem;
     display: flex;
     align-items: center;
     justify-content: center;
     img{
-      width: 12px;
-      height: 12px;
-      margin-left: 5px;
+      width: 0.32rem;
+      height: 0.32rem;
+      margin-left: 0.1333rem;
     }
   }
 }

+ 18 - 6
mobile/src/views/Home/index.vue

@@ -51,7 +51,7 @@ const processImageWithWatermark = async (item) => {
     const watermarkedImageUrl = await addWatermarkToCollectionImage(item.thumb, {
       position: 'center',
       opacity: 1,
-      scale: 0.7,
+      scale: 0.5,
       margin: 15
     });
     // 存储处理后的图片
@@ -59,7 +59,8 @@ const processImageWithWatermark = async (item) => {
     return watermarkedImageUrl;
   } catch (error) {
     console.error('水印处理失败:', error);
-    // 失败时使用原图
+    // 统一处理:失败时直接缓存并返回原图
+    watermarkedImages.value.set(item.id, item.thumb);
     return item.thumb;
   } finally {
     // 标记处理完成
@@ -133,11 +134,22 @@ onMounted(() => {
 const getRecommendList = async () => {
   isLoading.value = true;
   let data = await getBookCountApi.getRecommendListApi();
-
+  const searchParams = {
+      dim: 3,
+      searchText: '',
+      level: '',
+      category: '',
+      material: '',
+      era: '',
+      orderBy: '',
+      orderbyList: [],
+      pageNo: 1,
+      pageSize: 1
+    };
+    const totalData = await getBookCountApi.getArtifactListApi(searchParams);
+    const totalCount = totalData.total ?  totalData.total : 26;
   // 处理接口返回的数据
-  let list = data.pageData;
-  list = list.filter(i => i.thumbnail).slice(0, 60)
-  const totalCount = data.total;
+  let list = data;
 
   // 更新总数
   total.value = totalCount;

+ 107 - 73
mobile/src/views/collectpage/components/collectDetail.vue

@@ -43,20 +43,17 @@
             <div class="model-item">
               <iframe
                 id="ifr"
-                :src="`https://hybgc.4dage.com/front/modelLoad/model.html?m=${modelList[0]?.src}`"
+                :src="`/modelLoad/model.html?m=${modelList[0]?.src}`"
                 frameborder="0"
                 width="100%"
                 height="100%"
                 allowfullscreen
               ></iframe>
-              <!-- <iframe
-                id="ifr"
-                :src="`${modelList[0]?.src}`"
-                frameborder="0"
-                width="100%"
-                height="100%"
-                allowfullscreen
-              ></iframe> -->
+              <div class="waterMar" id="waterMar">
+                <img
+                  src="@/assets/img/waterMar.png"
+                />
+              </div>
             </div>
           </div>
           <div v-else class="empty-state">
@@ -125,13 +122,13 @@
           <img
             :src="isAudioPlaying ? iconVoiceOn : iconVoiceOff"
             alt="音频控制"
-            style="width: 24px; height: 24px;"
+            style="width: 0.64rem; height: 0.64rem;"
           />
         </div>
 
         <!-- 放大镜图标 -->
         <div v-if="activeTab === 'model' && artifactData && artifactData.modelFiles && artifactData.modelFiles.length > 0" class="zoom-icon" @click="toggleZoom">
-          <el-icon style="color: #7C6444" :size="24">
+          <el-icon style="color: #7C6444" size="0.64rem">
             <ZoomIn v-if="!isZoomed" />
             <ZoomOut v-else />
           </el-icon>
@@ -268,7 +265,7 @@ const processImageWithWatermark = async (item) => {
     const watermarkedImageUrl = await addWatermarkToCollectionImage(item.originalImage, {
       position: 'center',
       opacity: 1,
-      scale: 0.7,
+      scale: 0.5,
       margin: 15
     });
     // 存储处理后的图片
@@ -278,7 +275,9 @@ const processImageWithWatermark = async (item) => {
     return watermarkedImageUrl;
   } catch (error) {
     console.error('水印处理失败:', error);
-    // 失败时使用原图
+    // 统一处理:失败时直接缓存并返回原图
+    watermarkedImages.value.set(item.id, item.originalImage);
+    item.src = item.originalImage;
     return item.originalImage;
   } finally {
     // 标记处理完成
@@ -520,10 +519,25 @@ const toggleZoom = () => {
   if (activeTab.value === 'model') {
     const dom = document.querySelector('#ifr')
     if (dom && dom.contentWindow) {
-      if (isZoomed.value) {
-        dom.contentWindow.webview.zoomIn()
-      } else {
-        dom.contentWindow.webview.zoomOut()
+      try {
+        const wv = dom.contentWindow.webview
+        if (wv && typeof wv.zoomIn === 'function' && typeof wv.zoomOut === 'function') {
+          if (isZoomed.value) {
+            wv.zoomIn()
+          } else {
+            wv.zoomOut()
+          }
+        } else {
+          // Fallback: notify iframe via postMessage
+          dom.contentWindow.postMessage({ type: 'ZOOM', action: isZoomed.value ? 'in' : 'out' }, '*')
+        }
+      } catch (err) {
+        // Cross-origin access or other error; fallback to postMessage
+        try {
+          dom.contentWindow.postMessage({ type: 'ZOOM', action: isZoomed.value ? 'in' : 'out' }, '*')
+        } catch (e2) {
+          console.warn('Zoom command failed:', err)
+        }
       }
     }
   }
@@ -684,20 +698,20 @@ onMounted(() => {
   right: 0;
   display: flex;
   align-items: center;
-  padding: 20px;
+  padding: 0.5333rem;
   z-index: 100;
 }
 
 .back-button {
-  width: 30px;
-  height: 30px;
+  width: 0.8rem;
+  height: 0.8rem;
   border-radius: 50%;
   cursor: pointer;
-  margin-right: 20px;
+  margin-right: 0.5333rem;
 
   img {
-    width: 30px;
-    height: 30px;
+    width: 0.8rem;
+    height: 0.8rem;
   }
 }
 
@@ -705,18 +719,18 @@ onMounted(() => {
   flex: 1;
   display: flex;
   justify-content: center;
-  max-width: 280px;
+  max-width: 7.4667rem;
   margin: 0 auto;
 
   .tab-item {
-    width: 72px;
+    width: 1.92rem;
     text-align: center;
     color: #ffffff;
-    font-size: 16px;
+    font-size: 0.4267rem;
     cursor: pointer;
     transition: all 0.3s ease;
     position: relative;
-    border-bottom: 2px solid transparent;
+    border-bottom: 0.0533rem solid transparent;
 
     &.active {
       color: #ffffff;
@@ -741,7 +755,7 @@ onMounted(() => {
 }
 
 .content-area {
-  margin: 20px 0;
+  margin: 0.5333rem 0;
   display: flex;
   justify-content: center;
 }
@@ -797,8 +811,8 @@ onMounted(() => {
 
     /*指示器按钮*/
     :deep(.el-carousel__button) {
-      width: 10px;
-      height: 10px;
+      width: 0.2667rem;
+      height: 0.2667rem;
       border: none;
       border-radius: 50%;
       background-color: rgba(255, 255, 255, 0.5);
@@ -813,14 +827,14 @@ onMounted(() => {
   }
 
   :deep(.el-carousel__indicators--horizontal){
-    max-width: 300px;
+    max-width: 8rem;
     background: rgba(0,0,0,0.2);
     text-align: center;
-    bottom: 24px;
-    border-radius: 50px;
-    padding: 0px 4px 0px 8px;
+    bottom: 0.64rem;
+    border-radius: 1.3333rem;
+    padding: 0 0.1067rem 0 0.2133rem;
     .el-carousel__indicator--horizontal{
-      padding: 4px 4px 0 0;
+      padding: 0.1067rem 0.1067rem 0 0;
     }
   }
 
@@ -833,6 +847,7 @@ onMounted(() => {
   }
 
   .model-item, .image-item, .video-item {
+    position: relative;
     width: 100%;
     height: 100%;
     display: flex;
@@ -843,10 +858,25 @@ onMounted(() => {
       width: 100%;
       height: 100%;
     }
+    .waterMar{
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%, -50%);
+        width: 10.6667rem;
+        height: 0.5333rem;
+        pointer-events: none;
+        img{
+          pointer-events: none;
+          width: 100%;
+          height: 100%;
+          object-fit: contain;
+        }
+      }
 
     img {
-      max-width: 100%;
-      max-height: 100%;
+      width: 80%;
+      height: 80%;
       object-fit: contain;
     }
 
@@ -875,14 +905,14 @@ onMounted(() => {
 
 .audio-control {
   position: absolute;
-  bottom: 60px;
-  right: 16px;
+  bottom: 1.6rem;
+  right: 0.4267rem;
   cursor: pointer;
   transition: all 0.3s ease;
   // background: rgba(255, 255, 255, 0.9);
   border-radius: 50%;
-  width: 40px;
-  height: 40px;
+  width: 1.0667rem;
+  height: 1.0667rem;
   display: flex;
   align-items: center;
   justify-content: center;
@@ -900,14 +930,14 @@ onMounted(() => {
 
 .zoom-icon {
   position: absolute;
-  bottom: 26px;
-  right: 16px;
+  bottom: 0.6933rem;
+  right: 0.4267rem;
   cursor: pointer;
   transition: all 0.3s ease;
   // background: rgba(255, 255, 255, 0.9);
   border-radius: 50%;
-  width: 40px;
-  height: 40px;
+  width: 1.0667rem;
+  height: 1.0667rem;
   display: flex;
   align-items: center;
   justify-content: center;
@@ -929,9 +959,9 @@ onMounted(() => {
   width: 90%;
   height: 47%;
   background: #F4EFE9;
-  border-radius: 15px 15px 0 0;
-  padding: 20px;
-  margin: 20px 0;
+  border-radius: 0.4rem 0.4rem 0 0;
+  padding: 0.5333rem;
+  margin: 0.5333rem 0;
   overflow: auto;
   transition:
     opacity 0.3s ease,
@@ -943,24 +973,24 @@ onMounted(() => {
   }
 
   .artifact-title {
-    font-size: 20px;
+    font-size: 0.5333rem;
     font-weight: bold;
     color: #7C6444;
-    padding-bottom: 15px;
-    margin-bottom: 24px;
-    border-bottom: 1px solid #B49D7E;
+    padding-bottom: 0.4rem;
+    margin-bottom: 0.64rem;
+    border-bottom: 0.0267rem solid #B49D7E;
   }
 
   .info-table {
     background: #F4EFE9;
-    border-radius: 8px;
-    margin-bottom: 15px;
+    border-radius: 0.2133rem;
+    margin-bottom: 0.4rem;
 
     .info-row {
       display: flex;
       justify-content: space-between;
-      margin-bottom: 15px;
-      gap: 40px;
+      margin-bottom: 0.4rem;
+      gap: 1.0667rem;
 
       .info-label-container {
         flex: 1;
@@ -970,11 +1000,11 @@ onMounted(() => {
         .info-label {
           display: flex;
           justify-content: space-between;
-          font-size: 16px;
+          font-size: 0.4267rem;
           color: #464646;
-          margin-bottom: 4px;
+          margin-bottom: 0.1067rem;
           .info-label-text{
-            font-size: 16px;
+            font-size: 0.4267rem;
             color: #D1BB9E;
             font-weight: bold;
           }
@@ -982,7 +1012,7 @@ onMounted(() => {
 
         .bottom-img {
           width: 100%;
-          height: 2px;
+          height: 0.0533rem;
           background: url('@/assets/img/line.png') no-repeat center center;
           background-size: 100% 100%;
         }
@@ -995,51 +1025,51 @@ onMounted(() => {
   }
 
   .artifact-size {
-    font-size: 14px;
+    font-size: 0.3733rem;
     color: #584735;
-    margin-bottom: 15px;
+    margin-bottom: 0.4rem;
   }
 
   .divider {
     width: 100%;
-    margin: 24px 0;
+    margin: 0.64rem 0;
     display: flex;
     align-items: center;
     .divider-text{
-      font-size: 16px;
+      font-size: 0.4267rem;
       color: #D1BB9E;
       font-weight: bold;
     }
   }
 
   .artifact-description {
-    font-size: 13px;
+    font-size: 0.3467rem;
     line-height: 1.6;
     color: #584735;
     text-align: justify;
   }
 }
 
-@media (max-width: 480px) {
+@media (max-width: 12.8rem) {
   .tab-navigation {
-    max-width: 280px;
-    margin: 50px auto 15px;
+    max-width: 7.4667rem;
+    margin: 1.3333rem auto 0.4rem;
 
     .tab-item {
-      padding: 8px 15px;
-      font-size: 13px;
+      padding: 0.2133rem 0.4rem;
+      font-size: 0.3467rem;
     }
   }
 
   .artifact-info {
-    padding: 15px;
+    padding: 0.4rem;
 
     .artifact-title {
-      font-size: 18px;
+      font-size: 0.48rem;
     }
 
     .artifact-description {
-      font-size: 12px;
+      font-size: 0.32rem;
     }
   }
 }
@@ -1054,4 +1084,8 @@ onMounted(() => {
     color: #fff;
   }
 }
+.empty-text{
+  font-size: 0.4267rem;
+  color: #fff;
+}
 </style>

+ 301 - 137
mobile/src/views/collectpage/index.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="collectpage">
     <!-- 搜索页面 -->
-    <div v-if="showSearchPage" class="search-page">
+    <!-- <div v-if="showSearchPage" class="search-page">
       <SearchPageComponent
         :initialSearch="searchParams"
         :categoryOptions="categoryOptions"
@@ -12,7 +12,7 @@
         @search="handleSearch"
         @close="closeSearchPage"
       />
-    </div>
+    </div> -->
 
     <!-- 主要内容 -->
     <div v-if="!showSearchPage" class="main-content">
@@ -28,34 +28,96 @@
                 </el-button>
               </template>
             </el-input>
-            <button @click="openSearchPage" class="search-btn">
-              <img src="@/assets/img/icon_select.png" alt="检索" class="search-icon" />
-              <span>检索</span>
-            </button>
           </div>
-
-          <!-- 复选框区域 -->
-          <div class="checkbox-group">
-            <el-checkbox @change="quickSearch" v-model="filters.hasImage" label="二维文物" />
-            <el-checkbox @change="quickSearch" v-model="filters.hasModel" label="三维文物" />
+          <!-- 搜索筛选(拆分自搜索页组件,放到输入框下方) -->
+          <div class="filter-row">
+            <!-- 类别 -->
+            <el-select v-model="searchParams.category" class="cut-corner-select" placeholder="类别" clearable @change="quickSearch">
+              <el-option
+                v-for="item in categoryOptions"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+
+            <!-- 级别 -->
+            <el-select v-model="searchParams.level" class="cut-corner-select" placeholder="级别" clearable @change="quickSearch">
+              <el-option
+                v-for="item in levelOptions"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+
+            <!-- 年代 -->
+            <el-select v-model="searchParams.era" class="cut-corner-select" placeholder="年代" clearable @change="quickSearch">
+              <el-option
+                v-for="item in eraOptions"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+
+            <!-- 排序 -->
+            <el-select v-model="searchParams.orderBy" class="cut-corner-select" placeholder="排序" clearable @change="quickSearch">
+              <el-option
+                v-for="item in orderByOptions"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+
+            <!-- 全部文物(维度) -->
+            <el-select v-model="selectedDim" class="cut-corner-select cut-corner-select-dim" placeholder="全部文物" @change="quickSearch">
+              <el-option :label="'全部文物'" :value="3" />
+              <el-option :label="'二维文物'" :value="1" />
+              <el-option :label="'三维文物'" :value="2" />
+            </el-select>
+
+            <!-- 重置按钮 -->
+            <button class="reset-inline-btn" @click="resetInline">
+              <img src="@/assets/img/icon_restart.png" alt="重置" />
+            </button>
           </div>
         </div>
       </div>
 
       <!-- 搜索结果列表 -->
       <div v-loading="loading" class="search-results" @scroll="handleScroll">
-        <div
-          v-for="item in searchResults"
-          :key="item.id"
-          class="result-item"
-          :class="{ 'loading-waterfall': item.isLoading }"
-          @click="viewDetails(item)"
-        >
-          <div class="waterfall-mask" v-if="item.isLoading"></div>
-          <img :src="getImageUrl(item)" :alt="item.name" class="result-image" />
-          <div class="result-info">
-            <div class="result-name">{{ item.name }}</div>
-            <div class="more-info"><img src="@/assets/img/icon_collect_more.png" alt="更多" class="more-icon" /></div>
+        <div class="results-columns">
+          <div class="column left">
+            <div
+              v-for="item in leftResult"
+              :key="item.id"
+              class="result-item"
+              :class="{ 'loading-waterfall': item.isLoading }"
+              @click="viewDetails(item)"
+            >
+              <div class="waterfall-mask" v-if="item.isLoading"></div>
+              <img :src="getImageUrl(item)" :alt="item.name" class="result-image" />
+              <div class="result-info">
+                <div class="result-name">{{ item.name }}</div>
+              </div>
+            </div>
+          </div>
+          <div class="column right">
+            <div
+              v-for="item in rightResult"
+              :key="item.id"
+              class="result-item"
+              :class="{ 'loading-waterfall': item.isLoading }"
+              @click="viewDetails(item)"
+            >
+              <div class="waterfall-mask" v-if="item.isLoading"></div>
+              <img :src="getImageUrl(item)" :alt="item.name" class="result-image" />
+              <div class="result-info">
+                <div class="result-name">{{ item.name }}</div>
+              </div>
+            </div>
           </div>
         </div>
 
@@ -74,7 +136,7 @@
 </template>
 
 <script setup>
-import { ref, onMounted, computed } from 'vue'
+import { ref, computed, onMounted } from 'vue'
 import { useRouter } from 'vue-router'
 import SearchPageComponent from './components/SearchPageComponent.vue'
 import getBookCountApi from '@/api'
@@ -92,19 +154,20 @@ const router = useRouter()
 // 响应式数据
 const showSearchPage = ref(false)
 const searchParams = ref({searchText: '', dim: 3, orderBy: 'grade', level: '', category: '', material: '', era: ''})
-const searchResults = ref([]) // 当前显示的搜索结果(分页显示)
+const searchResults = ref([]) // 当前显示的搜索结果(服务端分页)
+// 将搜索结果拆分为左右两列:左列为索引为双数(0,2,4...),右列为索引为单数(1,3,5...)
+const leftResult = computed(() => searchResults.value.filter((_, index) => index % 2 === 0))
+const rightResult = computed(() => searchResults.value.filter((_, index) => index % 2 === 1))
 const searched = ref(false)
 const loading = ref(false)
 
-// 前端分页相关数据
-const allSearchResults = ref([]) // 所有搜索结果数据(API返回的完整数据集)
+// 服务端分页相关数据
 const currentPage = ref(1)
-const pageSize = 20 // 每页显示20条
+const pageSize = 40 // 默认每页40条
+const hasMoreData = ref(true)
 
-const filters = ref({
-  hasImage: false,
-  hasModel: false
-})
+// 维度选择(全部文物/二维/三维)
+const selectedDim = ref(3)
 
 // 所有选项列表 - 从接口获取
 const levelOptions = ref([])
@@ -113,10 +176,7 @@ const materialOptions = ref([])
 const eraOptions = ref([])
 const orderByOptions = ref([])
 
-// 计算总页数
-const totalPages = computed(() => {
-  return Math.ceil(allSearchResults.value.length / pageSize);
-})
+// 移除前端总页数计算,依赖服务端分页
 
 // 构建orderbyList的通用方法
 const buildOrderbyList = (orderByValue = searchParams.value.orderBy) => {
@@ -130,10 +190,6 @@ const buildOrderbyList = (orderByValue = searchParams.value.orderBy) => {
       categoryOptions.value.forEach(item => {
         orderbyList.push(item.value);
       });
-    } else if (orderByValue === 'material') {
-      materialOptions.value.forEach(item => {
-        orderbyList.push(item.value);
-      });
     } else if (orderByValue === 'era') {
       eraOptions.value.forEach(item => {
         orderbyList.push(item.value);
@@ -164,42 +220,14 @@ const quickSearch = () => {
 
 // 计算dim参数值
 const getSelectedDim = () => {
-  const { hasImage, hasModel } = filters.value
-
-  // 都选中或都不选中时传3-全部
-  if ((hasImage && hasModel) || (!hasImage && !hasModel)) {
-    return 3;
-  }
-  // 只选hasImage时传1-二维
-  if (hasImage && !hasModel) {
-    return 1;
-  }
-  // 只选hasModel时传2-三维
-  if (!hasImage && hasModel) {
-    return 2;
-  }
-
-  return 3; // 默认全部
+  // 从下拉框直接返回维度值
+  return Number(selectedDim.value) || 3;
 }
 
-// 加载显示的数据(分页)
-const loadDisplayedData = () => {
-  const startIndex = (currentPage.value - 1) * pageSize;
-  const endIndex = startIndex + pageSize;
-  const newData = allSearchResults.value.slice(startIndex, endIndex);
-
-  if (currentPage.value === 1) {
-    // 第一页,直接替换
-    searchResults.value = newData.map(item => ({ ...item, isLoading: true }));
-  } else {
-    // 后续页,追加数据
-    const newItems = newData.map(item => ({ ...item, isLoading: true }));
-    searchResults.value.push(...newItems);
-  }
-
-  // 延迟触发瀑布效果
+// 追加瀑布加载效果的统一方法
+const applyWaterfallEffect = (items) => {
   setTimeout(() => {
-    newData.forEach((item, index) => {
+    items.forEach((item, index) => {
       setTimeout(() => {
         const foundItem = searchResults.value.find(listItem => listItem.id === item.id && listItem.isLoading);
         if (foundItem) {
@@ -242,17 +270,20 @@ const handleSearch = async (searchFilters = {}) => {
       texture: searchParams.value.material,
       agetype: searchParams.value.era,
       orderBy: searchParams.value.orderBy,
-      orderbyList: searchFilters.orderbyList || []
+      orderbyList: searchFilters.orderbyList || [],
+      pageNo: 1,
+      pageSize
     }
     // 重置分页数据
     currentPage.value = 1;
+    hasMoreData.value = true;
     searchResults.value = []; // 清空当前显示的数据
 
     // 调用实际的搜索API,使用与CollectionContent相同的API
     const response = await getBookCountApi.getArtifactListApi(searchObj)
 
     // 处理返回的数据
-    const artifacts = response.list || response.records || response || [];
+    const artifacts = response.pageData || response.list || response.records || [];
 
     // console.log('获取到搜索结果:', artifacts.length, '条');
 
@@ -269,20 +300,22 @@ const handleSearch = async (searchFilters = {}) => {
       originalImage: `${imgUrl}${item.thumbnail}`,
       hasImage: item.hasImage !== false,
       hasModel: item.hasModel !== false,
-      isLoading: false
+      isLoading: true
     }))
 
-    // 存储所有结果数据
-    allSearchResults.value = processedArtifacts;
+    // 第一页直接替换
+    searchResults.value = processedArtifacts;
+    applyWaterfallEffect(processedArtifacts);
 
-    // 加载第一页数据
-    loadDisplayedData();
+    // 检查是否还有更多数据
+    if (artifacts.length < pageSize) {
+      hasMoreData.value = false;
+    }
 
     searched.value = true
 
   } catch (error) {
     console.error('搜索失败:', error)
-    allSearchResults.value = []
     searchResults.value = []
     searched.value = true
   } finally {
@@ -300,6 +333,56 @@ const viewDetails = (item) => {
   })
 }
 
+// 加载更多(服务端分页)
+const loadMoreData = async () => {
+  if (loading.value || !hasMoreData.value) return;
+  loading.value = true;
+  try {
+    const searchObj = {
+      dim: searchParams.value.dim,
+      searchText: searchParams.value.searchText,
+      grade: searchParams.value.level,
+      category: searchParams.value.category,
+      texture: searchParams.value.material,
+      agetype: searchParams.value.era,
+      orderBy: searchParams.value.orderBy,
+      orderbyList: buildOrderbyList(),
+      pageNo: currentPage.value,
+      pageSize
+    };
+
+    const response = await getBookCountApi.getArtifactListApi(searchObj);
+    const artifacts = response.pageData || response.list || response.records || [];
+
+    const processedArtifacts = artifacts.map(item => ({
+      id: item.id,
+      name: item.name || item.title,
+      description: item.description || item.desc || '暂无描述',
+      era: item.era || item.agetype || '近现代',
+      category: item.category || '其他',
+      level: item.level || item.grade || '未定级',
+      material: item.texture || '其他',
+      image: `${imgUrl}${item.thumbnail}`,
+      originalImage: `${imgUrl}${item.thumbnail}`,
+      hasImage: item.hasImage !== false,
+      hasModel: item.hasModel !== false,
+      isLoading: true
+    }));
+
+    searchResults.value.push(...processedArtifacts);
+    applyWaterfallEffect(processedArtifacts);
+
+    if (artifacts.length < pageSize) {
+      hasMoreData.value = false;
+    }
+  } catch (error) {
+    console.error('加载更多失败:', error);
+    hasMoreData.value = false; // 防止重复触发
+  } finally {
+    loading.value = false;
+  }
+};
+
 // 滚动处理 - 加载更多
 const handleScroll = async (event) => {
   const container = event.target;
@@ -308,10 +391,10 @@ const handleScroll = async (event) => {
   // 检查是否滚动到底部
   const isScrolledToBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - 50;
 
-  if (isScrolledToBottom && currentPage.value < totalPages.value) {
+  if (isScrolledToBottom && hasMoreData.value) {
     console.log('加载下一页数据...');
     currentPage.value++;
-    loadDisplayedData();
+    await loadMoreData();
   }
 };
 
@@ -353,11 +436,10 @@ const loadDictionaryData = async () => {
       }));
     }
 
-    // 处理排序数据
+    // 处理排序数据(去掉材质)
     orderByOptions.value = [
       { label: '等级', value: 'grade' },
       { label: '类别', value: 'category' },
-      { label: '材质', value: 'material' },
       { label: '年代', value: 'era' }
     ];
 
@@ -391,7 +473,6 @@ const loadDictionaryData = async () => {
     orderByOptions.value = [
       { label: '等级', value: 'grade' },
       { label: '类别', value: 'category' },
-      { label: '材质', value: 'material' },
       { label: '年代', value: 'era' }
     ];
 
@@ -442,7 +523,7 @@ const processImageWithWatermark = async (item) => {
     const watermarkedImageUrl = await addWatermarkToCollectionImage(item.originalImage, {
       position: 'center',
       opacity: 1,
-      scale: 0.9,
+      scale: 0.5,
       margin: 15
     });
     // console.log('水印处理成功,结果URL:', watermarkedImageUrl);
@@ -452,8 +533,9 @@ const processImageWithWatermark = async (item) => {
     item.image = watermarkedImageUrl;
     return watermarkedImageUrl;
   } catch (error) {
-    console.error('水印处理失败:', error);
-    // 失败时使用原图
+    // 加载失败时,直接使用并缓存原图,避免重复尝试引发报错
+    watermarkedImages.value.set(item.id, item.originalImage);
+    item.image = item.originalImage;
     return item.originalImage;
   } finally {
     // 标记处理完成
@@ -477,6 +559,18 @@ onMounted(() => {
   // 加载字典数据,字典数据加载完成后会自动进行初始化搜索
   loadDictionaryData();
 })
+
+// 内联重置:清空除排序外的筛选,同时维度恢复全部
+const resetInline = () => {
+  searchParams.value.searchText = ''
+  searchParams.value.level = ''
+  searchParams.value.category = ''
+  searchParams.value.material = ''
+  searchParams.value.era = ''
+  searchParams.value.orderBy = 'grade'
+  selectedDim.value = 3
+  quickSearch()
+}
 </script>
 
 <style lang="scss" scoped>
@@ -505,14 +599,14 @@ onMounted(() => {
 
 .top-title {
   position: absolute;
-  top: 20px;
+  top: 0.5333rem;
   left: 0;
   width: 100%;
   text-align: center;
   z-index: 10;
 
   img {
-    max-width: 300px;
+    max-width: 8rem;
     height: auto;
   }
 }
@@ -534,30 +628,57 @@ onMounted(() => {
 
 .search-container {
   position: absolute;
-  top: 20px;
+  top: 0.5333rem;
   left: 50%;
   transform: translateX(-50%);
   z-index: 10;
-  width: calc(100% - 40px);
-  max-width: 600px;
+  width: calc(100% - 0.5867rem);
+  max-width: 16rem;
 }
 
 .search-wrapper {
   display: flex;
   flex-direction: column;
-  border-bottom: 1px dotted #d3bfa2;
-  border-radius: 6px;
-  gap: 2px;
+  border-bottom: 0.0267rem dotted #d3bfa2;
+  border-radius: 0.16rem;
+  gap: 0.0533rem;
+}
+
+// 内联筛选行
+.filter-row {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 0.1067rem;
+  margin: 0.2667rem 0;
+}
+
+.reset-inline-btn {
+  position: relative;
+    width: 0.8rem;
+    height: 0.8rem;
+    border: none;
+    background: url('@/assets/img/btn_active.png') no-repeat;
+    background-size: 0.8rem 0.8rem;
+    img{
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      width: 0.5333rem;
+      height: 0.5333rem;
+      object-fit: contain;
+    }
 }
 
 .checkbox-group {
   display: flex;
-  gap: 6px;
+  gap: 0.16rem;
   white-space: nowrap;
 
   :deep(.el-checkbox) {
     .el-checkbox__label {
-      font-size: 14px;
+      font-size: 0.3733rem;
       color: #666;
       font-weight: 400;
     }
@@ -584,21 +705,21 @@ onMounted(() => {
 .search-input-wrapper {
   display: flex;
   align-items: center;
-  gap: 2px;
+  gap: 0.0533rem;
   .quick-search-btn {
     position: relative;
-    width: 30px;
-    height: 30px;
+    width: 0.8rem;
+    height: 0.8rem;
     border: none;
     background: url('@/assets/img/btn_active.png') no-repeat;
-    background-size: 30px 30px;
+    background-size: 0.8rem 0.8rem;
     .search-icon{
       position: absolute;
       top: 50%;
       left: 50%;
       transform: translate(-50%, -50%);
-      width: 20px;
-      height: 20px;
+      width: 0.5333rem;
+      height: 0.5333rem;
       object-fit: contain;
     }
   }
@@ -606,15 +727,15 @@ onMounted(() => {
 
 .search-input {
   flex: 1;
-  width: 288px;
+  width: 7.68rem;
 
   :deep(.el-input__wrapper) {
-    height: 40px;
-    border-radius: 4px;
+    height: 1.0667rem;
+    border-radius: 0.1067rem;
   }
 
   :deep(.el-input__inner) {
-    font-size: 14px;
+    font-size: 0.3733rem;
     color: #333;
 
     &::placeholder {
@@ -627,29 +748,29 @@ onMounted(() => {
   display: flex;
   align-items: center;
   justify-content: center;
-  gap: 2px;
+  gap: 0.0533rem;
   border: none;
-  border-radius: 4px;
+  border-radius: 0.1067rem;
   color: #B49D7E;
-  font-size: 16px;
+  font-size: 0.4267rem;
   font-weight: 500;
   cursor: pointer;
   white-space: nowrap;
   transition: all 0.3s ease;
-  height: 40px;
-  min-width: 80px;
+  height: 1.0667rem;
+  min-width: 2.1333rem;
   background: transparent;
 
   .search-icon {
-    width: 30px;
-    height: 30px;
+    width: 0.8rem;
+    height: 0.8rem;
     object-fit: contain;
   }
 
   &:hover {
     background: #6a5a36;
-    transform: translateY(-1px);
-    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+    transform: translateY(-0.0267rem);
+    box-shadow: 0 0.0533rem 0.1067rem rgba(0, 0, 0, 0.2);
   }
 
   &:active {
@@ -659,22 +780,48 @@ onMounted(() => {
 
 .search-results {
   position: absolute;
-  top: 110px;
+  top: 3.68rem;
   left: 0;
   width: 100%;
-  height: calc(100% - 130px);
+  height: calc(100% - 3.8933rem);
   overflow-y: auto;
+  overflow-x: hidden;
   z-index: 10;
-  padding: 0 20px;
+  padding: 0 0.2667rem;
   box-sizing: border-box;
 }
 
-.result-item {
+.results-columns {
+  display: flex;
+  justify-content: space-between;
+}
+
+.column {
+  width: 50%;
   display: flex;
   flex-direction: column;
+}
+.left{
+  .result-image{
+    height: 4.4267rem;
+  }
+}
+.right {
+  margin-left: 3%;
+  .result-item:first-child{
+    .result-image{
+      height: 3.2rem;
+    }
+  }
+}
+
+
+.result-item {
+  display: inline-flex;
+  flex-direction: column;
   background: rgba(255, 255, 255, 0.9);
-  margin-bottom: 15px;
-  padding: 10px;
+  margin-bottom: 0.4rem;
+  padding: 0.2667rem;
   cursor: pointer;
   transition: all 0.3s ease;
   position: relative;
@@ -682,8 +829,8 @@ onMounted(() => {
 
   &:hover {
     background: rgba(255, 255, 255, 1);
-    transform: translateY(-2px);
-    box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
+    transform: translateY(-0.0533rem);
+    box-shadow: 0 0.1067rem 0.4rem rgba(0, 0, 0, 0.1);
   }
 
   // 瀑布加载状态
@@ -707,7 +854,7 @@ onMounted(() => {
       rgba(0, 0, 0, 0.9) 100%);
     z-index: 10;
     transform: translateY(-100%);
-    border-radius: 8px;
+    border-radius: 0.2133rem;
     animation: waterfallDown 1.0s ease-out forwards, waterfallUp 1.0s ease-in 1.0s forwards;
     will-change: transform, opacity;
   }
@@ -715,9 +862,9 @@ onMounted(() => {
 
 .result-image {
   width: 100%;
-  height: 181px;
-  object-fit: cover;
-  margin-right: 15px;
+  height: 4.4267rem;
+  display: block;
+  margin-right: 0.4rem;
   transition: opacity 0.3s ease, filter 0.3s ease;
 }
 
@@ -729,11 +876,16 @@ onMounted(() => {
   transition: opacity 0.3s ease, filter 0.3s ease;
 
   .result-name {
-    width: 85%;
-    font-size: 16px;
+    display: -webkit-box;
+    -webkit-line-clamp: 2;
+    -webkit-box-orient: vertical;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    width: 100%;
+    font-size: 0.4267rem;
     color: #464646;
     font-weight: 400;
-    padding-top: 6px;
+    padding-top: 0.16rem;
   }
 
   .more-info {
@@ -744,8 +896,8 @@ onMounted(() => {
 
   .result-category,
   .result-era {
-    margin: 4px 0;
-    font-size: 14px;
+    margin: 0.1067rem 0;
+    font-size: 0.3733rem;
     color: #666;
   }
 }
@@ -759,7 +911,7 @@ onMounted(() => {
   z-index: 10;
 
   p {
-    font-size: 16px;
+    font-size: 0.4267rem;
     color: #999;
   }
 }
@@ -767,7 +919,7 @@ onMounted(() => {
 .loading-more {
   grid-column: 1 / -1;
   text-align: center;
-  padding: 20px;
+  padding: 0.5333rem;
   color: #666;
 }
 
@@ -800,3 +952,15 @@ onMounted(() => {
   }
 }
 </style>
+<style lang="scss">
+.el-select-dropdown__wrap{
+  max-height: 10.6667rem;
+}
+.el-select-dropdown__item{
+  min-width: 7.3867rem!important;
+  height: 0.9067rem;
+  line-height: 0.9067rem;
+  padding: 0 0.8533rem 0 0.5333rem;
+  font-size: 0.4267rem;
+}
+</style>

+ 4 - 2
mobile/src/views/contentPage/index.vue

@@ -98,7 +98,7 @@ const processImageWithWatermark = async (item) => {
     const watermarkedImageUrl = await addWatermarkToCollectionImage(item.originalImage, {
       position: 'center',
       opacity: 1,
-      scale: 0.7,
+      scale: 0.5,
       margin: 15
     });
     // 存储处理后的图片
@@ -108,7 +108,9 @@ const processImageWithWatermark = async (item) => {
     return watermarkedImageUrl;
   } catch (error) {
     console.error('水印处理失败:', error);
-    // 失败时使用原图
+    // 统一处理:失败时直接缓存并返回原图
+    watermarkedImages.value.set(item.id, item.originalImage);
+    item.image = item.originalImage;
     return item.originalImage;
   } finally {
     // 标记处理完成

+ 15 - 0
mobile/vite.config.js

@@ -3,6 +3,7 @@ import { fileURLToPath, URL } from 'node:url'
 import { defineConfig, loadEnv } from 'vite'
 import vue from '@vitejs/plugin-vue'
 import vueDevTools from 'vite-plugin-vue-devtools'
+import pxtorem from 'postcss-pxtorem'
 
 // https://vite.dev/config/
 export default defineConfig(({ mode }) => {
@@ -14,6 +15,20 @@ export default defineConfig(({ mode }) => {
     plugins: [
       vue(),
     ],
+    css: {
+      postcss: {
+        plugins: [
+          pxtorem({
+            // 以 2160 设计稿为基准,1rem = 216px(结合脚本按视口宽度动态缩放)
+            rootValue: 216,
+            propList: ['*'],
+            replace: true,
+            mediaQuery: false,
+            exclude: /node_modules/i,
+          }),
+        ],
+      },
+    },
     resolve: {
       alias: {
         '@': fileURLToPath(new URL('./src', import.meta.url))

+ 5 - 7
pc/src/api/index.js

@@ -5,20 +5,16 @@ const hhbangBookApi = {
   // 首页获取推荐列表
   getRecommendListApi(params = {}) {
     return request({
-      url: '/hyb/artArtworks/index/page',
+      url: '/hyb/artArtworks/index/list',
       method: 'get',
-      params: {
-        pageNo: Math.floor(Math.random() * 10) + 1, // 随机页码
-        pageSize: 60,
-        ...params
-      }
+      params: {}
     })
   },
 
   // 获取文物列表 - 用于收藏页面和详情页上下页切换
   getArtifactListApi(params = {}) {
     return request({
-      url: '/hyb/artArtworks/list',
+      url: '/hyb/artArtworks/page',
       method: 'get',
       params: {
         agetype: params.era || '', // 年代
@@ -26,6 +22,8 @@ const hhbangBookApi = {
         grade: params.level || '', // 级别
         searchText: params.keyword || '', // 关键词
         texture: params.material || '', // 材质
+        pageNo: params.pageNo || 1, // 页码
+        pageSize: params.pageSize || 40, // 每页数量
         ...params
       },
       //序列化

+ 9 - 7
pc/src/utils/index.js

@@ -25,7 +25,7 @@ export function addWatermarkToCollectionImage(imageUrl, options = {}) {
           // console.log(`图片加载完成 ${imagesLoaded}/2`);
           if (imagesLoaded === 2) {
               try {
-                  console.log('开始绘制水印');
+                //   console.log('开始绘制水印');
                   // 两张图片都加载完成
                   const canvas = document.createElement('canvas');
                   const ctx = canvas.getContext('2d');
@@ -88,8 +88,9 @@ export function addWatermarkToCollectionImage(imageUrl, options = {}) {
                   // console.log('水印处理完成,生成的URL长度:', watermarkedImageUrl.length);
                   resolve(watermarkedImageUrl);
               } catch (error) {
-                  console.error('Canvas绘制错误:', error);
-                  reject(new Error('水印处理失败: ' + error.message));
+                //   console.error('Canvas绘制错误:', error);
+                  // 统一处理:发生异常时直接返回原图URL,避免反复报错
+                  resolve(imageUrl);
               }
           }
       }
@@ -97,13 +98,14 @@ export function addWatermarkToCollectionImage(imageUrl, options = {}) {
       image.onload = onImageLoad;
       watermark.onload = onImageLoad;
       
+      // 主图或水印加载失败时,直接返回原链接,避免继续添加水印造成反复报错
       image.onerror = () => {
-          console.error('主图片加载失败:', imageUrl);
-          reject(new Error('主图片加载失败'));
+        //   console.error('主图片加载失败:', imageUrl);
+          resolve(imageUrl);
       };
       watermark.onerror = () => {
-          console.error('水印图片加载失败');
-          reject(new Error('水印图片加载失败'));
+        //   console.error('水印图片加载失败');
+          resolve(imageUrl);
       };
       
       // 开始加载图片

+ 19 - 5
pc/src/views/Home/index.vue

@@ -80,7 +80,7 @@ const processImageWithWatermark = async (item) => {
     const watermarkedImageUrl = await addWatermarkToCollectionImage(item._thumb, {
       position: 'center',
       opacity: 1,
-      scale: 0.7,
+      scale: 0.5,
       margin: 15
     });
     // 存储处理后的图片
@@ -88,7 +88,8 @@ const processImageWithWatermark = async (item) => {
     return watermarkedImageUrl;
   } catch (error) {
     console.error('水印处理失败:', error);
-    // 失败时使用原图
+    // 统一处理:失败时直接缓存并返回原图
+    watermarkedImages.value.set(item.id, item._thumb);
     return item._thumb;
   } finally {
     // 标记处理完成
@@ -176,10 +177,23 @@ const getRecommendList = async () => {
   isLoading.value = true;
   let data = await getBookCountApi.getRecommendListApi();
 
+  const searchParams = {
+      dim: 3,
+      searchText: '',
+      level: '',
+      category: '',
+      material: '',
+      era: '',
+      orderBy: '',
+      orderbyList: [],
+      pageNo: 1,
+      pageSize: 1
+    };
+    const totalData = await getBookCountApi.getArtifactListApi(searchParams);
+    const totalCount = totalData.total ?  totalData.total : 26;
+
   // 处理接口返回的数据
-  let list = data.pageData;
-  list = list.filter(i => i.thumbnail).slice(0, 60)
-  const totalCount = data.total;
+  let list = data;
 
   // 更新总数
   total.value = totalCount;

+ 4 - 2
pc/src/views/collect/components/AnnouncementContent.vue

@@ -256,7 +256,7 @@ const processImageWithWatermark = async (item) => {
     const watermarkedImageUrl = await addWatermarkToCollectionImage(item.originalImage, {
       position: 'center',
       opacity: 1,
-      scale: 0.7,
+      scale: 0.5,
       margin: 15
     });
     // 存储处理后的图片
@@ -266,7 +266,9 @@ const processImageWithWatermark = async (item) => {
     return watermarkedImageUrl;
   } catch (error) {
     console.error('水印处理失败:', error);
-    // 失败时使用原图
+    // 统一处理:失败时直接缓存并返回原图
+    watermarkedImages.value.set(item.id, item.originalImage);
+    item.image = item.originalImage;
     return item.originalImage;
   } finally {
     // 标记处理完成

+ 113 - 23
pc/src/views/collect/components/CollectionContent.vue

@@ -119,7 +119,7 @@
     </div>
 
     <!-- 文物列表 -->
-    <div class="artifact-list" v-if="artifactList.length > 0" ref="listContainer" @scroll="handleScroll">
+    <div class="artifact-list" v-if="artifactList.length > 0" ref="listContainer">
       <div
         class="artifact-item"
         :class="{ 'loading-waterfall': item.isLoading }"
@@ -132,7 +132,9 @@
           <img :src="getImageUrl(item)" :alt="item.name" />
         </div>
         <div class="item-info">
-          <div class="item-name">{{ item.name }}</div>
+          <el-tooltip :content="item.name" :disabled="item.name.length <= 40" placement="top">
+            <div class="item-name">{{ item.name }}</div>
+          </el-tooltip>
         </div>
       </div>
 
@@ -141,6 +143,18 @@
         <span>加载中...</span>
       </div>
     </div>
+    <!-- 分页 -->
+    <div class="pagination-bar" v-if="artifactList.length > 0">
+      <el-pagination
+        :current-page="currentPage"
+        :page-size="pageSize"
+        :pager-count="8"
+        layout="pager"
+        :page-count="totalPageCount"
+        @current-change="handlePageChange"
+      />
+      <el-button class="next-page-btn" @click="goNextPage" :disabled="currentPage >= totalPageCount">下一页</el-button>
+    </div>
     <div class="empty-data" v-else>
       <span>暂无数据</span>
     </div>
@@ -148,7 +162,7 @@
 </template>
 
 <script setup>
-import { onMounted, ref, reactive, computed } from "vue";
+import { onMounted, ref, reactive, computed, nextTick } from "vue";
 import { useStore } from 'vuex';
 import { useRoute, useRouter } from 'vue-router';
 import getBookCountApi from "@/api";
@@ -190,7 +204,16 @@ const allArtifacts = ref([]); // 所有文物数据(API返回的完整数据
 const displayedArtifacts = ref([]); // 当前显示的文物数据(分页显示)
 const watermarkedImages = ref(new Map()); // 存储处理后的水印图片
 const currentPage = ref(1);
-const pageSize = 20; // 每页显示20条
+const pageSize = 40; // 每页显示40条
+const totalPageCount = ref(1); // 后端返回的总页数
+
+// 将列表滚动至顶部(分页切换时调用)
+const scrollListToTop = () => {
+  const container = listContainer.value;
+  if (!container) return;
+  // 立即将滚动条重置到顶部,避免分页后停留在旧位置
+  container.scrollTop = 0;
+};
 
 // 计算dim参数值
 const dimValue = computed(() => {
@@ -255,7 +278,7 @@ const processImageWithWatermark = async (item) => {
     const watermarkedImageUrl = await addWatermarkToCollectionImage(item.originalImage, {
       position: 'center',
       opacity: 1,
-      scale: 0.9,
+      scale: 0.5,
       margin: 15
     });
     // console.log('水印处理成功,结果URL:', watermarkedImageUrl);
@@ -266,7 +289,9 @@ const processImageWithWatermark = async (item) => {
     return watermarkedImageUrl;
   } catch (error) {
     console.error('水印处理失败:', error);
-    // 失败时使用原图
+    // 统一处理:失败时直接缓存并返回原图,避免重复尝试
+    watermarkedImages.value.set(item.id, item.originalImage);
+    item.image = item.originalImage;
     return item.originalImage;
   } finally {
     // 标记处理完成
@@ -471,12 +496,20 @@ const loadDictionaryData = async () => {
   }
 };
 
-// 获取全部文物数据
-const loadAllArtifacts = async (orderbyList) => {
+// 解析后端分页总页数
+const parseTotalPages = (data) => {
+  // 兼容不同字段命名
+  const total = data.total ?? data.totalCount;
+  const pages = data.pages ?? data.pageCount ?? data.totalPage ?? data.totalPages;
+  if (typeof pages === 'number' && pages > 0) return pages;
+  if (typeof total === 'number' && total > 0) return Math.ceil(total / pageSize);
+  return totalPages.value || 1; // 回退
+};
+
+// 根据页码获取文物数据(服务端分页)
+const fetchArtifactsPage = async (pageNo, orderbyList) => {
   loading.value = true;
   try {
-    console.log('开始获取全部文物数据...');
-
     const searchParams = {
       dim: dimValue.value,
       searchText: searchForm.searchText,
@@ -485,18 +518,13 @@ const loadAllArtifacts = async (orderbyList) => {
       material: searchForm.material,
       era: searchForm.era,
       orderBy: searchForm.orderBy,
-      orderbyList: orderbyList ? orderbyList : []
+      orderbyList: orderbyList ? orderbyList : [],
+      pageNo,
+      pageSize
     };
-
-    console.log('API调用参数:', searchParams);
-
     const data = await getBookCountApi.getArtifactListApi(searchParams);
-
     // 处理返回的数据
-    const artifacts = data.list || data.records || data || [];
-
-    console.log('获取到文物数据:', artifacts.length, '条');
-
+    const artifacts = data.pageData || data.list || data.records || [];
     // 数据格式化处理
     const processedArtifacts = artifacts.map(item => ({
       id: item.id,
@@ -514,13 +542,27 @@ const loadAllArtifacts = async (orderbyList) => {
       watermarkProcessing: false // 水印处理状态
     }));
 
+    // 当前页显示的数据
+    displayedArtifacts.value = processedArtifacts.map(item => ({ ...item, isLoading: true }));
+    // 存储到Vuex(用于详情页上下文)
     allArtifacts.value = processedArtifacts;
-
-    // 存储到Vuex
     store.dispatch('setAllArtifacts', processedArtifacts);
 
-    // 加载第一页数据
-    loadDisplayedData();
+    // 解析并设置总页数与当前页
+    totalPageCount.value = parseTotalPages(data);
+    currentPage.value = data.pageNo || pageNo || 1;
+
+    // 延迟触发瀑布效果
+    setTimeout(() => {
+      processedArtifacts.forEach((item, index) => {
+        setTimeout(() => {
+          const foundItem = displayedArtifacts.value.find(listItem => listItem.id === item.id && listItem.isLoading);
+          if (foundItem) {
+            foundItem.isLoading = false;
+          }
+        }, index * 150 + 200);
+      });
+    }, 100);
 
   } catch (error) {
     console.error('获取文物数据失败:', error);
@@ -529,6 +571,11 @@ const loadAllArtifacts = async (orderbyList) => {
   }
 };
 
+// 初始加载(第一页)
+const loadAllArtifacts = async (orderbyList) => {
+  await fetchArtifactsPage(1, orderbyList);
+};
+
 // 加载显示的数据(分页)
 const loadDisplayedData = () => {
   const startIndex = (currentPage.value - 1) * pageSize;
@@ -572,6 +619,25 @@ const handleScroll = async () => {
   }
 };
 
+// 页码变更(点击数字)
+const handlePageChange = async (page) => {
+  if (loading.value) return;
+  console.log('分页切换到:', page);
+  // 切换分页时先清空列表,触发 v-if 重新挂载,滚动容器重置
+  displayedArtifacts.value = [];
+  await nextTick();
+  const orderbyList = buildOrderbyList();
+  await fetchArtifactsPage(page, orderbyList);
+  // 分页切换完成后将滚动条重置到顶部
+  scrollListToTop();
+};
+
+// 下一页文字按钮
+const goNextPage = async () => {
+  if (currentPage.value >= totalPageCount.value) return;
+  await handlePageChange(currentPage.value + 1);
+};
+
 onMounted(() => {
 
   if (store.getters.getHomeSearchText) {
@@ -784,6 +850,10 @@ onMounted(() => {
     transition: opacity 0.3s ease, filter 0.3s ease;
 
     .item-name {
+      display: -webkit-box;
+      -webkit-line-clamp: 2;
+      -webkit-box-orient: vertical;
+      overflow: hidden;
       font-size: 14px;
       font-weight: bold;
       margin-bottom: 8px;
@@ -801,6 +871,26 @@ onMounted(() => {
   color: #666;
 }
 
+.pagination-bar {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 16px;
+  padding: 6px 0;
+  :deep(.el-pager li){
+    background: transparent;
+  }
+}
+
+.next-page-btn {
+  border: 1px solid #B49D7E;
+  background: transparent;
+  color: #8B7355;
+  border-radius: 20px;
+  height: 32px;
+  padding: 0 16px;
+}
+
 // 瀑布动画关键帧
 @keyframes waterfallDown {
   0% {

+ 25 - 8
pc/src/views/collect/components/collectDetails.vue

@@ -31,14 +31,14 @@
               <iframe :src="`https://hybgc.4dage.com/front/modelLoad/model.html?m=${modelList[0].src}`"
                 frameborder="0"
                 width="100%"
-                ref=""
+                ref="ifr"
                 height="100%"
                 allowfullscreen></iframe>
-                <!-- <iframe :src="`${modelList[0].src}`"
-                  frameborder="0"
-                  width="100%"
-                  height="100%"
-                  allowfullscreen></iframe> -->
+                <div class="waterMar" id="waterMar">
+                  <img
+                    src="@/assets/img/waterMar.png"
+                  />
+                </div>
                 <!-- 音频播放控件 -->
                 <div class="fullscreen-icon">
                   <el-icon style="color: #7C6444" :size="24">
@@ -382,7 +382,7 @@ const processImageWithWatermark = async (item) => {
     const watermarkedImageUrl = await addWatermarkToCollectionImage(item.originalImage, {
       position: 'center',
       opacity: 1,
-      scale: 0.7,
+      scale: 0.5,
       margin: 15
     });
     // 存储处理后的图片
@@ -392,7 +392,9 @@ const processImageWithWatermark = async (item) => {
     return watermarkedImageUrl;
   } catch (error) {
     console.error('水印处理失败:', error);
-    // 失败时使用原图
+    // 统一处理:失败时直接缓存并返回原图
+    watermarkedImages.value.set(item.id, item.originalImage);
+    item.src = item.originalImage;
     return item.originalImage;
   } finally {
     // 标记处理完成
@@ -726,6 +728,21 @@ onMounted(() => {
           // transform: scale(1.1);
         }
       }
+      .waterMar{
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%, -50%);
+        width: 400px;
+        height: 20px;
+        pointer-events: none;
+        img{
+          pointer-events: none;
+          width: 100%;
+          height: 100%;
+          object-fit: contain;
+        }
+      }
     }
 
     .empty-item {

+ 21 - 1
pc/src/views/collect/components/modelLook.vue

@@ -21,6 +21,11 @@
           height="100%"
           allowfullscreen
         ></iframe>
+        <div class="waterMar" v-if="modelSrc" id="waterMar">
+          <img
+            src="@/assets/img/waterMar.png"
+          />
+        </div>
         <div v-else class="no-model">
           <div class="no-model-text">暂无模型数据</div>
         </div>
@@ -140,6 +145,7 @@ watch(() => props.visible, (newVal) => {
     }
 
     .model-content {
+      position: relative;
       flex: 1;
       position: relative;
       background: #f5f5f5;
@@ -150,7 +156,21 @@ watch(() => props.visible, (newVal) => {
         border: none;
         background: #f5f5f5;
       }
-
+      .waterMar{
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%, -50%);
+        width: 400px;
+        height: 60px;
+        pointer-events: none;
+        img{
+          pointer-events: none;
+          width: 100%;
+          height: 100%;
+          object-fit: contain;
+        }
+      }
       .no-model {
         width: 100%;
         height: 100%;