8 Commits

Author SHA1 Message Date
05966b2acb 优化ntrp和性别picker偶尔选不上值 2026-02-10 18:00:26 +08:00
张成
4cf2b959b5 1 2026-02-10 12:42:42 +08:00
张成
43610dcf99 修复首页数据少的问题 2026-02-10 11:46:39 +08:00
张成
05aa820466 Merge branch 'master' of https://git.bimwe.com/bimwe/mini-programs 2026-02-09 22:03:59 +08:00
筱野
b154e31f8f Merge branch 'master' of https://git.bimwe.com/bimwe/mini-programs 2026-02-09 22:03:09 +08:00
筱野
669ee2fe4e 解决按钮问题与键盘弹出问题 2026-02-09 22:03:01 +08:00
张成
281ee2b746 Merge branch 'master' of https://git.bimwe.com/bimwe/mini-programs 2026-02-09 22:02:46 +08:00
张成
132c74d27c 1 2026-02-09 22:02:43 +08:00
13 changed files with 139 additions and 67 deletions

View File

@@ -4,7 +4,11 @@ export default {
quiet: false,
stats: true
},
mini: {},
mini: {
webpackChain(chain) {
chain.devtool('source-map')
}
},
h5: {},
// 添加这个配置来显示完整错误信息
compiler: {

View File

@@ -48,7 +48,14 @@ const CustomPopup: React.FC<CustomPopupProps> = ({
const touchStartY = useRef(0)
// 使用全局键盘状态
const { keyboardHeight, isKeyboardVisible, addListener, initializeKeyboardListener } = useKeyboardHeight()
const { keyboardHeight, isKeyboardVisible, addListener, initializeKeyboardListener, setKeyboardVisible } = useKeyboardHeight()
// 当弹窗显示时,设置键盘为不可见
useEffect(() => {
if (visible) {
setKeyboardVisible(false)
}
}, [visible, setKeyboardVisible])
// 使用全局键盘状态监听
useEffect(() => {

View File

@@ -52,7 +52,7 @@ const PopupPicker = ({
ntrpTested,
}: PickerProps) => {
const [defaultValue, setDefaultValue] = useState<(string | number)[]>([]);
const [defaultOptions, setDefaultOptions] = useState<PickerOption[][]>([]);
const [defaultOptions, setDefaultOptions] = useState<PickerOption[][]>([...options]);
const [pickerCurrentValue, setPickerCurrentValue] =
useState<(string | number)[]>(value);

View File

@@ -10,6 +10,7 @@ import {
useUserActions,
useNicknameChangeStatus,
useLastTestResult,
useUserInfo,
} from "@/store/userStore";
import { UserInfoType } from "@/services/userService";
import {
@@ -73,7 +74,6 @@ const on_edit = () => {
// 用户信息卡片组件
const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
editable = true,
user_info,
is_current_user,
is_following = false,
collapseProfile,
@@ -84,6 +84,8 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
set_user_info,
onTab,
}) => {
const user_info = useUserInfo();
const nickname_change_status = useNicknameChangeStatus();
const { setShowGuideBar } = useGlobalState();
const { updateUserInfo, updateNickname, fetchLastTestResult } =
@@ -122,11 +124,15 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
useState(false);
// 表单状态
const [form_data, set_form_data] = useState<Partial<UserInfoType>>({});
const [form_data, set_form_data] = useState<Partial<UserInfoType>>({ ...user_info });
useDidShow(() => {
// useDidShow(() => {
// set_form_data({ ...user_info });
// });
useEffect(() => {
set_form_data({ ...user_info });
});
}, [user_info])
useEffect(() => {
const visibles = [
@@ -372,7 +378,6 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
urls: [url],
});
};
return (
<View className="user_info_card">
{/* 头像和基本信息 */}
@@ -413,11 +418,11 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
<View className="stats_section">
<View
className="stats_container"
// style={{
// marginBottom: `${
// collapseProfile && setMarginBottom ? "16px" : "unset"
// }`,
// }}
// style={{
// marginBottom: `${
// collapseProfile && setMarginBottom ? "16px" : "unset"
// }`,
// }}
>
<View
className="stat_item clickable"
@@ -650,16 +655,16 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
<PopupPicker
showHeader={true}
title="选择性别"
options={[
options={
[
{ text: "男", value: "0" },
{ text: "女", value: "1" },
{ text: "保密", value: "2" },
],
]}
]
}
visible={gender_picker_visible}
setvisible={setGenderPickerVisible}
value={form_data.gender === "" ? ["0"] : [form_data.gender]}
value={!form_data.gender ? ["0"] : [form_data.gender]}
onChange={handle_gender_change}
/>
)}
@@ -868,9 +873,8 @@ export const GameTabs: React.FC<GameTabsProps> = ({
<Text className="tab_text">{hosted_text}</Text>
</View>
<View
className={`tab_item ${
active_tab === "participated" ? "active" : ""
}`}
className={`tab_item ${active_tab === "participated" ? "active" : ""
}`}
onClick={() => on_tab_change("participated")}
>
<Text className="tab_text">{participated_text}</Text>

View File

@@ -132,40 +132,37 @@ const ListContainer = (props) => {
);
};
// 插入 banner 卡片
// showNumber 为 0 表示尚未同步,不参与截断;截断时只限制「数据条数」,插卡不占数据条数
const shouldLimitByShowNumber = showNumber > 0;
// 插入 banner 卡片(在 bannerListIndex 位置插入,不替换数据)
function insertBannerCard(list) {
if (!bannerListImage) return list;
if (!list || !Array.isArray(list)) return list ?? [];
const idx = Number(bannerListIndex);
return [
...list.slice(0, Number(bannerListIndex)),
...list.slice(0, idx),
{ type: "banner", banner_image_url: bannerListImage, banner_detail_url: bannerDetailImage },
...list.slice(Number(bannerListIndex))
...list.slice(idx),
];
}
// 对于没有ntrp等级的用户每个月展示一次, 插在第二个位置后面
// insertBannerCard 需在最后统一执行,否则前面分支直接 return 时 banner 不会被插入
// 对于没有 ntrp 等级的用户每个月展示一次插在第 2 条数据后面;插卡是插入不替换,保留全部 showNumber 条数据
function insertEvaluateCard(list) {
let result: any[];
if (!list || !Array.isArray(list)) return insertBannerCard(list ?? []);
if (!evaluateFlag) {
result = showNumber !== undefined ? list.slice(0, showNumber) : list;
} else if (!list || list.length === 0) {
result = list;
} else if (hasTestInLastMonth) {
result = showNumber !== undefined ? list.slice(0, showNumber) : list;
} else if (list.length <= 2) {
result = [...list, { type: "evaluateCard" }];
} else {
const [item1, item2, ...rest] = list;
result = [
item1,
item2,
{ type: "evaluateCard" },
...(showNumber !== undefined ? rest.slice(0, showNumber - 3) : rest),
];
const limitedList = shouldLimitByShowNumber ? list.slice(0, showNumber) : list;
if (!evaluateFlag || hasTestInLastMonth) {
return insertBannerCard(limitedList);
}
if (limitedList.length <= 2) {
return insertBannerCard([...limitedList, { type: "evaluateCard" }]);
}
const [item1, item2, ...rest] = limitedList;
const result = [item1, item2, { type: "evaluateCard" }, ...rest];
return insertBannerCard(result);
}

View File

@@ -145,6 +145,7 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
const qrCodeUrl = qrCodeUrlRes.data.ossPath;
await delay(100);
// Taro.showLoading({ title: "生成中..." });
console.log('url', qrCodeUrl)
const url = await generatePosterImage({
playType: play_type,
ntrp: `NTRP ${genNTRPRequirementText(skill_level_min, skill_level_max)}`,
@@ -160,6 +161,8 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
time: `${startTime.format("ah")}${gameLength}`,
qrCodeUrl,
});
console.log('urlend', url)
// Taro.hideLoading();
Taro.showShareImageMenu({
path: url,

View File

@@ -1,6 +1,7 @@
export default definePageConfig({
navigationBarTitleText: '首页',
navigationStyle: 'custom',
navigationBarBackgroundColor: '#FAFAFA'
})
navigationBarTitleText: '首页',
navigationStyle: 'custom',
navigationBarBackgroundColor: '#FAFAFA',
enableShareAppMessage: true,
})

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect, useCallback } from "react";
import { View } from "@tarojs/components";
import Taro from "@tarojs/taro";
import Taro, { useRouter, useShareAppMessage } from "@tarojs/taro";
import { OSS_BASE } from "@/config/api";
import { wechat_auth_login, save_login_state } from "@/services/loginService";
import { useUserActions } from "@/store/userStore";
import { useGlobalState } from "@/store/global";
@@ -18,7 +19,11 @@ import { useDictionaryStore } from "@/store/dictionaryStore";
type TabType = "list" | "message" | "personal";
const MainPage: React.FC = () => {
const [currentTab, setCurrentTab] = useState<TabType>("list");
const { params } = useRouter();
const [currentTab, setCurrentTab] = useState<TabType>(() => {
const tab = params?.tab as TabType | undefined;
return tab === "list" || tab === "message" || tab === "personal" ? tab : "list";
});
const [isPublishMenuVisible, setIsPublishMenuVisible] = useState(false);
const [isDistanceFilterVisible, setIsDistanceFilterVisible] = useState(false);
const [isCityPickerVisible, setIsCityPickerVisible] = useState(false);
@@ -35,6 +40,14 @@ const MainPage: React.FC = () => {
const { showGuideBar, guideBarZIndex, setShowGuideBar, setGuideBarZIndex } =
useGlobalState();
// 从分享链接进入时根据 ?tab= 定位到对应 tab
useEffect(() => {
const tab = params?.tab as TabType | undefined;
if (tab === "list" || tab === "message" || tab === "personal") {
setCurrentTab(tab);
}
}, [params?.tab]);
// 初始化:自动微信授权并获取用户信息
useEffect(() => {
const init = async () => {
@@ -153,6 +166,21 @@ const MainPage: React.FC = () => {
[]
);
// 分享:首页、个人页均支持转发
// 分享图:配置 OSS 地址 + 路径(不含 ? 后参数),首页用 share_home.png个人页用 share_self.png
useShareAppMessage(() => {
const isList = currentTab === "list";
const isPersonal = currentTab === "personal";
const title = isList ? "约球 - 发现身边的球局" : isPersonal ? "约球 - 我的约球" : "约球";
const image_path = isPersonal ? "system/share_self.png" : "system/share_home.png";
const imageUrl = OSS_BASE ? `${OSS_BASE.replace(/\/$/, "")}/${image_path}` : "";
return {
title,
path: "/main_pages/index" + (isList ? "?tab=list" : isPersonal ? "?tab=personal" : ""),
imageUrl: imageUrl,
};
});
// 滚动到顶部
const scrollToTop = useCallback(() => {
// 如果当前是列表页,触发列表页内部滚动

View File

@@ -72,7 +72,6 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
}, ref) => {
const [openPicker, setOpenPicker] = useState(false); //为了解决上传图片时按钮样式问题
const [scrollTop, setScrollTop] = useState(0);
const [isTextareaFocused, setIsTextareaFocused] = useState(false); // 记录 TextareaTag 是否 focus
const { getDictionaryValue } = useDictionaryActions()
const court_type = getDictionaryValue('court_type') || []
const court_surface = getDictionaryValue('court_surface') || []
@@ -200,13 +199,6 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
}
}
// 当键盘显示且 TextareaTag 当前 focus 时才触发 changeTextarea
useEffect(() => {
if (isKeyboardVisible && isTextareaFocused) {
changeTextarea(true)
}
}, [isKeyboardVisible, isTextareaFocused])
const changePicker = (value:boolean) => {
setOpenPicker(value);
}
@@ -272,11 +264,9 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
onChange={(value) => {
updateFormData(item.prop, value)
}}
onBlur={() => {
setIsTextareaFocused(false)
}}
// onBlur={() => {
// }}
onFocus={() => {
setIsTextareaFocused(true)
changeTextarea(true)
}}
placeholder='有其他场地信息可备注'

View File

@@ -169,7 +169,7 @@ class GameDetailService {
}
async getLinkUrl(req: { path: string, query: string }): Promise<ApiResponse<{ url_link: string, path: string, query: string }>> {
return httpService.post('/user/generate_url_link', req, { showLoading: true })
return httpService.post('/user/generate_url_link', req, { showLoading: false })
}
}

View File

@@ -150,12 +150,13 @@ export const useKeyboardStore = create<KeyboardStore>((set, get) => ({
// 导出便捷的 hooks
export const useKeyboardHeight = () => {
const { keyboardHeight, isKeyboardVisible, addListener, initializeKeyboardListener } = useKeyboardStore()
const { keyboardHeight, isKeyboardVisible, addListener, initializeKeyboardListener, setKeyboardVisible } = useKeyboardStore()
return {
keyboardHeight,
isKeyboardVisible,
addListener,
initializeKeyboardListener
initializeKeyboardListener,
setKeyboardVisible
}
}

View File

@@ -56,9 +56,27 @@ function getImageWh(src: string): Promise<{ width: number; height: number }> {
/** 加载图片 */
function loadImage(canvas: any, src: string): Promise<any> {
return new Promise((resolve) => {
return new Promise((resolve, reject) => {
let timer: any;
const img = canvas.createImage();
img.onload = () => resolve(img);
img.crossOrigin = "anonymous"
img.onload = () => {
clearTimeout(timer);
resolve(img);
};
img.onerror = () => {
clearTimeout(timer);
console.log('img error', src)
}
timer = setTimeout(() => {
reject(new Error(`Image load timeout: ${src}`));
}, 8000);
img.src = src;
});
}
@@ -146,6 +164,7 @@ async function drawRotateCoverImage(
rotate = 0 // 旋转角度(弧度)
) {
const { width, height } = await getImageWh(src);
console.log('width', width, 'height', height)
const scale = Math.max(w / width, h / height);
const newW = width * scale;
const newH = height * scale;
@@ -179,6 +198,7 @@ async function drawRotateCoverImage(
// 绘制 cover
ctx.drawImage(img, offsetX, offsetY, newW, newH);
console.log('drawImage', offsetX, offsetY, newW, newH)
ctx.restore();
}
@@ -290,6 +310,8 @@ function drawTextWrap(
/** 核心纯函数:生成海报图片 */
export async function generatePosterImage(data: any): Promise<string> {
console.log("start !!!!");
// const dpr = Taro.getWindowInfo().pixelRatio;
const dpr = 1;
@@ -297,19 +319,30 @@ export async function generatePosterImage(data: any): Promise<string> {
const width = 600;
const height = 1000;
console.log('width', width, 'height', height)
const canvas = Taro.createOffscreenCanvas({ type: "2d", width: width * dpr, height: height * dpr });
const ctx = canvas.getContext("2d");
ctx.scale(dpr, dpr);
console.log('ctx', ctx)
// 背景渐变
roundRectGradient(ctx, 0, 0, width, height, 24, "#BFFFEF", "#F2FFFC");
console.log('bgUrl', bgUrl)
const bgImg = await loadImage(canvas, bgUrl);
ctx.drawImage(bgImg, 0, 0, width, height);
console.log('bgUrlend', )
roundRotateRect(ctx, 70, 100, width - 140, width - 140, 20, '#fff', deg2rad(-6));
// 顶部图片
const mainImg = await loadImage(canvas, data.mainCoursal);
console.log('mainCoursal', data.mainCoursal)
await drawRotateCoverImage(
ctx,
canvas,
@@ -378,6 +411,8 @@ export async function generatePosterImage(data: any): Promise<string> {
left = 20;
const dateImg = await loadImage(canvas, dateIcon);
console.log('dateIcon', dateIcon)
await drawCoverImage(
ctx,
canvas,
@@ -404,6 +439,7 @@ export async function generatePosterImage(data: any): Promise<string> {
left = 20;
top += 24;
const mapImg = await loadImage(canvas, mapIcon);
await drawCoverImage(ctx, canvas, mapIcon, mapImg, left, top, 40, 40, 12);
@@ -440,11 +476,13 @@ export async function generatePosterImage(data: any): Promise<string> {
ctx.font = "400 20px sans-serif";
ctx.fillText("长按识别二维码,快来加入,有你就有场!", left, height - 30/* top */);
console.log('canvas', canvas)
// 导出图片
const { tempFilePath } = await Taro.canvasToTempFilePath({
canvas,
fileType: 'png',
quality: 0.7,
});
console.log('tempFilePath', tempFilePath)
return tempFilePath;
}

View File

@@ -533,7 +533,6 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro
const locationPath = await loadImage(`${OSS_BASE}/front/ball/images/adc9a167-2ea9-4e3b-b963-6a894a1fd91b.jpg`)
ctx.drawImage(locationPath, iconX, locationInfoY, iconSize, iconSize)
drawBoldText(ctx, data.venueName, danDaX, locationInfoY + 10, locationFontSize, '#000000')
try {
const wxAny: any = (typeof (globalThis as any) !== 'undefined' && (globalThis as any).wx) ? (globalThis as any).wx : null
if (wxAny && typeof wxAny.canvasToTempFilePath === 'function') {