页面优化

This commit is contained in:
Dev
2026-10-19 15:46:10 +08:00
parent 44405743bb
commit 6beeeb2383
14 changed files with 229 additions and 120 deletions
+4 -16
View File
@@ -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();
}
},
});
@@ -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) => {
@@ -1,7 +1,7 @@
<wxs src="./utils.wxs" module="Utils" />
<t-image
t-class="J-image"
src="{{Utils.getSrc({src, thumbWidth: thumbWidth || 0, thumbHeight: thumbHeight || 0, systemInfo, webp, mode})}}"
class="J-image"
src="{{Utils.getSrc({src, thumbWidth: thumbWidth || 0, thumbHeight: thumbHeight || 0, screenWidth, pixelRatio, webp, mode})}}"
t-class="t-class"
t-class-load="t-class-load"
mode="{{ mode }}"
@@ -119,13 +119,11 @@ function getSrc(options) {
return imageMogr(options.src, {
width:
options.mode !== 'heightFix'
? rpx2px(options.thumbWidth, options.systemInfo.screenWidth) *
options.systemInfo.pixelRatio
? rpx2px(options.thumbWidth, options.screenWidth) * (options.pixelRatio || 2)
: null,
height:
options.mode !== 'widthFix'
? rpx2px(options.thumbHeight, options.systemInfo.screenWidth) *
options.systemInfo.pixelRatio
? rpx2px(options.thumbHeight, options.screenWidth) * (options.pixelRatio || 2)
: null,
format: options.webp ? 'webp' : null,
});
+33 -19
View File
@@ -9,16 +9,16 @@ Component({
lifetimes: {
attached() {
//先用 globalData 缓存渲染,避免切换 tab 时红点闪没,再拉取最新
// 先用全局缓存渲染,避免切换 tab 时红点闪没。网络请求交给 show,并与其他实例共用节流
const app = getApp();
if (app && Number(app.globalData?.unread) > 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();
}
},
},
});
@@ -16,7 +16,7 @@
<t-cell title="工期" note="{{_calc.withUnit(demandInfo.limit_time, '天')}}" bordered hover />
<t-cell title="联系方式" note="{{demandInfo.contactText}}" bordered hover />
<view class="qrcode-row">
<image class="qrcode-img" wx:if="{{demandInfo.demand_group_chat.qr_code && (demandInfo.isMy || demandInfo.isMyWin )}}" src="{{demandInfo.demand_group_chat.qr_code}}" data-index="{{0}}" data-images="{{[demandInfo.demand_group_chat.qr_code]}}" mode="widthFix" bind:tap="onImageView" show-menu-by-longpress="{{true}}" />
<image class="qrcode-img" wx:if="{{demandInfo.demand_group_chat.qr_code && (demandInfo.isMy || demandInfo.isMyWin )}}" src="{{demandInfo.demand_group_chat.qr_code}}" data-index="{{0}}" data-images="{{[demandInfo.demand_group_chat.qr_code]}}" mode="widthFix" bind:tap="onImageView" show-menu-by-longpress="{{true}}"></image>
</view>
<!-- 任务内容 -->
<t-collapse value="{{activeValues}}" bind:change="handleChange">
@@ -59,7 +59,7 @@
<view class="delivery-section">
<view class="delivery-title">开发完成凭证截图</view>
<view class="delivery-images">
<image wx:for="{{_calc.getDelivery(demandInfo).images}}" wx:key="index" class="delivery-image" src="{{item.url}}" mode="aspectFill" data-index="{{index}}" data-images="{{_calc.getDelivery(demandInfo).images}}" bindtap="onDeliveryImagePreview" />
<image wx:for="{{_calc.getDelivery(demandInfo).images}}" wx:key="index" class="delivery-image" src="{{item.url}}" mode="aspectFill" data-index="{{index}}" data-images="{{_calc.getDelivery(demandInfo).images}}" bindtap="onDeliveryImagePreview"></image>
</view>
<view class="delivery-title">交付附件</view>
<view class="file-list">
@@ -44,7 +44,7 @@
{{item.expanded ? '收起' : '查看全文'}}
</view>
<view class="content-img-list">
<image wx:for="{{item.images}}" wx:for-item="img" wx:for-index="imgIndex" wx:key="*this" class="content-img" mode="aspectFill" data-index="{{imgIndex}}" data-images="{{item.images}}" src="{{img}}" bind:tap="onImageView"></image>
<image wx:for="{{item.images}}" wx:for-item="img" wx:for-index="imgIndex" wx:key="*this" class="content-img" mode="aspectFill" src="{{img}}" data-index="{{imgIndex}}" data-images="{{item.images}}" bind:tap="onImageView"></image>
</view>
</view>
<view class="bottom-row">
+20 -5
View File
@@ -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 || [];
@@ -5,7 +5,7 @@ function tagTheme(type) {
'1': 'warning',
'2': 'success'
};
return map[String(type)] || 'default';
return map['' + type] || 'default';
}
module.exports = {
tagTheme: tagTheme
@@ -9,7 +9,7 @@ function tagTheme(type) {
'1': 'warning',
'2': 'success'
};
return map[String(type)] || 'default';
return map['' + type] || 'default';
}
module.exports = {
ellipsis: ellipsis,
@@ -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,
});
},
+77 -42
View File
@@ -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({
+40 -10
View File
@@ -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<number>}
@@ -87,9 +120,6 @@ export async function fetchPlatformCommissionRatio() {
return DEFAULT_COMMISSION_RATIO;
}
const CASE_VIEW_STORAGE_KEY = 'goodsCaseView';
let caseViewCache = null;
/**
* 商品是否按案例展示(不显示价格和购买入口)
* 成功后缓存;失败时用上次成功的本地记录
+7 -8
View File
@@ -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`;
}