Преглед на файлове

📦 请求缓存优化,贴图管理升级功能

快乐的梦鱼 преди 1 седмица
родител
ревизия
8e4985289a

+ 1 - 0
.npmrc

@@ -0,0 +1 @@
+registry=https://registry.npmjs.com/

+ 4 - 4
package-lock.json

@@ -24,7 +24,7 @@
         "@dcloudio/uni-mp-weixin": "3.0.0-5000720260410001",
         "@dcloudio/uni-mp-xhs": "3.0.0-5000720260410001",
         "@dcloudio/uni-quickapp-webview": "3.0.0-5000720260410001",
-        "@imengyu/imengyu-utils": "^0.0.28",
+        "@imengyu/imengyu-utils": "^0.1.0",
         "@imengyu/js-request-transform": "^0.4.0",
         "async-validator": "^4.2.5",
         "crypto-js": "^4.2.0",
@@ -3709,9 +3709,9 @@
       }
     },
     "node_modules/@imengyu/imengyu-utils": {
-      "version": "0.0.28",
-      "resolved": "https://registry.npmmirror.com/@imengyu/imengyu-utils/-/imengyu-utils-0.0.28.tgz",
-      "integrity": "sha512-DE0hrBJjdAsyiNJER/ZS8cuEl9+nWZtyUiJBKFimxuVLLiBtPZC2PQRcrHBDdyhxeQHUhfjCvqRbbFjDY6jloQ==",
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/@imengyu/imengyu-utils/-/imengyu-utils-0.1.0.tgz",
+      "integrity": "sha512-AuHAusrXPTlZxpjhG6VN4S47hn4WiExlzJgdpiOXoHaCZSm/+dUnJySehSEInow/jepGU1Z2D8PYIdZ/ZNRNUA==",
       "license": "MIT",
       "dependencies": {
         "@imengyu/js-request-transform": "^0.4.0"

+ 1 - 1
package.json

@@ -51,7 +51,7 @@
     "@dcloudio/uni-mp-weixin": "3.0.0-5000720260410001",
     "@dcloudio/uni-mp-xhs": "3.0.0-5000720260410001",
     "@dcloudio/uni-quickapp-webview": "3.0.0-5000720260410001",
-    "@imengyu/imengyu-utils": "^0.0.28",
+    "@imengyu/imengyu-utils": "^0.1.0",
     "@imengyu/js-request-transform": "^0.4.0",
     "async-validator": "^4.2.5",
     "crypto-js": "^4.2.0",

+ 2 - 0
src/App.vue

@@ -12,6 +12,7 @@ import { RequestApiConfig, RequestApiError } from '@imengyu/imengyu-utils';
 import ApiCofig from './common/config/ApiCofig';
 import MemoryTimeOut from './components/composeabe/MemoryTimeOut';
 import BugReporter from './common/BugReporter';
+import CacheManager from './api/CacheManager';
 import '@/common/style/icons';
 
 const authStore = useAuthStore();
@@ -22,6 +23,7 @@ const redirectThrottle = new MemoryTimeOut('RedirectThrottle', 50000);
 onLaunch(async () => {
   console.log('App Launch');
 
+  CacheManager.cleanExpired();
   loadFontFace();
   await loadAppConfiguration();
 

+ 12 - 2
src/api/BaseAppServerRequestModule.ts

@@ -1,8 +1,9 @@
 import BugReporter from "@/common/BugReporter";
 import ApiCofig from "@/common/config/ApiCofig";
 import AppCofig, { isDev } from "@/common/config/AppCofig";
-import { RequestCoreInstance, RequestApiError, UniappImplementer, StringUtils, appendGetUrlParams, appendPostParams, RequestResponse, RequestOptions, type RequestApiInfoStruct, RequestApiResult, type RequestApiErrorType, defaultResponseDataGetErrorInfo, defaultResponseDataHandlerCatch } from "@imengyu/imengyu-utils";
+import { RequestCoreInstance, RequestApiError, UniappImplementer, StringUtils, appendGetUrlParams, appendPostParams, RequestResponse, RequestOptions, type RequestApiInfoStruct, RequestApiResult, type RequestApiErrorType, defaultResponseDataGetErrorInfo, defaultResponseDataHandlerCatch, ObjectUtils } from "@imengyu/imengyu-utils";
 import type { DataModel, KeyValue, NewDataModel } from "@imengyu/js-request-transform";
+import CacheManager from "./CacheManager";
 
 /**
  * 说明:业务相关的请求模块
@@ -73,9 +74,18 @@ export function reportError<T extends DataModel>(instance: RequestCoreInstance<T
   }
 }
 
+const myUniappImplementer = ObjectUtils.clone(UniappImplementer);
+myUniappImplementer.getCache = async (key) => await CacheManager.get(key);
+myUniappImplementer.setCache = async (key, value) => {
+  if (value)
+    CacheManager.set(key, value, value.expireAt)
+  else
+    CacheManager.remove(key);
+};
+
 export class BaseAppServerRequestModule<T extends DataModel> extends RequestCoreInstance<T> {
   constructor(baseUrl: string) {
-    super(UniappImplementer);
+    super(myUniappImplementer);
     this.config.baseUrl = baseUrl;
     this.config.errCodes = [];
     //请求拦截器

+ 67 - 0
src/api/CacheManager.ts

@@ -0,0 +1,67 @@
+const CACHE_PREFIX = '__cache_';
+
+interface CacheEntry<T = any> {
+  value: T;
+  expireAt: number | null;
+}
+
+class CacheManager {
+  private buildKey(key: string) {
+    return CACHE_PREFIX + key;
+  }
+
+  private isExpired(entry: CacheEntry): boolean {
+    return entry.expireAt !== null && Date.now() > entry.expireAt;
+  }
+
+  cleanExpired() {
+    try {
+      const { keys } = uni.getStorageInfoSync();
+      for (const key of keys) {
+        if (!key.startsWith(CACHE_PREFIX)) continue;
+        try {
+          const raw = uni.getStorageSync(key);
+          if (raw && typeof raw === 'object' && 'expireAt' in raw && this.isExpired(raw)) {
+            uni.removeStorageSync(key);
+          }
+        } catch {
+          // 单条解析失败不影响其他清理
+        }
+      }
+    } catch {
+      // getStorageInfoSync 失败时静默处理
+    }
+  }
+
+  get<T = any>(key: string): T | null {
+    try {
+      const raw = uni.getStorageSync(this.buildKey(key));
+      if (!raw) return null;
+      const entry = raw as CacheEntry<T>;
+      if (this.isExpired(entry)) {
+        uni.removeStorageSync(this.buildKey(key));
+        return null;
+      }
+      return entry.value;
+    } catch {
+      return null;
+    }
+  }
+
+  /**
+   * @param ttl 过期时间,单位毫秒。不传则永不过期
+   */
+  set(key: string, value: any, ttl?: number) {
+    const entry: CacheEntry = {
+      value,
+      expireAt: ttl ? Date.now() + ttl : null,
+    };
+    uni.setStorageSync(this.buildKey(key), entry);
+  }
+
+  remove(key: string) {
+    uni.removeStorageSync(this.buildKey(key));
+  }
+}
+
+export default new CacheManager();

+ 8 - 2
src/api/light/LightVillageApi.ts

@@ -301,7 +301,10 @@ export class LightVillageApi extends AppServerRequestModule<DataModel> {
       sex_text?: string;
       status_text?: string;
       image?: string;
-    }[]>('/village/volunteer/getRanklist', '志愿者排行榜', params);
+    }[]>('/village/volunteer/getRanklist', '志愿者排行榜', params, undefined, undefined, {
+      cacheEnable: true,
+      cacheTime: 1000 * 60 * 10, //10min
+    });
     return res.requireData();
   }
 
@@ -328,7 +331,10 @@ export class LightVillageApi extends AppServerRequestModule<DataModel> {
       region_text?: string;
       is_light_text?: string;
       image?: string;
-    }[]>('/village/village/getRanklist', '村社排行榜', params);
+    }[]>('/village/village/getRanklist', '村社排行榜', params, undefined, undefined, {
+      cacheEnable: true,
+      cacheTime: 1000 * 60 * 10, //10min
+    });
     return res.requireData();
   }
 

+ 84 - 0
src/api/light/OfficialApi.ts

@@ -133,6 +133,71 @@ export interface StickerSearchResult {
   id: StickerMsgId;
 }
 
+export class StaffUpgradeOrder extends DataModel<StaffUpgradeOrder> {
+  constructor() {
+    super(StaffUpgradeOrder, '升级订单');
+    this.setNameMapperCase('Camel', 'Snake');
+    this._convertTable = {
+      id: { clientSide: 'number', serverSide: 'number', clientSideRequired: true },
+      userId: { clientSide: 'number', serverSide: 'number' },
+      villageId: { clientSide: 'number', serverSide: 'number' },
+      amount: { clientSide: 'number', serverSide: 'number' },
+      status: { clientSide: 'number', serverSide: 'number' },
+      orderType: { clientSide: 'number', serverSide: 'number' },
+      payType: { clientSide: 'number', serverSide: 'number' },
+      paytime: { clientSide: 'number', serverSide: 'number' },
+      staffLevelId: { clientSide: 'number', serverSide: 'number' },
+      days: { clientSide: 'number', serverSide: 'number' },
+      timeType: { clientSide: 'number', serverSide: 'number' },
+      rewardStatus: { clientSide: 'number', serverSide: 'number' },
+    };
+  }
+
+  id!: number;
+  /** 订单号 */
+  orderNo = '';
+  /** 用户ID */
+  userId = 0;
+  /** 订单类型: 1=赐福, 2=村社VIP升级, 3=个人升级 */
+  orderType = 0;
+  /** 管理等级ID */
+  staffLevelId = 0;
+  /** 村社ID */
+  villageId = 0;
+  /** 支付金额 */
+  amount = 0;
+  /** 时间类型 */
+  timeType = 0;
+  /** 天数 */
+  days = 0;
+  /** 状态: -1=已取消, 0=未支付, 1=已支付, 2=待审核 */
+  status = 0;
+  /** 支付方式: 1=线上支付, 2=对公付款 */
+  payType = 0;
+  /** 支付时间 */
+  paytime = 0;
+  /** 奖励状态: 0=待发放, 1=已发放 */
+  rewardStatus = 0;
+  /** 创建时间 */
+  createtime = '';
+  /** 更新时间 */
+  updatetime = '';
+}
+
+export interface WxPayParams {
+  appId: string;
+  timeStamp: string;
+  nonceStr: string;
+  package: string;
+  signType: string;
+  paySign: string;
+}
+
+export interface StaffUpgradeResult {
+  order: StaffUpgradeOrder;
+  pay: WxPayParams;
+}
+
 export class OfficialApi extends AppServerRequestModule<DataModel> {
 
   constructor() {
@@ -259,6 +324,25 @@ export class OfficialApi extends AppServerRequestModule<DataModel> {
     });
     return res.requireData();
   }
+
+  /**
+   * 个人升级村社管理
+   * @param staffLevelId 管理等级ID: 1=村社管理, 2=测试
+   */
+  async upgradeStaff(villageId: number, staffLevelId = 1) {
+    const res = await this.post<{
+      order: KeyValue,
+      pay: WxPayParams,
+    }>('/village/growth/staff', '升级村社管理', {
+      village_id: villageId,
+      staff_level_id: staffLevelId,
+    });
+    const data = res.requireData();
+    return {
+      order: transformDataModel<StaffUpgradeOrder>(StaffUpgradeOrder, data.order),
+      pay: data.pay,
+    } as StaffUpgradeResult;
+  }
 }
 
 

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

@@ -481,6 +481,9 @@ export class TreeApi extends AppServerRequestModule<DataModel> {
     const res = await this.post<PagedGrowthResponse>(url, label, {
       ...this.growthLogParams({ ...search, page: 1, pageSize }),
       ...extra,
+    }, undefined, undefined, {
+      cacheEnable: true,
+      cacheTime: 1000 * 60 * 10, //10min
     });
     return this.parsePagedList<T>(model, res.requireData(), label).list;
   }

+ 7 - 0
src/pages.json

@@ -232,6 +232,13 @@
       }
     },
     {
+      "path": "pages/home/village/upgrade/my-upgrade-management",
+      "style": {
+        "navigationBarTitleText": "升级成为管理员",
+        "navigationStyle": "custom"
+      }
+    },
+    {
       "path": "pages/article/details",
       "style": {
         "navigationBarTitleText": "新闻详情"

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

@@ -226,10 +226,10 @@ function publish() {
         console.error(e);
       });
       uni.removeStorageSync('postDraft');
-      toast('发布成功');
+      toast('发布成功, 文章可能需要审核稍后才能展示');
       setTimeout(() => {
         backAndCallOnPageBack('refreshOfficialAccount', { })
-      }, 1000);
+      }, 2000);
     },
     fail: (error: any) => {
       console.error(error);

+ 1 - 1
src/pages/home/components/VillageUserRankList.vue

@@ -1,6 +1,6 @@
 <template>
   <Empty v-if="list.length === 0" description="暂无排名数据" />
-  <FlexRow align="flex-end">
+  <FlexRow align="flex-end" justify="space-around">
     <Touchable 
       v-for="(item) in list" 
       position="relative" 

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

@@ -417,7 +417,6 @@ async function loadInfo() {
     const currentVillage = villageStore.loadCurrentVillage();
     const id = (villageStore.myFollowVillages.find(p => p.id === currentVillage) as VillageListItem || villageStore.myFollowVillages[0]).id;
     const res = await LightVillageApi.getVillageDetails(id)
-    console.log('加载我的关注村庄', res);
     villageStore.setCurrentVillage(res);
   }
 }
@@ -440,7 +439,6 @@ onMounted(async () => {
     console.error(error);
     toast('获取当前位置失败,您可以手动选择城市');
   }
-  await loadInfo();
 });
 defineExpose({
   onPageBack: (name: string, data: Record<string, unknown>) => {

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

@@ -47,7 +47,7 @@
           <Width :width="30" />
           <HomeLargeTitle title="乡源树" titleColorAnim :active="tab === 'tree'" @click="tab = 'tree'">
             <template #icon>
-              <Image src="https://xy.wenlvti.net/app_static/images/village/TreeIconAmin1.gif" :width="34" :height="50" mode="heightFix" />
+              <Image src="https://xy.wenlvti.net/app_static/images/village/TreeIconAmin2.gif" :width="34" :height="50" mode="heightFix" />
             </template>
           </HomeLargeTitle>
         </FlexRow>

+ 14 - 9
src/pages/home/village/introd/card.vue

@@ -285,7 +285,7 @@ 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 } from '@imengyu/imengyu-utils';
+import { ArrayUtils, assertNotNull, waitTimeOut } from '@imengyu/imengyu-utils';
 import { navTo } from '@/components/utils/PageAction';
 import HomeTitle from '@/common/components/parts/HomeTitle.vue';
 import Icon from '@/components/basic/Icon.vue';
@@ -463,19 +463,21 @@ function handleGoGallery() {
 }
 async function handleGoOfficalManage() {
   assertNotNull(villageStore.currentVillage?.id)
-  /* const isAdmin = await OfficialApi.checkTopicRule(villageStore.currentVillage.id);
+  const isAdmin = await OfficialApi.checkTopicRule(villageStore.currentVillage.id);
   if (!isAdmin) {
     const goUpgrade = await confirm({
       title: '提示',
-      content: '您还不是管理员,无法管理贴图哦',
+      content: '您还不是管理员,无法管理贴图哦。您可以联系村社管理员将你添加为管理员,若当前村社暂无管理员,您可以点击“去升级”将您升级为管理员。',
       confirmText: '去升级',
       cancelText: '取消',
     });
     if (goUpgrade) {
-      upgradeRef.value?.show();
+      navTo('/pages/home/village/upgrade/my-upgrade-management', {
+        villageId: villageStore.currentVillage.id,
+      });
     }
     return;
-  } */
+  }
   navTo('/pages/home/village/post/management-list', {
     villageId: villageStore.currentVillage.id,
     topic: recommendTagName.value,
@@ -487,10 +489,13 @@ const { currentNoticeContent, noticeListLoader } = useGetNotice(() => villageSto
 const upgradeRef = ref<InstanceType<typeof UpgradeDialog>>();
 const villageGalleryRef = ref<InstanceType<typeof VillageGallery>>();
 
-watch(() => villageStore.currentVillage, () => {
-  villageInfoLoader.reload();
-  villageUserRankListLoader.reload();
-  noticeListLoader.reload();
+watch(() => villageStore.currentVillage, async () => {
+  await waitTimeOut(100);
+  await villageInfoLoader.reload();
+  await waitTimeOut(100);
+  await villageUserRankListLoader.reload();
+  await waitTimeOut(100);
+  await noticeListLoader.reload();
 }, { immediate: true });
 
 defineExpose({

+ 1 - 1
src/pages/home/village/post/management-list.vue

@@ -70,7 +70,7 @@ const listLoader = useSimplePageListLoader(40, async (page, pageSize) => {
     })
 
   for (const element of list) {
-    if (!element.jumpUrl)
+    if (!element || !element.jumpUrl)
       continue;
     const res = await OfficialApi.stickerSearch(element.topicName, element.jumpUrl);
     element.ext = res;

+ 37 - 0
src/pages/home/village/upgrade/dialogs/UpgradeManagementSuccess.vue

@@ -0,0 +1,37 @@
+<template>
+  <CommonDialog v-model:show="show" title="升级完成" :showDivider="false" :showCloseButton="false">
+    <FlexCol gap="gap.lg" padding="padding.md" width="600rpx">
+      <Result
+        status="success"
+        title="感谢您的贡献!"
+        description="您已升级村社管理员,可以管理村社"
+      />
+      <FlexRow justify="space-around" gap="gap.md">
+        <FrameButton primary text="完成" @click="handleFinish" width="220rpx" />
+      </FlexRow>
+    </FlexCol>
+  </CommonDialog>
+</template>
+
+<script setup lang="ts">
+import { ref } from "vue";
+import CommonDialog from "@/common/components/CommonDialog.vue";
+import FrameButton from "@/common/components/FrameButton.vue";
+import FlexCol from "@/components/layout/FlexCol.vue";
+import FlexRow from "@/components/layout/FlexRow.vue";
+import Result from "@/components/feedback/Result.vue";
+
+const emit = defineEmits(['success']);
+const show = ref(false);
+
+function handleFinish() {
+  emit('success');
+  show.value = false;
+}
+
+defineExpose({
+  open() {
+    show.value = true;
+  },
+});
+</script>

+ 136 - 0
src/pages/home/village/upgrade/my-upgrade-management.vue

@@ -0,0 +1,136 @@
+
+
+<template>
+  <CommonTopBanner 
+    title="升级成为管理员"
+    showNav
+  >
+    <FlexCol gap="gap.lg" padding="padding.md">
+      
+      <!-- <FlexRow justify="flex-end">
+        <Button icon="https://xy.wenlvti.net/app_static/images/home/IconOrder.png" text="我的订单" @click="handleMyOrders()" />
+      </FlexRow>
+      <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
+          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)" />
+        </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)" />
+        </BoxMid>
+      </FlexCol>
+    </FlexCol>
+    <UpgradeManagementSuccessDialog  
+      ref="upgradeManagementSuccessDialog"
+      @success="handlePaySuccess"
+    />
+  </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 OfficialApi from '@/api/light/OfficialApi';
+import UpgradeManagementSuccessDialog from './dialogs/UpgradeManagementSuccess.vue';
+
+const upgradeManagementSuccessDialog = ref<InstanceType<typeof UpgradeManagementSuccessDialog>>();
+
+const { requireLoginAsync } = useRequireLogin();
+
+function handlePaySuccess() {
+  setTimeout(() => {
+    backAndCallOnPageBack('paySuccessAndRefresh', {});
+  }, 100);
+}
+
+const { querys } = useLoadQuerys({
+  villageId: 0,
+}, () => {
+
+});
+
+async function handleMyOrders() {
+  if (!await requireLoginAsync('登录后查看我的升级订单哦'))
+    return;
+  navTo('/pages/home/village/upgrade/my-orders', {
+    villageId: querys.value.villageId,
+  });
+}
+
+async function handleDirectPay(levelType: number) {
+  if (!requireLoginAsync('登录后为村社升级,做出你的贡献哦'))
+    return;
+  try {
+    uni.showLoading({ title: '创建订单中...' });
+    const { order: orderInfo, pay: payInfo } = await OfficialApi.upgradeStaff(
+      querys.value.villageId,
+      levelType,
+    );
+    if (payInfo) {
+      uni.requestPayment({
+        provider: 'wxpay',
+        appId: payInfo.appId,
+        timeStamp: payInfo.timeStamp,
+        nonceStr: payInfo.nonceStr,
+        package: payInfo.package,
+        signType: payInfo.signType,
+        paySign: payInfo.paySign,
+        success: () => {
+          upgradeManagementSuccessDialog.value?.open();
+        },
+        fail: (err) => {
+          showError(`支付失败: ${err.errMsg}`);
+        },
+      });
+    }
+  } catch (e) {
+    showError(e);
+  } finally {
+    uni.hideLoading();
+  }
+}
+
+defineExpose({
+  onPageBack(name: string, data: any) {
+    if (name === 'handlePaySuccess') {
+      handlePaySuccess();
+    }
+  },
+});
+</script>

+ 1 - 1
src/store/village.ts

@@ -17,7 +17,7 @@ export const useVillageStore = defineStore('village', () => {
 
   async function setCurrentVillage(village: VillageListItem) {
     currentVillage.value = await LightVillageApi.getVillageDetails(village.id);
-    console.log('currentVillage', currentVillage.value);
+    console.log('切换当前激活村社', currentVillage.value);
     uni.setStorageSync('currentVillage', village.id);
   }
   function loadCurrentVillage() {