imengyu 16 годин тому
батько
коміт
413eee05d7

+ 36 - 0
src/api/CommonContent.ts

@@ -136,6 +136,25 @@ export class GetColumContentList extends DataModel<GetColumContentList> {
   name = '';
   overview = '';
 }
+export class GetModelColumContentList extends DataModel<GetColumContentList> {
+  constructor() {
+    super(GetColumContentList, "模型的主体栏目列表");
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+      name: { clientSide: 'string', serverSide: 'string', clientSideRequired: true },
+      iscontribute: { clientSide: 'boolean' },
+    }
+  }
+
+  id = 0;
+  name = '';
+  modelId =  0;
+  image = '';
+  diyname = '';
+  iscontribute = false;
+  statusText = '';
+}
 export class GetContentListItem extends DataModel<GetContentListItem> {
   constructor() {
     super(GetContentListItem, "内容列表");
@@ -347,6 +366,23 @@ export class CommonContentApi extends AppServerRequestModule<DataModel> {
       .catch(e => { throw e });
   }
   /**
+   * 模型的主体栏目列表
+   * @param params 参数 
+   * @param querys 额外参数
+   * @returns 
+   */
+  getModelColumList<T extends DataModel = GetModelColumContentList>(model_id: number, page: number, pageSize: number = 10,querys?: QueryParams) {
+    return this.get('/content/main_body_column/getColumnList', `${this.debugName} 模型的主体栏目列表`, {
+      main_body_id: this.mainBodyId,
+      model_id: model_id ?? this.modelId,
+      page,
+      pageSize,
+      ...querys
+    })
+      .then(res => transformArrayDataModel<T>(GetModelColumContentList, res.data2, `${this.debugName} 模型的主体栏目列表`, true))
+      .catch(e => { throw e });
+  }
+  /**
    * 主体栏目列表
    * @param params 参数 
    * @param querys 额外参数

+ 82 - 0
src/common/components/SimpleListAudioPlayer.vue

@@ -0,0 +1,82 @@
+<template>
+  <slot 
+    :currentPlayItemIndex="currentPlayItemIndex" 
+    :currentPlayItemId="currentPlayItemId"
+    :isPlaying="audioPlayer.isPlaying.value"
+  />
+</template>
+
+<script setup lang="ts">
+import { ref, watch } from 'vue'
+import { useSimpleListAudioPlayer } from '../composeabe/SimpleAudioPlayer';
+import { RandomUtils } from '@imengyu/imengyu-utils';
+
+const props = defineProps<{
+  list: { 
+    id: number,
+    title: string,
+    src: string,
+  }[],
+  playMode: 'loop'|'random'|'list',
+  playAuto: boolean,
+}>();
+
+let lock = false;
+const currentPlayItemIndex = ref(0);
+const currentPlayItemId = ref(0);
+const audioPlayer = useSimpleListAudioPlayer(async () => props.list, false, () => {
+  if (lock)
+    return;
+  lock = true;
+  setTimeout(() => {
+    if (props.playAuto === true) {
+      switch (props.playMode) {
+        case 'loop': audioPlayer.restart(); break;
+        case 'random': playItem(props.list[RandomUtils.genRandom(0, props.list.length - 1)].id); break;
+        case 'list': 
+          if (currentPlayItemIndex.value < props.list.length - 1)
+            playItem(props.list[currentPlayItemIndex.value + 1].id); 
+          else
+            currentPlayItemId.value = 0;
+          break;
+      }
+    }
+    lock = false;
+  }, 100);
+});
+
+function playItem(id: number) {
+  currentPlayItemIndex.value = props.list.findIndex((item) => item.id == id);
+  currentPlayItemId.value = id;
+  const item = props.list[currentPlayItemIndex.value];
+  if (item) {
+    audioPlayer.load(item.src);
+    audioPlayer.play();
+  }
+}
+
+watch(() => props.list, () => {
+  audioPlayer.reloadList();
+});
+
+defineExpose({
+  playFirst() {
+    if (props.list.length > 0)
+      playItem(props.list[0].id)
+  },
+  playpause(id: number) {
+    if (id == currentPlayItemId.value) {
+      if (audioPlayer.isEnded.value) 
+        audioPlayer.restart();
+      else
+        audioPlayer.playpause();
+    }
+    else {
+      playItem(id);
+    }
+  },
+  pause() {
+    audioPlayer.pause();
+  },
+})
+</script>

+ 37 - 16
src/common/composeabe/SimpleAudioPlayer.ts

@@ -9,14 +9,17 @@ export function useSimpleAudioPlayer(config: {
   const timeSec = ref(0);
   const timeString = ref('00:00');
   const isPlaying = ref(false);
+  const isEnded = ref(false);
   const loadedState = ref(false);
   const innerAudioContext = uni.createInnerAudioContext();
+  let loading = false
 
   function load(src: string) {
-    if (isPlaying.value) 
-      pause();
+    loading = true;
     loadedState.value = true;
-    innerAudioContext.src = src;
+    innerAudioContext.stop();
+    if (innerAudioContext.src != src)
+      innerAudioContext.src = src;
     innerAudioContext.play();
     timeString.value = '加载中';
   }
@@ -27,6 +30,10 @@ export function useSimpleAudioPlayer(config: {
   function pause() {
     innerAudioContext.pause();
   }
+  function restart() {
+    innerAudioContext.seek(0);
+    innerAudioContext.play();
+  } 
   function playpause() {
     if (isPlaying.value) {
       innerAudioContext.pause();
@@ -38,46 +45,50 @@ export function useSimpleAudioPlayer(config: {
   function seek(sec: number) {
     innerAudioContext.seek(sec);
   }
+  function stop() {
+    innerAudioContext.stop();
+  }
 
   innerAudioContext.onEnded(() => {
     isPlaying.value = false;
+    isEnded.value = true;
+    if (loading)
+      return;
     if (config.onEnded)
       config.onEnded();
   });
   innerAudioContext.onPlay(() => {
+    loading = false;
     isPlaying.value = true;
+    isEnded.value = false;
   });
   innerAudioContext.onPause(() => {
     isPlaying.value = false;
+    isEnded.value = false;
   });
   innerAudioContext.onSeeking(() => {
+    loading = true;
     timeString.value = '加载中';
   });
-  innerAudioContext.onSeeking(() => {
-    timeString.value = '加载中';
+  innerAudioContext.onSeeked(() => {
+    loading = false;
   });
   innerAudioContext.onWaiting(() => {
+    loading = true;
     timeString.value = '加载中';
   });
   innerAudioContext.onError((err) => {
     console.error(err);
     timeString.value = '错误';
-    toast('播放音频失败');
-    isPlaying.value = false;
-  });
-  innerAudioContext.onError((err) => {
-    console.error(err);
-    toast('播放音频失败');
     isPlaying.value = false;
+    isEnded.value = false;
+    loading = 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();
   });
@@ -87,11 +98,14 @@ export function useSimpleAudioPlayer(config: {
     timeString,
     loadedState,
     isPlaying, 
+    isEnded,
     duration,
     seek,
     load, 
+    stop,
     play, 
     playpause,
+    restart,
     pause,
   };
 }
@@ -104,9 +118,11 @@ export interface SimpleListAudioPlayerItem {
 export function useSimpleListAudioPlayer<T extends SimpleListAudioPlayerItem>(
   loadList: () => Promise<T[]>,
   autoNext: boolean = false,
+  onEnded?: () => void,
 ) {
   const player = useSimpleAudioPlayer({
     onEnded: () => {
+      onEnded?.();
       if (autoNext) {
         next();
       } 
@@ -125,10 +141,14 @@ export function useSimpleListAudioPlayer<T extends SimpleListAudioPlayerItem>(
     return list.value[currentIndex.value] || null;
   })
 
-  onMounted(async () => {
-    list.value = await loadList();
+  onMounted(() => {
+    reloadList();
   })
 
+  async function reloadList() {
+    list.value = await loadList();
+  }
+
   function loadToPlayer() {
     player.load(list.value[currentIndex.value].src);
   }
@@ -168,5 +188,6 @@ export function useSimpleListAudioPlayer<T extends SimpleListAudioPlayerItem>(
     currentItem,
     next,
     prev,
+    reloadList,
   }
 }

+ 6 - 0
src/pages.json

@@ -149,6 +149,12 @@
       }
     },
     {
+      "path": "pages/inhert/language/list",
+      "style": {
+        "navigationBarTitleText": "闽南话"
+      }
+    },
+    {
       "path": "pages/inhert/village/list",
       "style": {
         "navigationBarTitleText": "村落列表",

+ 12 - 19
src/pages/home.vue

@@ -45,7 +45,7 @@
             @playPauseClick="indexAudioPlayer.playpause"
             @nextClick="indexAudioPlayer.next"
             @prevClick="indexAudioPlayer.prev"
-            @arrowClick="handleGoAudioList"
+            @click="handleGoAudioList"
           />
         </view>
       </view>
@@ -178,7 +178,7 @@
 </template>
 
 <script setup lang="ts">
-const MainBoxIcon1 = 'https://mncdn.wenlvti.net/app_static/minnan/images/home/MainBoxIcon1.png';
+const MainBoxIcon1 = 'https://mncdn.wenlvti.net/app_static/minnan/images/home/MainBoxIcon10.png';
 const MainBoxIcon2 = 'https://mncdn.wenlvti.net/app_static/minnan/images/home/MainBoxIcon2.png';
 const MainBoxIcon3 = 'https://mncdn.wenlvti.net/app_static/minnan/images/home/MainBoxIcon3.png';
 const MainBoxIcon4 = 'https://mncdn.wenlvti.net/app_static/minnan/images/home/MainBoxIcon4.png';
@@ -230,15 +230,9 @@ const subTabs = [
     onClick: () => navTo('/pages/inhert/village/list')
   },
   { 
-    name: '闽南', 
+    name: '闽南', 
     icon: MainBoxIcon1, 
-    onClick: () => navTo('/pages/article/common/list', {
-      title: '闽南语',
-      mainBodyColumnId: '257,235,237,210',
-      modelId: 5,
-      itemType: 'article-common',
-      detailsPage: '/pages/video/details',
-    }) 
+    onClick: () => navTo('/pages/inhert/language/list') 
   },
   { 
     name: '闽南美食', 
@@ -318,13 +312,7 @@ const indexAudioPlayer = useSimpleListAudioPlayer(async () => {
   });
 })
 function handleGoAudioList() {
-  navTo('/pages/article/common/list', {
-    title: '闽南语',
-    mainBodyColumnId: 313,
-    modelId: 5,
-    itemType: 'article-common',
-    detailsPage: '/pages/video/details',
-  }) 
+  navTo('/pages/inhert/language/list') 
 }
 
 const activityLoader = useSimpleDataLoader(async () => {
@@ -380,13 +368,18 @@ const statsLoader = useSimpleDataLoader(async () => {
     {
       title: '非遗传承人',
       type: '2',
-      datas: data.inheritorData.filter((p: any) => [ '国家级', '省级', '市级', '区县级' ].includes(p.title)).map((item: any) => {
+      datas: data.inheritorData.filter((p: any) => [ '国家级', '省级', '市级' ].includes(p.title)).map((item: any) => {
         return {
           title: item.title,
           value: item.total,
           onClick: () => navTo('/pages/inhert/inheritor/list', { level: item.level }),
         }
-      })
+      }).concat([
+        {
+          title: '',
+          value: '',
+        }
+      ]),
     },
     {
       datas: [

+ 7 - 7
src/pages/inhert.vue

@@ -181,9 +181,9 @@
         </scroll-view>
       </SimplePageContentLoader>
 
-      <!-- 闽南知识百科 -->
-      <!-- <view class="d-flex flex-col wing-l">
-        <HomeTitle title="闽南知识百科" showMore @clickMore="goTopicsList" />
+      <!-- 闽南文化百科 -->
+      <view class="d-flex flex-col wing-l">
+        <HomeTitle title="闽南文化百科" showMore @clickMore="goTopicsList" />
         <SimplePageContentLoader :loader="topicsData">
           <Box2LineRightShadow
             v-for="(item, i) in topicsData.content.value" 
@@ -193,7 +193,7 @@
             @click="goTopicsDetail(item.id)"
           />
         </SimplePageContentLoader>
-      </view> -->
+      </view>
 
     </view>
   </view>
@@ -333,9 +333,9 @@ const {
   goList: goTopicsList,
   goDetail: goTopicsDetail,
 } = useHomePageMiniCommonListGoMoreAndGoDetail({
-  title: '闽南知识百科',
-  mainBodyColumnId: 43,
-  modelId: 8,
+  title: '闽南文化百科',
+  mainBodyColumnId: 320,
+  modelId: 18,
   itemType: 'article-common',
   detailsPage: '/pages/article/details',
 });

+ 133 - 0
src/pages/inhert/language/list.vue

@@ -0,0 +1,133 @@
+<template>
+  <view class="d-flex flex-col bg-base" style="min-height:100vh"> 
+    <view class="top-tab bg-base">
+      <u-tabs 
+        :list="tabs.content.value || []" 
+        lineWidth="30"
+        lineColor="#d9492e"
+        :activeStyle="{
+          color: '#000',
+          fontWeight: 'bold',
+          transform: 'scale(1.05)'
+        }"
+        :inactiveStyle="{
+          color: '#606266',
+          transform: 'scale(1)'
+        }"
+        :scrollable="true"
+        class="top-tab"
+        @click="(e: any) => tab = e.id"
+      />
+    </view>
+
+    <view class="d-flex flex-col p-2">
+      <uni-search-bar 
+        v-model="searchValue"
+        radius="100" 
+        bgColor="#fff" 
+        placeholder="搜索闽南语" 
+        clearButton="auto" 
+        cancelButton="none"
+        @clear="doSearch"
+        @confirm="doSearch"
+      />
+      <view class="d-flex flex-row justify-between align-center pl-3 pr-3">
+        <view class="d-flex flex-row align-center">
+          自动连播
+          <switch 
+            class="ml-1"
+            color="#d9492e"
+            :checked="playAuto" 
+            @change="(e: any) => playAuto = e.detail.value" 
+            style="transform:scale(0.6)" 
+          />
+        </view>
+        <view>
+          <text class="iconfont icon-play mr-1"></text>
+          <text v-if="playMode=='list'" @click="playMode='loop'">列表播放</text>
+          <text v-else-if="playMode=='loop'" @click="playMode='random'">单个循环</text>
+          <text v-else-if="playMode=='random'" @click="playMode='list'">随机播放</text>
+          <text v-else @click="playMode='list'">播放模式</text>
+        </view>
+      </view>
+    </view>
+    <view class="d-flex flex-col p-2 pt-0">
+      <SimpleListAudioPlayer
+        ref="player"
+        :list="listLoader.list.value ?? []"
+        :playMode="playMode"
+        :playAuto="playAuto"
+      >
+        <template #default="{ currentPlayItemId, isPlaying }">
+          <view
+            v-for="item in listLoader.list.value"
+            :key="item.id"
+          >
+            <Box2LinePlayRightArrow 
+              classNames="ml-2 mb-3"
+              titleColor="title-text"
+              :image="item.image"
+              :title="item.title"
+              :isCurrent="item.id == currentPlayItemId"
+              :isPlaying="isPlaying"
+              @click="player.playpause(item.id)"
+            />
+          </view>
+        </template>
+      </SimpleListAudioPlayer>
+    </view>
+    <SimplePageListLoader :loader="listLoader" />
+  </view>
+</template>
+
+<script setup lang="ts">
+
+
+import { ref, watch } from 'vue';
+import { useSimplePageListLoader } from '@/common/composeabe/SimplePageListLoader';
+import { useSimpleDataLoader } from '@/common/composeabe/SimpleDataLoader';
+import { navTo } from '@/common/utils/PageAction';
+import CommonContent, { GetContentListParams } from '@/api/CommonContent';
+import SimplePageListLoader from '@/common/components/SimplePageListLoader.vue';
+import AppCofig from '@/common/config/AppCofig';
+import Box2LinePlayRightArrow from '@/pages/parts/Box2LinePlayRightArrow.vue';
+import SimpleListAudioPlayer from '@/common/components/SimpleListAudioPlayer.vue';
+
+const tabs = useSimpleDataLoader(async () => {
+  const res = await CommonContent.getModelColumList(5, 1, 50);
+  tab.value = res[0].id
+  return res;
+}, true);
+
+const player = ref();
+const playMode = ref<'loop'|'random'|'list'>(uni.getStorageSync('LanguagePlayMode') || 'list');
+const playAuto = ref(Boolean(uni.getStorageSync('LanguageAutoPlay')));
+
+console.log(playAuto);
+
+const searchValue = ref('');
+const tab = ref(0)
+const listLoader = useSimplePageListLoader(8, async (page, pageSize) => {
+  const res = await CommonContent.getContentList(new GetContentListParams()
+    .setModelId(5)
+    .setKeywords(searchValue.value)
+    .setMainBodyColumnId(tab.value)
+  , page, pageSize);
+  return { list: res.list.map((item) => {
+    return {
+      id: item.id,
+      image: item.thumbnail || item.image || AppCofig.defaultImage,
+      title: item.title,
+      src: item.audio as string,
+    }
+  }), total: res.total }
+});
+
+watch(playMode, () => uni.setStorageSync('LanguagePlayMode', playMode.value));
+watch(playAuto, () => uni.setStorageSync('LanguageAutoPlay', playAuto.value))
+watch(tab, () => listLoader.loadData(undefined, true));
+
+function doSearch() {
+  listLoader.loadData(undefined, true);
+}
+</script>

+ 2 - 2
src/pages/inhert/village/list.vue

@@ -8,7 +8,7 @@
     :dropDownNames="dropdownNames"
     :load="loadData" 
     :tabs="tabs"
-    :startTabIndex="1"
+    :startTabIndex="0"
     :loadMounted="false"
     @goCustomDetails="goDetails"
   />
@@ -59,7 +59,7 @@ onMounted(async () => {
   const it2 = res.find(p => p.title == '省级');
   if (it1) it1.title = '特色村舍';
   if (it2) it2.title = '传统村落';
-  tabs.value = res.map((p) => ({ id: p.id, name: p.title }));
+  tabs.value = res.slice(1).map((p) => ({ id: p.id, name: p.title }));
   await waitTimeOut(400);
   list.value.load();
 })

+ 4 - 4
src/pages/parts/Box1AudioPlay.vue

@@ -19,25 +19,25 @@
           class="width-50 height-50 mr-2" 
           style="transform:rotate(180deg);" 
           src="https://mncdn.wenlvti.net/app_static/minnan/images/home/NextButtonSmall.png" 
-          @click="$emit('prevClick')"
+          @click.stop="$emit('prevClick')"
         />
         <image 
           v-if="playState"
           class="width-50 height-50 mr-2" 
           src="https://mncdn.wenlvti.net/app_static/minnan/images/home/PauseButtonSmall.png"
-          @click="$emit('playPauseClick')"
+          @click.stop="$emit('playPauseClick')"
         />
         <image 
           v-else
           class="width-50 height-50 mr-2"
           src="https://mncdn.wenlvti.net/app_static/minnan/images/home/PlayButtonSmall.png"
-          @click="$emit('playPauseClick')"
+          @click.stop="$emit('playPauseClick')"
         />
         <image 
           v-if="showNext"
           class="width-50 height-50 mr-2"
           src="https://mncdn.wenlvti.net/app_static/minnan/images/home/NextButtonSmall.png"
-          @click="$emit('nextClick')"
+          @click.stop="$emit('nextClick')"
         />
         <text class="color-second-text size-s ml-2">{{ playTime }}</text>
       </view>

+ 57 - 4
src/pages/parts/Box2LinePlayRightArrow.vue

@@ -3,12 +3,20 @@
     class="d-flex w-100 flex-row align-center bg-light-light-primary radius-base mt-2 p-2"
     @click="$emit('click')"
   >
-    <image class="width-100 height-100 radius-base" src="https://mncdn.wenlvti.net/app_static/minnan/images/discover/PlayButtonLarge.png" mode="aspectFill" />
+    <image class="width-100 height-100 radius-base" 
+      :src="'https://mncdn.wenlvti.net/app_static/minnan/images/discover/' + (isPlaying && isCurrent ? 'PauseButtonLarge.png' : 'PlayButtonLarge.png')" 
+      mode="aspectFill" 
+    />
     <view class="d-flex flex-col ml-3 flex-one">
       <text class="color-primary">{{ title }}</text>
       <text v-if="desc" class="color-primary-second-text">{{ desc }}</text>
     </view>
-    <text class="iconfont icon-arrow-right color-primary" />
+    <view v-if="isCurrent" class="playing-anim">
+      <view class="line line1"></view>
+      <view class="line line2"></view>
+      <view class="line line3"></view>
+    </view>
+    <text v-else class="iconfont icon-arrow-right color-primary" />
   </view>
 </template>
 
@@ -21,6 +29,51 @@ defineProps({
   desc: {
     type: String,
     default: ''
-  }
+  },
+  isCurrent: {
+    type: Boolean,
+    default: false
+  },
+  isPlaying: {
+    type: Boolean,
+    default: false
+  },
 })
-</script>
+</script>
+
+<style lang="scss">
+.playing-anim {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  width: 50rpx;
+  height: 50rpx;
+
+  .line {
+    width: 4rpx;
+    border-radius: 2rpx;
+    margin-right: 4rpx;
+    background-color: #d94a2f;
+    animation: playing-anim-loading 1.2s infinite ease-in-out;
+  }
+  .line1 {
+    animation-delay: -0.4s;
+  }
+  .line2 {
+    animation-delay: -0.2s;
+  }
+  .line3 {
+    animation-delay: 0;
+    margin-right: 0;
+  }
+}
+
+@keyframes playing-anim-loading {
+  0%, 100% {
+    height: 25rpx;
+  }
+  50% {
+    height: 40rpx;
+  }
+}
+</style>

+ 2 - 2
src/pages/travel.vue

@@ -177,10 +177,10 @@ const subTabs = [
     }) 
   },
   { 
-    name: '非旅融合', 
+    name: '融合发展', 
     icon: CategoryIcon5 , 
     onClick: () => navTo('/pages/article/common/list', {
-      title: '非遗与旅游融合发展推荐',
+      title: '融合发展',
       mainBodyColumnId: 278,
       modelId: 17,
       itemType: 'article-common',