소스 검색

对接内容2

快乐的梦鱼 2 달 전
부모
커밋
f78d783f99

+ 4 - 1
src/App.vue

@@ -6,10 +6,12 @@
 import AppConfig from '@/common/config/AppCofig'
 import { onLaunch } from '@dcloudio/uni-app'
 import { useAuthStore } from './store/auth'
+import { useCollectStore } from './store/collect';
 import { configTheme } from './components/theme/ThemeDefine';
 import { getCurrentPageUrl, navTo } from './components/utils/PageAction';
 
 const authStore = useAuthStore();
+const collectStore = useCollectStore();
 
 onLaunch(async () => {
   console.log('App Launch');
@@ -27,8 +29,9 @@ onLaunch(async () => {
       if (noLoginPages.indexOf('/' + pageUrl) == -1 && noLoginPages.indexOf(pageUrl) == -1)
         navTo('/pages/user/login');
     }, 1500);
-
   }
+  //加载采集板块信息
+  await collectStore.loadCollectableModules();
 })
 
 //修改默认主题颜色

+ 53 - 1
src/api/inhert/VillageInfoApi.ts

@@ -28,15 +28,25 @@ export class CommonInfoModel extends DataModel<CommonInfoModel> {
     this.setNameMapperCase('Camel', 'Snake');
     this._convertTable = {
       id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
-
+      keywords: { clientSide: 'splitCommaArray', serverSide: 'commaArrayMerge' },
     },
     this._blackList.toServer.push(
       'updatedAt', 'createdAt', 'deletedAt',
     );
+    this._convertKeyType = (key, direction) => {
+      if (key.endsWith('At'))
+        return {
+          clientSide: 'date',
+          serverSide: 'string',
+        };
+      return undefined;
+    };
     this._afterSolveServer = () => {
       if (this.province && this.city && this.district) {
         this.cityAddress = [this.province as string, this.city as string, this.district as string];
       }
+      if (!this.title && this.name)
+        this.title = this.name;
     };
     this._afterSolveClient = (data) => {
       if (this.cityAddress) {
@@ -48,6 +58,26 @@ export class CommonInfoModel extends DataModel<CommonInfoModel> {
   }
   id !: number;
   cityAddress?: string[];
+  title = '';
+  desc = '';
+  image = '';
+  images = [] as string[];
+  content = '';
+  villageId = 0;
+  villageVolunteerId = 0;
+  villageName = '';
+  villageVolunteerName = '';
+  contentId = 0;
+  name = '';
+  type = 0;
+  audio = '';
+  video = '';
+  archives = '';
+  annex = [] as string[];
+  keywords = [] as string[];
+  createdAt = new Date();
+  updatedAt = new Date();
+  publishAt = new Date();
 }
 
 export class VillageEnvInfo extends DataModel<VillageEnvInfo> {
@@ -116,6 +146,7 @@ export class VillageListItem extends DataModel<VillageListItem> {
   createdAt = new Date();
   updatedAt = new Date();
   title = '';
+  desc = '';
   image = '';
 }
 export class VillageBulidingInfo extends DataModel<VillageBulidingInfo> {
@@ -212,6 +243,27 @@ export class VillageInfoApi extends AppServerRequestModule<DataModel> {
       .then(res => transformArrayDataModel<T>(modelClassCreator, (res.data2.data || res.data2) ?? [], `获取分类列表`, true))
       .catch(e => { throw e });
   }
+
+  async getListForDiscover(collectModuleId: number|undefined, page: number, pageSize: number, keywords?: string) {
+    return (this.post(`/village/collect/list`, {
+      collect_module_id: collectModuleId,
+      page,
+      page_size: pageSize,
+      keywords,
+    }, '获取信息详情'))
+      .then(res => ({
+        total: res.data2.total as number,
+        list: transformArrayDataModel<CommonInfoModel>(CommonInfoModel, (res.data2.data || res.data2) ?? [], `获取分类列表`, true)
+      }))
+      .catch(e => { throw e });
+  }
+  async getInfoForDiscover(collectModuleId: number|undefined, id?: number) {
+    return (await this.post(`/village/collect/info`, {
+      collect_module_id: collectModuleId,
+      id,
+    }, '通用获取信息详情', undefined, CommonInfoModel)).data as CommonInfoModel
+  }
+
   async updateInfo<T extends DataModel>(
     collectModuleId: number|undefined,
     subType: string,

+ 1 - 1
src/components/basic/Image.vue

@@ -21,7 +21,7 @@
       @load="isLoadState = false"
       @error="isErrorState = true; isLoadState = false"
     />
-    <view v-if="showFailed && isErrorState" class="inner-view error">
+    <view v-if="showFailed && isErrorState && !failedImage" class="inner-view error">
       <!-- todo: failed -->
       <Text color="second" :text="src ? '暂无图片' : '加载失败'" />
     </view>

+ 14 - 0
src/pages.json

@@ -127,6 +127,20 @@
         "navigationBarTitleText": "村社详情",
         "enablePullDownRefresh": true
       }
+    },
+    {
+      "path": "pages/home/discover/list",
+      "style": {
+        "navigationBarTitleText": "发现利比亚",
+        "enablePullDownRefresh": true
+      }
+    },
+    {
+      "path": "pages/home/discover/details",
+      "style": {
+        "navigationBarTitleText": "发现详情页",
+        "enablePullDownRefresh": true
+      }
     }
   ],
   "globalStyle": {

+ 6 - 6
src/pages/article/details.vue

@@ -68,18 +68,18 @@
 </template>
 
 <script setup lang="ts">
-import type { GetContentDetailItem } from "@/api/CommonContent";
-import { onShareTimeline, onShareAppMessage } from "@dcloudio/uni-app";
-import { DataDateUtils } from "@imengyu/js-request-transform";
+import { computed } from "vue";
+import { useSimpleDataLoader } from "@/common/composeabe/SimpleDataLoader";
 import { useSimplePageContentLoader } from "@/common/composeabe/SimplePageContentLoader";
 import { useSwiperImagePreview } from "@/common/composeabe/SwiperImagePreview";
 import { useLoadQuerys } from "@/common/composeabe/LoadQuerys";
+import { navTo } from "@/components/utils/PageAction";
+import { onShareTimeline, onShareAppMessage } from "@dcloudio/uni-app";
+import { DataDateUtils } from "@imengyu/js-request-transform";
+import type { GetContentDetailItem } from "@/api/CommonContent";
 import commonParserStyle from "@/common/style/commonParserStyle";
 import SimplePageContentLoader from "@/common/components/SimplePageContentLoader.vue";
 import Parse from "@/components/display/parse/Parse.vue";
-import { computed } from "vue";
-import { useSimpleDataLoader } from "@/common/composeabe/SimpleDataLoader";
-import { navTo } from "@/components/utils/PageAction";
 import CommonContent, { GetContentListParams } from "@/api/CommonContent";
 import Box2LineImageRightShadow from "@/common/components/parts/Box2LineImageRightShadow.vue";
 import AppCofig from "@/common/config/AppCofig";

+ 1 - 15
src/pages/dig/index.vue

@@ -84,21 +84,7 @@ import Height from '@/components/layout/space/Height.vue';
 const authStore = useAuthStore();
 const collectStore = useCollectStore();
 const villageListLoader = useSimpleDataLoader(async () => await VillageApi.getClaimedVallageList(), true);
-const volunteerInfoLoader = useSimpleDataLoader(async () =>{
-  const res = await VillageApi.getVolunteerInfo();
-  const collectableModules = res.collectModule || [];
-  const collectableModulesMap = await VillageApi.getCollectModuleMap();
-  const needRemoveKeys = new Set<string>();
-  if (!authStore.isAdmin) {
-    for (const [key,id] of collectableModulesMap)
-      if (!collectableModules.includes(key))
-        needRemoveKeys.add(key);
-  }
-  for (const key of needRemoveKeys)
-    collectableModulesMap.delete(key);
-  collectStore.setCollectableModules(collectableModulesMap);
-  return res;
-}, true);
+const volunteerInfoLoader = useSimpleDataLoader(async () => await VillageApi.getVolunteerInfo(), true);
 const rankListLoader = useSimpleDataLoader(async () => await VillageApi.getVolunteerRanklist(), true);
 
 function goSubmitDigPage(item: VillageListItem) {

+ 134 - 0
src/pages/home/discover/details.vue

@@ -0,0 +1,134 @@
+<template>
+  <view class="d-flex flex-column bg-base pb-45">
+    <SimplePageContentLoader :loader="loader">
+      <template v-if="loader.content.value">
+        <view class="d-flex flex-col">
+          <swiper 
+            v-if="loader.content.value.images.length > 0"
+            circular 
+            :indicator-dots="true"
+            :autoplay="true"
+            :interval="3000"
+            :duration="1000"
+            class="height-500"
+          >
+            <swiper-item v-for="(item, key) in loader.content.value.images" :key="key">
+              <view class="item">
+                <image 
+                  :src="item" 
+                  class="w-100 height-500 radius-base"
+                  mode="aspectFill" 
+                  @click="onPreviewImage(key)"
+                />
+              </view>
+            </swiper-item>
+          </swiper>
+          <image 
+            v-else-if="loader.content.value.image"
+            class="w-100 radius-base"
+            :src="loader.content.value.image"
+            mode="widthFix"
+          />
+          <view class="d-flex flex-col p-3">
+            <view class="size-ll color-title-text">{{ loader.content.value.title }}</view>
+            <view class="d-flex flex-row mt-2">
+              <text v-if="loader.content.value.from" class="size-s color-text-content-second mr-2 ">来源:{{ loader.content.value.from }}</text>
+              <text class="size-s color-text-content-second">{{ DataDateUtils.formatDate(loader.content.value.publishAt, 'YYYY-MM-dd') }}</text>
+            </view>
+          </view>
+          <view class="p-3 radius-ll bg-light mt-3">
+            <Parse
+              v-if="loader.content.value.content"
+              :content="loader.content.value.content"
+              :tagStyle="commonParserStyle"
+            />
+            <text v-if="emptyContent">暂无简介</text>
+          </view>
+          
+          <!-- 推荐 -->
+          <view v-if="recommendListLoader.content.value?.length" class="d-flex flex-col p-3">
+            <text class="size-base text-bold mb-3">相关推荐</text>
+            <Box2LineImageRightShadow
+              class="w-100"
+              titleColor="title-text"
+              v-for="item in recommendListLoader.content.value"
+              :key="item.id"
+              :image="item.image || AppCofig.defaultImage"
+              :title="item.title"
+              :desc="item.desc"
+              :badge="item.badge"
+              :wideImage="true"
+              @click="goDetails(item.id)"
+            />
+          </view>
+        </view>
+      </template>
+    </SimplePageContentLoader>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { computed } from "vue";
+import { useSimpleDataLoader } from "@/common/composeabe/SimpleDataLoader";
+import { useSimplePageContentLoader } from "@/common/composeabe/SimplePageContentLoader";
+import { useSwiperImagePreview } from "@/common/composeabe/SwiperImagePreview";
+import { useLoadQuerys } from "@/common/composeabe/LoadQuerys";
+import { navTo } from "@/components/utils/PageAction";
+import { onShareTimeline, onShareAppMessage } from "@dcloudio/uni-app";
+import { DataDateUtils } from "@imengyu/js-request-transform";
+import commonParserStyle from "@/common/style/commonParserStyle";
+import SimplePageContentLoader from "@/common/components/SimplePageContentLoader.vue";
+import Parse from "@/components/display/parse/Parse.vue";
+import Box2LineImageRightShadow from "@/common/components/parts/Box2LineImageRightShadow.vue";
+import AppCofig from "@/common/config/AppCofig";
+import VillageInfoApi from "@/api/inhert/VillageInfoApi";
+
+const { onPreviewImage } = useSwiperImagePreview(() => loader.content.value?.images || []);
+const emptyContent = computed(() => (loader.content.value?.content || '').trim() === '');
+
+const loader = useSimplePageContentLoader(async () => {
+  const res = await VillageInfoApi.getInfoForDiscover(
+    querys.value.collectModelId,
+    querys.value.id,
+  );
+  uni.setNavigationBarTitle({ title: res.title });
+  return res;
+});
+const recommendListLoader = useSimpleDataLoader(async () => {
+  if (!querys.value.collectModelId)
+    return []
+  return (await VillageInfoApi.getListForDiscover(querys.value.collectModelId, 1, 5))
+    .list
+    .filter((p) => p.id !== querys.value.id);
+});
+
+const { querys } = useLoadQuerys({ 
+  id: 0,
+  collectModelId: 0,
+  collectModelInternalName: '',
+}, (t) => loader.loadData(t));
+
+function goDetails(id: number) {
+  navTo('details', { 
+    id, 
+    collectModelId: querys.value.collectModelId, 
+    collectModelInternalName: querys.value.collectModelInternalName 
+  });
+}
+
+function getPageShareData() {
+  if (!loader.content.value)
+    return { title: '文章详情', imageUrl: '' }
+  return {
+    title: loader.content.value.title,
+    imageUrl: loader.content.value.images[0],
+  }
+}
+
+onShareTimeline(() => {
+  return getPageShareData(); 
+})
+onShareAppMessage(() => {
+  return getPageShareData();
+})
+</script>

+ 40 - 21
src/pages/home/discover/index.vue

@@ -7,28 +7,29 @@
         GridItemPaddingHorizontal: 0,
       }">
         <Grid :borderGrid="false" :mainAxisCount="4">
-          <GridItem title="全部" icon="/static/images/icons/icon-all.png" touchable />
-          <GridItem title="历史文化" icon="/static/images/icons/icon-history.png" touchable />
-          <GridItem title="环境格局" icon="/static/images/icons/icon-envirounment.png" touchable />
-          <GridItem title="传统建筑" icon="/static/images/icons/icon-buliding.png" touchable />
-          <GridItem title="民俗文化" icon="/static/images/icons/icon-location.png" touchable />
-          <GridItem title="地道美食" icon="/static/images/icons/icon-foods.png" touchable />
-          <GridItem title="物产资源" icon="/static/images/icons/icon-resource.png" touchable />
-          <GridItem title="旅游线路" icon="/static/images/icons/icon-route.png" touchable />
+          <GridItem title="全部" icon="/static/images/icons/icon-all.png" touchable @click="goList('all', '全部')" />
+          <GridItem title="历史文化" icon="/static/images/icons/icon-history.png" touchable @click="goList('cultural', '历史文化')" />
+          <GridItem title="环境格局" icon="/static/images/icons/icon-envirounment.png" touchable @click="goList('environment', '环境格局')" />
+          <GridItem title="传统建筑" icon="/static/images/icons/icon-buliding.png" touchable @click="goList('building', '传统建筑')" />
+          <GridItem title="民俗文化" icon="/static/images/icons/icon-location.png" touchable @click="goList('folk_culture', '民俗文化')" />
+          <GridItem title="地道美食" icon="/static/images/icons/icon-foods.png" touchable @click="goList('food_product', '美食物产')" />
+          <GridItem title="物产资源" icon="/static/images/icons/icon-resource.png" touchable @click="goList('figure', '历史人物')" />
+          <GridItem title="旅游线路" icon="/static/images/icons/icon-route.png" touchable @click="goList('route', '旅游线路')" />
         </Grid>
       </ProvideVar>
     </Box>
-    <Box title="文章" icon="/static/images/home/icon-article.png">  
+    <Box title="最新推荐" icon="/static/images/home/icon-article.png">  
       <SimplePageContentLoader :loader="discoverLoader">
         <FlexCol :gap="25">
           <Touchable 
             v-for="(item, i) in discoverLoader.content.value"
             :key="i"
-            align="center"
             direction="column"
+            @click="goDetails(item)"
           > 
             <Image 
-              :src="item.thumbnail || item.image" 
+              :src="item.image" 
+              :failedImage="AppCofig.defaultImage"
               width="100%" 
               :height="350" 
               :radius="15" 
@@ -36,7 +37,7 @@
               round
             />
             <Height :height="20" />
-            <FlexCol>
+            <FlexCol >
               <Text :text="item.title" fontConfig="h4" />
               <Text :text="item.desc" fontConfig="subText" />
             </FlexCol>
@@ -51,7 +52,6 @@
 
 <script setup lang="ts">
 import { useSimpleDataLoader } from '@/common/composeabe/SimpleDataLoader';
-import { RandomUtils } from '@imengyu/imengyu-utils';
 import Box from '@/common/components/parts/Box.vue';
 import SimplePageContentLoader from '@/common/components/SimplePageContentLoader.vue';
 import Image from '@/components/basic/Image.vue';
@@ -63,17 +63,36 @@ import Height from '@/components/layout/space/Height.vue';
 import ProvideVar from '@/components/theme/ProvideVar.vue';
 import Grid from '@/components/layout/grid/Grid.vue';
 import GridItem from '@/components/layout/grid/GridItem.vue';
+import { navTo } from '@/components/utils/PageAction';
+import { useCollectStore } from '@/store/collect';
+import VillageInfoApi from '@/api/inhert/VillageInfoApi';
+import AppCofig from '@/common/config/AppCofig';
 
-function testImage() {
-  return 'https://mncdn.wenlvti.net/app_static/minnan/images/home/ImageTest' + RandomUtils.genRandom(1, 5) +'.jpg';
+const { getCollectModuleId } = useCollectStore();
+
+function goList(collectModelName: string, title: string) {
+  navTo('/pages/home/discover/list', {
+    collectModelInternalName: collectModelName,
+    collectModelId: collectModelName === 'all' ? undefined : getCollectModuleId(collectModelName),
+    title,
+  });
+}
+function goDetails(item: any) {
+  navTo('/pages/home/discover/details', {
+    id: item.id,
+    collectModelId: item.collectModuleId,
+    collectModelInternalName: item.collectModuleInternalName,
+  });
 }
 
 const discoverLoader = useSimpleDataLoader(async () => {
-  return new Array(26).fill(0).map((_, i) => ({ 
-    title: '张家大院·门楼' + i, 
-    desc: '多馆联展,沉浸式交互体验。多馆联展,沉浸式交互体验。',
-    image: testImage(),
-    thumbnail: testImage(),
-  }));
+  return (await VillageInfoApi.getListForDiscover(undefined, 1, 30)).list.map((item) => {
+    return {
+      ...item,
+      image: (item.thumbnail || item.image) as string,
+      desc: item.desc || '',
+      title: item.title,
+    }
+  })
 });
 </script>

+ 49 - 0
src/pages/home/discover/list.vue

@@ -0,0 +1,49 @@
+<template>
+  <CommonListPage
+    :title="querys.title || undefined"
+    :load="loadData"
+    itemType="article-common"
+    detailsPage="custom"
+    @goCustomDetails="goCustomDetails"
+  />
+</template>
+
+<script setup lang="ts">
+import CommonListPage from '../../article/common/CommonListPage.vue';
+import VillageInfoApi from '@/api/inhert/VillageInfoApi';
+import { useLoadQuerys } from '@/common/composeabe/LoadQuerys';
+import { navTo } from '@/components/utils/PageAction';
+
+const { querys } = useLoadQuerys({
+  collectModelId: 0,
+  collectModelInternalName: '',
+  title: '',
+}, (querys) => {
+  uni.setNavigationBarTitle({
+    title: querys.title || '发现',
+  });
+});
+
+function goCustomDetails(item: any) {
+  navTo('details', {
+    id: item.id,
+    collectModelId: querys.value.collectModelId ?? item.collectModuleId,
+    collectModelInternalName: querys.value.collectModelInternalName,
+  });
+}
+
+async function loadData(
+  page: number, 
+  pageSize: number,
+  searchText: string,
+  dropDownValues: number[]
+) {
+  if (page > 1)
+    return { list: [], total: 0 };
+  return (await VillageInfoApi.getListForDiscover(
+    querys.value.collectModelId > 0 ? 
+      querys.value.collectModelId : undefined
+    , page, pageSize, searchText
+  ));
+}
+</script>

+ 21 - 25
src/pages/home/index.vue

@@ -58,6 +58,7 @@
             justify="space-between"
             align="center"
             direction="row"
+            @click="goDiscoverDetails(item)"
           > 
             <FlexCol flex="1" :gap="20">
               <Text :text="item.title" fontConfig="h5" />
@@ -65,7 +66,8 @@
             </FlexCol>
             <Width :width="25" />
             <Image 
-              :src="item.thumbnail || item.image" 
+              :src="item.image" 
+              :failedImage="AppCofig.defaultImage"
               :width="120" 
               :height="120" 
               :radius="15" 
@@ -84,6 +86,7 @@
 <script setup lang="ts">
 import CommonContent from '@/api/CommonContent';
 import VillageApi from '@/api/inhert/VillageApi';
+import VillageInfoApi from '@/api/inhert/VillageInfoApi';
 import Box from '@/common/components/parts/Box.vue';
 import ImageSwiper from '@/common/components/parts/ImageSwiper.vue';
 import SimplePageContentLoader from '@/common/components/SimplePageContentLoader.vue';
@@ -101,10 +104,6 @@ import { navTo } from '@/components/utils/PageAction';
 import { RandomUtils } from '@imengyu/imengyu-utils';
 import { getCurrentInstance } from 'vue';
 
-function testImage() {
-  return 'https://mncdn.wenlvti.net/app_static/minnan/images/home/ImageTest' + RandomUtils.genRandom(1, 5) +'.jpg';
-}
-
 const instance = getCurrentInstance();
 const mapCtx = uni.createMapContext('prevMap', instance);
 const mapLoader = useSimpleDataLoader(async () => {
@@ -150,25 +149,22 @@ const recommendLoader = useSimpleDataLoader(async () => {
 });
 
 const discoverLoader = useSimpleDataLoader(async () => {
-  return [
-    { 
-      title: '茶艺传承作坊', 
-      desc: '多馆联展,沉浸式交互体验。多馆联展,沉浸式交互体验。',
-      image: testImage(),
-      thumbnail: testImage(),
-    },
-    { 
-      title: '茶艺传承作坊', 
-      desc: '多馆联展,沉浸式交互体验。多馆联展,沉浸式交互体验。', 
-      image: testImage(),
-      thumbnail: testImage(),
-    },
-    { 
-      title: '茶艺传承作坊',  
-      desc: '多馆联展,沉浸式交互体验。多馆联展,沉浸式交互体验。',
-      image: testImage(),
-      thumbnail: testImage(),
-    },
-  ]
+  return (await VillageInfoApi.getListForDiscover(undefined, 1, 10)).list.map((item) => {
+    return {
+      ...item,
+      image: (item.thumbnail || item.image) as string,
+      desc: item.desc || '',
+      title: item.title,
+    }
+  })
 });
+
+function goDiscoverDetails(item: any) {
+  navTo('/pages/home/discover/details', {
+    id: item.id,
+    collectModelId: item.collectModuleId,
+    collectModelInternalName: item.collectModuleInternalName,
+  });
+}
+
 </script>

+ 2 - 2
src/pages/home/village/details.vue

@@ -61,7 +61,7 @@
           </view>
         </view>
 
-        <view class="d-flex flex-col mt-3 mb-2">
+        <view class="d-flex flex-col mt-3 mb-3">
           <SubTitle title="地理位置" />
           <div class="d-flex flex-column radius-base bg-white mt-3">
             <map id="map"
@@ -88,7 +88,7 @@
           v-for="(tag, index) in tagsDataRecommend"
           :key="index"
         >
-          <SubTitle :title="tag.title" showMore @clickMore="tag.goList()" />
+          <SubTitle :title="tag.title" showMore @moreClicked="tag.goList()" />
           <SimplePageContentLoader :loader="tag.loader" >
             <view class="d-flex flex-col">
               <Box2LineLargeImageUserShadow 

+ 20 - 0
src/store/collect.ts

@@ -1,6 +1,7 @@
 import { computed, ref } from 'vue'
 import { defineStore } from 'pinia'
 import { useAuthStore } from './auth';
+import VillageApi from '@/api/inhert/VillageApi';
 
 const CollectableModulesNameMapping : Record<string, string> = {
   'overview': '村落概况',
@@ -40,8 +41,11 @@ export const useCollectStore = defineStore('collect', () => {
     return collectableModules.value.has(module);
   }
   function getCollectModuleId(module: string) {
+    console.log('getCollectModuleId', module, collectableModules.value);
+    
     if (collectableModules.value.has(CollectableModulesNameMapping[module]))
       return collectableModules.value.get(CollectableModulesNameMapping[module]);
+
     return collectableModules.value.get(module);
   }
   function getCollectModuleInternalNameById(id: number) {
@@ -56,6 +60,21 @@ export const useCollectStore = defineStore('collect', () => {
     }
     return '';
   }
+
+  async function loadCollectableModules() {
+    const res = await VillageApi.getVolunteerInfo();
+    const collectableModules = res.collectModule || [];
+    const collectableModulesMap = await VillageApi.getCollectModuleMap();
+    const needRemoveKeys = new Set<string>();
+    if (!authStore.isAdmin) {
+      for (const [key,id] of collectableModulesMap)
+        if (!collectableModules.includes(key))
+          needRemoveKeys.add(key);
+    }
+    for (const key of needRemoveKeys)
+      collectableModulesMap.delete(key);
+    setCollectableModules(collectableModulesMap);
+  }
   
   const isEmpty = computed(() => collectableModules.value.size === 0);
 
@@ -66,5 +85,6 @@ export const useCollectStore = defineStore('collect', () => {
     getCollectModuleInternalNameById,
     getCollectModuleId,
     canCollect,
+    loadCollectableModules,
   }
 })