diff --git a/frontend/miniapp/app.js b/frontend/miniapp/app.js
index ca88769..1929867 100644
--- a/frontend/miniapp/app.js
+++ b/frontend/miniapp/app.js
@@ -56,26 +56,14 @@ App({
bindPendingPromoter();
}
updateManager();
- //从后台切回前台时刷新一次未读红点(不使用定时轮询,避免请求堆积导致页面卡死)
- this.refreshUnread();
+ // 切回前台的时间。Tab 页用它区分「从后台回来」和「切换 Tab」,避免每次 onShow 都打接口
+ this.globalData.appShowAt = Date.now();
},
onHide: function () {},
globalData: {
unread: 0, //未读消息数(tabbar红点共享缓存)
+ unreadFetchedAt: 0, //上次拉取未读数的时间,各 tabBar 实例共用,避免切 Tab 重复请求
+ appShowAt: 0, //最近一次 App.onShow 的时间
refreshDemandId: 0, //操作后需要刷新状态的需求ID(跨页面通知列表页)
},
-
- /**
- * 刷新当前 tab 页 tabbar 实例的红点(非tab页无tabbar实例,自动跳过)
- */
- refreshUnread: function () {
- if (!wx.getStorageSync('token')) {
- return;
- }
- const page = getCurrentPages().pop();
- const tabBar = page && typeof page.getTabBar === 'function' ? page.getTabBar() : null;
- if (tabBar && typeof tabBar.refreshUnread === 'function') {
- tabBar.refreshUnread();
- }
- },
});
diff --git a/frontend/miniapp/components/webp-image/index.js b/frontend/miniapp/components/webp-image/index.js
index 1706049..67c14bf 100644
--- a/frontend/miniapp/components/webp-image/index.js
+++ b/frontend/miniapp/components/webp-image/index.js
@@ -6,7 +6,25 @@
* @Description: webp-image组件对t-image包裹了一层,主要实现图片裁剪、webp压缩功能
* @FilePath: /tdesign-miniprogram-starter/components/webp-image/index.js
*/
-const systemInfo = wx.getSystemInfoSync();
+let screenWidth = 375;
+let pixelRatio = 2;
+let screenReady = false;
+
+function ensureScreen() {
+ if (screenReady) {
+ return;
+ }
+ screenReady = true;
+ try {
+ const info = wx.getWindowInfo();
+ screenWidth = info.screenWidth || info.windowWidth || 375;
+ pixelRatio = info.pixelRatio || 2;
+ } catch (err) {
+ screenWidth = 375;
+ pixelRatio = 2;
+ }
+}
+
Component({
externalClasses: ['t-class', 't-class-load'],
properties: {
@@ -42,9 +60,14 @@ Component({
data: {
thumbHeight: 375,
thumbWidth: 375,
- systemInfo,
+ screenWidth: 375,
+ pixelRatio: 2,
},
lifetimes: {
+ attached() {
+ ensureScreen();
+ this.setData({ screenWidth, pixelRatio });
+ },
ready() {
const { mode } = this.properties;
// 获取容器的真实宽高,设置图片的裁剪宽度
@@ -66,7 +89,8 @@ Component({
},
methods: {
px2rpx(px) {
- return (750 / (systemInfo.screenWidth || 375)) * px;
+ ensureScreen();
+ return (750 / (screenWidth || 375)) * px;
},
getRect(selector) {
return new Promise((resolve) => {
diff --git a/frontend/miniapp/components/webp-image/index.wxml b/frontend/miniapp/components/webp-image/index.wxml
index 1a917b2..ef82f8d 100644
--- a/frontend/miniapp/components/webp-image/index.wxml
+++ b/frontend/miniapp/components/webp-image/index.wxml
@@ -1,7 +1,7 @@
0) {
- this.setData({ unread: Number(app.globalData.unread) });
+ const unread = app && app.globalData ? Number(app.globalData.unread) || 0 : 0;
+ if (unread > 0) {
+ this.setData({ unread });
}
- this.refreshUnread();
},
},
- //所在 tab 页面显示时刷新(切换 tab、从其他页面返回均会触发)
+ // 当前 tab 显示时刷新。30s 内各实例只发一次请求
pageLifetimes: {
show() {
this.refreshUnread();
@@ -48,35 +48,46 @@ Component({
/**
* 从服务端拉取未读消息数
- * 未登录/请求失败时保持 0(不显示红点),不影响 tabbar 正常使用
- * 5s 内重复触发只发一次请求(app轮询/页面onShow/tabbar show 多处触发)
+ * 未登录时不显示红点。节流记在 globalData,切换 Tab 新建的 tabBar 实例也会共用
+ * 30s 内只发一次;消息页改未读数走 setUnread,不走这里
*/
refreshUnread(force = false) {
- if (!wx.getStorageSync('token')) {
+ const app = getApp();
+ const token = (wx.$util && wx.$util.getToken && wx.$util.getToken()) || '';
+ if (!token) {
this.setData({ unread: 0 });
- const app = getApp();
- if (app) app.globalData.unread = 0;
+ if (app && app.globalData) {
+ app.globalData.unread = 0;
+ }
return;
}
const now = Date.now();
- if (!force && now - (this._lastFetchAt || 0) < 5000) {
+ const last = app && app.globalData ? Number(app.globalData.unreadFetchedAt) || 0 : 0;
+ if (!force && now - last < 30000) {
+ if (app && app.globalData) {
+ this.setData({ unread: Number(app.globalData.unread) || 0 });
+ }
return;
}
- //上一次请求尚未结束时不再发起,避免请求堆积占满并发导致页面假死
- if (this._fetching) {
+ if (app && app.globalData && app.globalData.unreadFetching) {
return;
}
- this._lastFetchAt = now;
- this._fetching = true;
+ if (app && app.globalData) {
+ app.globalData.unreadFetching = true;
+ app.globalData.unreadFetchedAt = now;
+ }
const done = () => {
- this._fetching = false;
+ if (app && app.globalData) {
+ app.globalData.unreadFetching = false;
+ }
};
fetchMessageUnreadCount().then((res) => {
if (res.code == 1) {
const unread = Number(res.data?.unread_count) || 0;
this.setData({ unread });
- const app = getApp();
- if (app) app.globalData.unread = unread;
+ if (app && app.globalData) {
+ app.globalData.unread = unread;
+ }
}
}).catch(() => {}).then(done, done);
},
@@ -88,7 +99,10 @@ Component({
const unread = Number(count) || 0;
this.setData({ unread });
const app = getApp();
- if (app) app.globalData.unread = unread;
+ if (app && app.globalData) {
+ app.globalData.unread = unread;
+ app.globalData.unreadFetchedAt = Date.now();
+ }
},
},
});
diff --git a/frontend/miniapp/pages/demand/detail/index.wxml b/frontend/miniapp/pages/demand/detail/index.wxml
index bcfc571..f5bea14 100644
--- a/frontend/miniapp/pages/demand/detail/index.wxml
+++ b/frontend/miniapp/pages/demand/detail/index.wxml
@@ -16,7 +16,7 @@
-
+
@@ -59,7 +59,7 @@
开发完成凭证截图
-
+
交付附件
diff --git a/frontend/miniapp/pages/demand/list/index.wxml b/frontend/miniapp/pages/demand/list/index.wxml
index 9d6eae7..feed157 100644
--- a/frontend/miniapp/pages/demand/list/index.wxml
+++ b/frontend/miniapp/pages/demand/list/index.wxml
@@ -44,7 +44,7 @@
{{item.expanded ? '收起' : '查看全文'}}
-
+
diff --git a/frontend/miniapp/pages/home/home.js b/frontend/miniapp/pages/home/home.js
index 962fec3..55ca35a 100644
--- a/frontend/miniapp/pages/home/home.js
+++ b/frontend/miniapp/pages/home/home.js
@@ -48,7 +48,13 @@ Page({
onShow() {
this.getTabBar().init();
- this.refreshRole();
+ const app = getApp();
+ const appShowAt = app && app.globalData ? app.globalData.appShowAt : 0;
+ // 冷启动和从后台回到前台才拉角色;切换 Tab 不重复请求
+ if (!this._roleLoadedAt || appShowAt !== this._seenAppShowAt) {
+ this._seenAppShowAt = appShowAt;
+ this.refreshRole();
+ }
},
onLoad() {
@@ -64,12 +70,12 @@ Page({
},
onPullDownRefresh() {
- this.init();
+ this.loadHomePage(true);
this.refreshRole();
},
init() {
- this.loadHomePage();
+ this.loadHomePage(false);
},
/**
@@ -77,6 +83,10 @@ Page({
* 入驻审核通过后,用户回到小程序触发 onShow 即可看到「进入接单大厅」入口
*/
refreshRole() {
+ if (this._roleLoading) {
+ return Promise.resolve();
+ }
+ this._roleLoading = true;
return fetchPerson().then((res) => {
if (res.code === 1 && res.data) {
const role = res.data.role || '';
@@ -87,15 +97,20 @@ Page({
wx.setStorageSync('userInfo', userInfo);
}
}
+ }).catch((err) => {
+ console.error('refreshRole failed:', err);
+ }).then(() => {
+ this._roleLoading = false;
+ this._roleLoadedAt = Date.now();
});
},
- async loadHomePage() {
+ async loadHomePage(force = false) {
this.setData({
pageLoading: true,
});
try {
- const [configRes, bannerRes] = await Promise.all([fetchConfig(), fetchHomeBanners()]);
+ const [configRes, bannerRes] = await Promise.all([fetchConfig({ force }), fetchHomeBanners()]);
const next = { pageLoading: false };
if (configRes.code === 1) {
next.tabList = configRes.data.developTypes || [];
diff --git a/frontend/miniapp/pages/message/detail/index.wxs b/frontend/miniapp/pages/message/detail/index.wxs
index 73c0139..59d922e 100644
--- a/frontend/miniapp/pages/message/detail/index.wxs
+++ b/frontend/miniapp/pages/message/detail/index.wxs
@@ -5,7 +5,7 @@ function tagTheme(type) {
'1': 'warning',
'2': 'success'
};
- return map[String(type)] || 'default';
+ return map['' + type] || 'default';
}
module.exports = {
tagTheme: tagTheme
diff --git a/frontend/miniapp/pages/message/list/index.wxs b/frontend/miniapp/pages/message/list/index.wxs
index 0641e43..03a96df 100644
--- a/frontend/miniapp/pages/message/list/index.wxs
+++ b/frontend/miniapp/pages/message/list/index.wxs
@@ -9,7 +9,7 @@ function tagTheme(type) {
'1': 'warning',
'2': 'success'
};
- return map[String(type)] || 'default';
+ return map['' + type] || 'default';
}
module.exports = {
ellipsis: ellipsis,
diff --git a/frontend/miniapp/pages/order/apply-service/index.js b/frontend/miniapp/pages/order/apply-service/index.js
index 9dd9621..52f5d24 100644
--- a/frontend/miniapp/pages/order/apply-service/index.js
+++ b/frontend/miniapp/pages/order/apply-service/index.js
@@ -298,7 +298,7 @@ Page({
onAmountTap() {
this.setData({
- 'serviceFrom.amount.temp': priceFormat(this.data.serviceFrom.amount.current),
+ 'serviceFrom.amount.temp': priceFormat(this.data.serviceFrom.amount.current, 2),
'serviceFrom.amount.focus': true,
inputDialogVisible: true,
});
@@ -307,8 +307,13 @@ Page({
confirmBtn: '确定',
});
this.inputDialog._onConfirm = () => {
+ const max = Number(this.data.serviceFrom.amount.max) || 0;
+ let amount = parseFloat(this.data.serviceFrom.amount.temp) || 0;
+ if (amount > max) {
+ amount = max;
+ }
this.setData({
- 'serviceFrom.amount.current': this.data.serviceFrom.amount.temp * 100,
+ 'serviceFrom.amount.current': amount,
});
};
this.inputDialog._onCancel = () => {};
@@ -327,12 +332,13 @@ Page({
let { value } = e.detail;
const regRes = value.match(/\d+(\.?\d+)?/); // 失去焦点时,不允许末尾为小数点
value = regRes ? regRes[0] : '0';
- value = parseFloat(value) * 100;
- if (value > this.data.serviceFrom.amount.max) {
- value = this.data.serviceFrom.amount.max;
+ let amount = parseFloat(value) || 0;
+ const max = Number(this.data.serviceFrom.amount.max) || 0;
+ if (amount > max) {
+ amount = max;
}
this.setData({
- 'serviceFrom.amount.temp': priceFormat(value),
+ 'serviceFrom.amount.temp': priceFormat(amount, 2),
'serviceFrom.amount.focus': false,
});
},
diff --git a/frontend/miniapp/pages/usercenter/index.js b/frontend/miniapp/pages/usercenter/index.js
index 4b5ba49..b5c1fce 100644
--- a/frontend/miniapp/pages/usercenter/index.js
+++ b/frontend/miniapp/pages/usercenter/index.js
@@ -3,6 +3,8 @@ import {
} from '../../services/usercenter/fetchUsercenter';
import Toast from 'tdesign-miniprogram/toast/index';
+const USER_CENTER_TTL = 30000;
+
const menuData = [
[{
title: '我发布的需求',
@@ -94,6 +96,31 @@ const orderTagInfos = [{
},
];
+function cloneMenu(list) {
+ return list.map((row) => row.map((item) => ({ ...item })));
+}
+
+/** 按角色组装菜单,每次返回新数组,避免改到模块上的模板 */
+function buildMenu(userInfo = {}, countsData = []) {
+ const menu = cloneMenu(menuData);
+ menu[0] = menu[0].filter((item) => item.type !== 'quote' && item.type !== 'promoter');
+ if (userInfo.role === 'dev') {
+ menu[0].splice(1, 0, { title: '我投标的需求', tit: '', url: '', icon: '', type: 'quote' });
+ }
+ if (userInfo.group_name === PROMOTER_GROUP) {
+ const balanceIndex = menu[0].findIndex((item) => item.type === 'balance');
+ menu[0].splice(balanceIndex === -1 ? menu[0].length : balanceIndex + 1, 0, { ...promoterMenu });
+ }
+ const counts = Array.isArray(countsData) ? countsData : [];
+ menu[0].forEach((item) => {
+ const matched = counts.find((count) => count.type === item.type);
+ if (matched) {
+ item.tit = matched.num;
+ }
+ });
+ return menu;
+}
+
const getDefaultData = () => ({
showMakePhone: false,
userInfo: {
@@ -101,8 +128,8 @@ const getDefaultData = () => ({
nickName: '正在登录...',
phoneNumber: '',
},
- menuData,
- orderTagInfos,
+ menuData: cloneMenu(menuData),
+ orderTagInfos: orderTagInfos.map((item) => ({ ...item })),
customerServiceInfo: {},
currAuthStep: 1,
showKefu: true,
@@ -118,10 +145,22 @@ Page({
onShow() {
this.getTabBar().init();
- this.init();
+ const app = getApp();
+ const appShowAt = app && app.globalData ? app.globalData.appShowAt : 0;
+ const fromBackground = appShowAt !== this._seenAppShowAt;
+ this._seenAppShowAt = appShowAt;
+ const stale = !this._loadedAt || Date.now() - this._loadedAt > USER_CENTER_TTL;
+ // 从后台回来,或距离上次成功超过 30 秒,才重新拉用户中心
+ if (fromBackground || stale) {
+ this.init();
+ }
},
onPullDownRefresh() {
- this.init();
+ if (this._loading) {
+ wx.stopPullDownRefresh();
+ return;
+ }
+ this.fetUseriInfoHandle();
},
init() {
@@ -129,44 +168,40 @@ Page({
},
fetUseriInfoHandle() {
- fetchUserCenter().then(
- (res) => {
- const {
- userInfo,
- countsData,
- orderTagInfos: orderTags,
- customerServiceInfo,
- } = res.data;
- // eslint-disable-next-line no-unused-expressions
- menuData[0] = menuData[0].filter((item) => item.type !== 'quote' && item.type !== 'promoter');
- if (userInfo.role === 'dev') {
- menuData[0].splice(1, 0, { title: '我投标的需求', tit: '', url: '', icon: '', type: 'quote' });
- }
- if (userInfo.group_name === PROMOTER_GROUP) {
- const balanceIndex = menuData[0].findIndex((item) => item.type === 'balance');
- menuData[0].splice(balanceIndex === -1 ? menuData[0].length : balanceIndex + 1, 0, { ...promoterMenu });
- }
- menuData[0].forEach((v) => {
- countsData.forEach((counts) => {
- if (counts.type === v.type) {
- v.tit = counts.num;
- }
- });
- });
- const info = orderTagInfos.map((v, index) => ({
- ...v,
- ...orderTags[index],
- }));
- this.setData({
- userInfo,
- menuData,
- orderTagInfos: info,
- customerServiceInfo,
- currAuthStep: 2,
- });
- wx.stopPullDownRefresh();
- },
- );
+ if (this._loading) {
+ return;
+ }
+ this._loading = true;
+ fetchUserCenter().then((res) => {
+ if (!res || res.code !== 1 || !res.data) {
+ wx.showToast({ title: (res && res.msg) || '加载失败', icon: 'none' });
+ return;
+ }
+ const {
+ userInfo,
+ countsData,
+ orderTagInfos: orderTags,
+ customerServiceInfo,
+ } = res.data;
+ const info = orderTagInfos.map((item, index) => ({
+ ...item,
+ ...(orderTags && orderTags[index] ? orderTags[index] : {}),
+ }));
+ this._loadedAt = Date.now();
+ this.setData({
+ userInfo,
+ menuData: buildMenu(userInfo, countsData),
+ orderTagInfos: info,
+ customerServiceInfo,
+ currAuthStep: 2,
+ });
+ }).catch((err) => {
+ console.error('fetchUserCenter failed:', err);
+ wx.showToast({ title: '网络异常,请重试', icon: 'none' });
+ }).then(() => {
+ this._loading = false;
+ wx.stopPullDownRefresh();
+ });
},
onClickCell({
diff --git a/frontend/miniapp/services/home/home.js b/frontend/miniapp/services/home/home.js
index ee17e7d..9597b39 100644
--- a/frontend/miniapp/services/home/home.js
+++ b/frontend/miniapp/services/home/home.js
@@ -52,9 +52,46 @@ export function fetchHome() {
});
}
-/** 获取配置数据 */
-export function fetchConfig() {
- return wx.$util.request(wx.$api.CommonGetMiniConfig, {}, 'GET', { auth: false });
+const CONFIG_TTL = 60000;
+let configCache = null;
+let configTask = null;
+// 与后端 site.platform_commission_ratio 默认值一致,接口失败时兜底
+const DEFAULT_COMMISSION_RATIO = 0.1;
+let commissionRatioCache = null;
+const CASE_VIEW_STORAGE_KEY = 'goodsCaseView';
+let caseViewCache = null;
+
+/**
+ * 获取小程序配置。成功结果缓存 60 秒,同时进行中的请求合并成一次
+ * @param {{force?: boolean}} options force 为 true 时忽略缓存(下拉刷新)
+ */
+export function fetchConfig(options = {}) {
+ const force = options === true || options.force === true;
+ if (configTask) {
+ return configTask;
+ }
+ if (!force && configCache && Date.now() - configCache.at < CONFIG_TTL) {
+ return Promise.resolve(configCache.res);
+ }
+ configTask = wx.$util.request(wx.$api.CommonGetMiniConfig, {}, 'GET', { auth: false }).then((res) => {
+ if (res && res.code === 1) {
+ configCache = { at: Date.now(), res };
+ const ratio = Number(res.data?.platform_commission_ratio);
+ if (ratio >= 0 && ratio <= 1) {
+ commissionRatioCache = ratio;
+ }
+ caseViewCache = res.data?.goods_case_view === true || res.data?.goods_case_view === 1;
+ wx.setStorageSync(CASE_VIEW_STORAGE_KEY, caseViewCache);
+ }
+ return res;
+ }).then((res) => {
+ configTask = null;
+ return res;
+ }, (err) => {
+ configTask = null;
+ throw err;
+ });
+ return configTask;
}
/** 获取首页轮播(分类类型 banner) */
@@ -62,10 +99,6 @@ export function fetchHomeBanners() {
return wx.$util.request(wx.$api.CatalogList, { type: 'banner' }, 'GET', { auth: false });
}
-// 与后端 site.platform_commission_ratio 默认值一致,接口失败时兜底
-const DEFAULT_COMMISSION_RATIO = 0.1;
-let commissionRatioCache = null;
-
/**
* 获取平台服务费比例(0~1),成功后缓存,失败返回默认值且不缓存,下次重新请求
* @returns {Promise}
@@ -87,9 +120,6 @@ export async function fetchPlatformCommissionRatio() {
return DEFAULT_COMMISSION_RATIO;
}
-const CASE_VIEW_STORAGE_KEY = 'goodsCaseView';
-let caseViewCache = null;
-
/**
* 商品是否按案例展示(不显示价格和购买入口)
* 成功后缓存;失败时用上次成功的本地记录
diff --git a/frontend/miniapp/utils/util.js b/frontend/miniapp/utils/util.js
index 20f5ecb..025d00a 100644
--- a/frontend/miniapp/utils/util.js
+++ b/frontend/miniapp/utils/util.js
@@ -406,24 +406,23 @@ function getUid(prefix) {
}
/**
- * 格式化价格数额为字符串
- * 可对小数部分进行填充,默认不填充
- * @param price 价格数额,以分为单位!
- * @param fill 是否填充小数部分 0-不填充 1-填充第一位小数 2-填充两位小数
+ * 格式化金额为字符串,单位为元
+ * @param price 金额,单位元
+ * @param fill 小数位数 0-不填充 1-一位 2-两位
*/
function priceFormat(price, fill = 0) {
if (isNaN(price) || price === null || price === Infinity) {
return price;
}
- let priceFormatValue = Math.round(parseFloat(`${price}`) * 10 ** 8) / 10 ** 8; // 恢复精度丢失
- priceFormatValue = `${Math.ceil(priceFormatValue) / 100}`; // 向上取整,单位转换为元,转换为字符串
+ const rounded = Math.round(parseFloat(`${price}`) * 100) / 100;
+ let priceFormatValue = `${rounded}`;
if (fill > 0) {
- // 补充小数位数
if (priceFormatValue.indexOf('.') === -1) {
priceFormatValue = `${priceFormatValue}.`;
}
- const n = fill - priceFormatValue.split('.')[1]?.length;
+ const decimal = priceFormatValue.split('.')[1] || '';
+ const n = fill - decimal.length;
for (let i = 0; i < n; i++) {
priceFormatValue = `${priceFormatValue}0`;
}