From 6beeeb2383331a58a898492426074ad3da79fb45 Mon Sep 17 00:00:00 2001 From: Dev Date: Mon, 19 Oct 2026 15:46:10 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/miniapp/app.js | 20 +-- .../miniapp/components/webp-image/index.js | 30 ++++- .../miniapp/components/webp-image/index.wxml | 4 +- .../miniapp/components/webp-image/utils.wxs | 6 +- frontend/miniapp/custom-tab-bar/index.js | 52 +++++--- .../miniapp/pages/demand/detail/index.wxml | 4 +- frontend/miniapp/pages/demand/list/index.wxml | 2 +- frontend/miniapp/pages/home/home.js | 25 +++- .../miniapp/pages/message/detail/index.wxs | 2 +- frontend/miniapp/pages/message/list/index.wxs | 2 +- .../pages/order/apply-service/index.js | 18 ++- frontend/miniapp/pages/usercenter/index.js | 119 +++++++++++------- frontend/miniapp/services/home/home.js | 50 ++++++-- frontend/miniapp/utils/util.js | 15 ++- 14 files changed, 229 insertions(+), 120 deletions(-) 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`; }