Explorar el Código

🎨 优化首页以及其他列表页

快乐的梦鱼 hace 1 mes
padre
commit
53d4381773

+ 37 - 5
src/api/inhert/VillageInfoApi.ts

@@ -269,7 +269,7 @@ export class VillageInfoApi extends AppServerRequestModule<DataModel> {
     );
   }
   async getList<T extends DataModel = CommonInfoModel>(data: {
-    collectModuleId?: number|undefined,
+    collectModuleId?: number|string|undefined,
     subId?: number|undefined,
     subKey?: string|undefined,
     villageId?: number|undefined,
@@ -304,14 +304,20 @@ export class VillageInfoApi extends AppServerRequestModule<DataModel> {
       .catch(e => { throw e });
   }
 
-  async getListForDiscover(page: number, pageSize: number, keywords?: string) {
+  async getListForDiscover(options: {
+    page: number, 
+    pageSize: number, 
+    keywords?: string,
+    collectModuleId?: number|string|undefined,
+  }) {
     return (this.post<{
       total: number,
       data: any[],
     }>(`/village/collect/list`, '获取信息详情', {
-      page,
-      page_size: pageSize,
-      keywords,
+      page: options.page,
+      pageSize: options.pageSize,
+      keywords: options.keywords,
+      collect_module_id: options.collectModuleId,
       status: 4,
     }))
       .then(res => ({
@@ -320,6 +326,32 @@ export class VillageInfoApi extends AppServerRequestModule<DataModel> {
       }))
       .catch(e => { throw e });
   }
+  async getMiltListForDiscover(options: {
+    page: number, 
+    pageSize: number, 
+    keywords?: string,
+    collectModuleIds?: number[],
+  }) {
+
+    const results = await Promise.all(options.collectModuleIds?.map(async (id) => await this.getListForDiscover({
+      page: options.page,
+      pageSize: options.pageSize,
+      keywords: options.keywords,
+      collectModuleId: id,
+    })) || []);
+
+    const result: CommonInfoModel[] = [];
+    const total = results.reduce((acc, cur) => acc + cur.total, 0);
+    results.forEach((item) => {
+      result.push(...item.list);
+    });
+    return {
+      total,
+      list: result,
+    };
+  }
+
+
   async getInfoForDiscover(id: number) {
     return (await this.post(`/village/collect/info`, '通用获取信息详情', {
       id,

+ 5 - 0
src/api/light/LightVillageApi.ts

@@ -291,6 +291,11 @@ export class VillageMapItem extends DataModel<VillageMapItem> {
       latitude: { clientSide: 'number' },
       longitude: { clientSide: 'number' },
     };
+    this._afterSolveServer = (data) => {
+      this.lightValue = data.isLight === 1 ? (
+        Math.random()
+      ) : 0;
+    }
   }
 
 

+ 3 - 3
src/api/light/OfficialApi.ts

@@ -467,7 +467,7 @@ export class OfficialApi extends AppServerRequestModule<DataModel> {
       per_page: number,
       current_page: number,
       last_page: number,
-    }>('/village/ai/package', '获取个人升级套餐列表', {
+    }>('/village/ai/packageList', '获取AI对话套餐列表', {
       page,
       pageSize,
     });
@@ -479,13 +479,13 @@ export class OfficialApi extends AppServerRequestModule<DataModel> {
   }
 
   /**
-   * 
+   * 升级AI对话套餐
    */
   async payAiPackage(villageId: number, packageId = 1, payType: 1 | 2 | 3 = 1) {
     const res = await this.post<{
       order: KeyValue,
       pay: WxPayParams,
-    }>('/village/growth/staff', '升级村社管理', {
+    }>('/village/ai/package', '升级AI对话套餐', {
       village_id: villageId,
       package_id: packageId,
       pay_type: payType,

+ 4 - 4
src/common/components/form/RichTextEditor.vue

@@ -31,7 +31,7 @@ export interface SpEditorProps {
   disabled: boolean;
   readonly: boolean;
   enableAi: boolean;
-  extraProps: {
+  extraProps?: {
     get: () => {
       title: string,
       images: string[],
@@ -45,7 +45,7 @@ export interface SpEditorProps {
       uploadPath: string,
     },
   },
-  getExtraAiInfo: () => string[]|Promise<string[]>,
+  getExtraAiInfo?: () => string[]|Promise<string[]>,
   maskColor: string;
 }
 
@@ -85,7 +85,7 @@ async function edit() {
   editorOpened = true;
   const extraProps = props.extraProps.get();
   const storageInfo = props.extraProps.storageInfo?.();
-  const extraAiInfo = await props.getExtraAiInfo();
+  const extraAiInfo = await props.getExtraAiInfo?.();
 
   console.log('加载编辑器', props.modelValue, extraProps, extraAiInfo);
 
@@ -99,7 +99,7 @@ async function edit() {
     success: () => navTo('/pages/home/chat/dependent/post/editor', {  
       maxLength: props.maxLength,
       enableAi: props.enableAi,
-      extraAiInfo: extraAiInfo.join(','),
+      extraAiInfo: extraAiInfo?.join(','),
       uploadSubpath: storageInfo?.uploadPath,
       villageId: storageInfo?.villageId,
     }),

+ 8 - 4
src/common/components/parts/IndexCommonImageItem.vue

@@ -3,9 +3,9 @@
     :src="image"
     :title="title"
     :desc="desc"
-    :width="340"
+    :width="width"
     :imageWidth="340"
-    :imageHeight="260"
+    :imageHeight="fixHeight ? 270 : undefined"
     :imageRadius="15"
     :defaultImage="defaultImage"
     :titleProps="{ fontSize: 34, color: 'gray', lines: 2 }"
@@ -35,16 +35,20 @@ import Avatar from '@/components/display/Avatar.vue';
 import Icon from '@/components/basic/Icon.vue';
 import ImageBlock2 from '@/components/display/block/ImageBlock2.vue';
 
-const props = defineProps<{
+const props = withDefaults(defineProps<{
   image?: string;
   title?: string;
   desc?: string;
+  width?: number;
   userName?: string;
   userAvatar?: string;
   likes?: number;
   isLike?: boolean;
   defaultImage?: string;
-}>()
+  fixHeight?: boolean;
+}>(), {
+  width: 340,
+})
 
 const emit = defineEmits([ 'click' ]);
 

+ 16 - 17
src/common/components/parts/RoundTags.vue

@@ -1,25 +1,24 @@
 <template>
   <scroll-view scroll-x>
-    <FlexRow gap="gap.md">
-      <template
+    <FlexRow gap="gap.xl">
+      <Touchable 
         v-for="(tag, k) in tags"
         :key="k" 
+        @click="emit('update:active', tag)"
       >
-        <Touchable @click="emit('update:active', tag)">
-          <BackgroundBox 
-            v-if="tag"
-            :backgroundImage="tag === active ? 
-              'https://xy.wenlvti.net/app_static/images/village/TagActive.png' : 
-              'https://xy.wenlvti.net/app_static/images/village/TagNormal.png'"
-            :backgroundCutBorder="[10, 10, 10, 10]"
-            :backgroundCutBorderSize="[10, 10, 10, 10]"
-            :padding="[20, 30]"
-            :text="tag"
-          >
-            <Text :text="tag" fontConfig="contentText" :color="tag === active ? 'white' : 'text.content'" />
-          </BackgroundBox>
-        </Touchable>
-      </template>
+        <BackgroundBox 
+          v-if="tag"
+          :backgroundImage="tag === active ? 
+            'https://xy.wenlvti.net/app_static/images/village/TagActive.png' : 
+            'https://xy.wenlvti.net/app_static/images/village/TagNormal.png'"
+          :backgroundCutBorder="[10, 10, 10, 10]"
+          :backgroundCutBorderSize="[10, 10, 10, 10]"
+          :padding="[20, 30]"
+          :text="tag"
+        >
+          <Text :text="tag" fontConfig="contentText" :color="tag === active ? 'white' : 'text.content'" />
+        </BackgroundBox>
+      </Touchable>
     </FlexRow>
   </scroll-view>
 </template>

+ 21 - 0
src/common/data/CollectableModulesIdMap.ts

@@ -0,0 +1,21 @@
+export const CollectableModulesIdMap : Record<string, number> = {
+  'overview': 11,//村落概况
+  'cultural': 2,//历史文化
+  'story': 14,//掌故轶事
+  'spots': 13,//风景名胜
+  'speaker': 17,//口述者
+  'figure': 7,//历史人物
+  'element': 5,//环境要素
+  'environment': 6,//环境格局
+  'building': 16,//传统建筑
+  'distribution': 4,//建筑分布
+  'relic': 3,//文物古迹
+  'folk_culture': 8,//民俗文化
+  'ich': 10,//非遗
+  'food_product': 9,//美食物产
+  'route': 12,//旅游路线
+  'travel_guide': 15,//旅游导览
+  'collect': 18,//随手记
+  'glory': 23,//乡源荣光
+  'goods': 24,//乡源好物
+};

+ 7 - 4
src/components/feedback/BackToTop.vue

@@ -20,6 +20,7 @@
               ...innerStyle,
             }"
             shape="round"
+            @click="onClick"
           />
         </slot>
       </view>
@@ -82,11 +83,11 @@ export interface BackToTopInstance {
   /**
    * 滚动事件,用于自定义滚动容器,若要更换监听的滚动容器,将其绑定至scroll-view的滚动事件上即可:
    * ```
-   * <scroll-view @scroll="onScroll"></scroll-view>
+   * <scroll-view @scroll="(e) => onScroll(e.detail.scrollTop)"></scroll-view>
    * ```
    * @param e 
    */
-  onScroll: (e: any) => void;
+  onScroll: (e: number) => void;
 
   /**
    * 自定义滚动容器的滚动距离
@@ -95,6 +96,7 @@ export interface BackToTopInstance {
 }
 
 const emit = defineEmits([ 
+  'scroll',
   'click',
   'show',
   'hide'
@@ -130,6 +132,7 @@ watch(showState, (v) => {
 function onScroll(value: number) {
   customScrollTop.value = value;
   showState.value = value > props.showScrollValue;
+  emit('scroll', value);
 }
 function onClick() {
   if (props.customScrollValue) {
@@ -150,9 +153,9 @@ onPageScroll((e) => {
 
 defineExpose<BackToTopInstance>({
   customScrollTop,
-  onScroll(e) {
+  onScroll(scrollTop: number) {
     if (props.customScrollValue)
-      onScroll(e.detail.scrollTop);
+      onScroll(scrollTop);
   },
 })
 defineOptions({

+ 60 - 23
src/components/layout/masonry/MasonryGrid.vue

@@ -1,41 +1,78 @@
 <template>
-  <!-- <FlexView v-if="supportsGridMasonry" v-bind="props" :innerStyle="{
-    display: 'grid',
-    gridTemplateColumns: `repeat(${columnCount}, 1fr)`, /* 定义3列 */
-    gridTemplateRows: 'masonry', /* 行方向采用瀑布流布局 */
-    rowGap: resolveThemeSize(rowSpacing), /* 间距 */
-    columnGap: resolveThemeSize(columnSpacing), /* 间距 */
-    ...(innerStyle || {}),
-  }">
-    <slot />
-  </FlexView> -->
-  <FlexView v-bind="props" :innerStyle="{
-    display: 'block',
-    columnCount: columnCount, 
-    columnGap: resolveThemeSize(columnSpacing),
-    ...(innerStyle || {}),
-  }">
-    <slot />
-  </FlexView>
+  <view class="masonry-grid" :style="containerStyle">
+    <view
+      v-for="(column, colIdx) in columns"
+      :key="colIdx"
+      class="masonry-grid-column"
+      :style="columnStyle"
+    >
+      <view
+        v-for="item in column"
+        :key="itemKey ? item[itemKey] : item.__masonryIdx"
+        class="masonry-grid-item"
+        :style="itemStyle"
+      >
+        <slot :item="item" :index="item.__masonryIdx" />
+      </view>
+    </view>
+  </view>
 </template>
 
 <script setup lang="ts">
+import { computed } from 'vue';
 import { useTheme } from '@/components/theme/ThemeDefine';
-import FlexView, { type FlexProps } from '../FlexView.vue';
 
-export interface MasonryGridProps extends Omit<FlexProps, 'justify' | 'align' | 'direction'> {
+export interface MasonryGridProps {
+  items?: any[];
   columnCount?: number;
   columnSpacing?: number;
   rowSpacing?: number;
+  itemKey?: string;
 }
 
-// const supportsGridMasonry = CSS.supports('grid-template-rows', 'masonry');
-
 const props = withDefaults(defineProps<MasonryGridProps>(), {
+  items: () => [],
   columnCount: 2,
   columnSpacing: 10,
   rowSpacing: 10,
 });
 
 const { resolveThemeSize } = useTheme();
-</script>
+
+const columns = computed(() => {
+  const cols: any[][] = Array.from({ length: props.columnCount }, () => []);
+  const items = props.items || [];
+  for (let i = 0; i < items.length; i++) {
+    const colIdx = i % props.columnCount;
+    const item = { ...items[i], __masonryIdx: i };
+    cols[colIdx].push(item);
+  }
+  return cols;
+});
+
+const containerStyle = computed(() => ({
+  display: 'flex',
+  flexDirection: 'row',
+  gap: resolveThemeSize(props.columnSpacing),
+}));
+
+const columnStyle = computed(() => ({
+  flex: '1',
+  display: 'flex',
+  flexDirection: 'column',
+  gap: resolveThemeSize(props.rowSpacing),
+}));
+
+const itemStyle = computed(() => ({
+  breakInside: 'avoid',
+}));
+</script>
+
+<style lang="scss" scoped>
+.masonry-grid {
+  width: 100%;
+}
+.masonry-grid-column {
+  min-width: 0;
+}
+</style>

+ 2 - 0
src/components/theme/ThemeTools.ts

@@ -11,6 +11,7 @@ export function solveUrl(v: string) {
 const info = {
   screenWidth: globalThis.window.innerWidth,
   screenHeight: globalThis.window.innerHeight,
+  statusBarHeight: 0,
 }
 // #endif
 // #ifndef H5
@@ -19,6 +20,7 @@ const info = uni.getWindowInfo() ;
 
 export const screenWidth = info.screenWidth;
 export const screenHeight = info.screenHeight;
+export const statusBarHeight = info.statusBarHeight;
 
 export function rpx2px(rpx: number) {
   return rpx / 750 * screenWidth;

+ 2 - 0
src/pages.json

@@ -4,6 +4,7 @@
       "path": "pages/index",
       "style": {
         "navigationStyle": "custom",
+        "navigationBarTextStyle": "black",
         "navigationBarTitleText" : "村社文化资源挖掘平台",
         "enablePullDownRefresh" : false
       }
@@ -60,6 +61,7 @@
       "style": {
         "navigationBarTitleText": "亮乡源地图",
         "navigationStyle": "custom",
+        "navigationBarTextStyle": "black",
         "enablePullDownRefresh": false
       }
     },

+ 1 - 1
src/pages/dig/admin/preview.vue

@@ -75,7 +75,7 @@ import IntroBlock from '@/pages/article/common/IntroBlock.vue';
 import Parse from '@/components/display/parse/Parse.vue';
 import Text from '@/components/basic/Text.vue';
 import ImagesUrls from '@/common/config/ImagesUrls';
-import { CollectableModulesIdMap } from '../forms/forms';
+import { CollectableModulesIdMap } from "@/common/data/CollectableModulesIdMap";
 
 const { querys } = useLoadQuerys({ 
   villageId: 0,  

+ 2 - 1
src/pages/dig/components/CollectModuleList.vue

@@ -28,13 +28,14 @@ import { useTaskEntryForm } from '../forms/composeable/TaskEntryForm';
 import { useAuthStore } from '@/store/auth';
 import { type TaskMenuDefGoForm, type TaskMenuDefItem } from '../forms/tasks';
 import { alert } from '@/components/utils/DialogAction';
-import { CollectableModulesIdMap, getVillageInfoForm } from '../forms/forms';
+import { CollectableModulesIdMap } from "@/common/data/CollectableModulesIdMap";
 import { navTo } from '@/components/utils/PageAction';
 import FlexCol from '@/components/layout/FlexCol.vue';
 import Image from '@/components/basic/Image.vue';
 import TaskList from '../components/TaskList.vue';
 import VillageApi from '@/api/inhert/VillageApi';
 import { waitTimeOut } from '@imengyu/imengyu-utils';
+import { getVillageInfoForm } from '../forms/forms';
 
 const { goForm, goPreviewForm } = useTaskEntryForm();
 const authStore = useAuthStore();

+ 1 - 1
src/pages/dig/details.vue

@@ -81,7 +81,7 @@ import { useUserTools } from '@/common/composeabe/UserTools';
 import { useSimpleDataLoader } from '@/components/composeabe/loader/SimpleDataLoader';
 import { onMounted, ref } from 'vue';
 import { navTo } from '@/components/utils/PageAction';
-import { CollectableModulesIdMap } from './forms/forms';
+import { CollectableModulesIdMap } from "@/common/data/CollectableModulesIdMap";
 import BannerApi from '@/api/system/Banner';
 import Icon from '@/components/basic/Icon.vue';
 import Text from '@/components/basic/Text.vue';

+ 2 - 1
src/pages/dig/forms/common.vue

@@ -59,7 +59,8 @@
 <script setup lang="ts">
 import { nextTick, ref, watch, type Ref } from 'vue';
 import { useLoadQuerys } from '@/components/composeabe/LoadQuerys';
-import { CollectableModulesIdMap, getVillageInfoForm, getVillageInfoFormIds, type SingleForm } from './forms';
+import { getVillageInfoForm, getVillageInfoFormIds, type SingleForm } from './forms';
+import { CollectableModulesIdMap } from "@/common/data/CollectableModulesIdMap";
 import { showError } from '@/common/composeabe/ErrorDisplay';
 import { backAndCallOnPageBack, navTo } from '@/components/utils/PageAction';
 import { toast } from '@/components/utils/DialogAction';

+ 2 - 1
src/pages/dig/forms/data/cultural.ts

@@ -1,5 +1,6 @@
 import VillageInfoApi, { CommonInfoModel, VillageBulidingInfo } from "@/api/inhert/VillageInfoApi";
-import { CollectableModulesIdMap, type GroupForm, type SingleForm } from "../forms";
+import { type GroupForm, type SingleForm } from "../forms";
+import { CollectableModulesIdMap } from "@/common/data/CollectableModulesIdMap";
 import type { IDynamicFormItemCallback, IDynamicFormItemCallbackAdditionalProps } from "@/components/dynamic";
 import type { PickerIdFieldProps } from "@/components/dynamic/wrappers/PickerIdField";
 import type { FieldProps } from "@/components/form/Field.vue";

+ 0 - 22
src/pages/dig/forms/forms.ts

@@ -29,28 +29,6 @@ export type SingleForm = [NewDataModel, (formRef: Ref<IDynamicFormRef>) => IDyna
 }]
 export type GroupForm = Record<number, SingleForm>;
 
-export const CollectableModulesIdMap : Record<string, number> = {
-  'overview': 11,
-  'cultural': 2,
-  'story': 14,
-  'spots': 13,
-  'speaker': 17,
-  'figure': 7,
-  'element': 5,
-  'environment': 6,
-  'building': 16,
-  'distribution': 4,
-  'relic': 3,
-  'folk_culture': 8,
-  'food_product': 9,
-  'route': 12,
-  'travel_guide': 15,
-  'ich': 10,
-  'collect': 18,
-  'glory': 23,
-  'goods': 24,
-};
-
 /**
  * 采集模块ID到表单定义的映射
  */

+ 2 - 1
src/pages/dig/forms/list-ordinary.vue

@@ -98,7 +98,8 @@ import { useAuthStore } from '@/store/auth';
 import { useUserTools } from '@/common/composeabe/UserTools';
 import { navTo } from '@/components/utils/PageAction';
 import { confirm } from '@/components/utils/DialogAction';
-import { CollectableModulesIdMap, getVillageInfoForm } from './forms';
+import { getVillageInfoForm } from './forms';
+import { CollectableModulesIdMap } from "@/common/data/CollectableModulesIdMap";
 import { type TaskMenuDefGoForm } from './tasks';
 import SimplePageListLoader from '@/components/loader/SimplePageListLoader.vue';
 import VillageInfoApi from '@/api/inhert/VillageInfoApi';

+ 72 - 25
src/pages/home/components/LightMap.vue

@@ -23,6 +23,7 @@
       @markertap="onMarkerTap" 
     />
 
+    <!-- 状态显示 -->
     <FlexCol v-if="mapLoader.error.value" 
       gap="gap.md" 
       position="absolute" 
@@ -43,6 +44,7 @@
       <ActivityIndicator :size="64" />
     </FlexCol>
 
+    <!-- 导航栏 -->
     <FlexCol 
       position="absolute" 
       :inset="{ l: 0, b: 0,r: 0 }"
@@ -52,12 +54,7 @@
       <FlexRow align="center" justify="space-between">
         <view v-if="small"></view>
         <template v-else>
-          <FrameButton
-            :icon="searchKeyword ? 'close' : 'search'" 
-            :padding="30"
-            @click="showSearch" 
-          />
-          <BoxMid direction="row" gap="gap.md" align="center" :padding="15">
+          <BoxMid direction="row" wrap gap="gap.md" align="center" :padding="15">
             <NButton
               :text="city + ' ▼'"
               color="transparent"
@@ -77,16 +74,64 @@
               @update:modelValue="onSelectedRegion"
               :columns="regionLoader.content.value" 
             />
+            <SimpleDropDownPicker
+              class="light-map-region-picker" 
+              backgroundColor="transparent"
+              v-model:modelValue="lightState" 
+              :columns="[
+                {
+                  id: 1,
+                  name: '点亮状态',
+                },
+                {
+                  id: 2,
+                  name: '已点亮',
+                },
+                {
+                  id: 3,
+                  name: '未点亮',
+                },
+              ]" 
+            />
+            
+            <FrameButton
+              :icon="searchKeyword ? 'close' : 'search'" 
+              size="small"
+              @click="showSearch" 
+            />
+            <FrameButton
+              icon="navigation" 
+              size="small"
+              @click="emit('getCurrentLonlat')" 
+            />
+            <Width />
           </BoxMid>
         </template>
-        <FrameButton
-          icon="navigation" 
-          :padding="30"
-          @click="emit('getCurrentLonlat')" 
-        />
       </FlexRow>
       <StatusBarSpace v-if="full" />
     </FlexCol>
+
+    <!-- 上下渐变遮罩 -->
+    <BackgroundBox 
+      v-if="full"
+      position="fixed" 
+      :inset="{ l: 0, t: 0, r: 0 }"
+      :height="500"
+      :zIndex="19"
+      color2="transparent"
+      color1="white"
+    />
+    <BackgroundBox 
+      v-if="full"
+      position="fixed" 
+      :inset="{ l: 0, b: 0, r: 0 }"
+      :zIndex="19"
+      :height="300"
+      color1="transparent"
+      color2="white"
+    />
+
+    <!-- 搜索村社 -->
     <Dialog 
       v-model:show="searchDialogShow" 
       title="搜索村社"
@@ -102,7 +147,7 @@
 </template>
 
 <script setup lang="ts">
-import { getCurrentInstance, onMounted, ref, watch } from 'vue';
+import { computed, getCurrentInstance, onMounted, ref, watch } from 'vue';
 import { assertNotNull, waitTimeOut } from '@imengyu/imengyu-utils';
 import { toast } from '@/components/dialog/CommonRoot';
 import { useSimpleDataLoader } from '@/components/composeabe/loader/SimpleDataLoader';
@@ -124,6 +169,8 @@ import { hideLoading, loading } from '@/components/utils/DialogAction';
 import { showError } from '@/common/composeabe/ErrorDisplay';
 import FrameButton from '@/common/components/FrameButton.vue';
 import BoxMid from '@/common/components/box/BoxMid.vue';
+import Width from '@/components/layout/space/Width.vue';
+import BackgroundBox from '@/components/display/block/BackgroundBox.vue';
 
 const instance = getCurrentInstance();
 const mapCtx = uni.createMapContext('prevMap', instance);
@@ -158,13 +205,14 @@ const props = withDefaults(defineProps<{
   isLightMode?: boolean;
   small?: boolean;
   full?: boolean;
-  showLighted?: boolean;
-  showUnLighted?: boolean;
 }>(), {
-  showLighted: true,
-  showUnLighted: true,
 });
 
+
+const lightState = ref(1);
+const showLighted = computed(() => lightState.value === 2 || lightState.value === 1);
+const showUnLighted = computed(() => lightState.value === 3 || lightState.value === 1);
+
 const regionLoader = useSimpleDataLoader(async () => {
   if (!props.cityCode)
     return [];
@@ -194,9 +242,9 @@ const mapLoader = useSimpleDataLoader<MapMarker[]>(async () => {
   hasResItems.value = res.length > 0;
   const list = res
     .filter(p => {
-      if (p.isLight && props.showLighted)
+      if (p.isLight && showLighted.value)
         return true;
-      if (!p.isLight && props.showUnLighted)
+      if (!p.isLight && showUnLighted.value)
         return true;
       return false;
     })
@@ -233,13 +281,13 @@ const mapLoader = useSimpleDataLoader<MapMarker[]>(async () => {
       } else {
         maker.iconPath = `https://mncdn.wenlvti.net/app_static/xiangyuan/images/map/Light1.png`;
       }
-      const size = Math.floor(35 +(p.lightValue) * 10);
+      const size = Math.floor(30 +(p.lightValue) * 40);
       maker.width = size;
-      maker.height = size;
+      maker.height = size * 1.05;
       
     } else {
-      maker.width = 35;
-      maker.height = 35;
+      maker.width = 25;
+      maker.height = 27;
       maker.iconPath = `https://mncdn.wenlvti.net/app_static/xiangyuan/images/map/LightUnLight.png`;
     }
 
@@ -391,9 +439,8 @@ watch(() => props.cityCode, async () => {
   await regionLoader.reload();
   loadFirstRegion();
 }, { immediate: true });
-watch(() => [ props.showLighted, props.showUnLighted ], async () => {
-  await mapLoader.reload();
-}, { deep: true });
+watch(showLighted, () => mapLoader.reload());
+watch(showUnLighted, () => mapLoader.reload());
 
 onMounted(async () => {
   mapCtx.initMarkerCluster({

+ 1 - 1
src/pages/home/discover/details.vue

@@ -181,7 +181,7 @@ const loader = useSimpleDataLoader(async () => {
   return res;
 }, false);
 const recommendListLoader = useSimpleDataLoader(async () => {
-  return (await VillageInfoApi.getListForDiscover(1, 5))
+  return (await VillageInfoApi.getListForDiscover({ page: 1, pageSize: 5 }))
     .list
     .filter((p) => p.id !== querys.value.id);
 });

+ 8 - 12
src/pages/home/discover/index.vue

@@ -77,12 +77,8 @@
 
     <HomeTitle title="最新推荐" />
     <SimplePageListLoader :loader="discoverLoader">
-      <MasonryGrid>
-        <MasonryGridItem
-          v-for="(item, i) in discoverLoader.list.value"
-          :key="i"
-          :width="340"
-        >
+      <MasonryGrid :items="discoverLoader.list.value" itemKey="id">
+        <template #default="{ item }">
           <IndexCommonImageItem
             :image="item.image"
             :title="item.title"
@@ -92,7 +88,7 @@
             :isLike="false"
             @click="handleGoRecommendDetails(item)"
           />
-        </MasonryGridItem>
+        </template>
       </MasonryGrid>
     </SimplePageListLoader>
     
@@ -115,7 +111,6 @@ import HomeTitle from '@/common/components/parts/HomeTitle.vue';
 import ImageSwiper from '@/common/components/parts/ImageSwiper.vue';
 import SimplePageListLoader from '@/components/loader/SimplePageListLoader.vue';
 import MasonryGrid from '@/components/layout/masonry/MasonryGrid.vue';
-import MasonryGridItem from '@/components/layout/masonry/MasonryGridItem.vue';
 import VillageInfoApi, { type CommonInfoModel } from '@/api/inhert/VillageInfoApi';
 import IndexCommonImageItem from '@/common/components/parts/IndexCommonImageItem.vue';
 import LightVillageApi, { type VillageListItem } from '@/api/light/LightVillageApi';
@@ -128,6 +123,7 @@ import BoxMid from '@/common/components/box/BoxMid.vue';
 import RankVolunteerItem from '../village/rank/components/RankVolunteerItem.vue';
 import RankVillageItem from '../village/rank/components/RankVillageItem.vue';
 import SimplePageContentLoader from '@/components/loader/SimplePageContentLoader.vue';
+import { CollectableModulesIdMap } from '@/common/data/CollectableModulesIdMap';
 
 const emit = defineEmits([ 'goVillage' ]);
 
@@ -143,8 +139,8 @@ const bannerLoader = useSimpleDataLoader(async () => {
   }));
 });
 const discoverLoader = useSimplePageListLoader(15, async (page, pageSize) => {
-  return await VillageInfoApi.getListForDiscover(page, pageSize);
-});
+  return await VillageInfoApi.getListForDiscover({ page, pageSize });
+}); 
 const villageRankListLoader = useSimpleDataLoader(async () => {
   const res = await LightVillageApi.getVillageRankList({ num: 10 });
   return res.map((item, i) => ({
@@ -167,8 +163,8 @@ const villageUserRankListLoader = useSimpleDataLoader(async () => {
   return res
 });
 const goodsLoader = useSimpleDataLoader(async () => {
-  return (await VillageInfoApi.getList({
-    collectModuleId: 24,
+  return (await VillageInfoApi.getListForDiscover({
+    collectModuleId: CollectableModulesIdMap['goods'],
     page: 1, pageSize: 10,
   })).list;
 });

+ 202 - 20
src/pages/home/index.vue

@@ -26,6 +26,18 @@
           <Width :width="150" />
         </FlexRow>
       </FlexCol>
+      <SimpleTransition name="fade" :show="currentScrollTop > 1000" :duration="300">
+        <template #show="{ classNames }">
+          <view
+            :class="['top-nav-fade ',...classNames]"
+            :style="{ backgroundColor: themeContext.resolveThemeColor('background.primary') }"
+            :left="0" :top="0" :right="0" :zIndex="200"
+           >
+            <StatusBarSpace />
+          </view>
+        </template>
+      </SimpleTransition>
+
       <Height height="150px" />
       <FlexCol :gap="20" align="center">
         <FlexRow justify="space-between" align="center" width="100%">
@@ -130,24 +142,98 @@
         scoreSuffix="积分"
         @goDetails="navTo('/pages/home/village/volunteer/detail', { id: $event.id })"
       />
+    </FlexCol>
 
-      <HomeTitle title="精选记忆">
-        <template #right>
-          <FrameButton
-            size="small"
-            icon="https://xy.wenlvti.net/app_static/images/village/IconLargeHistory.png"
-            text="我也来写"
-            @click="handleGoPublish()"
-          />
-        </template>
-      </HomeTitle>
+    <view class="recommmend-header" :style="stickyStyle">
+      <FlexCol :padding="[0,30]">
+        <HomeTitle title="精选记忆">
+          <template #right>
+            <FrameButton
+              size="small"
+              icon="https://xy.wenlvti.net/app_static/images/village/IconLargeHistory.png"
+              text="我也来写"
+              @click="handleGoPublish()"
+            />
+          </template>
+        </HomeTitle>
+      </FlexCol>
+      <Tabs 
+        v-model:currentIndex="recommendTabCurrent"
+        :tabs="recommendTabs"
+      />
+    </view>
 
-      <SimplePageListLoader :loader="recommendLoader">
-        <FlexRow justify="space-between" wrap gap="gap.md">
+    <FlexCol padding="padding.md">
+      <SimplePageListLoader v-if="recommendTabCurrent === 0" :loader="recommendLoader">
+        <MasonryGrid :items="recommendLoader.list.value" itemKey="id">
+          <template #default="{ item }">
+            <IndexCommonImageItem
+              :width="354"
+              :image="item.image"
+              :title="item.title"
+              :desc="item.content ?? ''"
+              :userName="item.villageVolunteerName ?? ''"
+              :userAvatar="item.villageVolunteerAvatar"
+              :likes="item.likeNum"
+              :isLike="item.likeType === 1"
+              @click="handleGoRecommendDetails(item)"
+            />
+          </template>
+        </MasonryGrid>
+      </SimplePageListLoader>
+      <SimplePageListLoader v-else-if="recommendTabCurrent === 1" :loader="goodsLoader">
+        <FlexRow wrap justify="space-around" gap="gap.sm">
+          <BoxMid
+            v-for="good in goodsLoader.list.value" :key="good.id"
+            :innerStyle="{
+              width: 'calc(50% - 70rpx)',
+            }"
+          >
+            <Touchable direction="column" gap="gap.lg" @click="handleGoRecommendDetails(good)">
+              <Image 
+                :src="good.image"
+                :radius="20"
+                width="100%"
+                height="300rpx"
+                mode="aspectFill"
+              />
+              <Text :text="good.title" fontConfig="titleText" />
+              <FlexRow align="center" gap="gap.md">
+                <Avatar 
+                  :src="good.villageVolunteerAvatar"
+                  :size="40"
+                  defaultAvatar="https://xy.wenlvti.net/app_static/images/mine/DefaultAvatar.png"
+                />
+                <Text :text="`发布人: ${good.villageVolunteerName}`" fontConfig="contentText" />
+              </FlexRow>
+            </Touchable>
+          </BoxMid>
+          <FlexCol v-if="goodsLoader.list.value && goodsLoader.list.value.length % 2 !== 0" width="calc(50%)" />
+        </FlexRow>
+      </SimplePageListLoader>
+      <SimplePageListLoader v-else-if="recommendTabCurrent === 2" :loader="travelLoader">
+        <FlexRow wrap justify="space-between" gap="gap.sm">
           <IndexCommonImageItem
-            v-for="(item, i) in recommendLoader.list.value"
-            :key="i"
-            :width="340"
+            v-for="item in travelLoader.list.value" :key="item.id"
+            :width="354"
+            fixHeight
+            :image="item.image"
+            :title="item.title"
+            :desc="item.content ?? ''"
+            :userName="item.villageVolunteerName ?? ''"
+            :userAvatar="item.villageVolunteerAvatar"
+            :likes="item.likeNum"
+            :isLike="item.likeType === 1"
+            @click="handleGoRecommendDetails(item)"
+          />
+        </FlexRow>
+      </SimplePageListLoader>
+      <SimplePageListLoader v-else-if="recommendTabCurrent === 3" :loader="culturalLoader">
+        <FlexRow wrap justify="space-between" gap="gap.sm">
+          <IndexCommonImageItem
+            v-for="item in culturalLoader.list.value" :key="item.id"
+            :width="354"
+            fixHeight
             :image="item.image"
             :title="item.title"
             :desc="item.content ?? ''"
@@ -207,6 +293,7 @@ import { waitTimeOut } from '@imengyu/imengyu-utils';
 import { toast, loading, hideLoading, confirm } from '@/components/utils/DialogAction';
 import { navTo } from '@/components/utils/PageAction';
 import { injectAppConfiguration } from '@/api/system/useAppConfiguration';
+import { CollectableModulesIdMap } from '@/common/data/CollectableModulesIdMap';
 import Image from '@/components/basic/Image.vue';
 import FlexCol from '@/components/layout/FlexCol.vue';
 import FlexRow from '@/components/layout/FlexRow.vue';
@@ -235,7 +322,21 @@ import FrameButton from '@/common/components/FrameButton.vue';
 import type { RegionItem } from '@/api/map/RegionApi';
 import BubbleTip from '@/components/feedback/BubbleTip.vue';
 import BannerApi from '@/api/system/Banner';
+import Tabs from '@/components/nav/Tabs.vue';
+import BoxMid from '@/common/components/box/BoxMid.vue';
+import Touchable from '@/components/feedback/Touchable.vue';
+import Text from '@/components/basic/Text.vue';
+import Avatar from '@/components/display/Avatar.vue';
+import MasonryGrid from '@/components/layout/masonry/MasonryGrid.vue';
+import { DynamicColor, statusBarHeight } from '@/components/theme/ThemeTools';
+import SimpleTransition from '@/components/anim/SimpleTransition.vue';
 
+const props = defineProps({
+  currentScrollTop: {
+    type: Number,
+    default: 0,
+  },
+});
 const emit = defineEmits(['goVillage','goDig']);
 const authStore = useAuthStore();
 const villageStore = useVillageStore();
@@ -293,10 +394,6 @@ const villageUserRankListLoader = useSimpleDataLoader(async () => {
   return res
 });
 
-const recommendLoader = useSimplePageListLoader(20, async (page, pageSize) => {
-  return await VillageInfoApi.getListForDiscover(page, pageSize);
-}, true);
-
 
 const showLightTipTimeout = new MemoryTimeOut('LightTip', 1000 * 3600 * 2);//2h
 const showLightTip = ref(false);
@@ -309,6 +406,70 @@ function handleCloseLightTip() {
   showLightTipTimeout.recordTime();
 }
 
+const recommendTabCurrent = ref(0);
+const recommendTabs = ref([
+  {
+    text: '推荐',
+    value: 0,
+  },
+  {
+    text: '好物',
+    value: 1,
+  },
+  {
+    text: '乡游',
+    value: 2,
+  },
+  {
+    text: '文化',
+    value: 3,
+  },
+]);
+const recommendLoader = useSimplePageListLoader(20, async (page, pageSize) => {
+  return await VillageInfoApi.getListForDiscover({ page, pageSize });
+}, true);
+const goodsLoader = useSimplePageListLoader(20, async (page, pageSize) => {
+  return await VillageInfoApi.getListForDiscover({
+    page,
+    pageSize, 
+    collectModuleId: CollectableModulesIdMap['goods'],
+  });
+}, false);
+const travelLoader = useSimplePageListLoader(20, async (page, pageSize) => {
+  return await VillageInfoApi.getMiltListForDiscover({
+    page,
+    pageSize, 
+    collectModuleIds: [
+      CollectableModulesIdMap['travel_guide'],
+      CollectableModulesIdMap['route'],
+      CollectableModulesIdMap['environment'],
+      CollectableModulesIdMap['relic'],
+      CollectableModulesIdMap['spots'],
+
+    ],
+  });
+}, true);
+const culturalLoader = useSimplePageListLoader(20, async (page, pageSize) => {
+  return await VillageInfoApi.getMiltListForDiscover({
+    page,
+    pageSize, 
+    collectModuleIds: [
+      CollectableModulesIdMap['cultural'],
+      CollectableModulesIdMap['folk_culture'],
+      CollectableModulesIdMap['story'],
+    ],
+  });
+}, true);
+
+const stickyStyle = themeContext.useThemeStyle({
+  position: 'sticky',
+  top: (statusBarHeight - 10) + 'px',
+  left: 0,
+  right: 0,
+  zIndex: '100',
+  backgroundColor: DynamicColor('background.primary'),
+});
+
 function handleGoRecommendDetails(item: CommonInfoModel) {
   navTo(`/pages/home/discover/details`, { id: item.id });
 }
@@ -431,4 +592,25 @@ defineExpose({
     }
   }
 })
-</script>
+</script>
+
+<style lang="scss">
+.top-nav-fade {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  z-index: 200;
+  transition: opacity 0.3s ease-in-out;
+
+  &.fade-enter-active,
+  &.fade-leave-active {
+    opacity: 1;
+  }
+
+  &.fade-enter-from,
+  &.fade-leave-to {
+    opacity: 0;
+  }
+}
+</style>

+ 1 - 15
src/pages/home/light/map.vue

@@ -7,25 +7,19 @@
         <BoxMid 
           :innerStyle="{
             width: '670rpx',
+            marginTop: '-44px',
           }"
           direction="column"
           align="center"
           gap="gap.md"
         > 
           <FlexCol center>
-            <Image src="https://xy.wenlvti.net/app_static/images/home/BadgeNew.png" :width="220" mode="widthFix" />
             <Text text="亮乡源地图" fontConfig="primaryTitle" />
           </FlexCol>
           <FlexCol gap="gap.lg">
             <Text text="这里可看到您附近的村落,村落乡源光贡献越多,村落越亮哦!快去为自己家乡建设做贡献吧!" textAlign="center" fontConfig="contentText" :fontSize="26" />
             <PrimaryButton text="去点亮我的家乡" @click="navTo('/pages/home/light/create-village')" />
           </FlexCol>
-          <CommonDivider /> 
-          <FlexRow gap="gap.lg" align="center">
-            <Text text="地图显示" textAlign="center" fontConfig="contentText" :fontSize="26" />
-            <CheckBox v-model="showLighted" text="已点亮" />
-            <CheckBox v-model="showUnLighted" text="未点亮" />
-          </FlexRow>
         </BoxMid>
       </FlexRow>
     </FlexCol>
@@ -41,8 +35,6 @@
       :cityCode="currentCityCode"
       :district="currentDistrict"
       :lonlat="currentLocation.currentLonlat.value"
-      :showLighted="showLighted"
-      :showUnLighted="showUnLighted"
       @lightVillage="handleLightVillage"
       @createVillage="handleCreateVillage"
       @getCurrentLonlat="currentLocation.getCurrentExactLocation"
@@ -74,15 +66,12 @@ import StatusBarSpace from '@/components/layout/space/StatusBarSpace.vue';
 import NavBar from '@/components/nav/NavBar.vue';
 import FlexRow from '@/components/layout/FlexRow.vue';
 import Text from '@/components/basic/Text.vue';
-import Image from '@/components/basic/Image.vue';
 import BoxMid from '@/common/components/box/BoxMid.vue';
 import JoinDialog from '../village/dialogs/JoinDialog.vue';
 import CitySelect from '../components/CitySelect.vue';
 import Popup from '@/components/dialog/Popup.vue';
 import CommonRoot from '@/components/dialog/CommonRoot.vue';
 import PrimaryButton from '@/common/components/PrimaryButton.vue';
-import CheckBox from '@/components/form/CheckBox.vue';
-import CommonDivider from '@/common/components/CommonDivider.vue';
 
 const { querys } = useLoadQuerys({
   latitude: AppCofig.defaultLonLat[1],
@@ -97,9 +86,6 @@ const { querys } = useLoadQuerys({
   currentDistrict.value = querys.district;
 });
 
-const showLighted = ref(true);
-const showUnLighted = ref(true);
-
 const currentLocation = useGetCurrentLocation({
   onCityChanged: (city, code, district) => {
     currentCity.value = city;

+ 3 - 3
src/pages/home/village/goods/index.vue

@@ -59,6 +59,7 @@ import FlexRow from '@/components/layout/FlexRow.vue';
 import SimplePageListLoader from '@/components/loader/SimplePageListLoader.vue';
 import VillageInfoApi from '@/api/inhert/VillageInfoApi';
 import CommonTopBanner from '@/common/components/CommonTopBanner.vue';
+import { CollectableModulesIdMap } from '@/common/data/CollectableModulesIdMap';
 
 const { querys } = useLoadQuerys({
   villageId: 0,
@@ -66,11 +67,10 @@ const { querys } = useLoadQuerys({
 
 const { getIsVolunteer, getCanCollect, volunteerInfo } = useUserTools();
 const canCollect = ref(false);
-const CollectModuleId = 24; // 乡源好物模块id
 
 const goodsLoader = useSimplePageListLoader(20, async (page, pageSize, params) => {
   return await VillageInfoApi.getList({
-    collectModuleId: CollectModuleId,
+    collectModuleId: CollectableModulesIdMap['goods'],
     villageId: querys.value.villageId,
     page, pageSize,
     keywords: searchValue.value,
@@ -105,7 +105,7 @@ const handlePublish = () => {
     id: -1,
     villageId: querys.value.villageId,  
     villageVolunteerId: volunteerInfo.value?.id || 0,  
-    collectModuleId: CollectModuleId,  
+    collectModuleId: CollectableModulesIdMap['goods'],  
     subTitle: '乡源好物',
   });
 }

+ 3 - 0
src/pages/home/village/index.vue

@@ -65,6 +65,7 @@
           </FlexRow>
         </FlexCol>
         <Around
+          v-if="delayShowAround"
           @selectVillage="onSelectVillage" 
         />
       </FlexCol>
@@ -127,6 +128,7 @@ import { useRequireLogin } from '@/common/composeabe/RequireLogin';
 const topTab = ref<'village' | 'around'>('village');
 const tab = ref('card');
 const delayShow = ref(false);
+const delayShowAround = ref(false);
 const villageStore = useVillageStore();
 const { isFollowed } = useFollow();
 const { requireLogin } = useRequireLogin();
@@ -190,6 +192,7 @@ onMounted(async () => {
   delayShow.value = true;
   await waitTimeOut(1000);
   handleTestGoTree();
+  delayShowAround.value = true;
 });
 
 function handleTestGoTree() {

+ 4 - 9
src/pages/home/village/recommed/around.vue

@@ -5,19 +5,15 @@
       <Width :width="60" />
     </FlexRow>
     <SimplePageListLoader :loader="recommendLoader">
-      <MasonryGrid>
-        <MasonryGridItem
-          v-for="(item, i) in recommendLoader.list.value"
-          :key="i"
-          :width="340"
-        >
-          <IndexCommonImageItem 
+      <MasonryGrid :items="recommendLoader.list.value" itemKey="id">
+        <template #default="{ item }">
+          <IndexCommonImageItem
             :title="item.name"
             :image="item.image"
             defaultImage="https://xy.wenlvti.net/app_static/images/village/PlaceholderVillage.jpg"
             @click="emit('selectVillage', item)"
           />
-        </MasonryGridItem>
+        </template>
       </MasonryGrid>
     </SimplePageListLoader>
   </FlexCol>
@@ -28,7 +24,6 @@ import { useSimplePageListLoader } from '@/components/composeabe/loader/SimplePa
 import FlexCol from '@/components/layout/FlexCol.vue';
 import HomeTitle from '@/common/components/parts/HomeTitle.vue';
 import MasonryGrid from '@/components/layout/masonry/MasonryGrid.vue';
-import MasonryGridItem from '@/components/layout/masonry/MasonryGridItem.vue';
 import IndexCommonImageItem from '@/common/components/parts/IndexCommonImageItem.vue';
 import LightVillageApi from '@/api/light/LightVillageApi';
 import SimplePageListLoader from '@/components/loader/SimplePageListLoader.vue';

+ 27 - 3
src/pages/index.vue

@@ -17,7 +17,7 @@
         align="left"
         :zIndex="200"
       />
-      <HomeIndex v-show="tabIndex === 0" @goVillage="tabIndex = 1" @goDig="tabIndex = 2" />
+      <HomeIndex v-show="tabIndex === 0" :currentScrollTop="currentScrollTop" @goVillage="tabIndex = 1" @goDig="tabIndex = 2" />
       <VillageIndex v-if="tabIndex === 1" showSwitch ref="villageIndex" @goHome="tabIndex=0" />
       <DigIndex v-show="tabIndex === 2" />
       <DiscoverIndex v-show="tabIndex === 3" @goVillage="tabIndex = 1" />
@@ -38,12 +38,20 @@
         <TabBarItem icon="https://xy.wenlvti.net/app_static/images/tabs/TabIconMine.png" activeIcon="https://xy.wenlvti.net/app_static/images/tabs/TabIconMineActive.png" text="我的" :iconSize="70" />
       </TabBar>
     </CommonRoot>
+
+    <BackToTop 
+      ref="backToTop"
+      customScrollValue
+      :bottom="200"
+      @click="handleScrollTop"
+      @scroll="(v) => currentScrollTop = v"
+    />
 	</view>
 </template>
 
 <script setup lang="ts">
 import { onMounted, ref, watch } from 'vue';
-import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app';
+import { onPageScroll, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app';
 import { useTheme } from '@/components/theme/ThemeDefine';
 import { useLoadQuerys } from '@/components/composeabe/LoadQuerys';
 import { useVillageStore } from '@/store/village';
@@ -63,13 +71,16 @@ import CommonRoot from '@/components/dialog/CommonRoot.vue';
 import LightVillageApi from '@/api/light/LightVillageApi';
 import { waitTimeOut } from '@imengyu/imengyu-utils';
 import { injectAppConfiguration } from '@/api/system/useAppConfiguration';
+import BackToTop from '@/components/feedback/BackToTop.vue';
 
 const tabIndex = ref(0);
 const themeContext = useTheme();
 const villageStore = useVillageStore();
 const authStore = useAuthStore();
 const appConfiguration = injectAppConfiguration();
+const currentScrollTop = ref(0);
 
+const backToTop = ref<InstanceType<typeof BackToTop>>();
 const villageIndex = ref<InstanceType<typeof VillageIndex>>();
 
 const { querys } = useLoadQuerys({ 
@@ -151,6 +162,12 @@ watch(tabIndex, () => {
   uni.pageScrollTo({ scrollTop: 0, duration: 0 });
 });
 
+onPageScroll((e) => {
+  backToTop.value?.onScroll(e.scrollTop);
+});
+function handleScrollTop() {
+  uni.pageScrollTo({ scrollTop: 0, duration: 300 });
+}
 
 defineExpose({
   onPageBack: (name: string, data: Record<string, unknown>) => {
@@ -191,4 +208,11 @@ onMounted(() => {
     tabIndex.value = Number(uni.getStorageSync('lastTabIndex') ?? 0);
   }
 })
-</script>
+</script>
+
+<style>
+.index {
+  overflow-x: hidden;
+  overflow-y: auto;
+}
+</style>