Przeglądaj źródła

📦 首页地图与音乐修改

imengyu 1 miesiąc temu
rodzic
commit
8e128653a8

+ 7 - 0
package-lock.json

@@ -23,6 +23,7 @@
         "@dcloudio/uni-mp-weixin": "3.0.0-4030620241128001",
         "@dcloudio/uni-mp-xhs": "3.0.0-4030620241128001",
         "@dcloudio/uni-quickapp-webview": "3.0.0-4030620241128001",
+        "@imengyu/imengyu-utils": "^0.0.12",
         "@imengyu/js-request-transform": "^0.3.3",
         "@imengyu/vue-dynamic-form": "^0.1.1",
         "async-validator": "^4.2.5",
@@ -2889,6 +2890,12 @@
         "node": ">=12"
       }
     },
+    "node_modules/@imengyu/imengyu-utils": {
+      "version": "0.0.12",
+      "resolved": "https://registry.npmmirror.com/@imengyu/imengyu-utils/-/imengyu-utils-0.0.12.tgz",
+      "integrity": "sha512-+iQJkYNafCNMEfJGGMGEdSnXWkmzb2sL32vHPIxZ/4L7Hhzv9blmuNsEQ5xkqbNQ/n8p38CJjX/rHlsr3GCGsQ==",
+      "license": "MIT"
+    },
     "node_modules/@imengyu/js-request-transform": {
       "version": "0.3.3",
       "resolved": "https://registry.npmmirror.com/@imengyu/js-request-transform/-/js-request-transform-0.3.3.tgz",

+ 1 - 0
package.json

@@ -50,6 +50,7 @@
     "@dcloudio/uni-mp-weixin": "3.0.0-4030620241128001",
     "@dcloudio/uni-mp-xhs": "3.0.0-4030620241128001",
     "@dcloudio/uni-quickapp-webview": "3.0.0-4030620241128001",
+    "@imengyu/imengyu-utils": "^0.0.12",
     "@imengyu/js-request-transform": "^0.3.3",
     "@imengyu/vue-dynamic-form": "^0.1.1",
     "async-validator": "^4.2.5",

+ 172 - 0
src/common/composeabe/SimpleAudioPlayer.ts

@@ -0,0 +1,172 @@
+import { computed, onMounted, onUnmounted, ref, type Ref } from "vue";
+import { TimeUtils } from "@imengyu/imengyu-utils";
+import { toast } from "../utils/DialogAction";
+
+export function useSimpleAudioPlayer(config: {
+  onEnded?: () => void;
+}) {
+  const duration = ref(0);
+  const timeSec = ref(0);
+  const timeString = ref('00:00');
+  const isPlaying = ref(false);
+  const loadedState = ref(false);
+  const innerAudioContext = uni.createInnerAudioContext();
+
+  function load(src: string) {
+    if (isPlaying.value) 
+      pause();
+    loadedState.value = true;
+    innerAudioContext.src = src;
+    innerAudioContext.play();
+    timeString.value = '加载中';
+  }
+    
+  function play() {
+    innerAudioContext.play();
+  }
+  function pause() {
+    innerAudioContext.pause();
+  }
+  function playpause() {
+    if (isPlaying.value) {
+      innerAudioContext.pause();
+      return;
+    } else {
+      innerAudioContext.play();
+    }
+  }
+  function seek(sec: number) {
+    innerAudioContext.seek(sec);
+  }
+
+  innerAudioContext.onEnded(() => {
+    isPlaying.value = false;
+    if (config.onEnded)
+      config.onEnded();
+  });
+  innerAudioContext.onPlay(() => {
+    isPlaying.value = true;
+  });
+  innerAudioContext.onPause(() => {
+    isPlaying.value = false;
+  });
+  innerAudioContext.onSeeking(() => {
+    timeString.value = '加载中';
+  });
+  innerAudioContext.onSeeking(() => {
+    timeString.value = '加载中';
+  });
+  innerAudioContext.onWaiting(() => {
+    timeString.value = '加载中';
+  });
+  innerAudioContext.onError((err) => {
+    console.error(err);
+    timeString.value = '错误';
+    toast('播放音频失败');
+    isPlaying.value = false;
+  });
+  innerAudioContext.onError((err) => {
+    console.error(err);
+    toast('播放音频失败');
+    isPlaying.value = false;
+  });
+  innerAudioContext.onTimeUpdate(() => {
+    duration.value = innerAudioContext.duration; 
+    timeSec.value = innerAudioContext.currentTime;
+    timeString.value = TimeUtils.getTimeStringSec(timeSec.value) + '/' + TimeUtils.getTimeStringSec(duration.value);
+  });
+  innerAudioContext.onEnded(() => {
+    isPlaying.value = false;
+  });
+  onUnmounted(() => {
+    innerAudioContext.destroy();
+  });
+
+  return { 
+    timeSec,
+    timeString,
+    loadedState,
+    isPlaying, 
+    duration,
+    seek,
+    load, 
+    play, 
+    playpause,
+    pause,
+  };
+}
+
+export interface SimpleListAudioPlayerItem {
+  src: string; 
+  title: string;
+}
+
+export function useSimpleListAudioPlayer<T extends SimpleListAudioPlayerItem>(
+  loadList: () => Promise<T[]>,
+  autoNext: boolean = false,
+) {
+  const player = useSimpleAudioPlayer({
+    onEnded: () => {
+      if (autoNext) {
+        next();
+      } 
+    }
+  });
+  const list = ref<T[]>([]) as Ref<T[]>;
+  const currentIndex = ref(0);
+  const currentTitle = computed(() => {
+    if (currentIndex.value < 0) 
+      return '未在播放';
+    return list.value[currentIndex.value]?.title || '未在播放';
+  })
+  const currentItem = computed(() => {
+    if (currentIndex.value < 0) 
+      return null;
+    return list.value[currentIndex.value] || null;
+  })
+
+  onMounted(async () => {
+    list.value = await loadList();
+  })
+
+  function loadToPlayer() {
+    player.load(list.value[currentIndex.value].src);
+  }
+  function next() {
+    if (list.value.length == 0) {
+      toast('没有更多了');
+      return;
+    }
+    currentIndex.value ++;
+    if (currentIndex.value > list.value.length - 1) 
+      currentIndex.value = 0;
+    loadToPlayer();
+  }
+  function prev() {
+    if (list.value.length == 0) {
+      toast('没有更多了');
+      return;
+    }
+    currentIndex.value--;
+    if (currentIndex.value < 0)
+      currentIndex.value = list.value.length - 1;
+    loadToPlayer();
+  }
+
+  return {
+    ...player,
+    playpause: () => {
+      if (!player.loadedState.value) {
+        loadToPlayer();
+        return;
+      }
+      player.playpause();
+    },
+    list,
+    currentIndex,
+    currentTitle,
+    currentItem,
+    next,
+    prev,
+  }
+}

+ 31 - 9
src/pages/home.vue

@@ -37,9 +37,14 @@
 
           <Box1AudioPlay
             class="w-100 mt-3 mb-3" 
-            :title="langLoader.content.value?.[0].title"
-            :image="langLoader.content.value?.[0].image"
-            @click="navTo('/pages/video/details', { id: langLoader.content.value?.[0].id })"
+            :title="indexAudioPlayer.currentTitle.value"
+            :image="indexAudioPlayer.currentItem?.value?.image"
+            :playState="indexAudioPlayer.isPlaying.value"
+            :playTime="indexAudioPlayer.timeString.value"
+            @playPauseClick="indexAudioPlayer.playpause"
+            @nextClick="indexAudioPlayer.next"
+            @prevClick="indexAudioPlayer.prev"
+            @arrowClick="subTabs1[0].onClick"
           />
 
           <view  
@@ -73,6 +78,10 @@
           </view>
           <view :class="mapTab == 2 ? 'active' : ''" @click="mapTab=2">
             <text class="iconfont icon-task-trip" />
+            非遗
+          </view>
+          <view :class="mapTab == 3 ? 'active' : ''" @click="mapTab=3">
+            <text class="iconfont icon-task-trip" />
             非遗传习所
           </view>
         </view>
@@ -161,6 +170,8 @@ import CommonContent, { GetContentListParams } from '@/api/CommonContent';
 import { ref, watch } from 'vue';
 import UnmoveableContent from '@/api/inheritor/UnmoveableContent';
 import SeminarContent from '@/api/inheritor/SeminarContent';
+import ProjectsContent from '@/api/inheritor/ProjectsContent';
+import { useSimpleListAudioPlayer } from '@/common/composeabe/SimpleAudioPlayer';
 
 const subTabs1 = [
   { 
@@ -243,6 +254,9 @@ const mapLoader = useSimpleDataLoader(async () => {
       list = (await UnmoveableContent.getContentList(new GetContentListParams(), 1, 6)).list
       break;
     case 2:
+      list = (await ProjectsContent.getContentList(new GetContentListParams(), 1, 6)).list
+      break;
+    case 3:
       list = (await SeminarContent.getContentList(new GetContentListParams(), 1, 6)).list
       break;
   }  
@@ -269,6 +283,20 @@ const mapLoader = useSimpleDataLoader(async () => {
 
 watch(mapTab, () => mapLoader.loadData(undefined, true));
 
+const indexAudioPlayer = useSimpleListAudioPlayer(async () => {
+  return (await CommonContent.getContentList(new GetContentListParams()
+    .setModelId(5)
+    .setMainBodyColumnId(313)
+  , 1, 6)).list.sort(() => Math.random()>0.5?-1:1).map((p) => {
+    return {
+      id: p.id,
+      title: p.title,
+      image: p.thumbnail || p.image,
+      src: p.audio as string, 
+    }
+  });
+})
+
 const activityLoader = useSimpleDataLoader(async () => {
   //TODO: 活动接口
   return [
@@ -283,12 +311,6 @@ const activityLoader = useSimpleDataLoader(async () => {
     }
   ]
 })
-const langLoader = useSimpleDataLoader(async () => {
-  return (await CommonContent.getContentList(new GetContentListParams()
-    .setModelId(16)
-    .setMainBodyColumnId(189)
-  , 1, 1)).list;
-})
 const recommendLoader = useSimpleDataLoader(async () => {
   const list = [];
   list.push(...(await CommonContent.getContentList(new GetContentListParams()

+ 115 - 20
src/pages/inhert/map/index.vue

@@ -1,5 +1,23 @@
 <template>
   <view class="d-flex flex-col bg-base">
+    
+    <u-tabs 
+      :list="tabs" 
+      lineWidth="30"
+      lineColor="#d9492e"
+      :activeStyle="{
+        color: '#000',
+        fontWeight: 'bold',
+        transform: 'scale(1.05)'
+      }"
+      :inactiveStyle="{
+        color: '#606266',
+        transform: 'scale(1)'
+      }"
+      :scrollable="false"
+      class="top-tab"
+      @click="(e: any) => tab = e.index"
+    />
     <view class="d-flex flex-col p-2">
       <uni-search-bar 
         v-model="searchValue"
@@ -12,16 +30,17 @@
       />
     </view>
     <view class="d-flex flex-row justify-around p-2 pt-0">
-      <SimpleDropDownPicker v-model="selectedTag" :columns="categoryData.content.value" />
+      <SimpleDropDownPicker v-if="tab == 0" v-model="selectedTag" :columns="categoryData.content.value" />
       <SimpleDropDownPicker v-model="selectedLevel" :columns="levelData.content.value" />
       <SimpleDropDownPicker v-model="selectedRegion" :columns="regionData.content.value" />
     </view>
     <view class="d-flex flex-row flex-wrap justify-between">
       <map 
         id="map"
-        class="w-100 h-100vh"
-        :markers="listLoader.list.value || []"
-        :scale="15"
+        class="w-100"
+        style="height:80vh"
+        :makers="[]"
+        :scale="13"
         @markertap="onMarkerTap"
       />
     </view>
@@ -29,14 +48,29 @@
 </template>
 
 <script setup lang="ts">
-import { onMounted, ref, watch } from 'vue';
-import CommonContent, { GetContentListParams } from '@/api/CommonContent';
+import { ref, watch } from 'vue';
 import { useSimplePageListLoader } from '@/common/composeabe/SimplePageListLoader';
 import { navTo } from '@/common/utils/PageAction';
-import UnmoveableContent from '@/api/inheritor/UnmoveableContent';
-import SimpleDropDownPicker from '@/common/components/SimpleDropDownPicker.vue';
 import { useSimpleDataLoader } from '@/common/composeabe/SimpleDataLoader';
+import SimpleDropDownPicker from '@/common/components/SimpleDropDownPicker.vue';
+import SeminarContent from '@/api/inheritor/SeminarContent';
+import CommonContent, { GetContentListParams } from '@/api/CommonContent';
+import UnmoveableContent from '@/api/inheritor/UnmoveableContent';
+import ProjectsContent from '@/api/inheritor/ProjectsContent';
+import { onLoad } from '@dcloudio/uni-app';
 
+const tab = ref(0)
+const tabs = [
+  {
+    name: '文物'
+  },
+  {
+    name: '非遗'
+  },
+  {
+    name: '非遗传习所'
+  },
+];
 const mapCtx = uni.createMapContext('map');
 const categoryData = useSimpleDataLoader(async () => 
   [{
@@ -69,21 +103,44 @@ const selectedTag = ref(0);
 const selectedLevel = ref(0);
 const selectedRegion = ref(0);
 const searchValue = ref('');
-const listLoader = useSimplePageListLoader(8, async (page, pageSize) => {
-  const res = (await UnmoveableContent.getContentList(new GetContentListParams().setSelfValues({
-    crType: selectedTag.value == 0 ? undefined: selectedTag.value,
-    level: selectedLevel.value == 0 ? undefined: selectedLevel.value,
-    region: selectedRegion.value == 0 ? undefined: selectedRegion.value,
-    keywords: searchValue.value,
-  }), page, pageSize)).list.map((p) => {
+const listLoader = useSimplePageListLoader(50, async (page, pageSize) => {
+  let list;
+  switch (tab.value) {
+    default:
+    case 0:
+      list = (await UnmoveableContent.getContentList(new GetContentListParams().setSelfValues({
+        crType: selectedTag.value == 0 ? undefined: selectedTag.value,
+        level: selectedLevel.value == 0 ? undefined: selectedLevel.value,
+        region: selectedRegion.value == 0 ? undefined: selectedRegion.value,
+        keywords: searchValue.value,
+      }), page, pageSize)).list;
+      break;
+    case 1:
+      list = (await ProjectsContent.getContentList(new GetContentListParams().setSelfValues({
+        level: selectedLevel.value == 0 ? undefined: selectedLevel.value,
+        region: selectedRegion.value == 0 ? undefined: selectedRegion.value,
+        keywords: searchValue.value,
+      }), page, pageSize)).list;
+      break;
+    case 2:
+      list = (await SeminarContent.getContentList(new GetContentListParams().setSelfValues({
+        level: selectedLevel.value == 0 ? undefined: selectedLevel.value,
+        region: selectedRegion.value == 0 ? undefined: selectedRegion.value,
+        keywords: searchValue.value,
+      }), page, pageSize)).list;
+      break;
+  }
+  
+  const res = list.map((p) => {
     return {
-      ...p,
+      //...p,
       id: p.id,
       longitude: Number(p.longitude),
       latitude: Number(p.latitude),
       iconPath: p.thumbnail,
       width: 40,
       height: 40,
+      joinCluster: true,
       callout: {
         content: p.title,
         color: "#ffffff",
@@ -95,7 +152,6 @@ const listLoader = useSimplePageListLoader(8, async (page, pageSize) => {
       },
     }
   })
-  
   mapCtx.includePoints({
     points: res.map(p => ({
       latitude: p.latitude,
@@ -103,6 +159,10 @@ const listLoader = useSimplePageListLoader(8, async (page, pageSize) => {
     })),
     padding: [20, 20, 20, 20],
   });
+  mapCtx.addMarkers({
+    clear: true,
+    markers: res, 
+  })
 
   return res;
 }, true);
@@ -116,10 +176,9 @@ watch(selectedRegion, () => {
 watch(selectedTag, () => {
   listLoader.loadData(undefined, true);
 });
-
-onMounted(() => {
+watch(tab, () => {
   listLoader.loadData(undefined, true);
-})
+});
 
 function doSearch() {
   listLoader.loadData(undefined, true);
@@ -130,4 +189,40 @@ function onMarkerTap(e: { markerId: number }) {
 function goDetails(id: number) {
   navTo('/pages/inhert/artifact/details', { id })
 }
+
+onLoad(() => {
+  mapCtx.initMarkerCluster({
+    enableDefaultStyle: false,
+    zoomOnClick: true,
+    gridSize: 60,
+  });
+  mapCtx.on('markerClusterCreate', (e: { clusters: any[] }) => {
+    const customClusters = e.clusters.map((cluster) => {
+      const { center, clusterId, markerIds } = cluster;
+      return {
+        ...center,
+        width: 0,
+        height: 0,
+        clusterId,
+        label: {
+          content: markerIds.length.toString(), // 聚合点的数量
+          fontSize: 16,
+          color: '#fff',
+          width: 50,
+          height: 50,
+          bgColor: '#419afcD9', // 背景颜色
+          borderRadius: 25,
+          textAlign: 'center',
+          anchorX: -10,
+          anchorY: -35,
+        },
+      };
+    });
+    mapCtx.addMarkers({
+      markers: customClusters,
+      clear: false,
+    });
+  })
+  listLoader.loadData(undefined, true);
+})
 </script>

+ 1 - 1
src/pages/inhert/village/details.vue

@@ -174,7 +174,7 @@ const contentLoader = useSimpleDataLoader(async () => {
 
   const menu = await VillageApi.getVillageMenuList(querys.value.id);
 
-  tagsData.value = menu.filter((i) => i.platform == 2).map((item, index) => {
+  tagsData.value = menu/* .filter((i) => i.platform == 2) */.map((item, index) => {
     return {
       title: item.name,
       image: item.logo || EmptyImage,

+ 67 - 5
src/pages/parts/Box1AudioPlay.vue

@@ -1,20 +1,66 @@
 <template>
     <view 
-      class="d-flex w-100 flex-row align-center bg-light-light-primary radius-base p-25"
+      class="position-relative d-flex w-100 flex-row align-center bg-light-light-primary radius-base p-25"
+    >
+    <image 
+      class="width-150 height-150 radius-base" 
+      :src="image || ImageTest" 
+      mode="aspectFill" 
+      @click="$emit('click')"
+    />
+    <view 
+      class="d-flex flex-col ml-25 flex-one"
       @click="$emit('click')"
     >
-    <image class="width-150 height-150 radius-base" :src="image" mode="aspectFill" />
-    <view class="d-flex flex-col ml-25 flex-one">
       <text class="color-second-text">{{ title }}</text>
-      <image class="width-50 height-50 mt-3" src="/static/images/home/PlayButtonSmall.png" />
+      <view class="d-flex flex-row align-center mt-3">
+        <image 
+          v-if="showPrev" 
+          class="width-50 height-50 mr-2" 
+          style="transform:rotate(180deg);" 
+          src="/static/images/home/NextButtonSmall.png" 
+          @click="$emit('prevClick')"
+        />
+        <image 
+          v-if="playState"
+          class="width-50 height-50 mr-2" 
+          src="/static/images/home/PauseButtonSmall.png"
+          @click="$emit('playPauseClick')"
+        />
+        <image 
+          v-else
+          class="width-50 height-50 mr-2"
+          src="/static/images/home/PlayButtonSmall.png"
+          @click="$emit('playPauseClick')"
+        />
+        <image 
+          v-if="showNext"
+          class="width-50 height-50 mr-2"
+          src="/static/images/home/NextButtonSmall.png"
+          @click="$emit('nextClick')"
+        />
+        <text class="color-second-text size-s ml-2">{{ playTime }}</text>
+      </view>
     </view>
     <text class="iconfont icon-arrow-right color-primary" />
+    <view 
+      class="position-absolute r-0 t-0 b-0 width-50"
+      :style="{zIndex:11}"
+      @click="$emit('arrowClick')"
+    ></view>
   </view>
 </template>
 
 <script setup lang="ts">
 import ImageTest from '/static/images/home/ImageTest.jpg';
 
+const props = defineEmits([	
+  "click",
+  "arrowClick",
+  "nextClick",
+  "prevClick",
+  'playPauseClick',
+])
 defineProps({
   title: {
     type: String,
@@ -23,7 +69,23 @@ defineProps({
   image: {
     type: String,
     default: ImageTest,
-  }
+  },
+  playState: {
+    type: Boolean,
+    default: false,
+  },
+  playTime: {
+    type: String,
+    default: '', 
+  },
+  showNext: {
+    type: Boolean,
+    default: true,
+  },
+  showPrev: {
+    type: Boolean,
+    default: true,
+  },
 })
 
 </script>

+ 40 - 1
src/pages/travel/nav/navto.vue

@@ -16,6 +16,7 @@
     <cover-view class="map-info">
       <cover-view class="title">{{ distance }}</cover-view>
       <cover-view class="sub-title">{{ taxi_cost }}</cover-view>
+      <cover-view class="go" @click="goSystem">其他导航</cover-view>
     </cover-view>
   </view>
 </template>
@@ -27,6 +28,7 @@ import AppCofig from '@/common/config/AppCofig';
 import { back } from '@/common/utils/PageAction';
 import { ref } from 'vue';
 import amapFile from '@/libs/amap-wx.130';
+import { toast } from '@/common/utils/DialogAction';
 
 const mapCtx = uni.createMapContext('map');
 const polyline = ref();
@@ -83,7 +85,17 @@ const { querys } = useLoadQuerys({
       }
     ];
 
+    function colorTrans(color1: number[], color2: number[], pec: number) {
+      return [
+        Math.round(color1[0] + (color2[0] - color1[0]) * pec),
+        Math.round(color1[1] + (color2[1] - color1[1]) * pec),
+        Math.round(color1[2] + (color2[2] - color1[2]) * pec),
+      ]
+    }
+
     polyline.value = await new Promise((resolve, reject) => {
+      const color1 = [223, 53, 51];
+      const color2 = [0, 145, 255];
 
       const myAmapFun = new amapFile.AMapWX({key:  AppCofig.amapKey});
       myAmapFun.getDrivingRoute({
@@ -91,7 +103,8 @@ const { querys } = useLoadQuerys({
         destination: `${querys.longitude.toFixed(5)},${querys.latitude.toFixed(5)}`,
         nosteps: 1,
         success: function(data: any){
-          var points = [];
+          let points = [];
+          let colorList : string[] = [];
           if(data.paths && data.paths[0] && data.paths[0].steps){
             var steps = data.paths[0].steps;
             for(var i = 0; i < steps.length; i++){
@@ -104,11 +117,15 @@ const { querys } = useLoadQuerys({
               } 
             }
           } 
+          for (let j = 0; j < points.length; j++) {
+            colorList.push('rgb(' + colorTrans(color1, color2, j / points.length).join(',') + ')');
+          }
           distance.value = '距离您约 ' + formatMeter(data.paths[0].distance) + '米'
           taxi_cost.value =  '打车约' + parseInt(data.taxi_cost) + '元'
           resolve([{
             points: points,
             color: "#0091ff",
+            colorList: colorList,
             width: 6
           }]);
         },
@@ -132,6 +149,19 @@ const { querys } = useLoadQuerys({
   }
 });
 
+function goSystem() {
+  uni.openLocation({
+    latitude: querys.value.latitude,
+    longitude: querys.value.longitude,
+    scale: 15,
+    name: '目标位置',
+    success() {},
+    fail(error) {
+      toast('导航启动失败');
+    },
+  });
+}
+
 function formatMeter(n: number) {
   if (n > 1000) {
     return (n / 1000).toFixed(1) + '千'; 
@@ -175,6 +205,15 @@ function formatMeter(n: number) {
       margin-top: 10rpx;
       color: #666;
     }
+    .go {
+      position: absolute;
+      padding: 15rpx;
+      background-color: #bdbdbd;
+      border-radius: 10rpx;
+      top: 50%;
+      right: 20rpx;
+      transform: translateY(-50%);
+    }
   }
 }
 </style>

BIN
src/static/images/home/NextButtonSmall.png


BIN
src/static/images/home/PauseButtonSmall.png