Quellcode durchsuchen

📦 赐福加成页面

快乐的梦鱼 vor 1 Monat
Ursprung
Commit
f32c7c9ddd

+ 546 - 0
src/api/light/TreeApi.ts

@@ -0,0 +1,546 @@
+import { DataModel, transformArrayDataModel, transformDataModel, type KeyValue, type NewDataModel } from '@imengyu/js-request-transform';
+import { AppServerRequestModule } from '../RequestModules';
+import { transformSomeToArray } from '../Utils';
+
+/** 流水/日志通用字段 */
+abstract class GrowthLogBase extends DataModel<GrowthLogBase> {
+  id!: number;
+  villageId = null as number | null;
+  userId = null as number | null;
+  sourceTable = '';
+  sourceId = null as number | null;
+  remark = '';
+  createtime = '' as string | null;
+  updatetime = '' as string | null;
+  deletetime = '' as string | null;
+  villageName = '';
+  nickname = '';
+  username = '';
+  volunteerName = '';
+}
+
+export class GrowthTaskLogItem extends GrowthLogBase {
+  constructor() {
+    super(GrowthTaskLogItem, '任务日志');
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+      rewardWater: { clientSide: 'number', serverSide: 'number' },
+      rewardLight: { clientSide: 'number', serverSide: 'number' },
+      rewardFruit: { clientSide: 'number', serverSide: 'number' },
+    };
+  }
+
+  taskKey = '';
+  taskName = '';
+  rewardWater = 0;
+  rewardLight = 0;
+  rewardFruit = 0;
+}
+
+export class GrowthFruitLogItem extends GrowthLogBase {
+  constructor() {
+    super(GrowthFruitLogItem, '乡源果流水');
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+      fruit: { clientSide: 'number', serverSide: 'number' },
+      beforeFruit: { clientSide: 'number', serverSide: 'number' },
+      afterFruit: { clientSide: 'number', serverSide: 'number' },
+    };
+  }
+
+  type = '' as 'pick' | 'task' | 'bless' | 'exchange' | string;
+  typeText = '';
+  fruit = 0;
+  beforeFruit = 0;
+  afterFruit = 0;
+}
+
+export class GrowthLightLogItem extends GrowthLogBase {
+  constructor() {
+    super(GrowthLightLogItem, '乡源光流水');
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+      light: { clientSide: 'number', serverSide: 'number' },
+      beforeLight: { clientSide: 'number', serverSide: 'number' },
+      afterLight: { clientSide: 'number', serverSide: 'number' },
+    };
+  }
+
+  type = '' as 'water' | 'fertilize' | 'task' | 'bless' | string;
+  typeText = '';
+  light = 0;
+  beforeLight = 0;
+  afterLight = 0;
+}
+
+export class BlessPackageItem extends DataModel<BlessPackageItem> {
+  constructor() {
+    super(BlessPackageItem, '赐福套餐');
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+      amount: { clientSide: 'number', serverSide: 'number' },
+      addLight: { clientSide: 'number', serverSide: 'number' },
+      addFruit: { clientSide: 'number', serverSide: 'number' },
+      multiple: { clientSide: 'number', serverSide: 'number' },
+      days: { clientSide: 'number', serverSide: 'number' },
+      status: { clientSide: 'number', serverSide: 'number' },
+    };
+  }
+
+  id!: number;
+  name = '';
+  amount = 0;
+  image = '';
+  addLight = 0;
+  addFruit = 0;
+  multiple = 0;
+  days = 0;
+  status = 0;
+  statusText = '';
+  createtime = '' as string | null;
+  updatetime = '' as string | null;
+  deletetime = '' as string | null;
+}
+
+export class BlessOrderItem extends DataModel<BlessOrderItem> {
+  constructor() {
+    super(BlessOrderItem, '赐福订单');
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+      villageId: { clientSide: 'number', serverSide: 'number' },
+      userId: { clientSide: 'number', serverSide: 'number' },
+      blessId: { clientSide: 'number', serverSide: 'number' },
+      amount: { clientSide: 'number', serverSide: 'number' },
+      addLight: { clientSide: 'number', serverSide: 'number' },
+      addFruit: { clientSide: 'number', serverSide: 'number' },
+      multiple: { clientSide: 'number', serverSide: 'number' },
+      status: { clientSide: 'number', serverSide: 'number' },
+    };
+    this._convertKeyType = (key) => {
+      if (key.endsWith('Text')) return undefined;
+      if (key === 'startTime' || key === 'endTime' || key === 'payTime') {
+        return { clientSide: 'date', serverSide: 'number' };
+      }
+      return undefined;
+    };
+  }
+
+  id!: number;
+  villageId = null as number | null;
+  userId = null as number | null;
+  blessId = null as number | null;
+  amount = 0;
+  addLight = 0;
+  addFruit = 0;
+  multiple = 0;
+  startTime = null as Date | null;
+  endTime = null as Date | null;
+  status = 0;
+  remark = '' as string | null;
+  payTime = null as Date | null;
+  createtime = '' as string | null;
+  updatetime = '' as string | null;
+  deletetime = '' as string | null;
+  villageName = '';
+  nickname = '';
+  username = '';
+  volunteerName = '';
+  blessName = '';
+  startTimeText = '';
+  endTimeText = '';
+  statusText = '';
+  payTimeText = '';
+}
+
+export class UpgradePackageItem extends DataModel<UpgradePackageItem> {
+  constructor() {
+    super(UpgradePackageItem, '升级套餐');
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+      vipLevel: { clientSide: 'number', serverSide: 'number' },
+      price: { clientSide: 'number', serverSide: 'number' },
+      imageLimit: { clientSide: 'number', serverSide: 'number' },
+      storageLimit: { clientSide: 'number', serverSide: 'number' },
+      managerLimit: { clientSide: 'number', serverSide: 'number' },
+      isRecommend: { clientSide: 'number', serverSide: 'number' },
+      sort: { clientSide: 'number', serverSide: 'number' },
+      status: { clientSide: 'number', serverSide: 'number' },
+    };
+  }
+
+  id!: number;
+  name = '';
+  vipLevel = 0;
+  price = 0;
+  imageLimit = 0;
+  storageLimit = 0;
+  managerLimit = 0;
+  isRecommend = 0;
+  isRecommendText = '';
+  desc = '';
+  content = '';
+  sort = 0;
+  status = 0;
+  statusText = '';
+  createtime = '' as string | null;
+  updatetime = '' as string | null;
+  deletetime = '' as string | null;
+}
+
+export class UpgradeOrderItem extends DataModel<UpgradeOrderItem> {
+  constructor() {
+    super(UpgradeOrderItem, '升级订单');
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+      villageId: { clientSide: 'number', serverSide: 'number' },
+      upgradeLevelId: { clientSide: 'number', serverSide: 'number' },
+      vipLevel: { clientSide: 'number', serverSide: 'number' },
+      userId: { clientSide: 'number', serverSide: 'number' },
+      price: { clientSide: 'number', serverSide: 'number' },
+      imageLimit: { clientSide: 'number', serverSide: 'number' },
+      storageLimit: { clientSide: 'number', serverSide: 'number' },
+      managerLimit: { clientSide: 'number', serverSide: 'number' },
+      money: { clientSide: 'number', serverSide: 'number' },
+      status: { clientSide: 'number', serverSide: 'number' },
+      volunteerId: { clientSide: 'number', serverSide: 'number' },
+    };
+    this._convertKeyType = (key) => {
+      if (key.endsWith('Text')) return undefined;
+      if (key === 'payTime') {
+        return { clientSide: 'date', serverSide: 'number' };
+      }
+      return undefined;
+    };
+  }
+
+  id!: number;
+  orderNo = '';
+  villageId = null as number | null;
+  upgradeLevelId = null as number | null;
+  vipLevel = 0;
+  userId = null as number | null;
+  levelName = '';
+  price = 0;
+  imageLimit = 0;
+  storageLimit = 0;
+  managerLimit = 0;
+  money = 0;
+  status = 0;
+  payTime = null as Date | null;
+  villageName = '';
+  volunteerName = '';
+  volunteerId = null as number | null;
+  statusText = '';
+  payTimeText = '';
+  createtime = '' as string | null;
+  updatetime = '' as string | null;
+  deletetime = '' as string | null;
+}
+
+type GrowthLogSearch = {
+  page?: number;
+  pageSize?: number;
+  keywords?: string;
+  villageId?: number;
+  userId?: number;
+};
+
+type PagedGrowthResponse = {
+  total: number;
+  per_page?: number;
+  current_page?: number;
+  last_page?: number;
+  data: KeyValue[];
+};
+
+export type GrowthLogFeedType = 'task' | 'fruit' | 'light';
+
+export type GrowthLogFeedItem =
+  | { logType: 'task'; item: GrowthTaskLogItem }
+  | { logType: 'fruit'; item: GrowthFruitLogItem }
+  | { logType: 'light'; item: GrowthLightLogItem };
+
+export class TreeApi extends AppServerRequestModule<DataModel> {
+  constructor() {
+    super();
+  }
+
+  private growthLogParams(search?: GrowthLogSearch) {
+    return {
+      page: search?.page,
+      pageSize: search?.pageSize,
+      keywords: search?.keywords,
+      village_id: search?.villageId,
+      user_id: search?.userId,
+    };
+  }
+
+  private parsePagedList<T extends DataModel>(
+    model: NewDataModel,
+    data: PagedGrowthResponse,
+    label: string,
+  ) {
+    return {
+      total: data.total ?? 0,
+      list: transformArrayDataModel<T>(model, transformSomeToArray(data.data), label, true),
+    };
+  }
+
+  /** 将 total 随机拆成 parts 份(每份 ≥ 0,之和为 total) */
+  private randomPartition(total: number, parts: number) {
+    const buckets = Array.from({ length: parts }, () => 0);
+    for (let i = 0; i < total; i++) {
+      buckets[Math.floor(Math.random() * parts)]++;
+    }
+    return buckets;
+  }
+
+  private shuffle<T>(items: T[]) {
+    const arr = [...items];
+    for (let i = arr.length - 1; i > 0; i--) {
+      const j = Math.floor(Math.random() * (i + 1));
+      [arr[i], arr[j]] = [arr[j], arr[i]];
+    }
+    return arr;
+  }
+
+  private async fetchGrowthLogSlice<T extends DataModel>(
+    url: string,
+    label: string,
+    model: NewDataModel,
+    pageSize: number,
+    search?: GrowthLogSearch,
+    extra?: KeyValue,
+  ) {
+    if (pageSize <= 0) return [] as T[];
+    const res = await this.post<PagedGrowthResponse>(url, label, {
+      ...this.growthLogParams({ ...search, page: 1, pageSize }),
+      ...extra,
+    });
+    return this.parsePagedList<T>(model, res.requireData(), label).list;
+  }
+
+  /**
+   * 聚合随机成长流水:将 count 随机分配到任务日志 / 乡源果 / 乡源光三个接口并行拉取,打乱后返回。
+   */
+  async getRandomGrowthLogFeed(
+    count: number,
+    search?: GrowthLogSearch & {
+      fruitType?: 'pick' | 'task' | 'bless' | 'exchange' | string;
+      lightType?: 'water' | 'fertilize' | 'task' | 'bless' | string;
+    },
+  ) {
+    if (count <= 0) {
+      return { list: [] as GrowthLogFeedItem[], total: 0 };
+    }
+
+    const [taskCount, fruitCount, lightCount] = this.randomPartition(count, 3);
+
+    const [taskList, fruitList, lightList] = await Promise.all([
+      this.fetchGrowthLogSlice<GrowthTaskLogItem>(
+        '/village/growth/taskLogList',
+        '任务日志',
+        GrowthTaskLogItem,
+        taskCount,
+        search,
+      ),
+      this.fetchGrowthLogSlice<GrowthFruitLogItem>(
+        '/village/growth/fruitLogList',
+        '乡源果流水',
+        GrowthFruitLogItem,
+        fruitCount,
+        search,
+        { type: search?.fruitType },
+      ),
+      this.fetchGrowthLogSlice<GrowthLightLogItem>(
+        '/village/growth/lightLogList',
+        '乡源光流水',
+        GrowthLightLogItem,
+        lightCount,
+        search,
+        { type: search?.lightType },
+      ),
+    ]);
+
+    const merged: GrowthLogFeedItem[] = [
+      ...taskList.map((item) => ({ logType: 'task' as const, item })),
+      ...fruitList.map((item) => ({ logType: 'fruit' as const, item })),
+      ...lightList.map((item) => ({ logType: 'light' as const, item })),
+    ];
+
+    const list = this.shuffle(merged).slice(0, count);
+    return { list, total: list.length };
+  }
+
+  /** 任务日志列表 */
+  async getTaskLogList(search?: GrowthLogSearch) {
+    const res = await this.post<PagedGrowthResponse>(
+      '/village/growth/taskLogList',
+      '任务日志列表',
+      this.growthLogParams(search),
+    );
+    return this.parsePagedList<GrowthTaskLogItem>(
+      GrowthTaskLogItem,
+      res.requireData(),
+      '任务日志',
+    );
+  }
+
+  /** 乡源果流水 */
+  async getFruitLogList(
+    search?: GrowthLogSearch & {
+      type?: 'pick' | 'task' | 'bless' | 'exchange' | string;
+    },
+  ) {
+    const res = await this.post<PagedGrowthResponse>(
+      '/village/growth/fruitLogList',
+      '乡源果流水',
+      {
+        ...this.growthLogParams(search),
+        type: search?.type,
+      },
+    );
+    return this.parsePagedList<GrowthFruitLogItem>(
+      GrowthFruitLogItem,
+      res.requireData(),
+      '乡源果流水',
+    );
+  }
+
+  /** 乡源光流水 */
+  async getLightLogList(
+    search?: GrowthLogSearch & {
+      type?: 'water' | 'fertilize' | 'task' | 'bless' | string;
+    },
+  ) {
+    const res = await this.post<PagedGrowthResponse>(
+      '/village/growth/lightLogList',
+      '乡源光流水',
+      {
+        ...this.growthLogParams(search),
+        type: search?.type,
+      },
+    );
+    return this.parsePagedList<GrowthLightLogItem>(
+      GrowthLightLogItem,
+      res.requireData(),
+      '乡源光流水',
+    );
+  }
+
+  /** 赐福套餐列表 */
+  async getBlessList(options?: {
+    page?: number;
+    pageSize?: number;
+    keywords?: string;
+  }) {
+    const res = await this.post<PagedGrowthResponse>(
+      '/village/growth/blessList',
+      '赐福套餐列表',
+      {
+        page: options?.page,
+        pageSize: options?.pageSize,
+        keywords: options?.keywords,
+      },
+    );
+    return this.parsePagedList<BlessPackageItem>(
+      BlessPackageItem,
+      res.requireData(),
+      '赐福套餐',
+    );
+  }
+
+  /** 赐福订单列表 */
+  async getBlessOrders(options?: {
+    page?: number;
+    pageSize?: number;
+    keywords?: string;
+    villageId?: number;
+    userId?: number;
+    blessId?: number;
+    /** -1=已取消, 0=待支付, 1=已支付 */
+    status?: -1 | 0 | 1 | number;
+  }) {
+    const res = await this.post<PagedGrowthResponse>(
+      '/village/growth/blessOrders',
+      '赐福订单列表',
+      {
+        page: options?.page,
+        pageSize: options?.pageSize,
+        keywords: options?.keywords,
+        village_id: options?.villageId,
+        user_id: options?.userId,
+        bless_id: options?.blessId,
+        status: options?.status,
+      },
+    );
+    return this.parsePagedList<BlessOrderItem>(
+      BlessOrderItem,
+      res.requireData(),
+      '赐福订单',
+    );
+  }
+
+  /** 升级套餐列表 */
+  async getUpgradeList(options?: {
+    page?: number;
+    pageSize?: number;
+    keywords?: string;
+  }) {
+    const res = await this.post<PagedGrowthResponse>(
+      '/village/growth/upgradeList',
+      '升级套餐列表',
+      {
+        page: options?.page,
+        pageSize: options?.pageSize,
+        keywords: options?.keywords,
+      },
+    );
+    return this.parsePagedList<UpgradePackageItem>(
+      UpgradePackageItem,
+      res.requireData(),
+      '升级套餐',
+    );
+  }
+
+  /** 升级套餐详情 */
+  async getUpgradeInfo(id: number) {
+    const res = await this.post<KeyValue>('/village/growth/upgradeInfo', '升级套餐详情', { id });
+    return transformDataModel<UpgradePackageItem>(UpgradePackageItem, res.requireData());
+  }
+
+  /** 升级订单列表 */
+  async getUpgradeOrderList(options?: {
+    page?: number;
+    pageSize?: number;
+    keywords?: string;
+    villageId?: number;
+    userId?: number;
+  }) {
+    const res = await this.post<PagedGrowthResponse>(
+      '/village/growth/upgradeOrderList',
+      '升级订单列表',
+      {
+        page: options?.page,
+        pageSize: options?.pageSize,
+        keywords: options?.keywords,
+        village_id: options?.villageId,
+        user_id: options?.userId,
+      },
+    );
+    return this.parsePagedList<UpgradeOrderItem>(
+      UpgradeOrderItem,
+      res.requireData(),
+      '升级订单',
+    );
+  }
+}
+
+export default new TreeApi();

+ 32 - 0
src/common/components/PrimaryButton.vue

@@ -0,0 +1,32 @@
+<template>
+  <BackgroundImageButton 
+    backgroundImage="https://xy.wenlvti.net/app_static/images/village/ButtonPrimary.png"
+    :backgroundCutBorder="20"
+    :backgroundCutBorderSize="20"
+    :padding="[22, 40]"
+    :width="width"
+    :innerStyle="innerStyle"
+    center
+    @click="emit('click')"
+  >
+    <ActivityIndicator v-if="loading" :size="36" color="text.content" />
+    <Text :text="text" fontFamily="SongtiSCBlack" color="text.content" />
+  </BackgroundImageButton>
+</template>
+
+<script setup lang="ts">
+import ActivityIndicator from '@/components/basic/ActivityIndicator.vue';
+import BackgroundImageButton from '@/components/basic/BackgroundImageButton.vue';
+import Text from '@/components/basic/Text.vue';
+
+const props = withDefaults(defineProps<{
+  text: string;
+  width?: string|number;
+  innerStyle?: object;
+  loading?: boolean;
+}>(), {
+  text: '去发布',
+}); 
+
+const emit = defineEmits(['click']);
+</script>

+ 2 - 3
src/pages/chat/dependent/post/publish.vue

@@ -39,11 +39,9 @@
         </BackgroundBox>
       </ProvideVar>
       <Height :height="50" />
-      <ImageButton
-        src="https://xy.wenlvti.net/app_static/images/village/ButtonPrimary.png"
+      <PrimaryButton
         text="发布"
         width="500rpx"
-        height="80rpx"
         @click="publish"
       />
     </FlexCol>
@@ -105,6 +103,7 @@ import Agent from './agent.vue';
 import LightVillageApi, { PostMessage } from '@/api/light/LightVillageApi';
 import CommonTopBanner from '@/common/components/CommonTopBanner.vue';
 import type { UploaderAction, UploaderItem } from '@/components/form/Uploader';
+import PrimaryButton from '@/common/components/PrimaryButton.vue';
 
 const { querys } = useLoadQuerys({
   tag: '',

+ 5 - 10
src/pages/dig/forms/common.vue

@@ -30,17 +30,11 @@
         <Button v-if="querys.isView" type="primary" :loading="loading" @click="backPrev(false)">返回</Button>
         <FlexCol v-else :gap="10" center>
           <Height :height="20" />
-          <BackgroundImageButton 
-            backgroundImage="https://xy.wenlvti.net/app_static/images/village/ButtonPrimary.png"
-            :backgroundCutBorder="20"
-            :backgroundCutBorderSize="20"
-            :padding="24"
+          <PrimaryButton 
+            text="提交"
             width="560rpx"
-            center
-            type="primary" :loading="loading" @click="debounceSubmit.executeWithDelay()"
-          >
-            提交
-          </BackgroundImageButton>
+            @click="debounceSubmit.executeWithDelay()"
+          />
           <Button v-if="querys.id" type="text" textColor="danger" :loading="loading" @click="deleteInfo">删除</Button>
         </FlexCol>
       </FlexCol>
@@ -73,6 +67,7 @@ import CommonTopBanner from '@/common/components/CommonTopBanner.vue';
 import ProvideVar from '@/components/theme/ProvideVar.vue';
 import BackgroundBox from '@/components/display/block/BackgroundBox.vue';
 import BackgroundImageButton from '@/components/basic/BackgroundImageButton.vue';
+import PrimaryButton from '@/common/components/PrimaryButton.vue';
 
 const loading = ref(false);
 const subTitle = ref('');

+ 4 - 8
src/pages/dig/forms/list-ordinary.vue

@@ -22,15 +22,10 @@
             <Width :width="10" />
           </FlexRow>
 
-          <BackgroundImageButton 
-            backgroundImage="https://xy.wenlvti.net/app_static/images/village/ButtonPrimary.png"
-            :backgroundCutBorder="20"
-            :backgroundCutBorderSize="20"
-            :padding="[16, 40]"
+          <PrimaryButton 
+            text="+ 编写"
             @click="newData"
-          >
-            <Text text="+ 编写" />
-          </BackgroundImageButton>
+          />
         </FlexRow>
         <FlexCol 
           v-if="!isJoined" 
@@ -131,6 +126,7 @@ import CommonTopBanner from '@/common/components/CommonTopBanner.vue';
 import BackgroundBox from '@/components/display/block/BackgroundBox.vue';
 import BackgroundImageButton from '@/components/basic/BackgroundImageButton.vue';
 import Width from '@/components/layout/space/Width.vue';
+import PrimaryButton from '@/common/components/PrimaryButton.vue';
 
 const subTitle = ref('');
 const searchText = ref('');

+ 4 - 8
src/pages/dig/forms/list.vue

@@ -12,16 +12,11 @@
             :innerStyle="{ width: querys.isView ? '600rpx' : '460rpx' }"
             @search="search"
           />
-          <BackgroundImageButton 
+          <PrimaryButton 
             v-if="!querys.isView"
-            backgroundImage="https://xy.wenlvti.net/app_static/images/village/ButtonPrimary.png"
-            :backgroundCutBorder="20"
-            :backgroundCutBorderSize="20"
-            :padding="[16, 40]"
+            text="+ 编写"
             @click="newData"
-          >
-            <Text text="+ 编写" />
-          </BackgroundImageButton>
+          />
         </FlexRow>
         <Height :height="5" />
         <SimplePageListLoader :loader="listLoader" :noEmpty="true">
@@ -102,6 +97,7 @@ import VillageApi from '@/api/inhert/VillageApi';
 import CommonTopBanner from '@/common/components/CommonTopBanner.vue';
 import BackgroundBox from '@/components/display/block/BackgroundBox.vue';
 import BackgroundImageButton from '@/components/basic/BackgroundImageButton.vue';
+import PrimaryButton from '@/common/components/PrimaryButton.vue';
 
 const subTitle = ref('');
 const searchText = ref('');

+ 28 - 2
src/pages/home/components/LightMap.vue

@@ -17,6 +17,7 @@
       :latitude="lonlat?.latitude"
       @markertap="onMarkerTap" 
     />
+
     <FlexCol v-if="isEmptyRegion" 
       gap="gap.xl" 
       position="absolute" 
@@ -32,6 +33,28 @@
         </FlexCol>
       </button>
     </FlexCol>
+    <FlexCol v-if="mapLoader.error.value" 
+      gap="gap.md" 
+      position="absolute" 
+      inset="0" 
+      padding="space.xl"
+      center 
+      backgroundColor="mask.default"
+    > 
+      <Icon name="warning" size="44" color="red" />
+      <Text textAlign="center" :text="mapLoader.error.value" />
+    </FlexCol>
+    <FlexCol v-else-if="mapLoader.status.value === 'loading'" 
+      gap="gap.md" 
+      position="absolute" 
+      inset="0" 
+      padding="space.xl"
+      center 
+      backgroundColor="mask.default"
+    > 
+      <ActivityIndicator :size="44" />
+    </FlexCol>
+
     <SimpleDropDownPicker 
       v-if="!isEmptyRegion"
       class="light-map-region-picker" 
@@ -81,6 +104,8 @@ import NButton from '@/components/basic/Button.vue';
 import FlexCol from '@/components/layout/FlexCol.vue';
 import Text from '@/components/basic/Text.vue';
 import CommonContent from '@/api/CommonContent';
+import Icon from '@/components/basic/Icon.vue';
+import ActivityIndicator from '@/components/basic/ActivityIndicator.vue';
 
 const instance = getCurrentInstance();
 const mapCtx = uni.createMapContext('prevMap', instance);
@@ -120,7 +145,7 @@ const mapLoader = useSimpleDataLoader<MapMarker[]>(async () => {
   if (!selectedRegion.value)
     return [];
   await waitTimeOut(200);
-  const res = (await LightVillageApi.getVillageList(undefined, selectedRegion.value, undefined, 1, 10000)).map((p, i) => {
+  const res = (await LightVillageApi.getVillageList(undefined, selectedRegion.value, undefined, 1, 1000)).map((p, i) => {
     villageData.set(p.id, p);
     const maker : MapMarker = {
       id: p.id ?? i,
@@ -190,7 +215,7 @@ const mapLoader = useSimpleDataLoader<MapMarker[]>(async () => {
 
   ready.value = true;
   return res;
-}, false, false, true);
+}, false, false);
 
 const isEmptyRegion = computed(() => {
   return (!mapLoader.content.value?.length) && ready.value;
@@ -279,6 +304,7 @@ onMounted(async () => {
   height: 600rpx;
   border-radius: 30rpx;
   overflow: hidden;
+  border: 1px solid #d45652;
 
   &.full {
     height: 100vh;

+ 111 - 0
src/pages/home/village/dialogs/BlessBuyDialog.vue

@@ -0,0 +1,111 @@
+<template>
+  <CommonDialog v-model:show="show">
+    <FlexCol gap="gap.lg" width="600rpx" align="center" :padding="35">
+      <Image
+        src="https://xy.wenlvti.net/app_static/images/home/bless/IconHeader.png"
+        :width="300" 
+        :height="100" 
+      />
+      <Height :height="20" />
+      <BackgroundBox 
+        v-for="(item, k) in infoGrids"
+        :key="k"
+        backgroundImage="https://xy.wenlvti.net/app_static/images/village/BoxMid.png"
+        :backgroundCutBorder="32"
+        :backgroundCutBorderSize="36"
+        :padding="24"
+        direction="row"
+        justify="space-between"
+        align="center"
+        gap="gap.md"
+        width="96%"
+      >
+        <FlexRow align="center" gap="gap.sm">
+          <Image 
+            :src="item.image"
+            :width="60"
+            :height="60"
+            round
+          />
+          <Text :text="item.label" fontConfig="lightImportantTitle"  />
+        </FlexRow>
+        <FlexRow align="center" gap="gap.sm">
+          <Text :text="item.value" fontConfig="lightGoldTitle" :fontSize="50" />
+          <Text :text="item.unit" fontConfig="lightGoldTitle" :fontSize="26" />
+        </FlexRow>
+      </BackgroundBox>
+
+      <BackgroundBox 
+        backgroundImage="https://xy.wenlvti.net/app_static/images/home/bless/PriceBg.png"
+        :backgroundCutBorder="36"
+        :backgroundCutBorderSize="36"
+        :padding="36"
+        direction="row"
+        justify="space-between"
+        align="center"
+        gap="gap.md"
+        width="90%"
+      >
+        <FlexRow align="center" gap="gap.sm">
+          <Text text="¥" fontConfig="lightImportantTitle" fontFamily="SongtiSCBlack" :fontSize="26" />
+          <Text :text="props.currentBless?.amount || 0" fontConfig="lightImportantTitle" :fontSize="42" fontFamily="SongtiSCBlack" />
+        </FlexRow>
+        <Text :text="`有效期至 ${effectiveDate}`" fontConfig="contentText" color="white" />
+      </BackgroundBox>
+        
+      <Height :height="5" />
+      <PrimaryButton text="立即赐福" @click="show = false" width="520rpx" />
+      <Height :height="5" />
+      <Text 
+        textAlign="center" 
+        text="倍率仅作用于用户乡源果,村社乡源光不翻倍赐福到期后,倍率自动恢复为1倍"
+        fontConfig="contentText"
+      />
+
+    </FlexCol>
+  </CommonDialog>
+</template>
+
+<script setup lang="ts">
+import type { BlessPackageItem } from '@/api/light/TreeApi';
+import CommonDialog from '@/common/components/CommonDialog.vue';
+import FrameButton from '@/common/components/FrameButton.vue';
+import PrimaryButton from '@/common/components/PrimaryButton.vue';
+import Image from '@/components/basic/Image.vue';
+import Text from '@/components/basic/Text.vue';
+import BackgroundBox from '@/components/display/block/BackgroundBox.vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import FlexRow from '@/components/layout/FlexRow.vue';
+import Height from '@/components/layout/space/Height.vue';
+import { DateUtils } from '@imengyu/imengyu-utils';
+import { computed, ref } from 'vue';
+
+const show = ref(false);
+
+const props = defineProps<{
+  currentBless?: BlessPackageItem;
+}>();
+const infoGrids = computed(() => [
+  {
+    label: '村社加乡源光',
+    image: 'https://xy.wenlvti.net/app_static/images/home/bless/IconLight.png',
+    value: props.currentBless?.addLight || 0,
+    unit: '光',
+  },
+  {
+    label: '用户得乡源果',
+    image: 'https://xy.wenlvti.net/app_static/images/home/bless/IconFruit.png',
+    value: props.currentBless?.addFruit || 0,
+    unit: '个',
+  },
+]);
+const effectiveDate = computed(() => {
+  return DateUtils.formatDate(DateUtils.dateAddDays(new Date(), props.currentBless?.days || 0), 'YYYY-MM-DD');
+});
+
+defineExpose({
+  show: () => {
+    show.value = true;
+  },
+});
+</script>

+ 2 - 3
src/pages/home/village/goods/detail.vue

@@ -33,11 +33,9 @@
           <Text :text="goodLoader.content.value.description" fontConfig="contentText" />
         </BackgroundBox>
         <Height :height="160" />
-        <ImageButton
-          src="https://xy.wenlvti.net/app_static/images/village/ButtonPrimary.png"
+        <PrimaryButton
           text="去咨询"
           width="500rpx"
-          height="80rpx"
           @click="handleConsult"
         />
       </template>
@@ -58,6 +56,7 @@ import FlexCol from '@/components/layout/FlexCol.vue';
 import FlexRow from '@/components/layout/FlexRow.vue';
 import Height from '@/components/layout/space/Height.vue';
 import SimplePageContentLoader from '@/components/loader/SimplePageContentLoader.vue';
+import PrimaryButton from '@/common/components/PrimaryButton.vue';
 const goodLoader = useSimpleDataLoader(async () => {
   return {
     id: 1,

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

@@ -73,6 +73,7 @@ import VillageMyFollow from '../components/VillageMyFollow.vue';
 import HomeLargeTitle from '@/common/components/parts/HomeLargeTitle.vue';
 import Around from './recommed/around.vue';
 import FrameButton from '@/common/components/FrameButton.vue';
+import { isDevEnv } from '@/common/config/AppCofig';
 
 const tab = ref('card');
 const villageStore = useVillageStore();
@@ -95,6 +96,9 @@ onMounted(() => {
       title: villageStore.currentVillage?.name || '未选择村庄',
     });
   }
+  if (isDevEnv) {
+    tab.value = 'tree';
+  }
 });
 
 function handleGoApply() {

+ 100 - 77
src/pages/home/village/introd/tree.vue

@@ -48,48 +48,53 @@
           <Image src="https://xy.wenlvti.net/app_static/images/village/IconWatering.png" :width="130" mode="widthFix" />
           <Text text="浇水" fontConfig="contentText" textAlign="center" />
         </Touchable>
-        <Touchable center direction="column" flexBasis="22%">
+        <Touchable center direction="column" flexBasis="22%" @click="handleGoBless">
           <Image src="https://xy.wenlvti.net/app_static/images/village/IconBlessing.png" :width="130" mode="widthFix" />
           <Text text="赐福" fontConfig="contentText" textAlign="center" />
         </Touchable>
       </FlexRow>
 
       <HomeTitle title="最新动态" />
-      <FlexCol gap="gap.lg">
-        <FlexRow 
-          v-for="item in infoLoader.content.value" :key="item.id"
-          backgroundColor="background.tertiary"
-          radius="radius.md"
-          :padding="[20, 30]"
-          gap="gap.lg"
-          align="center"
-        > 
-          <Avatar  
-            :url="item.head"
-            :size="80"
-            radius="50%"
-          />
-          <Text :text="item.content" fontConfig="contentText" :innerStyle="{ flex: 1 }" />
-          <BackgroundBox
-            backgroundImage="https://xy.wenlvti.net/app_static/images/village/TagNormal.png"
-            :backgroundCutBorder="[10, 10, 10, 10]"
-            :backgroundCutBorderSize="[10, 10, 10, 10]"
-            :padding="[15, 30]"
-          >
-            <Text :text="item.levelText" fontConfig="contentText" />
-          </BackgroundBox>
-        </FlexRow>
-      </FlexCol>
+      <SimplePageContentLoader :loader="infoLoader" :emptyView="{ text: '冷冷清清,等你来添光加彩' }">
+        <FlexCol gap="gap.lg">
+          <FlexRow 
+            v-for="item in infoLoader.content.value" :key="item.id"
+            backgroundColor="background.tertiary"
+            radius="radius.md"
+            :padding="[20, 30]"
+            gap="gap.lg"
+            align="center"
+          > 
+            <Avatar  
+              :url="item.head"
+              defaultAvatar="https://xy.wenlvti.net/app_static/images/village/PlaceholderVolunteer.png"
+              :size="80"
+              radius="50%"
+            />
+            <Text :text="item.content" fontConfig="contentText" :innerStyle="{ flex: 1 }" />
+            <BackgroundBox
+              backgroundImage="https://xy.wenlvti.net/app_static/images/village/TagNormal.png"
+              :backgroundCutBorder="[10, 10, 10, 10]"
+              :backgroundCutBorderSize="[10, 10, 10, 10]"
+              :padding="[15, 30]"
+            >
+              <Text :text="item.levelText" fontConfig="contentText" />
+            </BackgroundBox>
+          </FlexRow>
+        </FlexCol>
+      </SimplePageContentLoader>
 
       <HomeTitle title="乡源赐福" />
       <FlexRow wrap>
-        <FlexCol 
+        <Touchable 
           v-for="(item, index) in blessingInfoLoader.content.value"
           :key="item.id" 
           center 
           flexBasis="30%"
           gap="gap.sm"
+          direction="column"
           :margin="[10,0,0,0]"
+          @click="handleBuyBless(item)"
         >
           <BackgroundBox
             backgroundImage="https://xy.wenlvti.net/app_static/images/village/ImageBlessingCount.png"
@@ -104,16 +109,20 @@
             :padding="10"
             :width="210"
           >
-            <Text textAlign="center" :text="item.content" fontConfig="contentText" fontFamily="SongtiSCBlack" color="white" />
+            <Text textAlign="center" :text="item.name" fontConfig="contentText" fontFamily="SongtiSCBlack" color="white" />
           </BackgroundBox>
-        </FlexCol>
+        </Touchable>
       </FlexRow>
     </FlexCol>
   </FlexCol>
 
+  <BlessBuyDialog ref="blessBuyDialogRef" :currentBless="currentBless" />
 </template>
 
 <script setup lang="ts">
+import { ref, watch } from 'vue';
+import { useVillageStore } from '@/store/village';
+import { useSimpleDataLoader } from '@/components/composeabe/loader/SimpleDataLoader';
 import HomeTitle from '@/common/components/parts/HomeTitle.vue';
 import Text from '@/components/basic/Text.vue';
 import FlexCol from '@/components/layout/FlexCol.vue';
@@ -124,61 +133,75 @@ import Touchable from '@/components/feedback/Touchable.vue';
 import Image from '@/components/basic/Image.vue';
 import Height from '@/components/layout/space/Height.vue';
 import Avatar from '@/components/display/Avatar.vue';
-import { useSimpleDataLoader } from '@/components/composeabe/loader/SimpleDataLoader';
 import BackgroundBox from '@/components/display/block/BackgroundBox.vue';
 import Progress from '@/components/display/Progress.vue';
+import TreeApi, { type BlessPackageItem, type GrowthLogFeedItem } from '@/api/light/TreeApi';
+import SimplePageContentLoader from '@/components/loader/SimplePageContentLoader.vue';
+import BlessBuyDialog from '../dialogs/BlessBuyDialog.vue';
+
+const GROWTH_FEED_COUNT = 6;
+const DEFAULT_AVATAR = 'https://xy.wenlvti.net/app_static/images/village/PlaceholderVolunteer.png';
+
+const villageStore = useVillageStore();
+
+const blessBuyDialogRef = ref<InstanceType<typeof BlessBuyDialog>>();
+const currentBless = ref<BlessPackageItem>();
 
 const infoLoader = useSimpleDataLoader(async () => {
-  return [
-    {
-      id: 1,
-      head: 'https://mn.wenlvti.net/app_static/minnan/images/test/ImageTest1.png',
-      content: '福泽乡里 为全村加成+10%乡源果,可持续24小时',
-      levelText: '一级',
-    },
-    {
-      id: 2,
-      head: 'https://mn.wenlvti.net/app_static/minnan/images/test/ImageTest2.png',
-      content: '福泽乡里 为全村加成+10%乡源果,可持续24小时',
-      levelText: '五级',
-    },
-    {
-      id: 3,
-      head: 'https://mn.wenlvti.net/app_static/minnan/images/test/ImageTest3.png',
-      content: '福泽乡里 为全村加成+10%乡源果,可持续24小时',
-      levelText: '十级',
-    },
+  const villageId = villageStore.currentVillage?.id;
+  if (!villageId) return [];
+
+  function mapGrowthFeedToInfoItem(feed: GrowthLogFeedItem) {
+    const { item, logType } = feed;
+    let content = item.remark?.trim() || '';
+    let levelText = '';
+
+    if (logType === 'task') {
+      levelText = item.taskName || '任务';
+      if (!content) {
+        content = item.taskName ? `完成任务「${item.taskName}」` : '任务动态';
+      }
+    } else if (logType === 'fruit') {
+      levelText = item.typeText || item.type || '乡源果';
+      if (!content) {
+        const delta = item.fruit > 0 ? `+${item.fruit}` : `${item.fruit}`;
+        content = `${levelText} ${delta} 乡源果`;
+      }
+    } else {
+      levelText = item.typeText || item.type || '乡源光';
+      if (!content) {
+        const delta = item.light > 0 ? `+${item.light}` : `${item.light}`;
+        content = `${levelText} ${delta} 乡源光`;
+      }
+    }
+
+    return {
+      id: `${logType}-${item.id}`,
+      head: DEFAULT_AVATAR,
+      content,
+      levelText,
+    };
+  }
+  const { list } = await TreeApi.getRandomGrowthLogFeed(GROWTH_FEED_COUNT, { villageId });
+  return list.map(mapGrowthFeedToInfoItem);
+});
 
-  ];
+watch(() => villageStore.currentVillage?.id, () => {
+  infoLoader.reload();
 });
 const blessingInfoLoader = useSimpleDataLoader(async () => {
-  return [
-    {
-      id: 1,
-      image: 'https://xy.wenlvti.net/app_static/images/village/ImageBlessing1.png',
-      content: '福泽乡里'
-    },
-    {
-      id: 2,
-      image: 'https://xy.wenlvti.net/app_static/images/village/ImageBlessing2.png',
-      content: '德润乡邻'
-    },
-    {
-      id: 3,
-      image: 'https://xy.wenlvti.net/app_static/images/village/ImageBlessing3.png',
-      content: '光耀故土'
-    },
-    {
-      id: 4,
-      image: 'https://xy.wenlvti.net/app_static/images/village/ImageBlessing4.png',
-      content: '恩沐全村'
-    },
-    {
-      id: 5,
-      image: 'https://xy.wenlvti.net/app_static/images/village/ImageBlessing5.png',
-      content: '圣赐千秋'
-    },
-  ];
+  const res = await TreeApi.getBlessList({ page: 1, pageSize: 18 });
+  return res.list;
 });
 
+function handleBuyBless(bless: BlessPackageItem) {
+  currentBless.value = bless;
+  blessBuyDialogRef.value?.show();
+}
+function handleGoBless() {
+  uni.pageScrollTo({
+    scrollTop: 1000,
+    duration: 300,
+  });
+}
 </script>