6 Achegas 627395b054 ... 738c9468bb

Autor SHA1 Mensaxe Data
  快乐的梦鱼 738c9468bb 📦 乡源果支付功能 hai 3 días
  快乐的梦鱼 0b8e54a8b0 📦 村落昵称显示与修改 hai 3 días
  快乐的梦鱼 f3c9d4430d 🎨 优化采集提交细节问题 hai 3 días
  快乐的梦鱼 7b147e9cf3 📦 专家村落存储空间显示和上传记录 hai 3 días
  快乐的梦鱼 bbfee2e24b 💊 修复注册志愿者后状态未刷新问题 hai 4 días
  快乐的梦鱼 aab1d59cdb 🎨 缺少头像问题优化 hai 4 días
Modificáronse 49 ficheiros con 1059 adicións e 293 borrados
  1. 2 1
      .claude/settings.local.json
  2. 1 1
      src/api/auth/UserApi.ts
  3. 23 10
      src/api/inhert/VillageInfoApi.ts
  4. 38 5
      src/api/light/LightVillageApi.ts
  5. 137 0
      src/api/light/TreeApi.ts
  6. 1 1
      src/common/components/form/RichTextEditor.vue
  7. 2 1
      src/common/components/parts/IndexCommonImageItem.vue
  8. 40 13
      src/common/components/upload/AliOssUploadCo.ts
  9. 29 1
      src/common/components/upload/ImageUploadCo.ts
  10. 12 0
      src/components/dynamic/DynamicForm.ts
  11. 35 4
      src/components/dynamic/DynamicForm.vue
  12. 1 9
      src/components/dynamic/wrappers/PickerIdField.vue
  13. 3 0
      src/components/form/FormContext.ts
  14. 7 0
      src/components/form/PickerField.vue
  15. 2 2
      src/components/form/Uploader.vue
  16. 21 0
      src/pages.json
  17. 13 1
      src/pages/dig/details.vue
  18. 19 5
      src/pages/dig/forms/common.vue
  19. 4 1
      src/pages/dig/forms/data/building.ts
  20. 16 4
      src/pages/dig/forms/data/common.ts
  21. 8 3
      src/pages/dig/forms/data/cultural.ts
  22. 4 1
      src/pages/dig/forms/data/element.ts
  23. 1 1
      src/pages/dig/forms/data/food.ts
  24. 4 1
      src/pages/dig/forms/data/relic.ts
  25. 8 2
      src/pages/dig/forms/data/specker.ts
  26. 9 3
      src/pages/dig/forms/data/travel.ts
  27. 13 7
      src/pages/dig/forms/list-ordinary.vue
  28. 25 17
      src/pages/dig/forms/list.vue
  29. 2 1
      src/pages/home/discover/index.vue
  30. 2 1
      src/pages/home/index.vue
  31. 2 2
      src/pages/home/light/submit-volunteer.vue
  32. 102 0
      src/pages/home/village/bless/pay-select.vue
  33. 95 0
      src/pages/home/village/bless/recharge.vue
  34. 11 5
      src/pages/home/village/dialogs/BlessBuyDialog.vue
  35. 65 0
      src/pages/home/village/dialogs/ChangeNickDialog.vue
  36. 5 70
      src/pages/home/village/dialogs/UpgradeDialog.vue
  37. 1 0
      src/pages/home/village/goods/detail.vue
  38. 8 1
      src/pages/home/village/goods/index.vue
  39. 47 18
      src/pages/home/village/introd/card.vue
  40. 5 33
      src/pages/home/village/introd/tree.vue
  41. 1 1
      src/pages/home/village/rank/volunteer.vue
  42. 45 0
      src/pages/home/village/upgrade/components/BuyFruitInfo.vue
  43. 86 0
      src/pages/home/village/upgrade/components/UpgradeSelect.vue
  44. 20 26
      src/pages/home/village/upgrade/my-upgrade-management.vue
  45. 29 36
      src/pages/home/village/upgrade/select.vue
  46. 42 0
      src/pages/home/village/upgrade/upgrade-village.vue
  47. 2 0
      src/pages/home/village/volunteer/detail.vue
  48. 8 3
      src/pages/user/index.vue
  49. 3 2
      src/store/auth.ts

+ 2 - 1
.claude/settings.local.json

@@ -3,7 +3,8 @@
     "allow": [
       "mcp__chrome-devtools__new_page",
       "mcp__chrome-devtools__take_snapshot",
-      "mcp__chrome-devtools__navigate_page"
+      "mcp__chrome-devtools__navigate_page",
+      "Bash(npx vue-tsc *)"
     ]
   }
 }

+ 1 - 1
src/api/auth/UserApi.ts

@@ -117,7 +117,7 @@ export class UserApi extends AppServerRequestModule<DataModel> {
     return (await this.post('/content/main_body_user/changepwd', '更新密码', data))
   }
   async getUserInfo() {
-    return (await this.post('/content/main_body_user/getMainBodyUser', '获取用户信息', {}, undefined, UserInfo)).data as UserInfo;
+    return (await this.post('/user/userData', '获取用户信息', {}, undefined, UserInfo)).data as UserInfo;
   }  
   async updateUserInfo(data: {
     nickname?: string,

+ 23 - 10
src/api/inhert/VillageInfoApi.ts

@@ -23,17 +23,18 @@ export class CategoryListItem extends DataModel<CategoryListItem> {
   children?: CategoryListItem[];
 }
 export class CommonInfoModel extends DataModel<CommonInfoModel> {
-  constructor() {
-    super(CommonInfoModel, "信息详情");
+  constructor(name?: string) {
+    super(CommonInfoModel, name ?? "信息详情");
     this.setNameMapperCase('Camel', 'Snake');
     this._convertTable = {
       id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
       keywords: { clientSide: 'splitCommaArray', serverSide: 'commaArrayMerge' },
       culturalType: { clientSide: 'number', serverSide: 'number' },
       productType: { clientSide: 'number', serverSide: 'number' },
+      folkCultureType: { clientSide: 'number', serverSide: 'number' },
       type: { clientSide: 'number', serverSide: 'number' },
+      status: { clientSide: 'number', serverSide: 'number' },
       nature: { clientSide: 'number', serverSide: 'number' },
-      folkCultureType: { clientSide: 'number', serverSide: 'number' },
       landforms: [
         { clientSide: 'splitCommaArray', serverSide: 'commaArrayMerge' },
         { clientSide: 'arrayInt', serverSide: 'original' },
@@ -62,6 +63,8 @@ export class CommonInfoModel extends DataModel<CommonInfoModel> {
       if (this.longitude && this.latitude) {
         this.lonlat = [this.longitude as number, this.latitude as number];
       }
+      if (this.avatar)
+        this.villageVolunteerAvatar = this.avatar as string;
     };
     this._afterSolveClient = (data) => {
       if (this.cityAddress) {
@@ -191,11 +194,12 @@ export class VillageListItem extends DataModel<VillageListItem> {
   desc = '';
   image = '';
 }
-export class VillageBulidingInfo extends DataModel<VillageBulidingInfo> {
+export class VillageBulidingInfo extends CommonInfoModel {
   constructor() {
-    super(VillageBulidingInfo, "历史建筑信息");
+    super("历史建筑信息");
     this.setNameMapperCase('Camel', 'Snake');
     this._convertTable = {
+      ...this._convertTable,
       id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
     }
     this._blackList.toServer.push(
@@ -210,10 +214,14 @@ export class VillageBulidingInfo extends DataModel<VillageBulidingInfo> {
           { clientSide: 'splitCommaArray', serverSide: 'commaArrayMerge' },
           { clientSide: 'arrayInt', serverSide: 'original' },
         ];
+      if (key.endsWith('At'))
+        return {
+          clientSide: 'date',
+          serverSide: 'string',
+        };
       return undefined;
     };
   }
-  id !: number;
 }
 
 export class VillageInfoApi extends AppServerRequestModule<DataModel> {
@@ -253,10 +261,12 @@ export class VillageInfoApi extends AppServerRequestModule<DataModel> {
     id?: number, 
     modelClassCreator: (new () => T) = CommonInfoModel as any
   ) {
-    return (await this.post(`/village/collect/info`, '通用获取信息详情', {
-      collect_module_id: collectModuleId,
-      id,
-    }, undefined, modelClassCreator)).data as T
+    return new modelClassCreator().fromServerSide(
+      (await this.post<KeyValue>(`/village/collect/info`, '通用获取信息详情', {
+        collect_module_id: collectModuleId,
+        id,
+      })).requireData()
+    );
   }
   async getList<T extends DataModel = CommonInfoModel>(data: {
     collectModuleId?: number|undefined,
@@ -352,6 +362,9 @@ export class VillageInfoApi extends AppServerRequestModule<DataModel> {
     };
     if (subKey && subId && subId > 0)
       res[subKey] = subId;
+    if (Array.isArray(res.type))
+      debugger;
+
     return (await this.post(`/village/collect/save`, '通用更新信息详情', res));
   }
   async getInfoByVillageId(id: number) {

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

@@ -258,8 +258,13 @@ export class VillageListItem extends DataModel<VillageListItem> {
   imageLimit = 0;
   /** 存储内存限制(MB) */
   storageLimit = 0;
+  /** 存储内存已使用(MB) */
+  storageUsed = 0;
   /** 可设置管理人员数量 */
   managerLimit = 0;
+
+  /** 志愿者在村落昵称 */
+  villageNickname = '';
 }
 
 export interface VillageTreeAnimProps {
@@ -300,7 +305,7 @@ export class LightVillageApi extends AppServerRequestModule<DataModel> {
       type_text?: string;
       sex_text?: string;
       status_text?: string;
-      image?: string;
+      avatar?: string;
     }[]>('/village/volunteer/getRanklist', '志愿者排行榜', params, undefined, undefined, {
       cacheEnable: true,
       cacheTime: 1000 * 60 * 10, //10min
@@ -406,15 +411,43 @@ export class LightVillageApi extends AppServerRequestModule<DataModel> {
     area_code: number,
     /**
      * 村落类型:
-      95=自然村
-      96=行政村,
-      334=社区
+       95=自然村
+       96=行政村,
+       334=社区
      */
     village_type: number,
   }) {
     return await this.post<KeyValue>('/village/village/addVillage', '创建村社', data);
   }
-  
+
+  async updateStorage(params: {
+    /** 村社ID */
+    villageId: number;
+    /** 文件大小(字节) */
+    memorySize: number;
+  }) {
+    return await this.post<KeyValue>('/village/village/updateStorage', '更新存储', {
+      village_id: params.villageId,
+      memory_size: params.memorySize,
+    });
+  }
+
+  /**
+   * 修改本志愿者在本村的昵称
+   * POST /village/volunteer/nicknameSave
+   */
+  async nicknameSave(params: {
+    /** 村社ID */
+    villageId: number;
+    /** 村社昵称 */
+    villageNickname: string;
+  }) {
+    return await this.post<KeyValue>('/village/volunteer/nicknameSave', '修改志愿者昵称', {
+      village_id: params.villageId,
+      village_nickname: params.villageNickname,
+    });
+  }
+
 }
 
 

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

@@ -363,6 +363,112 @@ export class UpgradeOrderItem extends DataModel<UpgradeOrderItem> {
   deletetime = '' as string | null;
 }
 
+export class FruitDepositItem extends DataModel<FruitDepositItem> {
+  constructor() {
+    super(FruitDepositItem, '充值套餐');
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+      price: { clientSide: 'number', serverSide: 'number' },
+      addFruit: { clientSide: 'number', serverSide: 'number' },
+      sort: { clientSide: 'number', serverSide: 'number' },
+      status: { clientSide: 'number', serverSide: 'number' },
+    };
+  }
+
+  id!: number;
+  name = '';
+  /** 金额 */
+  price = 0;
+  /** 乡源果数量 */
+  addFruit = 0;
+  sort = 0;
+  /** 状态: 0=禁用, 1=启用 */
+  status = 0;
+  statusText = '';
+  createtime = '' as string | null;
+  updatetime = '' as string | null;
+  deletetime = '' as string | null;
+}
+
+export class FruitDepositOrderItem extends DataModel<FruitDepositOrderItem> {
+  constructor() {
+    super(FruitDepositOrderItem, '充值乡源果订单');
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+      userId: { clientSide: 'number', serverSide: 'number' },
+      orderType: { clientSide: 'number', serverSide: 'number' },
+      staffLevelId: { clientSide: 'number', serverSide: 'number' },
+      villageId: { clientSide: 'number', serverSide: 'number' },
+      amount: { clientSide: 'number', serverSide: 'number' },
+      days: { clientSide: 'number', serverSide: 'number' },
+      status: { clientSide: 'number', serverSide: 'number' },
+      paytime: { clientSide: 'number', serverSide: 'number' },
+    };
+  }
+
+  id!: number;
+  orderNo = '';
+  userId = null as number | null;
+  /** 订单类型: 3=乡源果充值 */
+  orderType = 3;
+  staffLevelId = 0;
+  villageId = null as number | null;
+  amount = 0;
+  days = 0;
+  /** 状态: 0=待支付, 1=已支付 */
+  status = 0;
+  paytime = 0;
+  createtime = '' as string | null;
+  updatetime = '' as string | null;
+  orderTypeText = '';
+  payTypeText = '';
+  statusText = '';
+  paytimeText = '';
+  rewardStatusText = '';
+}
+
+export class FruitDepositOrderConfirm extends DataModel<FruitDepositOrderConfirm> {
+  constructor() {
+    super(FruitDepositOrderConfirm, '充值乡源果下单确认');
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      order: {
+        clientSide: 'object',
+        clientSideChildDataModel: FruitDepositOrderItem,
+        serverSide: 'undefined',
+        clientSideRequired: true,
+      },
+      pay: {
+        clientSide: 'object',
+        clientSideChildDataModel: {
+          convertTable: {
+            appId: { clientSide: 'string', clientSideRequired: true },
+            timeStamp: { clientSide: 'string', clientSideRequired: true },
+            nonceStr: { clientSide: 'string', clientSideRequired: true },
+            package: { clientSide: 'string', clientSideRequired: true },
+            signType: { clientSide: 'string', clientSideRequired: true },
+            paySign: { clientSide: 'string', clientSideRequired: true },
+          },
+        },
+        serverSide: 'undefined',
+        clientSideRequired: true,
+      },
+    };
+  }
+
+  order!: FruitDepositOrderItem;
+  pay!: {
+    appId: string;
+    timeStamp: string;
+    nonceStr: string;
+    package: string;
+    signType: string;
+    paySign: string;
+  };
+}
+
 export class UpgradeOrderConfirm extends DataModel<UpgradeOrderConfirm> {
   constructor() {
     super(UpgradeOrderConfirm, '升级订单确认');
@@ -826,6 +932,37 @@ export class TreeApi extends AppServerRequestModule<DataModel> {
     });
     return this.getStringDataOrMessage(res);
   }
+
+  /** 充值套餐列表 */
+  async getFruitDepositList(options?: {
+    page?: number;
+    pageSize?: number;
+    keywords?: string;
+  }) {
+    const res = await this.post<PagedGrowthResponse>(
+      '/village/growth/fruitDepositList',
+      '充值套餐列表',
+      {
+        page: options?.page,
+        pageSize: options?.pageSize,
+        keywords: options?.keywords,
+      },
+    );
+    return this.parsePagedList<FruitDepositItem>(
+      FruitDepositItem,
+      res.requireData(),
+      '充值套餐',
+    );
+  }
+
+  /** 充值乡源果下单 */
+  async placeOrder(fruitDepositId: number, villageId?: number) {
+    const res = await this.post<KeyValue>('/village/growth/placeOrder', '充值乡源果下单', {
+      fruit_deposit_id: fruitDepositId,
+      village_id: villageId,
+    });
+    return transformDataModel<FruitDepositOrderConfirm>(FruitDepositOrderConfirm, res.requireData());
+  }
 }
 
 export default new TreeApi();

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

@@ -1,7 +1,7 @@
 <template>
   <FlexCol>
     <view class="richtext-preview-box" @click="() => !disabled && !readonly ? edit() : preview()">
-      <Parse v-if="modelValue" :content="modelValue" containerStyle="max-height:400px" />
+      <Parse v-if="modelValue" :content="modelValue" containerStyle="max-height:400px;word-break:break-all;" />
       <Text v-else color="text.second">{{placeholder}}</Text>
       <view v-if="modelValue" class="richtext-preview-mask" :style="maskStyle" />
     </view>

+ 2 - 1
src/common/components/parts/IndexCommonImageItem.vue

@@ -16,7 +16,7 @@
     <template #footer> 
       <FlexRow v-if="userName || likes" justify="space-between" align="center" :padding="[10,0]" :margin="[10,0,0,0]">
         <FlexRow align="center" :gap="10">
-          <Avatar :url="image" :size="40" />
+          <Avatar :url="userAvatar || image" defaultAvatar="https://xy.wenlvti.net/app_static/images/mine/DefaultAvatar.png" :size="40" />
           <Text :text="userName" :fontSize="24" color="gray" />
         </FlexRow>
         <FlexRow align="center" :gap="10">
@@ -40,6 +40,7 @@ const props = defineProps<{
   title?: string;
   desc?: string;
   userName?: string;
+  userAvatar?: string;
   likes?: number;
   isLike?: boolean;
   defaultImage?: string;

+ 40 - 13
src/common/components/upload/AliOssUploadCo.ts

@@ -1,6 +1,7 @@
 import { Base64Utils, RandomUtils, StringUtils } from "@imengyu/imengyu-utils";
 import { hmacSha1Base64 } from "./hmac";
 import type { UploaderAction } from "@/components/form/Uploader";
+import LightVillageApi from "@/api/light/LightVillageApi";
 
 const client = {
   region: 'oss-cn-shenzhen',
@@ -76,23 +77,49 @@ function uploadOSS(uploadPath: string, filePath: string, cancelHandler: { cancel
   })
 }
 
-export function useAliOssUploadCo(subPath: string) {
+export function useAliOssUploadCo(subPath: string, storageManage?: {
+  getVillageId: () => number,
+  overflow: () => void,
+}) {
   return (action: UploaderAction) => {
-    const name = StringUtils.path.getFileName(action.item.filePath);
-    const uploadPath = `${subPath}/${name.split('.')[0]}-${RandomUtils.genNonDuplicateID(26)}.${StringUtils.path.getFileExt(name)}`;  
     const cancelHandler: { cancel: () => void } = {
       cancel: () => {},
-    };    
-    uploadOSS(uploadPath, action.item.filePath, cancelHandler,(progress) => {
-      action.onProgress?.(progress)
-    }).then((res) => {  
-      action.onFinish?.({
-        uploadedUrl: res,
-        previewUrl: res,
+    }; 
+
+    if (storageManage && action.item.size) {
+      const villageId = storageManage.getVillageId();
+      //更新存储
+      LightVillageApi.updateStorage({
+        villageId,
+        memorySize: action.item.size,
+      }).then(() => {
+        doUpload();
+      }).catch((err) => {
+        if ((err + '').includes('超')) {
+          //存储已超限
+          storageManage.overflow();
+        }
+        action.onError?.(err);
       });
-    }).catch((err) => {
-      action.onError?.(err);
-    });
+    } else {
+      doUpload();
+    }
+
+    function doUpload() {
+      const name = StringUtils.path.getFileName(action.item.filePath);
+      const uploadPath = `${subPath}/${name.split('.')[0]}-${RandomUtils.genNonDuplicateID(26)}.${StringUtils.path.getFileExt(name)}`;  
+      uploadOSS(uploadPath, action.item.filePath, cancelHandler,(progress) => {
+        action.onProgress?.(progress)
+      }).then((res) => {
+        action.onFinish?.({
+          uploadedUrl: res,
+          previewUrl: res,
+        });
+      }).catch((err) => {
+        action.onError?.(err);
+      });
+    }
+
     return () => {
       //取消上传
       cancelHandler.cancel();

+ 29 - 1
src/common/components/upload/ImageUploadCo.ts

@@ -1,10 +1,36 @@
 import CommonContent from "@/api/CommonContent";
+import LightVillageApi from "@/api/light/LightVillageApi";
 import type { UploaderAction } from "@/components/form/Uploader";
 
-export function useImageSimpleUploadCo(additionData?: Record<string, any>) {
+export function useImageSimpleUploadCo(storageManage?: {
+  getVillageId: () => number,
+  overflow: () => void,
+}, additionData?: Record<string, any>) {
   return (action: UploaderAction) => {
     action.onStart('正在上传');
     let task: UniApp.UploadTask | null = null;
+
+    if (storageManage && action.item.size) {
+      const villageId = storageManage.getVillageId();
+      //更新存储
+      LightVillageApi.updateStorage({
+        villageId,
+        memorySize: action.item.size,
+      }).then(() => {
+        doUpload();
+      }).catch((err) => {
+        if ((err + '').includes('超')) {
+          //存储已超限
+          storageManage.overflow();
+        }
+        action.onError?.(err);
+      });
+    } else {
+      doUpload();
+    }
+    
+    function doUpload() {
+
     CommonContent.uploadFile(action.item.filePath, 'image', 'file', additionData, (t) => {
       task = t;
       task.onProgressUpdate((res) => {
@@ -20,6 +46,8 @@ export function useImageSimpleUploadCo(additionData?: Record<string, any>) {
       }).catch((err) => {
         action.onError?.(err);
       })
+    }
+
     return () => {
       task?.abort();
     };

+ 12 - 0
src/components/dynamic/DynamicForm.ts

@@ -249,6 +249,11 @@ export interface IDynamicFormRef {
    */
   getFormItemControlRef: <T>(key: string) => T;
   /**
+   * 获取通过 props.globalParams 传入的全局参数
+   * @returns 
+   */
+  getGlobalParams: () => IDynamicFormObject;
+  /**
    * 获取指定类型的所有表单项组件的 Ref
    * @param type 组件类型
    * @returns 
@@ -291,6 +296,13 @@ export interface IDynamicFormRef {
    */
   dispatchReload: () => void;
   /**
+   * 向顶层表单组件发送消息事件。
+   * @param messageName 消息名称。  
+   * @param data 可选参数。
+   * @returns 
+   */
+  emitMessage: (messageName: string, ...data: unknown[]) => void;
+  /**
    * 获取当前表单中可见的所有字段名
    */
   getVisibleFormNames: () => string[];

+ 35 - 4
src/components/dynamic/DynamicForm.vue

@@ -149,7 +149,33 @@ function dispatchReload() {
   dispatchMessage(MESSAGE_RELOAD);
 }
 
-//初始化默认值到模型
+/**
+ * 初始化默认值到模型
+ * 
+ * currentKey 递归规则
+ * * flat-simple/flat-group 忽略本级key继承父级:
+ *    例如: 以下结构应推断路径为 user.district
+        object (name: "user")
+          └─ flat-simple (name: "group1")
+            └─ flat-simple (name: "group2")
+              └─ dropdown1 (name: "district", defaultValue: "Beijing")
+ * * object/object-group 父级key.子级key
+ *   例如: 以下结构应推断路径为 user.info.district
+        object (name: "user")
+          └─ object (name: "info")
+            └─ dropdown1 (name: "district", defaultValue: "Beijing")
+ * * array 父级key.[子级索引]
+ *   例如: 以下结构应推断路径为 user.activityList[${index}]
+        object (name: "user")
+          └─ array (name: "activityList")
+            └─ text (name: "self", defaultValue: "Hello")
+
+ * * array-object 父级key.[子级索引]子级key
+ *   例如: 以下结构应推断路径为 user.activityList[${index}].name
+        object (name: "user")
+          └─ array-object (name: "activityList")
+            └─ text (name: "name", defaultValue: "Hello")
+ */
 function initDefaultValuesToModel() {
   function loopItems(key: string, parentKey: string, type: string, items: IDynamicFormItem[]) {
     let i = 0;
@@ -166,14 +192,15 @@ function initDefaultValuesToModel() {
           currentKey = (key ? key + '.' : '') + item.name;
           break
         case 'array':
-          currentKey = (parentKey ? parentKey + '.' : '') + `[${i}]`;
+          currentKey = (parentKey ? parentKey : '') + `[${i}]`;
           break;
         case 'array-object':
-          currentKey = (parentKey ? parentKey + '.' : '') + `[${i}]` + item.name;
+          currentKey = (parentKey ? parentKey : '') + `[${i}].` + item.name;
           break;
       }
       if (item.children) {
-        loopItems(currentKey, key, item.type || '', item.children);
+        const childParentKey = (item.type === 'flat-simple' || item.type === 'flat-group') ? parentKey : currentKey;
+        loopItems(currentKey, childParentKey, item.type || '', item.children);
       }
       if (item.defaultValue !== undefined) {
         const oldValue = accessFormModel(currentKey, false, undefined);
@@ -206,6 +233,9 @@ const formRef : IDynamicFormRef = {
       throw new Error('Form instance is not create.');
     return formEditor.value
   },
+  getGlobalParams() {
+    return props.globalParams;
+  },
   getFormItemControlRefsByType: getFormItemControlRefsByType as any,
   getFormItemControlRef: getFormItemControlRef as any,
   submit() { return this.getFormRef().validate(); },
@@ -222,6 +252,7 @@ const formRef : IDynamicFormRef = {
   },
   dispatchMessage,
   dispatchReload,
+  emitMessage: (m, ...p) => emit(m as any, ...p),
 };
 
 provide('formRef', formRef);

+ 1 - 9
src/components/dynamic/wrappers/PickerIdField.vue

@@ -4,7 +4,7 @@
     v-bind="$attrs"
     :singleValue="true"
     :modelValue="!(modelValue instanceof Array) ? [modelValue || ''] : modelValue"
-    @update:modelValue="handleUpdateModelValue"
+    @update:modelValue="emit('update:modelValue', $event)"
   />
 </template>
 
@@ -27,14 +27,6 @@ const loader = useDataLoader<PickerIdFieldOption[]>(async () => {
   immediate: true,
 });
 
-function handleUpdateModelValue(value: (string|number)[]) {
-  if (value.length === 1) {
-    emit('update:modelValue', value[0]);
-    return;
-  }
-  emit('update:modelValue', value);
-}
-
 defineExpose({
   reload() {
     loader.load();

+ 3 - 0
src/components/form/FormContext.ts

@@ -154,6 +154,7 @@ export function useFieldChildValueInjector<T>(
   },
   fieldClick?: () => void,
   initialValue?: T,
+  solveValue?: (value: T) => T,
 ) {
   const cellContext = useCellContext();
   const context = useInjectFormItemContext();
@@ -175,6 +176,8 @@ export function useFieldChildValueInjector<T>(
    * @param newValue 新值
    */
   function updateValue(newValue: T) {
+    if (solveValue)
+      newValue = solveValue(newValue);
     if (secondParentContext)
       secondParentContext.updateValue(newValue);
     else

+ 7 - 0
src/components/form/PickerField.vue

@@ -77,6 +77,11 @@ export interface PickerFieldProps extends Omit<PickerProps, 'value'> {
    * 是否只读
    */
   readonly?: boolean,
+  /**
+   * 在数据值更新后自动处理
+   * @default 默认有一个回调,在只有一列时,转换为单值,多列时,转换为数组。
+   */
+  valueSolve?: (value: (string | number)[]) => any,
 }
 
 const emit = defineEmits([ 'update:modelValue', 'cancel', 'confirm', 'selectTextChange', 'tempValueChange' ]);
@@ -90,6 +95,7 @@ const props = withDefaults(defineProps<PickerFieldProps>(), {
   }),
   showSelectText: true,
   singleValue: false,
+  valueSolve: (value: any) => (value.length === 1 ? value[0] : value),
 });
 
 const popupShow = ref(false);
@@ -106,6 +112,7 @@ const {
     popupShow.value = true;
   },
   props.initalValue,
+  props.valueSolve,
 );
 
 const {

+ 2 - 2
src/components/form/Uploader.vue

@@ -80,7 +80,7 @@ import UploaderListItem from './UploaderListItem.vue';
 import FlexView from '../layout/FlexView.vue';
 import FlexCol from '../layout/FlexCol.vue';
 import type { UploaderAction, UploaderItem } from './Uploader';
-import { Debounce, LogUtils } from '@imengyu/imengyu-utils';
+import { Debounce, formatError, LogUtils } from '@imengyu/imengyu-utils';
 import Text from '../basic/Text.vue';
 import { actionSheet } from '../dialog/CommonRoot';
 
@@ -536,7 +536,7 @@ function startUploadItem(item: UploaderItem) {
       item,
       onError(error) {
         item.state = 'fail';
-        item.message = ('' + error) || '上传失败';
+        item.message = ('' + formatError(error)).replace('Error: ', '');
         updateListItem(item);
         reject(error);
         LogUtils.printLog(TAG, 'error', `上传文件 ${item.filePath} 失败,错误信息:${error}`);

+ 21 - 0
src/pages.json

@@ -102,6 +102,27 @@
       }
     },
     {
+      "path": "pages/home/village/bless/recharge",
+      "style": {
+        "navigationBarTitleText": "充值乡源果",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/home/village/bless/pay-select",
+      "style": {
+        "navigationBarTitleText": "赐福支付",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/home/village/upgrade/upgrade-village",
+      "style": {
+        "navigationBarTitleText": "升级村社",
+        "navigationStyle": "custom"
+      }
+    },
+    {
       "path": "pages/home/village/orders",
       "style": {
         "navigationBarTitleText": "我的订单",

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

@@ -120,9 +120,21 @@ function goJoin() {
     villageId: querys.value.villageId,
   });
 }
-onMounted(async () => {
+async function loadVolunteerInfo() {
   await getIsVolunteer();
   isManagement.value = await getIsManagement(querys.value.villageId);
   isJoined.value = await getIsJoinedVillage(querys.value.villageId);
+}
+
+onMounted(async () => {
+  await loadVolunteerInfo();
 });
+
+
+defineExpose({
+  onPageBack(name: string, param: any) {
+    if (name === 'registerDone')
+      loadVolunteerInfo();
+  }
+})
 </script>

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

@@ -23,6 +23,7 @@
               :options="formDefine"
               :model="formModel"
               :globalParams="querys"
+              @storageOverflow="handleStorageOverflow"
             />
           </ProvideVar>
         </BackgroundBox>
@@ -48,7 +49,7 @@ import { nextTick, ref, watch, type Ref } from 'vue';
 import { useLoadQuerys } from '@/components/composeabe/LoadQuerys';
 import { CollectableModulesIdMap, getVillageInfoForm, getVillageInfoFormIds, type SingleForm } from './forms';
 import { showError } from '@/common/composeabe/ErrorDisplay';
-import { backAndCallOnPageBack } from '@/components/utils/PageAction';
+import { backAndCallOnPageBack, navTo } from '@/components/utils/PageAction';
 import { toast } from '@/components/utils/DialogAction';
 import { alert, confirm } from '@/components/dialog/CommonRoot';
 import { Debounce, RequestApiError, waitTimeOut } from '@imengyu/imengyu-utils';
@@ -56,7 +57,6 @@ import VillageInfoApi, { CommonInfoModel } from '@/api/inhert/VillageInfoApi';
 import DynamicForm from '@/components/dynamic/DynamicForm.vue';
 import LoadingPage from '@/components/display/loading/LoadingPage.vue';
 import Button from '@/components/basic/Button.vue';
-import CommonRoot from '@/components/dialog/CommonRoot.vue';
 import FlexCol from '@/components/layout/FlexCol.vue';
 import Height from '@/components/layout/space/Height.vue';
 import XBarSpace from '@/components/layout/space/XBarSpace.vue';
@@ -66,7 +66,6 @@ import type { UploaderFieldInstance } from '@/components/form/UploaderField.vue'
 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);
@@ -138,6 +137,7 @@ const { querys } = useLoadQuerys({
         findId,
         model,
       );
+      console.log('加载数据', formData);
     }
   } catch (e) {
     if (!(e instanceof RequestApiError && e.errorMessage.startsWith('请完成')))
@@ -259,10 +259,10 @@ function getSaveName() {
 async function loadLocalSave(formData: CommonInfoModel|undefined) {
   if (!currentFormInfo)
     return formData;
-  console.log('加载暂存数据');
   const saveName = getSaveName();
   const saveData = uni.getStorageSync(saveName);
   if (saveData) {
+    console.log('加载暂存数据');
     const res = await confirm({
       content: '您有上次编辑未完成的内容,是否要从上次的编辑数据继续?',
       cancelText: '取消',
@@ -273,7 +273,7 @@ async function loadLocalSave(formData: CommonInfoModel|undefined) {
       const d = JSON.parse(saveData);
       d.id = 0;
       formData = new currentFormInfo[0]().fromServerSide(d) as CommonInfoModel;  
-      console.log('暂存数据', formData);
+      console.log('读取暂存数据', formData);
     } else
       deleteLocalSave();
   }
@@ -291,6 +291,20 @@ function saveLocalSave() {
   console.log('保存暂存数据');
 }
 
+async function handleStorageOverflow() {
+  const res = await confirm({
+    title: '提示',
+    content: '当前村社采编数据库已超过存储限制啦,无法再上传资源,您可以选择升级村社获得更多存储空间。',
+    confirmText: '去升级',
+    cancelText: '取消',
+  });
+  if (res) {
+    navTo('/pages/home/village/upgrade/upgrade-village', {
+      villageId: querys.value.villageId,
+    });
+  }
+}
+
 watch(formModel, () => {
   if (!canSaveNow)
     return;

+ 4 - 1
src/pages/dig/forms/data/building.ts

@@ -532,7 +532,10 @@ export const villageInfoDistributionForm : SingleForm = [CommonInfoModel, (r) =>
       type: 'uploader', 
       defaultValue: '',
       additionalProps: {
-        upload: useAliOssUploadCo('xiangyuan/distribution'),
+        upload: useAliOssUploadCo('xiangyuan/distribution', {
+          getVillageId: () => r.value.getGlobalParams().villageId,
+          overflow: () => r.value.emitMessage('storageOverflow'),
+        }),
         maxFileSize: 1024 * 1024 * 20,
         single: true,
       } as UploaderFieldProps,

+ 16 - 4
src/pages/dig/forms/data/common.ts

@@ -95,7 +95,10 @@ export function villageCommonContent (ref: Ref<IDynamicFormRef>, options: {
             type: 'uploader',
             defaultValue: '',
             additionalProps: {
-              upload: useAliOssUploadCo('xiangyuan/common'),
+              upload: useAliOssUploadCo('xiangyuan/common', {
+                getVillageId: () => ref.value.getGlobalParams().villageId,
+                overflow: () => ref.value.emitMessage('storageOverflow'),
+              }),
               maxFileSize: 1024 * 1024 * 20, // 20MB
               maxUploadCount: 20,
             } as UploaderFieldProps,
@@ -110,7 +113,10 @@ export function villageCommonContent (ref: Ref<IDynamicFormRef>, options: {
             type: 'uploader',
             defaultValue: '',
             additionalProps: {
-              upload: useAliOssUploadCo('xiangyuan/video'),
+              upload: useAliOssUploadCo('xiangyuan/video', {
+                getVillageId: () => ref.value.getGlobalParams().villageId,
+                overflow: () => ref.value.emitMessage('storageOverflow'),
+              }),
               maxFileSize: 1024 * 1024 * 1000, // 1000MB
               single: true,
               chooseType: 'video',
@@ -142,7 +148,10 @@ export function villageCommonContent (ref: Ref<IDynamicFormRef>, options: {
             type: 'uploader',
             defaultValue: '',
             additionalProps: {
-              upload: useAliOssUploadCo('xiangyuan/archives'),
+              upload: useAliOssUploadCo('xiangyuan/archives', {
+                getVillageId: () => ref.value.getGlobalParams().villageId,
+                overflow: () => ref.value.emitMessage('storageOverflow'),
+              }),
               maxFileSize: 1024 * 1024 * 200, // 200MB
               chooseType: '',
               single: true,
@@ -158,7 +167,10 @@ export function villageCommonContent (ref: Ref<IDynamicFormRef>, options: {
             type: 'uploader',
             defaultValue: '',
             additionalProps: {
-              upload: useAliOssUploadCo('xiangyuan/annex'),
+              upload: useAliOssUploadCo('xiangyuan/annex', {
+                getVillageId: () => ref.value.getGlobalParams().villageId,
+                overflow: () => ref.value.emitMessage('storageOverflow'),
+              }),
               maxFileSize: 1024 * 1024 * 1000, // 1000MB
               maxUploadCount: 20,
               chooseType: '',

+ 8 - 3
src/pages/dig/forms/data/cultural.ts

@@ -52,7 +52,6 @@ export function villageInfoFolkCultureForm(title: string) : SingleForm {
       ...villageCommonContent(m, {
         title: title,
         showTitle: false,
-        contentKey: 'details'
       }).formItems
     ]
   }), { title: title, typeName: 'folkCultureType', }];
@@ -79,7 +78,10 @@ export const villageInfoCulture : GroupForm = {
         type: 'uploader',
         defaultValue: '',
         additionalProps: {
-          upload: useAliOssUploadCo('xiangyuan/cultural/scan'),
+          upload: useAliOssUploadCo('xiangyuan/cultural/scan', {
+            getVillageId: () => m.value.getGlobalParams().villageId,
+            overflow: () => m.value.emitMessage('storageOverflow'),
+          }),
           maxFileSize: 1024 * 1024 * 20,
           maxUploadCount: 20,
         } as UploaderFieldProps,
@@ -165,7 +167,10 @@ export const villageInfoCulture : GroupForm = {
           type: 'uploader',
           defaultValue: '',
           additionalProps: {
-            upload: useAliOssUploadCo('xiangyuan/cultural/video'),
+            upload: useAliOssUploadCo('xiangyuan/cultural/video', {
+              getVillageId: () => m.value.getGlobalParams().villageId,
+              overflow: () => m.value.emitMessage('storageOverflow'),
+            }),
             chooseType: '',
             maxFileSize: 1024 * 1024 * 1000, // 1000MB
             single: true,

+ 4 - 1
src/pages/dig/forms/data/element.ts

@@ -137,7 +137,10 @@ export const vilElementForm : SingleForm = [CommonInfoModel, (r) => ({
           type: 'uploader', 
           defaultValue: '',
           additionalProps: {
-            upload: useAliOssUploadCo('xiangyuan/element'),
+            upload: useAliOssUploadCo('xiangyuan/element', {
+              getVillageId: () => r.value.getGlobalParams().villageId,
+              overflow: () => r.value.emitMessage('storageOverflow'),
+            }),
             maxFileSize: 1024 * 1024 * 20,
             maxUploadCount: 20,
           } as UploaderFieldProps,

+ 1 - 1
src/pages/dig/forms/data/food.ts

@@ -20,7 +20,7 @@ export function villageInfoFoodProductsForm(title: string) : SingleForm {
       }, 
       {
         label: `${title}详情`,
-        name: 'details',
+        name: 'content',
         type: 'richtext',
         defaultValue: '',
         additionalProps: {

+ 4 - 1
src/pages/dig/forms/data/relic.ts

@@ -127,7 +127,10 @@ export const villageInfoRelicForm : SingleForm = [CommonInfoModel, (r) => ({
           type: 'uploader', 
           defaultValue: '',
           additionalProps: {
-            upload: useAliOssUploadCo('xiangyuan/relic'),
+            upload: useAliOssUploadCo('xiangyuan/relic', {
+              getVillageId: () => r.value.getGlobalParams().villageId,
+              overflow: () => r.value.emitMessage('storageOverflow'),
+            }),
             maxFileSize: 1024 * 1024 * 20,
             maxUploadCount: 20,
           } as UploaderFieldProps,

+ 8 - 2
src/pages/dig/forms/data/specker.ts

@@ -89,7 +89,10 @@ export const villageInfoSpeakerForm : SingleForm = [CommonInfoModel, (r) => ({
           type: 'uploader',
           defaultValue: '',
           additionalProps: {
-            upload: useAliOssUploadCo('xiangyuan/speaker'),
+            upload: useAliOssUploadCo('xiangyuan/speaker', {
+              getVillageId: () => r.value.getGlobalParams().villageId,
+              overflow: () => r.value.emitMessage('storageOverflow'),
+            }),
             maxFileSize: 1024 * 1024 * 20,
             maxUploadCount: 1,
           } as UploaderFieldProps,
@@ -111,7 +114,10 @@ export const villageInfoSpeakerForm : SingleForm = [CommonInfoModel, (r) => ({
           type: 'uploader',
           defaultValue: '',
           additionalProps: {
-            upload: useAliOssUploadCo('xiangyuan/speaker/audio'),
+            upload: useAliOssUploadCo('xiangyuan/speaker/audio', {
+              getVillageId: () => r.value.getGlobalParams().villageId,
+              overflow: () => r.value.emitMessage('storageOverflow'),
+            }),
             maxFileSize: 1024 * 1024 * 500, // 500MB
             maxUploadCount: 1,
             accept: 'audio/*',

+ 9 - 3
src/pages/dig/forms/data/travel.ts

@@ -7,7 +7,7 @@ import type { UploaderFieldProps } from "@/components/form/UploaderField.vue";
 import type { GroupForm, SingleForm } from "../forms";
 import { villageCommonContent } from "./common";
 
-export const villageInfoTravelGuideForm : SingleForm = [CommonInfoModel, () => ({
+export const villageInfoTravelGuideForm : SingleForm = [CommonInfoModel, (r) => ({
   formItems: [
     {
       label: '交通基本信息', 
@@ -161,7 +161,10 @@ export const villageInfoTravelGuideForm : SingleForm = [CommonInfoModel, () => (
           type: 'uploader', 
           defaultValue: '',
           additionalProps: {
-            upload: useAliOssUploadCo('xiangyuan/travel/panorama'),
+            upload: useAliOssUploadCo('xiangyuan/travel/panorama', {
+              getVillageId: () => r.value.getGlobalParams().villageId,
+              overflow: () => r.value.emitMessage('storageOverflow'),
+            }),
             maxFileSize: 1024 * 1024 * 20,
             single: true,
           } as UploaderFieldProps,
@@ -173,7 +176,10 @@ export const villageInfoTravelGuideForm : SingleForm = [CommonInfoModel, () => (
           type: 'uploader', 
           defaultValue: '',
           additionalProps: {
-            upload: useAliOssUploadCo('xiangyuan/travel/guide'),
+            upload: useAliOssUploadCo('xiangyuan/travel/guide', {
+              getVillageId: () => r.value.getGlobalParams().villageId,
+              overflow: () => r.value.emitMessage('storageOverflow'),
+            }),
             maxFileSize: 1024 * 1024 * 20,
             single: true,
           } as UploaderFieldProps,

+ 13 - 7
src/pages/dig/forms/list-ordinary.vue

@@ -290,19 +290,23 @@ function loadListCatalog(catalog: VillageCatalogListItem) {
   listLoader.load(true, currentLoadData.value)
 }
 
-const { querys } = useLoadQuerys({ 
-  collectModuleId: 0,
-  villageId: 0,  
-  title: '',
-}, async (querys) => {
-  isManagement.value = await getIsManagement(querys.villageId);
+async function loadVolunteerInfo() {
+  isManagement.value = await getIsManagement(querys.value.villageId);
   try {
     //普通用户进入预览模式
     await getIsVolunteer();
-    canCollect.value = await getCanCollect(querys.villageId);
+    canCollect.value = await getCanCollect(querys.value.villageId);
   } catch {
     canCollect.value = false;
   }
+}
+
+const { querys } = useLoadQuerys({ 
+  collectModuleId: 0,
+  villageId: 0,  
+  title: '',
+}, async (querys) => {
+  await loadVolunteerInfo();
 
   function pushCatalogWithCurrentCatalog(catalog: VillageCatalogListItem) {
     if (catalog.collectModuleId === querys.collectModuleId) {
@@ -336,6 +340,8 @@ defineExpose({
   onPageBack(name: string, param: any) {
     if (param && param.needRefresh)
       listLoader.reload();
+    if (name === 'registerDone')
+      loadVolunteerInfo();
   }
 })
 </script>

+ 25 - 17
src/pages/dig/forms/list.vue

@@ -46,8 +46,18 @@
                 <FlexCol>
                   <H4 :size="36">{{ item.title }}</H4>
                   <Text :size="23">{{ item.desc }}</Text>
+                  <Text fontConfig="secondText" :text="item.from" />
                 </FlexCol>
               </Touchable>
+              <FlexCol>
+                <FlexRow>
+                  <FrameButton :text="item.statusText" size="midium" @click="navTo('/pages/dig/about/goving')" />
+                </FlexRow>
+                <FlexRow gap="gap.md">
+                  <IconButton icon="appreciate-light-fill" size="30" :text="item.likes" color="primary" />
+                  <IconButton icon="appreciate-light" size="30" :rotate="180" :text="item.disLikes" color="primary" />
+                </FlexRow>
+              </FlexCol>
             </BoxMid>
           </FlexCol>
           <template #empty>
@@ -92,6 +102,8 @@ import { getVillageInfoForm } from './forms';
 import CommonTopBanner from '@/common/components/CommonTopBanner.vue';
 import PrimaryButton from '@/common/components/PrimaryButton.vue';
 import BoxMid from '@/common/components/box/BoxMid.vue';
+import FrameButton from '@/common/components/FrameButton.vue';
+import IconButton from '@/components/basic/IconButton.vue';
 
 const subTitle = ref('');
 const searchText = ref('');
@@ -102,19 +114,7 @@ const authStore = useAuthStore();
 
 const error = ref('');
 
-const listLoader = useSimplePageListLoader<{
-  id: number,
-  image: string,
-  title: string,
-  desc: string,
-  villageVolunteerId: number,
-}, {
-  villageId: number,  
-  villageVolunteerId: number,
-  collectModuleId: number,
-  subId: number,
-  subKey: string,
-}>(8, async (page, pageSize, params) => {
+const listLoader = useSimplePageListLoader(8, async (page, pageSize, params) => {
   if (!params )
     throw new Error("未传入参数,当前页面需要参数");
   if (!params.collectModuleId)
@@ -139,7 +139,11 @@ const listLoader = useSimplePageListLoader<{
       villageVolunteerId: item.villageVolunteerId,
       desc: DataDateUtils.formatDate(item.updatedAt, 'YYYY-MM-dd') + (
         authStore.isAdmin || isManagement.value ? (' 投稿人:' + item.villageVolunteerName) : ''
-      )
+      ),
+      from: '投稿人:' + item.villageVolunteerName,
+      disLikes: item.dislikeNum.toString(),
+      likes: item.likeNum.toString(),
+      statusText: item.statusText,
     }
   })
   return {
@@ -197,6 +201,10 @@ function goJoin() {
     villageId: querys.value.villageId,
   });
 }
+async function loadVolunteerInfo() {
+  isJoined.value = await getIsJoinedVillage(querys.value.villageId);
+  isManagement.value = await getIsManagement(querys.value.villageId);
+}
 
 const { querys } = useLoadQuerys({ 
   villageId: 0,  
@@ -208,9 +216,7 @@ const { querys } = useLoadQuerys({
   subTitle: '',
   isView: false,
 }, async (querys) => {
-  isJoined.value = await getIsJoinedVillage(querys.villageId);
-  isManagement.value = await getIsManagement(querys.villageId);
-
+  await loadVolunteerInfo();
   if (querys.collectModuleId) {
     //普通用户进入预览模式
     await getIsVolunteer();
@@ -251,6 +257,8 @@ defineExpose({
   onPageBack(name: string, param: any) {
     if (param && param.needRefresh)
       listLoader.reload();
+    if (name === 'registerDone')
+      loadVolunteerInfo();  
   }
 })
 </script>

+ 2 - 1
src/pages/home/discover/index.vue

@@ -65,6 +65,7 @@
               <Avatar 
                 :src="good.villageVolunteerAvatar"
                 :size="40"
+                defaultAvatar="https://xy.wenlvti.net/app_static/images/mine/DefaultAvatar.png"
               />
               <Text :text="`发布人: ${good.villageVolunteerName}`" fontConfig="contentText" />
             </FlexRow>
@@ -149,7 +150,7 @@ const villageUserRankListLoader = useSimpleDataLoader(async () => {
   const res = (await LightVillageApi.getVolunteerRankList({ num: 10 }))
     .map((item, i) => ({
       id: item.id,
-      image: item.image ?? '',
+      image: item.avatar ?? '',
       title: item.name,
       rank: i + 1,
       score: item.points,

+ 2 - 1
src/pages/home/index.vue

@@ -167,6 +167,7 @@
             :title="item.title"
             :desc="item.content ?? ''"
             :userName="item.villageVolunteerName ?? ''"
+            :userAvatar="item.villageVolunteerAvatar"
             :likes="0"
             :isLike="false"
             @click="handleGoRecommendDetails(item)"
@@ -288,7 +289,7 @@ const villageUserRankListLoader = useSimpleDataLoader(async () => {
   const res = (await LightVillageApi.getVolunteerRankList({ num: 3 }))
     .map((item, i) => ({
       id: item.id,
-      image: item.image ?? '',
+      image: item.avatar ?? '',
       title: item.name,
       rank: i + 1,
       score: item.points,

+ 2 - 2
src/pages/home/light/submit-volunteer.vue

@@ -94,7 +94,7 @@ import { UserApi } from '@/api/auth/UserApi';
 import { waitTimeOut } from '@imengyu/imengyu-utils';
 import { fillClaimFromVolunteer, getClaimVillageForm } from './form/claim';
 import { getVolunteerForm } from './form/volunteer';
-import { back } from '@/components/utils/PageAction';
+import { backAndCallOnPageBack } from '@/components/utils/PageAction';
 import { closeToast, toast } from '@/components/dialog/CommonRoot';
 import { showError } from '@/common/composeabe/ErrorDisplay';
 import type { IDynamicFormOptions, IDynamicFormRef } from '@/components/dynamic';
@@ -276,7 +276,7 @@ async function finishAndBack() {
     await villageStore.loadMyJoinedVillages();
   } finally {
     uni.hideLoading();
-    back();
+    backAndCallOnPageBack('registerDone', {});
   }
 }
 </script>

+ 102 - 0
src/pages/home/village/bless/pay-select.vue

@@ -0,0 +1,102 @@
+
+
+<template>
+  <CommonTopBanner 
+    title="赐福支付"
+    showNav
+  >
+    <FlexCol gap="gap.lg" padding="padding.md">
+      <FlexRow center>
+        <Image src="https://xy.wenlvti.net/app_static/images/village/IconBlessing.png" :width="100" :height="100" />
+      </FlexRow>
+      <FlexCol padding="padding.md" center>
+        <Text 
+          text="请选择您要付款方式" 
+          fontConfig="contentText" :fontSize="30" textAlign="center" 
+        />
+      </FlexCol>
+      <BuyFruitInfo :price="querys.blessPackagePrice" @pay="handleDirectPay(3)">
+        <template #prepend>
+          <BoxMid direction="row" justify="space-between" align="center" gap="gap.md">
+            <FlexCol width="74%">
+              <FlexRow align="center" gap="gap.md">
+                <Icon icon="wechat" size="36" />
+                <Text text="在线支付" fontConfig="lightImportantTitle" :fontSize="42" />
+              </FlexRow>
+              <Text text="推荐使用微信线支付方式,方便快捷,立即生效" fontConfig="lightGoldTitle" :fontSize="30" />
+            </FlexCol>
+            <FrameButton primary text="选择" @click="handleDirectPay(1)" />
+          </BoxMid>
+        </template>
+      </BuyFruitInfo>
+    </FlexCol>
+  </CommonTopBanner>
+</template>
+
+<script setup lang="ts">
+import { useLoadQuerys } from '@/components/composeabe/LoadQuerys';
+import { useRequireLogin } from '@/common/composeabe/RequireLogin';
+import { ref } from 'vue';
+import { navTo, backAndCallOnPageBack } from '@/components/utils/PageAction';
+import { showError } from '@/common/composeabe/ErrorDisplay';
+import BoxMid from '@/common/components/box/BoxMid.vue';
+import CommonTopBanner from '@/common/components/CommonTopBanner.vue';
+import FrameButton from '@/common/components/FrameButton.vue';
+import Text from '@/components/basic/Text.vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import FlexRow from '@/components/layout/FlexRow.vue';
+import Image from '@/components/basic/Image.vue';
+import DirectPayDialog from './dialogs/DirectPayDialog.vue';
+import TreeApi from '@/api/light/TreeApi';
+import Icon from '@/components/basic/Icon.vue';
+import BuyFruitInfo from '../upgrade/components/BuyFruitInfo.vue';
+import { assertNotNull } from '@imengyu/imengyu-utils';
+
+const { requireLoginAsync } = useRequireLogin();
+
+const directPayDialog = ref<InstanceType<typeof DirectPayDialog>>();
+
+function handlePaySuccess() {
+  setTimeout(() => {
+    backAndCallOnPageBack('blessPaySuccessRefresh', {});
+  }, 100);
+}
+
+const { querys } = useLoadQuerys({
+  villageId: 0,
+  blessPackageId: 0,
+  blessPackagePrice: 0,
+});
+
+async function handleDirectPay(payType: 1 | 3) {
+  if (!requireLoginAsync('登录后为村社升级,做出你的贡献哦'))
+    return;
+  try {
+    uni.showLoading({ title: '创建订单中...' });
+    const payInfo = await TreeApi.createBlessOrder(querys.value.villageId, querys.value.blessPackageId, payType);
+    if (payType === 1) {
+      assertNotNull(payInfo, '支付信息不存在');
+      await new Promise<void>((resolve, reject) => {
+        uni.requestPayment({
+          provider: 'wxpay',
+          appId: payInfo.pay.appId,
+          timeStamp: payInfo.pay.timeStamp,
+          nonceStr: payInfo.pay.nonceStr,
+          package: payInfo.pay.package,
+          signType: payInfo.pay.signType,
+          paySign: payInfo.pay.paySign,
+          success: () => resolve(),
+          fail: reject,
+        });
+      });
+      handlePaySuccess();
+    } else if (payType === 3) {
+      handlePaySuccess();
+    }
+  } catch (e) {
+    showError(e);
+  } finally {
+    uni.hideLoading();
+  }
+}
+</script>

+ 95 - 0
src/pages/home/village/bless/recharge.vue

@@ -0,0 +1,95 @@
+
+
+<template>
+  <CommonTopBanner 
+    title="充值乡源果"
+    showNav
+  >
+    <FlexCol gap="gap.lg" padding="padding.md">
+      <FlexRow center>
+        <Image src="https://xy.wenlvti.net/app_static/images/village/IconBlessing.png" :width="100" :height="100" />
+      </FlexRow>
+      <FlexCol padding="padding.md" center>
+        <Text 
+          text="请选择您要付款方式" 
+          fontConfig="contentText" :fontSize="30" textAlign="center" 
+        />
+      </FlexCol>
+
+      <FlexCol gap="gap.md">
+        <BoxMid v-for="item in listLoader.content.value" direction="row" justify="space-between" align="center" gap="gap.md">
+          <FlexCol width="74%">
+            <Text :text="item.name" fontConfig="lightImportantTitle" :fontSize="42" />
+            <Text :text="`+ ${item.addFruit} 乡源果`" fontConfig="lightGoldTitle" :fontSize="30" />
+          </FlexCol>
+          <FlexRow align="center" gap="gap.lg">
+            <Text :text="`¥${item.price}`" fontConfig="lightGoldTitle" :fontSize="42" />
+            <FrameButton primary text="选择" @click="handlePay(item.id)" />
+          </FlexRow>
+        </BoxMid>
+      </FlexCol>
+    </FlexCol>
+  </CommonTopBanner>
+</template>
+
+<script setup lang="ts">
+import { useLoadQuerys } from '@/components/composeabe/LoadQuerys';
+import { useRequireLogin } from '@/common/composeabe/RequireLogin';
+import { useSimpleDataLoader } from '@/components/composeabe/loader/SimpleDataLoader';
+import { toast } from '@/components/dialog/CommonRoot';
+import { backAndCallOnPageBack, navTo } from '@/components/utils/PageAction';
+import { showError } from '@/common/composeabe/ErrorDisplay';
+import BoxMid from '@/common/components/box/BoxMid.vue';
+import CommonTopBanner from '@/common/components/CommonTopBanner.vue';
+import FrameButton from '@/common/components/FrameButton.vue';
+import Text from '@/components/basic/Text.vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import FlexRow from '@/components/layout/FlexRow.vue';
+import Image from '@/components/basic/Image.vue';
+import TreeApi from '@/api/light/TreeApi';
+
+const { requireLoginAsync } = useRequireLogin();
+const { querys } = useLoadQuerys({
+  villageId: 0,
+});
+
+const listLoader = useSimpleDataLoader(async () => {
+  return (await TreeApi.getFruitDepositList({ page: 1, pageSize: 30 })).list;
+});
+
+async function handlePay(id: number) {
+  if (!requireLoginAsync('登录后为村社升级,做出你的贡献哦'))
+    return;
+  try {
+    uni.showLoading({ title: '创建订单中...' });
+    const payInfo = await TreeApi.placeOrder(
+      id,
+      querys.value.villageId,
+    );
+    if (payInfo && payInfo.pay) {
+      uni.requestPayment({
+        provider: 'wxpay',
+        appId: payInfo.pay.appId,
+        timeStamp: payInfo.pay.timeStamp,
+        nonceStr: payInfo.pay.nonceStr,
+        package: payInfo.pay.package,
+        signType: payInfo.pay.signType,
+        paySign: payInfo.pay.paySign,
+        success: () => {
+          toast('支付成功');
+          setTimeout(() => 
+            backAndCallOnPageBack('paySuccessAndRefresh', {})
+          , 1000);
+        },
+        fail: (err) => {
+          showError(`支付失败: ${err.errMsg}`);
+        },
+      });
+    }
+  } catch (e) {
+    showError(e);
+  } finally {
+    uni.hideLoading();
+  }
+}
+</script>

+ 11 - 5
src/pages/home/village/dialogs/BlessBuyDialog.vue

@@ -52,26 +52,30 @@
       <Height :height="5" />
       <PrimaryButton text="立即赐福" @click="show = false;emit('buyBless')" width="520rpx" />
       <Height :height="5" />
-      <Text 
-        textAlign="center" 
-        text="注:倍率仅作用于用户乡源果,村社乡源光不翻倍。赐福到期后,倍率自动恢复为1倍"
-        fontConfig="contentText"
-      />
+      <SimplePageContentLoader :loader="infoLoader">
+        <FlexCol gap="gap.md" padding="padding.md">
+          <Parse :content="infoLoader.content?.value?.content || ''" />
+        </FlexCol>
+      </SimplePageContentLoader>
     </FlexCol>
   </CommonDialog>
 </template>
 
 <script setup lang="ts">
+import CommonContent from '@/api/CommonContent';
 import type { BlessPackageItem } from '@/api/light/TreeApi';
 import BoxMid from '@/common/components/box/BoxMid.vue';
 import CommonDialog from '@/common/components/CommonDialog.vue';
 import PrimaryButton from '@/common/components/PrimaryButton.vue';
 import Image from '@/components/basic/Image.vue';
 import Text from '@/components/basic/Text.vue';
+import { useSimpleDataLoader } from '@/components/composeabe/loader/SimpleDataLoader';
 import BackgroundBox from '@/components/display/block/BackgroundBox.vue';
+import Parse from '@/components/display/parse/Parse.vue';
 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 { DateUtils } from '@imengyu/imengyu-utils';
 import { computed, ref } from 'vue';
 
@@ -120,6 +124,8 @@ const effectiveDate = computed(() => {
   return DateUtils.formatDate(DateUtils.dateAddDays(new Date(), props.currentBless?.days || 0), 'YYYY-MM-DD');
 });
 
+const infoLoader = useSimpleDataLoader(async () => await CommonContent.getContentDetail(7027, undefined, 18));
+
 defineExpose({
   show: () => {
     show.value = true;

+ 65 - 0
src/pages/home/village/dialogs/ChangeNickDialog.vue

@@ -0,0 +1,65 @@
+<template>
+  <CommonDialog v-model:show="show" title="修改昵称" :showDivider="false">
+    <FlexCol gap="gap.lg" padding="padding.md" width="600rpx">
+      <Text textAlign="center" text="修改您在本村的昵称,可以方便大家认识您" fontConfig="contentSpeicalText" />
+      <Height :height="10" />
+      <Field v-model="name" placeholder="请输入昵称" type="text" showWordLimit :maxLength="30" />
+      <Height :height="20" />
+      <FlexRow justify="space-around" gap="gap.md">
+        <FrameButton text="返回" @click="show = false" width="220rpx" />
+        <FrameButton primary text="提交" @click="addSubmit" :loading="submiting" width="220rpx" />
+      </FlexRow>
+    </FlexCol>
+  </CommonDialog>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue';
+import { toast } from '@/components/dialog/CommonRoot';
+import { showError } from '@/common/composeabe/ErrorDisplay';
+import { useVillageStore } from '@/store/village';
+import CommonDialog from '@/common/components/CommonDialog.vue';
+import FrameButton from '@/common/components/FrameButton.vue';
+import Text from '@/components/basic/Text.vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import FlexRow from '@/components/layout/FlexRow.vue';
+import Height from '@/components/layout/space/Height.vue';
+import Field from '@/components/form/Field.vue';
+import LightVillageApi from '@/api/light/LightVillageApi';
+
+const show = ref(false);
+const props = defineProps<{
+  villageId: number;
+}>();
+const emit = defineEmits(['finish']);
+const villageStore = useVillageStore();
+
+const submiting = ref(false);
+const name = ref('');
+
+async function addSubmit() {
+  try {
+    submiting.value = true;
+    await LightVillageApi.nicknameSave({
+      villageId: props.villageId,
+      villageNickname: name.value,
+    });
+    toast({ content: '提交成功' });
+    show.value = false;
+    villageStore.loadMyJoinedVillages();
+    setTimeout(() => {
+      villageStore.reloadVillageInfo();
+    }, 500);
+  } catch (e) {
+    showError(e);
+  } finally {
+    submiting.value = false;
+  }
+}
+
+defineExpose({
+  show: () => {
+    show.value = true;
+  },
+});
+</script>

+ 5 - 70
src/pages/home/village/dialogs/UpgradeDialog.vue

@@ -3,68 +3,18 @@
     <template #titleHeader>    
       <Image src="https://xy.wenlvti.net/app_static/images/village/IconBlessing.png" :width="100" :height="100" />
     </template>
-    <FlexCol gap="gap.lg">
-      
-      <FlexCol padding="padding.md">
-        <Text 
-          text="感谢您选择升级村社,升级后您将获得更多权益,包括管理功能,更多的相册、乡源光、乡源果奖励等。请选择您要升级的套餐" 
-          fontConfig="contentText" :fontSize="30" textAlign="center" 
-        />
-      </FlexCol>
-
-      <FlexCol gap="gap.md">
-        <BoxMid
-          v-for="(item, k) in upgradeList"
-          :key="k"
-          position="relative"
-          :padding="32"
-          direction="row"
-          justify="space-between"
-          align="center"
-          gap="gap.md"
-        >
-          <FlexCol>
-            <Text :text="item.name" fontConfig="lightImportantTitle" :fontSize="42" />
-            <Text :text="item.desc" fontConfig="lightGoldTitle" :fontSize="30" />
-          </FlexCol>
-
-          <FlexRow gap="gap.lg">
-            <FlexCol align="flex-end">
-              <Text :text="item.vipLevel + '级'" fontConfig="contentText" />
-              <FlexRow align="center" gap="gap.sm">
-                <Text text="¥" color="text.gold" />
-                <Text :text="item.price" fontFamily="SongtiSCBlack" fontConfig="lightGoldTitle" fontSize="50" />
-              </FlexRow>
-            </FlexCol>
-            <Badge :show="item.isRecommend" content="推荐">
-              <FrameButton primary text="选择" @click="handleSelect(item as UpgradePackageItem)" />
-            </Badge>
-          </FlexRow>
-        </BoxMid>
-      </FlexCol>
-
-      <BoxMid :padding="24" direction="column" center>
-        <Touchable @click="navTo('/pages/article/details', { id: 7021, modelId: 18, showRecommend: false })">
-          <Text text="了解各村社等级权益" fontConfig="lightImportantTitle" />
-        </Touchable>
-      </BoxMid>
-    </FlexCol>
+    <UpgradeSelect
+      :villageId="villageId"
+      @selected="show = false"
+    />
   </CommonDialog>
 </template>
 
 <script setup lang="ts">
 import { ref } from 'vue';
-import { navTo } from '@/components/utils/PageAction';
 import CommonDialog from '@/common/components/CommonDialog.vue';
-import FrameButton from '@/common/components/FrameButton.vue';
-import Text from '@/components/basic/Text.vue';
-import FlexCol from '@/components/layout/FlexCol.vue';
-import FlexRow from '@/components/layout/FlexRow.vue';
-import TreeApi, { type UpgradePackageItem } from '@/api/light/TreeApi';
 import Image from '@/components/basic/Image.vue';
-import Touchable from '@/components/feedback/Touchable.vue';
-import Badge from '@/components/display/Badge.vue';
-import BoxMid from '@/common/components/box/BoxMid.vue';
+import UpgradeSelect from '../upgrade/components/UpgradeSelect.vue';
 
 const show = ref(false);
 const props = defineProps({
@@ -74,24 +24,9 @@ const props = defineProps({
   },
 });
 
-const upgradeList = ref<UpgradePackageItem[]>([]);
-
-async function loadUpgradeList() {
-  const res = await TreeApi.getUpgradeList();
-  upgradeList.value = res.list;
-}
-function handleSelect(item: UpgradePackageItem) {
-  show.value = false;
-  navTo('/pages/home/village/upgrade/select', {
-    villageId: props.villageId,
-    upgradePackageId: item.id,
-  });
-}
-
 defineExpose({
   show: () => {
     show.value = true;
-    loadUpgradeList();
   },
 });
 </script>

+ 1 - 0
src/pages/home/village/goods/detail.vue

@@ -22,6 +22,7 @@
               <Avatar 
                 :src="goodLoader.content.value.villageVolunteerAvatar"
                 :size="40"
+                defaultAvatar="https://xy.wenlvti.net/app_static/images/mine/DefaultAvatar.png"
               />
               <Text :text="`发布人: ${goodLoader.content.value.villageVolunteerName}`" fontConfig="contentText" />
             </FlexRow>

+ 8 - 1
src/pages/home/village/goods/index.vue

@@ -27,6 +27,7 @@
                 <Avatar 
                   :src="good.villageVolunteerAvatar"
                   :size="40"
+                defaultAvatar="https://xy.wenlvti.net/app_static/images/mine/DefaultAvatar.png"
                 />
                 <Text :text="`发布人: ${good.villageVolunteerName}`" fontConfig="contentText" />
               </FlexRow>
@@ -113,16 +114,22 @@ const handleOpen = () => {
   toast('暂未开放,敬请期待!');
 }
 
-onMounted(async () => {
+async function loadVolunteerInfo() {
   await getIsVolunteer();
   canCollect.value = await getCanCollect(querys.value.villageId);
   await goodsLoader.load();
+}
+
+onMounted(() => {
+  loadVolunteerInfo();
 });
 
 defineExpose({
   onPageBack(name: string, param: any) {
     if (param && param.needRefresh)
       goodsLoader.reload();
+    if (name === 'registerDone')
+      loadVolunteerInfo();  
   },
 });
 </script>

+ 47 - 18
src/pages/home/village/introd/card.vue

@@ -33,13 +33,31 @@
               {{ isFollowed ? '已关注' : '关注' }}
             </Button>
           </BubbleTip>
+          <FlexRow 
+            v-if="isJoined"
+            gap="gap.md"
+            radius="radius.lgr"
+            backgroundColor="background.tertiary"
+          >
+            <Avatar 
+              :src="authStore.userInfo?.avatar" 
+              :size="65"
+              defaultAvatar="https://xy.wenlvti.net/app_static/images/mine/DefaultAvatar.png"
+            />
+            <FlexCol gap="gap.sm" :padding="[0,30,0,0]">
+              <Text :text="displayMyName" fontSize="23" :lines="1" :maxWidth="200" fontConfig="lightImportantTitle" />
+              <IconButton icon="edit-filling" size="26" @click="changeNickRef?.show()">
+                <Text text="改昵称" fontSize="22" fontConfig="subText" />
+              </IconButton>
+            </FlexCol>
+          </FlexRow>
           <Button 
+            v-else
             icon="https://xy.wenlvti.net/app_static/images/village/IconFollow.png"
             radius="radius.lgr"
+            text="加入"
             @click="handleGoJoin()"
-          >
-            {{ isJoined ? '已加入' : '加入' }}
-          </Button>
+          />
         </FlexRow>
       </FlexRow>
 
@@ -61,7 +79,7 @@
         <FlexRow align="center">
           <FlexCol gap="gap.md"> 
             <Text :text="`${villageInfoLoader.content.value?.levelText || '默认级别'}`" fontConfig="secondText" />  
-            <Text :text="`存储空间内存:${villageInfoLoader.content.value?.sizeText || 0}`" fontConfig="secondText" />
+            <Text :text="`存储空间内存:${villageInfoLoader.content.value?.sizeText || ''}`" fontConfig="secondText" />
           </FlexCol>
         </FlexRow>
         <FlexRow align="center" gap="gap.md">
@@ -263,6 +281,11 @@
     v-if="villageStore.currentVillage"
     :villageId="villageStore.currentVillage.id"
   />
+  <ChangeNickDialog 
+    v-if="villageStore.currentVillage"
+    :villageId="villageStore.currentVillage.id"
+    ref="changeNickRef"
+  />  
   <Popup 
     :show="showUnLight"
     :zIndex="100"
@@ -304,8 +327,10 @@ import { useSimpleDataLoader } from '@/components/composeabe/loader/SimpleDataLo
 import { useVillageStore } from '@/store/village';
 import { useRequireLogin } from '@/common/composeabe/RequireLogin';
 import { useFollow } from '../composeabe/Follow';
-import { ArrayUtils, assertNotNull, waitTimeOut } from '@imengyu/imengyu-utils';
+import { useGetNotice } from '../composeabe/GetNotice';
+import { assertNotNull, FormatUtils, waitTimeOut } from '@imengyu/imengyu-utils';
 import { navTo } from '@/components/utils/PageAction';
+import { confirm } from '@/components/dialog/CommonRoot';
 import HomeTitle from '@/common/components/parts/HomeTitle.vue';
 import Icon from '@/components/basic/Icon.vue';
 import Text from '@/components/basic/Text.vue';
@@ -331,13 +356,18 @@ import Button from '@/components/basic/Button.vue';
 import BubbleTip from '@/components/feedback/BubbleTip.vue';
 import MemoryTimeOut from '@/components/composeabe/MemoryTimeOut';
 import TextEllipsis from '@/components/display/TextEllipsis.vue';
-import { confirm, toast } from '@/components/dialog/CommonRoot';
-import { useGetNotice } from '../composeabe/GetNotice';
 import OfficialApi from '@/api/light/OfficialApi';
 import FrameButton from '@/common/components/FrameButton.vue';
 import Width from '@/components/layout/space/Width.vue';
 import Popup from '@/components/dialog/Popup.vue';
 import Image from '@/components/basic/Image.vue';
+import Avatar from '@/components/display/Avatar.vue';
+import ChangeNickDialog from '../dialogs/ChangeNickDialog.vue';
+
+const emit = defineEmits<{
+  (e: 'goTree'): void;
+  (e: 'goJoin'): void;
+}>();
 
 const authStore = useAuthStore();
 const { getIsVolunteer } = useUserTools();
@@ -351,9 +381,6 @@ const { onPublishSuccess } = useOfficialAccount(() => {
 });
 const villageStore = useVillageStore();
 const { isFollowed, onFollow, onUnFollow } = useFollow();
-const isLight = computed(() => {
-  return villageStore.currentVillage?.isLight ?? false;
-});
 const { getIsJoinedVillage } = useUserTools();
 const isOfficialEmpty = ref(false);
 const isJoined = ref(false);
@@ -375,7 +402,7 @@ const villageInfoLoader = useSimpleDataLoader(async () => {
     title: village?.name || '',
     desc: village?.desc || '',
     address: village?.address,
-    sizeText: village?.storageLimit || 0,
+    sizeText: `${FormatUtils.formatSize(village?.storageUsed || 0)}已用/${FormatUtils.formatSize(village?.storageLimit || 0)}空间`,
     level: village?.level.toString() || '',
     levelText: village?.levelText || '',
     rankText: village?.rank.toString() || '',
@@ -387,12 +414,13 @@ const villageInfoLoader = useSimpleDataLoader(async () => {
     latitude: village?.latitude || 0,
   };
 });
-
-const emit = defineEmits<{
-  (e: 'goTree'): void;
-  (e: 'goJoin'): void;
-}>();
-
+const isLight = computed(() => {
+  return villageStore.currentVillage?.isLight ?? false;
+});
+const displayMyName = computed(() => {
+  const villageInfoInList = villageStore.myJoinedVillages.find(v => v.id === villageStore.currentVillage?.id);
+  return villageInfoInList?.villageNickname || authStore.userInfo?.nickname || authStore.userInfo?.username || '暂无昵称';
+});
 
 const rankActiveTag = ref('文化积分');
 const listActiveTag = ref('广场');
@@ -404,7 +432,7 @@ const villageUserRankListLoader = useSimpleDataLoader(async () => {
   }))
     .map((item, i) => ({
       id: item.id,
-      image: item.image ?? '',
+      image: item.avatar ?? '',
       title: item.name,
       rank: i + 1,
       score: item.points,
@@ -533,6 +561,7 @@ const { currentNoticeContent, noticeListLoader } = useGetNotice(() => villageSto
 
 const upgradeRef = ref<InstanceType<typeof UpgradeDialog>>();
 const villageGalleryRef = ref<InstanceType<typeof VillageGallery>>();
+const changeNickRef = ref<InstanceType<typeof ChangeNickDialog>>();
 
 watch(() => villageStore.currentVillage, async () => {
   await waitTimeOut(100);

+ 5 - 33
src/pages/home/village/introd/tree.vue

@@ -174,7 +174,6 @@ const authStore = useAuthStore();
 const { requireLoginAsync } = useRequireLogin();
 
 const blessBuyDialogRef = ref<InstanceType<typeof BlessBuyDialog>>();
-const blessSuccessDialogRef = ref<InstanceType<typeof BlessSuccessDialog>>();
 const villageTreeRef = ref<InstanceType<typeof VillageTree>>();
 const currentBless = ref<BlessPackageItem>();
 
@@ -266,38 +265,11 @@ async function handleGoBlessOrders() {
   });
 }
 async function handleBuyBlessConfirm() {
-  if (!currentBless.value || !villageStore.currentVillage?.id) 
-    return;
-  if (!await requireLoginAsync('登录后为村社赐福,留下你的大名吧'))
-    return;
-  try {
-    uni.showLoading({
-      title: '请稍后...',
-    });
-    const payInfo = await TreeApi.createBlessOrder(villageStore.currentVillage?.id, currentBless.value.id, 1);
-    if (payInfo) {  
-      uni.requestPayment({
-        provider: 'wxpay',
-        appId: payInfo.pay.appId,
-        timeStamp: payInfo.pay.timeStamp,
-        nonceStr: payInfo.pay.nonceStr,
-        package: payInfo.pay.package,
-        signType: payInfo.pay.signType,
-        paySign: payInfo.pay.paySign,
-        success: () => {
-          blessSuccessDialogRef.value?.show();
-          handleBlessPaySuccessRefresh();
-        },
-        fail: (err) => {
-          showError(err);
-        },
-      });
-    }
-  } catch (error) {
-    showError(error);
-  } finally {
-    uni.hideLoading();
-  }
+  navTo('/pages/home/village/bless/pay-select', { 
+    villageId: villageStore.currentVillage?.id,
+    blessPackageId: currentBless.value?.id,
+    blessPackagePrice: currentBless.value?.amount,
+  });
 }
 function handleGoBless() {
   uni.pageScrollTo({

+ 1 - 1
src/pages/home/village/rank/volunteer.vue

@@ -52,7 +52,7 @@ const villageUserRankListLoader = useSimpleDataLoader(async () => {
   }))
     .map((item, i) => ({
       id: item.id,
-      image: item.image ?? '',
+      image: item.avatar ?? '',
       title: item.name,
       rank: i + 1,
       score: item.points,

+ 45 - 0
src/pages/home/village/upgrade/components/BuyFruitInfo.vue

@@ -0,0 +1,45 @@
+<template>
+  <FlexCol gap="gap.md">
+    <slot name="prepend" />
+    <BoxMid direction="row" justify="space-between" align="center" gap="gap.md">
+      <FlexCol width="74%">
+        <FlexRow align="center" gap="gap.md">
+          <Icon icon="https://xy.wenlvti.net/app_static/images/village/IconFruit.png" size="40" />
+          <Text text="乡源果支付" fontConfig="lightImportantTitle" :fontSize="42" />
+        </FlexRow>
+        <Text :text="`余额 ${authStore.userInfo?.fruit || '0'} 乡源果` + (price ? ` 需要 ${Math.ceil(price * 100)} 乡源果` : '')" fontConfig="lightGoldTitle" :fontSize="30" />
+      </FlexCol>
+      <FrameButton primary text="选择" @click="emit('pay')" />
+    </BoxMid>      
+    <SimplePageContentLoader :loader="infoLoader">
+      <FlexCol gap="gap.md" padding="padding.md">
+        <IconTextBlock icon="prompt" :title="infoLoader.content?.value?.title" />
+        <Parse :content="infoLoader.content?.value?.content || ''" />
+      </FlexCol>
+    </SimplePageContentLoader>
+    <slot name="extra" />
+  </FlexCol>
+</template>
+
+<script setup lang="ts">
+import { useSimpleDataLoader } from '@/components/composeabe/loader/SimpleDataLoader';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import BoxMid from '@/common/components/box/BoxMid.vue';
+import Text from '@/components/basic/Text.vue';
+import Icon from '@/components/basic/Icon.vue';
+import FrameButton from '@/common/components/FrameButton.vue';
+import CommonContent from '@/api/CommonContent';
+import Parse from '@/components/display/parse/Parse.vue';
+import SimplePageContentLoader from '@/components/loader/SimplePageContentLoader.vue';
+import FlexRow from '@/components/layout/FlexRow.vue';
+import { useAuthStore } from '@/store/auth';
+import IconTextBlock from '@/components/display/block/IconTextBlock.vue';
+
+const emit = defineEmits(['pay']);
+const props = defineProps<{
+  price?: number;
+}>()
+  
+const authStore = useAuthStore();
+const infoLoader = useSimpleDataLoader(async () => await CommonContent.getContentDetail(7023, undefined, 18));
+</script>

+ 86 - 0
src/pages/home/village/upgrade/components/UpgradeSelect.vue

@@ -0,0 +1,86 @@
+<template>
+  <FlexCol gap="gap.lg">
+    <FlexCol padding="padding.md">
+      <Text 
+        text="感谢您选择升级村社,升级后您将获得更多权益,包括管理功能,更多的相册、乡源光、乡源果奖励等。请选择您要升级的套餐" 
+        fontConfig="contentText" :fontSize="30" textAlign="center" 
+      />
+    </FlexCol>
+    <FlexCol gap="gap.md">
+      <BoxMid
+        v-for="(item, k) in upgradeList"
+        :key="k"
+        position="relative"
+        :padding="32"
+        direction="row"
+        justify="space-between"
+        align="center"
+        gap="gap.md"
+      >
+        <FlexCol>
+          <Text :text="item.name" fontConfig="lightImportantTitle" :fontSize="42" />
+          <Text :text="item.desc" fontConfig="lightGoldTitle" :fontSize="30" />
+        </FlexCol>
+
+        <FlexRow gap="gap.lg">
+          <FlexCol align="flex-end">
+            <Text :text="item.vipLevel + '级'" fontConfig="contentText" />
+            <FlexRow align="center" gap="gap.sm">
+              <Text text="¥" color="text.gold" />
+              <Text :text="item.price" fontFamily="SongtiSCBlack" fontConfig="lightGoldTitle" fontSize="50" />
+            </FlexRow>
+          </FlexCol>
+          <Badge :show="item.isRecommend" content="推荐">
+            <FrameButton primary text="选择" @click="handleSelect(item as UpgradePackageItem)" />
+          </Badge>
+        </FlexRow>
+      </BoxMid>
+    </FlexCol>
+    <BoxMid :padding="24" direction="column" center>
+      <Touchable @click="navTo('/pages/article/details', { id: 7021, modelId: 18, showRecommend: false })">
+        <Text text="了解各村社等级权益" fontConfig="lightImportantTitle" />
+      </Touchable>
+    </BoxMid>
+  </FlexCol>
+</template>
+
+<script setup lang="ts">
+import { onMounted, ref } from 'vue';
+import { navTo } from '@/components/utils/PageAction';
+import FrameButton from '@/common/components/FrameButton.vue';
+import Text from '@/components/basic/Text.vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import FlexRow from '@/components/layout/FlexRow.vue';
+import TreeApi, { type UpgradePackageItem } from '@/api/light/TreeApi';
+import Touchable from '@/components/feedback/Touchable.vue';
+import Badge from '@/components/display/Badge.vue';
+import BoxMid from '@/common/components/box/BoxMid.vue';
+
+const props = defineProps({
+  villageId: {
+    type: Number,
+    required: true,
+  },
+});
+
+const emit = defineEmits(['selected']);
+
+const upgradeList = ref<UpgradePackageItem[]>([]);
+
+async function loadUpgradeList() {
+  const res = await TreeApi.getUpgradeList();
+  upgradeList.value = res.list;
+}
+function handleSelect(item: UpgradePackageItem) {
+  emit('selected', item);
+  navTo('/pages/home/village/upgrade/select', {
+    villageId: props.villageId,
+    upgradePackageId: item.id,
+    upgradePackagePrice: item.price,
+  });
+}
+
+onMounted(() => {
+  loadUpgradeList();
+});
+</script>

+ 20 - 26
src/pages/home/village/upgrade/my-upgrade-management.vue

@@ -21,33 +21,26 @@
         />
       </FlexCol>
 
-      <FlexCol gap="gap.md">
-        <BoxMid direction="row" justify="space-between" align="center" gap="gap.md">
-          <FlexCol width="74%">
-            <Text text="在线支付" fontConfig="lightImportantTitle" :fontSize="42" />
-            <Text text="推荐使用微信线支付方式,方便快捷,立即生效" fontConfig="lightGoldTitle" :fontSize="30" />
-          </FlexCol>
-          <FrameButton primary text="选择" @click="handleDirectPay(1, 1)" />
-        </BoxMid>
-        <BoxMid direction="row" justify="space-between" align="center" gap="gap.md">
-          <FlexCol width="74%">
-            <FlexRow align="center" gap="gap.md">
-              <Icon icon="https://xy.wenlvti.net/app_static/images/village/IconFruit.png" size="40" />
-              <Text text="乡源果支付" fontConfig="lightImportantTitle" :fontSize="42" />
-            </FlexRow>
-            <Text :text="`余额 ${0} 乡源果`" fontConfig="lightGoldTitle" :fontSize="30" />
-          </FlexCol>
-          <FrameButton primary text="选择" @click="handleDirectPay(1, 3)" />
-        </BoxMid>
-        <BoxMid direction="row" justify="space-between" align="center" gap="gap.md">
-          <FlexCol width="74%">
-            <Text text="测试" fontConfig="lightImportantTitle" :fontSize="42" />
-            <Text text="¥ 0.01" fontConfig="lightGoldTitle" :fontSize="30" />
-          </FlexCol>
-          <FrameButton primary text="选择" @click="handleDirectPay(2, 1)" />
-        </BoxMid>
-      </FlexCol>
+      <BuyFruitInfo @pay="handleDirectPay(1, 3)">
+        <template #prepend>
+          <BoxMid direction="row" justify="space-between" align="center" gap="gap.md">
+            <FlexCol width="74%">
+              <Text text="在线支付" fontConfig="lightImportantTitle" :fontSize="42" />
+              <Text text="推荐使用微信线支付方式,方便快捷,立即生效" fontConfig="lightGoldTitle" :fontSize="30" />
+            </FlexCol>
+            <FrameButton primary text="选择" @click="handleDirectPay(1, 1)" />
+          </BoxMid>
+          <BoxMid direction="row" justify="space-between" align="center" gap="gap.md">
+            <FlexCol width="74%">
+              <Text text="测试" fontConfig="lightImportantTitle" :fontSize="42" />
+              <Text text="¥ 0.01" fontConfig="lightGoldTitle" :fontSize="30" />
+            </FlexCol>
+            <FrameButton primary text="选择" @click="handleDirectPay(2, 1)" />
+          </BoxMid>
+        </template>
+      </BuyFruitInfo>
     </FlexCol>
+
     <UpgradeManagementSuccessDialog  
       ref="upgradeManagementSuccessDialog"
       @success="handlePaySuccess"
@@ -70,6 +63,7 @@ import OfficialApi from '@/api/light/OfficialApi';
 import UpgradeManagementSuccessDialog from './dialogs/UpgradeManagementSuccess.vue';
 import FlexRow from '@/components/layout/FlexRow.vue';
 import Icon from '@/components/basic/Icon.vue';
+import BuyFruitInfo from './components/BuyFruitInfo.vue';
 
 const upgradeManagementSuccessDialog = ref<InstanceType<typeof UpgradeManagementSuccessDialog>>();
 

+ 29 - 36
src/pages/home/village/upgrade/select.vue

@@ -20,41 +20,33 @@
         />
       </FlexCol>
 
-      <FlexCol gap="gap.md">
-        <BoxMid direction="row" justify="space-between" align="center" gap="gap.md">
-          <FlexCol width="74%">
-            <FlexRow align="center" gap="gap.md">
-              <Icon icon="wechat" size="36" />
-              <Text text="在线支付" fontConfig="lightImportantTitle" :fontSize="42" />
-            </FlexRow>
-            <Text text="推荐使用微信线支付方式,方便快捷,立即生效" fontConfig="lightGoldTitle" :fontSize="30" />
-          </FlexCol>
-          <FrameButton primary text="选择" @click="handleDirectPay(1)" />
-        </BoxMid>
-        <BoxMid direction="row" justify="space-between" align="center" gap="gap.md">
-          <FlexCol width="74%">
-            <FlexRow align="center" gap="gap.md">
-              <Icon icon="https://xy.wenlvti.net/app_static/images/village/IconFruit.png" size="40" />
-              <Text text="乡源果支付" fontConfig="lightImportantTitle" :fontSize="42" />
-            </FlexRow>
-            <Text :text="`余额 ${0} 乡源果`" fontConfig="lightGoldTitle" :fontSize="30" />
-          </FlexCol>
-          <FrameButton primary text="选择" @click="handleDirectPay(3)" />
-        </BoxMid>
-        <BoxMid
-          v-if="false"
-          direction="row"
-          justify="space-between"
-          align="center"
-          gap="gap.md"
-        >
-          <FlexCol width="74%">
-            <Text text="对公打款" fontConfig="lightImportantTitle" :fontSize="42" />
-            <Text text="对公打款,需要提供对公账户信息。适用于村社政府做出贡献" fontConfig="lightGoldTitle" :fontSize="30" />
-          </FlexCol>
-          <FrameButton primary text="选择" @click="handlePublicPay" />
-        </BoxMid>
-      </FlexCol>
+      <BuyFruitInfo :price="querys.upgradePackagePrice" @pay="handleDirectPay(3)">
+        <template #prepend>
+          <BoxMid direction="row" justify="space-between" align="center" gap="gap.md">
+            <FlexCol width="74%">
+              <FlexRow align="center" gap="gap.md">
+                <Icon icon="wechat" size="36" />
+                <Text text="在线支付" fontConfig="lightImportantTitle" :fontSize="42" />
+              </FlexRow>
+              <Text text="推荐使用微信线支付方式,方便快捷,立即生效" fontConfig="lightGoldTitle" :fontSize="30" />
+            </FlexCol>
+            <FrameButton primary text="选择" @click="handleDirectPay(1)" />
+          </BoxMid>
+          <BoxMid
+            v-if="false"
+            direction="row"
+            justify="space-between"
+            align="center"
+            gap="gap.md"
+          >
+            <FlexCol width="74%">
+              <Text text="对公打款" fontConfig="lightImportantTitle" :fontSize="42" />
+              <Text text="对公打款,需要提供对公账户信息。适用于村社政府做出贡献" fontConfig="lightGoldTitle" :fontSize="30" />
+            </FlexCol>
+            <FrameButton primary text="选择" @click="handlePublicPay" />
+          </BoxMid>
+        </template>
+      </BuyFruitInfo>
     </FlexCol>
 
     <DirectPayDialog
@@ -79,8 +71,8 @@ import FlexRow from '@/components/layout/FlexRow.vue';
 import Button from '@/components/basic/Button.vue';
 import Image from '@/components/basic/Image.vue';
 import DirectPayDialog from './dialogs/DirectPayDialog.vue';
+import BuyFruitInfo from './components/BuyFruitInfo.vue';
 import TreeApi from '@/api/light/TreeApi';
-import Touchable from '@/components/feedback/Touchable.vue';
 import Icon from '@/components/basic/Icon.vue';
 
 const { requireLoginAsync } = useRequireLogin();
@@ -96,6 +88,7 @@ function handlePaySuccess() {
 const { querys } = useLoadQuerys({
   villageId: 0,
   upgradePackageId: 0,
+  upgradePackagePrice: 0,
 }, () => {
 
 });

+ 42 - 0
src/pages/home/village/upgrade/upgrade-village.vue

@@ -0,0 +1,42 @@
+
+
+<template>
+  <CommonTopBanner 
+    title="升级村社"
+    showNav
+  >
+    <FlexCol center padding="padding.md">
+      <Image src="https://xy.wenlvti.net/app_static/images/village/IconBlessing.png" :width="100" :height="100" />
+      <UpgradeSelect
+        :villageId="querys.villageId"
+      />
+    </FlexCol>
+  </CommonTopBanner>
+</template>
+
+<script setup lang="ts">
+import { useLoadQuerys } from '@/components/composeabe/LoadQuerys';
+import { backAndCallOnPageBack } from '@/components/utils/PageAction';
+import CommonTopBanner from '@/common/components/CommonTopBanner.vue';
+import FlexCol from '@/components/layout/FlexCol.vue';
+import Image from '@/components/basic/Image.vue';
+import UpgradeSelect from './components/UpgradeSelect.vue';
+
+const { querys } = useLoadQuerys({
+  villageId: 0,
+});
+
+function handlePaySuccess() {
+  setTimeout(() => {
+    backAndCallOnPageBack('paySuccessAndRefresh', {});
+  }, 100);
+}
+
+defineExpose({
+  onPageBack(name: string, data: any) {
+    if (name === 'handlePaySuccess') {
+      handlePaySuccess();
+    }
+  },
+});
+</script>

+ 2 - 0
src/pages/home/village/volunteer/detail.vue

@@ -13,6 +13,7 @@
           <Avatar
             :src="infoLoader.content.value.image"
             :size="200"
+            defaultAvatar="https://xy.wenlvti.net/app_static/images/village/PlaceholderVolunteerNew.png"
           />
           <Width :width="60" />
           <FlexCol gap="gap.md">
@@ -71,6 +72,7 @@
               :size="80"
               :round="false"
               :radius="10"
+              defaultAvatar="https://xy.wenlvti.net/app_static/images/village/PlaceholderVolunteerNew.png"
             />
             <Text :text="item.remark || '没有说明文字!'" fontConfig="contentText" :innerStyle="{ flex: 1 }" />
           </FlexRow>

+ 8 - 3
src/pages/user/index.vue

@@ -47,7 +47,8 @@
     >
       <FlexRow :flex="1" :gap="10" center>
         <Text>我的乡源果: </Text>
-        <Text fontConfig="lightGoldTitle">{{ volunteerInfoLoader.content.value?.fruit || 0 }}</Text>
+        <Text fontConfig="lightGoldTitle">{{ userInfo?.fruit || 0 }}</Text>
+        <FrameButton size="small" text="充值" @click="navTo('/pages/home/village/bless/recharge')" />
       </FlexRow>
       <FlexRow :flex="1" :gap="10" center>
         <Touchable direction="row" align="center" :gap="10" @click="navTo('/pages/dig/about/point')">
@@ -80,7 +81,7 @@
 </template>
 
 <script setup lang="ts">
-import { computed } from 'vue';
+import { computed, onMounted } from 'vue';
 import { navTo } from '@/components/utils/PageAction';
 import { confirm } from '@/components/dialog/CommonRoot';
 import { useAuthStore } from '@/store/auth';
@@ -105,6 +106,7 @@ import ProvideVar from '@/components/theme/ProvideVar.vue';
 import Grid from '@/components/layout/grid/Grid.vue';
 import GridItem from '@/components/layout/grid/GridItem.vue';
 import BoxMid from '@/common/components/box/BoxMid.vue';
+import FrameButton from '@/common/components/FrameButton.vue';
 
 const UserHead = 'https://mncdn.wenlvti.net/app_static/xiangyuan/images/user/avatar.png';
 
@@ -138,5 +140,8 @@ function goStore() {
   }), '登录后查看我的哦');
 }
 
-
+onMounted(() => {
+  if (authStore.isLogged)
+    authStore.refreshUserInfo();
+});
 </script>

+ 3 - 2
src/store/auth.ts

@@ -122,8 +122,9 @@ export const useAuthStore = defineStore('auth', {
         loginType: this.loginType,
       }
     },
-    refreshUserInfo() {
-      //TODO: 刷新用户信息
+    async refreshUserInfo() {
+      this.userInfo = await UserApi.getUserInfo();
+      this.saveLoginState();
     },
   },
   getters: {