41 Commits

Author SHA1 Message Date
c47ebce43c fix: 修复取消活动后还可以编辑和取消、详情页参与者卡片展示NTRP 等级、生成海报的图片质量降低到1M以下, 详情页海报与测试结果页海报 2026-02-08 22:57:35 +08:00
b0f4b5713d Merge branch 'master' into feat/liujie 2026-02-08 21:25:19 +08:00
f7f10f5d15 查询下载账单 2026-02-08 16:03:08 +08:00
李瑞
2bcdd93479 Merge branch 'feat/juguohong/20260206' 2026-02-08 12:46:27 +08:00
张成
8d0ed5b1b3 1 2026-02-08 12:36:21 +08:00
张成
e99986c52a 修改审核不通过的问题 2026-02-08 12:29:48 +08:00
张成
4b2f6707cc 1 2026-02-08 12:18:04 +08:00
张成
a019fe473b Merge branch 'master' of https://git.bimwe.com/bimwe/mini-programs 2026-02-08 12:14:11 +08:00
张成
1d0d2edaa2 修改项目 build 结构 2026-02-08 12:14:10 +08:00
5926e096b5 图片样式优化 2026-02-08 12:09:46 +08:00
筱野
e07f2ad2d1 解决按钮问题与键盘弹出问题 2026-02-07 23:37:28 +08:00
筱野
bfc6a149f0 修改日期问题弹出问题 2026-02-07 22:15:14 +08:00
李瑞
6f73bb6d99 Merge branch 'feat/juguohong/20260206' 2026-02-07 22:09:39 +08:00
54b7a27af5 Merge branch 'feat/liujie' 2026-02-07 20:25:51 +08:00
396ff4a347 fix: 修改取值 2026-02-07 20:25:37 +08:00
张成
b732bd361e Merge branch 'master' of https://git.bimwe.com/bimwe/mini-programs 2026-02-07 20:24:50 +08:00
张成
5146894d92 1 2026-02-07 20:24:49 +08:00
张成
07cf8e884e 1 2026-02-07 20:24:45 +08:00
5416ea127c Merge branch 'master' into feat/liujie 2026-02-07 20:19:23 +08:00
a7bc517fae Merge branch 'feat/liujie' 2026-02-07 20:19:15 +08:00
16b38539f6 fix: 修改取值 2026-02-07 20:19:04 +08:00
张成
0d46311bbc 1 2026-02-07 19:27:58 +08:00
e884b1f258 Merge branch 'master' into feat/liujie 2026-02-07 18:13:23 +08:00
84159a4835 Merge branch 'feat/liujie' 2026-02-07 18:13:15 +08:00
2acee85dd5 fix: 修复分享弹窗打开逻辑 2026-02-07 18:13:00 +08:00
ba72e0ec97 Merge branch 'master' into feat/liujie 2026-02-07 18:11:15 +08:00
张成
32f5339cc2 Merge branch 'master' of https://git.bimwe.com/bimwe/mini-programs 2026-02-07 18:11:14 +08:00
张成
2cbbc7f432 1 2026-02-07 18:11:12 +08:00
694b00e011 Merge branch 'feat/liujie' 2026-02-07 18:11:01 +08:00
87eaa31cef fix: 修复发布后分享弹窗打开问题 2026-02-07 18:10:44 +08:00
张成
f131c9896d 修改oss 路径 2026-02-07 18:07:33 +08:00
b08f3325e6 Merge branch 'feat/liujie' 2026-02-07 17:38:28 +08:00
ff864fe64d feat: 修改两处海报logo-text图片、修改创建球局后跳转详情页打开分享弹窗位置、修改分享二维码接口取值、修改ntrp修改弹窗的初始值、增加复制链接功能 2026-02-07 17:37:07 +08:00
张成
da0ae6046c 1 2026-02-07 16:45:10 +08:00
42025d49f8 Merge branch 'master' into feat/liujie 2026-02-07 16:03:34 +08:00
张成
536619ebfc 1 2026-02-07 13:08:28 +08:00
张成
5a10c73adf 修复一堆问题 2026-02-07 11:56:43 +08:00
李瑞
b29e000747 Merge branch 'master' of https://git.bimwe.com/bimwe/mini-programs 2026-02-07 01:00:17 +08:00
李瑞
02841222a2 Merge branch feat/juguohong/20260206 2026-02-07 00:59:44 +08:00
张成
b417b3a4c2 1 2026-02-07 00:58:57 +08:00
张成
8d729a0132 1 2026-02-07 00:51:30 +08:00
69 changed files with 2075 additions and 1185 deletions

2
.env.dev Normal file
View File

@@ -0,0 +1,2 @@
APP_ENV=dev
TARO_APP_ID=wx815b533167eb7b53

2
.env.dev_local Normal file
View File

@@ -0,0 +1,2 @@
APP_ENV=dev_local
TARO_APP_ID=wx815b533167eb7b53

2
.env.pr Normal file
View File

@@ -0,0 +1,2 @@
APP_ENV=pr
TARO_APP_ID=wx915ecf6c01bea4ec

2
.env.sit Normal file
View File

@@ -0,0 +1,2 @@
APP_ENV=sit
TARO_APP_ID=wx815b533167eb7b53

2
.gitignore vendored
View File

@@ -8,4 +8,4 @@ node_modules/
src/config/env.ts src/config/env.ts
.vscode .vscode
*.http *.http
env.ts

79
config/env.config.ts Normal file
View File

@@ -0,0 +1,79 @@
/**
* 统一环境配置dev/sit/pr
* 构建时通过 APP_ENV 选择defineConstants 注入业务代码
* project.config.json 的 appid 由 scripts/sync-project-config.js 同步
*/
export type EnvType = "dev" | "dev_local" | "sit" | "pr";
export interface EnvConfig {
name: string;
apiBaseURL: string;
ossBaseURL: string;
appid: string;
timeout: number;
enableLog: boolean;
enableMock: boolean;
customerService: {
corpId: string;
serviceUrl: string;
};
}
const baseConfig = {
apiBaseURL: "https://tennis.bimwe.com",
ossBaseURL: "https://bimwe.oss-cn-shanghai.aliyuncs.com",
appid: "wx815b533167eb7b53", // 测试号
timeout: 15000,
enableLog: true,
enableMock: false,
customerService: {
corpId: "ww51fc969e8b76af82",
serviceUrl: "https://work.weixin.qq.com/kfid/kfc64085b93243c5c91",
},
}
export const envConfigs: Record<EnvType, EnvConfig> = {
// 本地开发API 指向本地或测试服
dev: {
name: "DEV",
// apiBaseURL: "http://localhost:9098",
...baseConfig
},
// 本地联调API 指向本机
dev_local: {
name: "DEV_LOCAL",
...Object.assign(baseConfig, {
apiBaseURL: "http://localhost:9098",
})
},
// SIT 测试环境
sit: {
name: "SIT",
...Object.assign(baseConfig, {
apiBaseURL: "https://tennis.bimwe.com",
})
},
// PR 生产环境
pr: {
name: "PR",
apiBaseURL: "https://youchang.qiongjingtiyu.com",
ossBaseURL: "https://youchang2026.oss-cn-shanghai.aliyuncs.com",
appid: "wx915ecf6c01bea4ec", // 生产小程序 appid按实际填写
timeout: 10000,
enableLog: false,
enableMock: false,
customerService: {
corpId: "ww9a2d9a5d9410c664",
serviceUrl: "https://work.weixin.qq.com/kfid/kfcd355e162e0390684",
},
},
};
export function getEnvConfig(env: EnvType): EnvConfig {
return envConfigs[env];
}

View File

@@ -2,11 +2,21 @@ import { defineConfig, type UserConfigExport } from '@tarojs/cli'
import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin' import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin'
import devConfig from './dev' import devConfig from './dev'
import prodConfig from './prod' import prodConfig from './prod'
// import vitePluginImp from 'vite-plugin-imp' import { getEnvConfig, type EnvType } from './env.config'
import path from 'path' import path from 'path'
// 环境dev(本地) | dev_local(联调) | sit(测试) | pr(生产)
const ENV_LIST: EnvType[] = ['dev', 'dev_local', 'sit', 'pr']
// https://taro-docs.jd.com/docs/next/config#defineconfig-辅助函数 // https://taro-docs.jd.com/docs/next/config#defineconfig-辅助函数
export default defineConfig<'webpack5'>(async (merge, { command, mode }) => { export default defineConfig<'webpack5'>(async (merge, { command, mode }) => {
const appEnv = (
(ENV_LIST.includes(mode as EnvType) ? mode : process.env.APP_ENV) ||
(process.env.NODE_ENV === 'production' ? 'pr' : 'dev')
) as EnvType
const envConfig = getEnvConfig(appEnv)
const baseConfig: UserConfigExport<'webpack5'> = { const baseConfig: UserConfigExport<'webpack5'> = {
projectName: 'playBallTogether', projectName: 'playBallTogether',
date: '2025-8-9', date: '2025-8-9',
@@ -22,6 +32,13 @@ export default defineConfig<'webpack5'>(async (merge, { command, mode }) => {
outputRoot: 'dist', outputRoot: 'dist',
plugins: ['@tarojs/plugin-html'], plugins: ['@tarojs/plugin-html'],
defineConstants: { defineConstants: {
'process.env.APP_ENV': JSON.stringify(appEnv),
'process.env.API_BASE_URL': JSON.stringify(envConfig.apiBaseURL),
'process.env.OSS_BASE_URL': JSON.stringify(envConfig.ossBaseURL),
'process.env.ENABLE_LOG': JSON.stringify(envConfig.enableLog),
'process.env.TIMEOUT': JSON.stringify(envConfig.timeout),
'process.env.CUSTOMER_CORP_ID': JSON.stringify(envConfig.customerService.corpId),
'process.env.CUSTOMER_SERVICE_URL': JSON.stringify(envConfig.customerService.serviceUrl),
}, },
alias: { alias: {
'@': path.resolve(__dirname, '..', 'src'), '@': path.resolve(__dirname, '..', 'src'),
@@ -76,6 +93,9 @@ export default defineConfig<'webpack5'>(async (merge, { command, mode }) => {
}, },
// @ts-expect-error: Taro 类型定义缺少 mini.hot // @ts-expect-error: Taro 类型定义缺少 mini.hot
hot: true, hot: true,
projectConfig: {
appid: envConfig.appid,
},
}, },
h5: { h5: {
publicPath: '/', publicPath: '/',

View File

@@ -10,32 +10,17 @@
"framework": "React" "framework": "React"
}, },
"scripts": { "scripts": {
"build": "npm run build:weapp ", "dev": "npm run dev:weapp",
"dev": "npm run dev:weapp ", "dev:local": "npm run dev:weapp:dev_local",
"build:weapp": "taro build --type weapp --mode production", "dev:weapp": "node scripts/sync-project-config.js dev && taro build --type weapp --mode dev --watch",
"build:swan": "taro build --type swan", "dev:weapp:dev_local": "node scripts/sync-project-config.js dev_local && taro build --type weapp --mode dev_local --watch",
"build:alipay": "taro build --type alipay", "build": "npm run build:weapp",
"build:tt": "taro build --type tt", "build:weapp": "node scripts/sync-project-config.js pr && taro build --type weapp --mode pr",
"build:h5": "taro build --type h5", "build:sit": "node scripts/sync-project-config.js sit && taro build --type weapp --mode sit",
"build:rn": "taro build --type rn", "build:pr": "node scripts/sync-project-config.js pr && taro build --type weapp --mode pr",
"build:qq": "taro build --type qq", "dev:h5": "npm run build:h5 -- --watch"
"build:jd": "taro build --type jd",
"build:quickapp": "taro build --type quickapp",
"dev:weapp": "npm run build:weapp -- --watch",
"dev:swan": "npm run build:swan -- --watch",
"dev:alipay": "npm run build:alipay -- --watch",
"dev:tt": "npm run build:tt -- --watch",
"dev:h5": "npm run build:h5 -- --watch",
"dev:rn": "npm run build:rn -- --watch",
"dev:qq": "npm run build:qq -- --watch",
"dev:jd": "npm run build:jd -- --watch",
"dev:quickapp": "npm run build:quickapp -- --watch"
}, },
"browserslist": [ "browserslist": ["last 3 versions", "Android >= 4.1", "ios >= 8"],
"last 3 versions",
"Android >= 4.1",
"ios >= 8"
],
"author": "", "author": "",
"dependencies": { "dependencies": {
"@babel/plugin-transform-runtime": "^7.28.3", "@babel/plugin-transform-runtime": "^7.28.3",

View File

@@ -2,8 +2,7 @@
"miniprogramRoot": "dist/", "miniprogramRoot": "dist/",
"projectname": "playBallTogether", "projectname": "playBallTogether",
"description": "playBallTogether", "description": "playBallTogether",
"appid": "wx915ecf6c01bea4ec", "appid": "wx815b533167eb7b53",
"setting": { "setting": {
"urlCheck": true, "urlCheck": true,
"es6": true, "es6": true,
@@ -47,4 +46,4 @@
"simulatorType": "wechat", "simulatorType": "wechat",
"simulatorPluginLibVersion": {}, "simulatorPluginLibVersion": {},
"condition": {} "condition": {}
} }

View File

@@ -0,0 +1,25 @@
const fs = require('fs');
const path = require('path');
require('ts-node/register/transpile-only');
const envArg = process.argv[2];
const appEnv = envArg || process.env.APP_ENV || (process.env.NODE_ENV === 'production' ? 'pr' : 'dev');
const envConfigPath = path.resolve(__dirname, '../config/env.config.ts');
const { envConfigs } = require(envConfigPath);
const config = envConfigs[appEnv];
if (!config) {
console.error(`[sync-project-config] Unknown APP_ENV: ${appEnv}`);
process.exit(1);
}
const projectConfigPath = path.resolve(__dirname, '../project.config.json');
const projectConfigRaw = fs.readFileSync(projectConfigPath, 'utf-8');
const projectConfig = JSON.parse(projectConfigRaw);
projectConfig.appid = config.appid;
fs.writeFileSync(projectConfigPath, JSON.stringify(projectConfig, null, 2) + '\n', 'utf-8');
console.log(`[sync-project-config] project.config.json appid -> ${config.appid} (${appEnv})`);

View File

@@ -19,8 +19,7 @@ page {
@font-face { @font-face {
font-family: "Quicksand"; font-family: "Quicksand";
// 注意:此路径来自 @/config/api.ts 中的 OSS_BASE_URL 配置 // 注意:此路径对应 @/config/api.ts 中的 OSS_BASE
// 如需修改,请更新配置文件中的 OSS_BASE_URL src: url("https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/other/57dc951f-f10e-45b7-9157-0b1e468187fd.ttf") format("truetype");
src: url("https://youchang2026.oss-cn-shanghai.aliyuncs.com/front/ball/other/57dc951f-f10e-45b7-9157-0b1e468187fd.ttf") format("truetype");
font-display: swap; font-display: swap;
} }

View File

@@ -34,7 +34,7 @@ function toast(msg) {
interface CommentInputProps { interface CommentInputProps {
onConfirm?: ( onConfirm?: (
value: { content: string } & Partial<CommentInputReplyParamsType> value: { content: string } & Partial<CommentInputReplyParamsType>,
) => void; ) => void;
} }
@@ -49,119 +49,118 @@ interface CommentInputReplyParamsType {
nickname: string; nickname: string;
} }
const CommentInput = forwardRef<CommentInputRef, CommentInputProps>(function ( const CommentInput = forwardRef<CommentInputRef, CommentInputProps>(
props, function (props, ref) {
ref const { onConfirm } = props;
) { const [visible, setVisible] = useState(false);
const { onConfirm } = props; const [value, setValue] = useState("");
const [visible, setVisible] = useState(false); const [params, setParams] = useState<
const [value, setValue] = useState(""); CommentInputReplyParamsType | undefined
const [params, setParams] = useState< >();
CommentInputReplyParamsType | undefined
>();
const { const {
keyboardHeight, keyboardHeight,
isKeyboardVisible, isKeyboardVisible,
addListener, addListener,
initializeKeyboardListener, initializeKeyboardListener,
} = useKeyboardHeight(); } = useKeyboardHeight();
// 使用全局键盘状态监听 // 使用全局键盘状态监听
useEffect(() => { useEffect(() => {
// 初始化全局键盘监听器 // 初始化全局键盘监听器
initializeKeyboardListener(); initializeKeyboardListener();
// 添加本地监听器 // 添加本地监听器
const removeListener = addListener((height, visible) => { const removeListener = addListener(() => {
console.log("PublishBall 收到键盘变化:", height, visible); // 布局是否响应交由 shouldReactToKeyboard 决定
// 这里只记录或用于其他逻辑,布局是否响应交由 shouldReactToKeyboard 决定 });
});
return () => { return () => {
removeListener(); removeListener();
}; };
}, [initializeKeyboardListener, addListener]); }, [initializeKeyboardListener, addListener]);
const inputDomRef = useRef(null); const inputDomRef = useRef(null);
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
show: (_params: CommentInputReplyParamsType | undefined) => { show: (_params: CommentInputReplyParamsType | undefined) => {
setVisible(true); setVisible(true);
setTimeout(() => { setTimeout(() => {
inputDomRef.current && inputDomRef.current?.focus(); inputDomRef.current && inputDomRef.current?.focus();
}, 100); }, 100);
setParams(_params); setParams(_params);
}, },
})); }));
function handleSend() { function handleSend() {
if (!value) { if (!value) {
toast("评论内容不得为空"); toast("评论内容不得为空");
return; return;
}
if (value.length > 200) {
return;
}
onConfirm?.({ content: value, ...params });
onClose();
} }
if (value.length > 200) {
return;
}
onConfirm?.({ content: value, ...params });
onClose();
}
function onClose() { function onClose() {
setVisible(false); setVisible(false);
setValue(""); setValue("");
inputDomRef.current && inputDomRef.current?.blur(); inputDomRef.current && inputDomRef.current?.blur();
} }
console.log(keyboardHeight, "keyboardHeight"); return (
return ( <CommonPopup
<CommonPopup visible={visible}
visible={visible} showHeader={false}
showHeader={false} hideFooter
hideFooter zIndex={1002}
zIndex={1002} onClose={onClose}
onClose={onClose} style={{
style={{ // height: "60px!important",
// height: "60px!important", minHeight: "unset",
minHeight: "unset", bottom:
bottom: isKeyboardVisible && keyboardHeight > 0
isKeyboardVisible && keyboardHeight > 0 ? `${keyboardHeight}px` : "0", ? `${keyboardHeight}px`
}} : "0",
enableDragToClose={false} }}
> enableDragToClose={false}
<View className={styles.inputContainer}> >
<View className={styles.inputWrapper}> <View className={styles.inputContainer}>
<Textarea <View className={styles.inputWrapper}>
adjustPosition={false} <Textarea
ref={inputDomRef} adjustPosition={false}
className={styles.input} ref={inputDomRef}
value={value} className={styles.input}
onInput={(e) => setValue(e.detail.value)} value={value}
placeholder={ onInput={(e) => setValue(e.detail.value)}
params?.reply_to_user_id ? `回复 @${params.nickname}` : "写评论" placeholder={
} params?.reply_to_user_id ? `回复 @${params.nickname}` : "写评论"
confirmType="send" }
onConfirm={handleSend} confirmType="send"
focus onConfirm={handleSend}
maxlength={-1} focus
autoHeight maxlength={-1}
// showCount autoHeight
/> // showCount
<View />
className={classnames( <View
styles.limit, className={classnames(
value.length > 200 ? styles.red : "" styles.limit,
)} value.length > 200 ? styles.red : "",
> )}
<Text>{value.length}</Text>/<Text>200</Text> >
<Text>{value.length}</Text>/<Text>200</Text>
</View>
</View>
<View className={styles.sendIcon} onClick={handleSend}>
<Image className={styles.sendImage} src={sendImg} />
</View> </View>
</View> </View>
<View className={styles.sendIcon} onClick={handleSend}> </CommonPopup>
<Image className={styles.sendImage} src={sendImg} /> );
</View> },
</View> );
</CommonPopup>
);
});
function isReplyComment(item: BaseComment<any>): item is ReplyComment { function isReplyComment(item: BaseComment<any>): item is ReplyComment {
return "reply_to_user" in item; return "reply_to_user" in item;
@@ -208,7 +207,7 @@ function CommentItem(props: {
className={classnames( className={classnames(
styles.commentItem, styles.commentItem,
blink_id === comment.id && styles.blink, blink_id === comment.id && styles.blink,
styles.weight_super styles.weight_super,
)} )}
key={comment.id} key={comment.id}
id={`comment_id_${comment.id}`} id={`comment_id_${comment.id}`}
@@ -293,7 +292,8 @@ function CommentItem(props: {
/> />
))} ))}
{!isReplyComment(comment) && {!isReplyComment(comment) &&
comment.replies.length !== comment.reply_count && ( comment.replies.length !== comment.reply_count &&
comment.replies.length > 3 && (
<View <View
className={styles.viewMore} className={styles.viewMore}
onClick={() => handleLoadMore(comment)} onClick={() => handleLoadMore(comment)}
@@ -313,7 +313,7 @@ export default forwardRef(function Comments(
message_id?: number; message_id?: number;
onScrollTo: (id: string) => void; onScrollTo: (id: string) => void;
}, },
ref ref,
) { ) {
const { game_id, publisher_id, message_id, onScrollTo } = props; const { game_id, publisher_id, message_id, onScrollTo } = props;
const [comments, setComments] = useState<Comment[]>([]); const [comments, setComments] = useState<Comment[]>([]);
@@ -371,7 +371,7 @@ export default forwardRef(function Comments(
replies: [res.data, ...item.replies].sort((a, b) => replies: [res.data, ...item.replies].sort((a, b) =>
dayjs(a.create_time).isAfter(dayjs(b.create_time)) dayjs(a.create_time).isAfter(dayjs(b.create_time))
? 1 ? 1
: -1 : -1,
), ),
}; };
}); });
@@ -435,7 +435,7 @@ export default forwardRef(function Comments(
item.replies.splice( item.replies.splice(
page === 1 ? 0 : page * PAGESIZE - 1, page === 1 ? 0 : page * PAGESIZE - 1,
newReplies.length, newReplies.length,
...newReplies ...newReplies,
); );
item.reply_count = res.data.count; item.reply_count = res.data.count;
} }
@@ -502,7 +502,7 @@ export default forwardRef(function Comments(
return { return {
...item, ...item,
replies: item.replies.filter( replies: item.replies.filter(
(replyItem) => replyItem.id !== id (replyItem) => replyItem.id !== id,
), ),
reply_count: item.reply_count - 1, reply_count: item.reply_count - 1,
}; };

View File

@@ -0,0 +1,208 @@
import React, { useRef, useState, useEffect } from 'react'
import type { CSSProperties, ReactNode } from 'react'
import { View, Text } from '@tarojs/components'
import { Button } from '@nutui/nutui-react-taro'
import { useKeyboardHeight } from '@/store/keyboardStore'
import styles from './index.module.scss'
export interface CustomPopupProps {
visible: boolean
onClose: () => void
title?: ReactNode
showHeader?: boolean
hideFooter?: boolean
cancelText?: string
confirmText?: string
onCancel?: () => void
onConfirm?: () => void
children?: ReactNode
className?: string
style?: CSSProperties
// 与 CommonPopup 保持入参一致
position?: 'center' | 'bottom' | 'top' | 'left' | 'right'
round?: boolean
zIndex?: number
enableDragToClose?: boolean
}
const CustomPopup: React.FC<CustomPopupProps> = ({
visible,
onClose,
title,
showHeader = false,
hideFooter = false,
cancelText = '返回',
confirmText = '完成',
onCancel,
onConfirm,
children,
className,
style,
position = 'bottom',
round = true,
zIndex,
enableDragToClose = true,
}) => {
const [dragOffset, setDragOffset] = useState(0)
const [isDragging, setIsDragging] = useState(false)
const touchStartY = useRef(0)
// 使用全局键盘状态
const { keyboardHeight, isKeyboardVisible, addListener, initializeKeyboardListener } = useKeyboardHeight()
// 使用全局键盘状态监听
useEffect(() => {
// 初始化全局键盘监听器
initializeKeyboardListener()
// 添加本地监听器
const removeListener = addListener((height, visible) => {
console.log('CustomPopup 收到键盘变化:', height, visible)
})
return () => {
removeListener()
}
}, [initializeKeyboardListener, addListener])
if (!visible) {
return null
}
const handleCancel = () => {
if (onCancel) {
onCancel()
} else {
onClose()
}
}
const handleTouchStart = (e: any) => {
if (!enableDragToClose) return
touchStartY.current = e.touches[0].clientY
setIsDragging(true)
}
const handleTouchMove = (e: any) => {
if (!enableDragToClose || !isDragging) return
const currentY = e.touches[0].clientY
const deltaY = currentY - touchStartY.current
if (deltaY > 0) {
setDragOffset(Math.min(deltaY, 100))
}
}
const handleTouchEnd = () => {
if (!enableDragToClose || !isDragging) return
setIsDragging(false)
if (dragOffset > 50) {
onClose()
}
setDragOffset(0)
}
const overlayAlignItems =
position === 'center'
? 'center'
: position === 'top'
? 'flex-start'
: 'flex-end'
const handleOverlayClick = () => {
onClose()
}
// 阻止弹窗内的触摸事件冒泡
const handleTouchMoveInPopup = (e: any) => {
if (!isKeyboardVisible) {
e.stopPropagation()
}
}
return (
<View
className={styles['custom-popup-overlay']}
style={{ zIndex: zIndex ?? undefined, alignItems: overlayAlignItems }}
onClick={handleOverlayClick}
>
<View className={styles['custom-popup-move']} onTouchMove={handleTouchMoveInPopup} catchMove></View>
<View
className={`${styles['custom-popup']} ${className ? className : ''}`}
style={{
...style,
paddingBottom: isKeyboardVisible ? `${keyboardHeight}px` : undefined,
}}
onClick={(e) => {
e.stopPropagation()
}}
>
{enableDragToClose && (
<View
className={styles['custom-popup__drag-handle-container']}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
>
<View
className={styles['custom-popup__drag-handle']}
style={{
transform: `translateX(-50%) translateY(${dragOffset * 0.3}px)`,
opacity: isDragging ? 0.8 : 1,
transition: isDragging ? 'none' : 'all 0.3s ease-out',
}}
/>
</View>
)}
{showHeader && (
<View className={styles['custom-popup__header']}>
{typeof title === 'string' ? (
<Text className={styles['custom-popup__title']}>{title}</Text>
) : (
title
)}
<View className={styles['close_button']} onClick={onClose}>
<View className={styles['close_icon']}>
<View className={styles['close_line']} />
<View className={styles['close_line']} />
</View>
</View>
</View>
)}
<View className={styles['custom-popup__body']}>{children}</View>
{!hideFooter && !isKeyboardVisible && (
<View className={styles['custom-popup__footer']}>
<Button
className={`${styles['custom-popup__btn']} ${styles['custom-popup__btn-cancel']}`}
type="default"
size="small"
onClick={handleCancel}
>
{cancelText}
</Button>
<Button
className={`${styles['custom-popup__btn']} ${styles['custom-popup__btn-confirm']}`}
type="primary"
size="small"
onClick={onConfirm}
>
{confirmText}
</Button>
</View>
)}
</View>
</View>
)
}
export default CustomPopup

View File

@@ -0,0 +1,155 @@
@use "~@/scss/themeColor.scss" as theme;
.custom-popup-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
display: flex;
align-items: flex-end;
justify-content: center;
}
.custom-popup-move{
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 998;
}
.custom-popup {
position: relative;
z-index: 999;
width: 100%;
padding: 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
background-color: theme.$page-background-color;
border-radius: 20px 20px 0 0;
overflow: hidden;
transition: padding-bottom 0.3s ease;
.custom-popup__drag-handle-container {
position: relative;
height: 0;
}
.custom-popup__drag-handle {
position: absolute;
top: 6px;
left: 50%;
width: 90px;
height: 30px;
z-index: 10;
display: flex;
justify-content: center;
align-items: flex-start;
&::before {
content: "";
width: 32px;
height: 4px;
background-color: rgba(22, 24, 35, 0.2);
border-radius: 2px;
}
}
.custom-popup__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
.custom-popup__title {
font-family: "PingFang SC";
font-weight: 600;
font-size: 22px;
line-height: 1.27em;
color: #000000;
text-align: center;
}
.close_button {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: #ffffff;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 50%;
cursor: pointer;
box-shadow: 0px 4px 36px 0px rgba(0, 0, 0, 0.06);
.close_icon {
position: relative;
width: 24px;
height: 24px;
.close_line {
position: absolute;
top: 50%;
left: 50%;
width: 17px;
height: 3px;
border-radius: 3px;
background: #000000;
transform: translate(-50%, -50%) rotate(45deg);
&:nth-child(2) {
transform: translate(-50%, -50%) rotate(-45deg);
}
}
}
}
}
.custom-popup__body {
flex: 1 1 auto;
max-height: 80vh;
overflow-y: auto;
}
.custom-popup__footer {
padding: 8px 10px 0 10px;
display: flex;
gap: 8px;
background: #fafafa;
padding-bottom: max(10px, env(safe-area-inset-bottom));
}
.custom-popup__btn {
flex: 1;
font-feature-settings: "liga" off, "clig" off;
font-family: "PingFang SC";
font-size: 16px;
font-style: normal;
font-weight: 600;
line-height: normal;
}
.custom-popup__btn-cancel {
background: #f5f6f7;
color: #1f2329;
border: none;
width: 154px;
height: 44px;
border-radius: 12px !important;
border: 0.5px solid rgba(0, 0, 0, 0.06);
background: #fff;
padding: 4px 10px;
}
.custom-popup__btn-confirm {
width: 154px;
height: 44px;
border: 0.5px solid rgba(0, 0, 0, 0.06);
background: #000;
border-radius: 12px !important;
padding: 4px 10px;
}
}

View File

@@ -0,0 +1,4 @@
import CustomPopup from './CustomPopup'
export default CustomPopup
export * from './CustomPopup'

View File

@@ -13,7 +13,9 @@
align-items: center; align-items: center;
color: #000; color: #000;
text-align: center; text-align: center;
font-feature-settings: 'liga' off, 'clig' off; font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 16px; font-size: 16px;
font-style: normal; font-style: normal;
@@ -32,7 +34,9 @@
padding-top: 24px; padding-top: 24px;
color: #000; color: #000;
text-align: center; text-align: center;
font-feature-settings: 'liga' off, 'clig' off; font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 16px; font-size: 16px;
font-style: normal; font-style: normal;
@@ -48,8 +52,10 @@
align-items: center; align-items: center;
.tips { .tips {
color: rgba(60, 60, 67, 0.60); color: rgba(60, 60, 67, 0.6);
font-feature-settings: 'liga' off, 'clig' off; font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 16px; font-size: 16px;
font-style: normal; font-style: normal;
@@ -62,13 +68,15 @@
margin-top: 8px; margin-top: 8px;
padding: 8px; padding: 8px;
border-radius: 4px; border-radius: 4px;
background: #F0F0F0; background: #f0f0f0;
.input { .input {
width: 100%; width: 100%;
&:placeholder-shown { &:placeholder-shown {
color: rgba(60, 60, 67, 0.30); color: rgba(60, 60, 67, 0.3);
font-feature-settings: 'liga' off, 'clig' off; font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 14px; font-size: 14px;
font-style: normal; font-style: normal;
@@ -84,11 +92,12 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
height: 44px; height: 44px;
border-top: 0.5px solid #CECECE; border-top: 0.5px solid #cecece;
background: #FFF; background: #fff;
margin-top: 2px; margin-top: 2px;
.confirm, .cancel { .confirm,
.cancel {
width: 50%; width: 50%;
height: 44px; height: 44px;
display: flex; display: flex;
@@ -96,7 +105,9 @@
align-items: center; align-items: center;
color: #000; color: #000;
text-align: center; text-align: center;
font-feature-settings: 'liga' off, 'clig' off; font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 16px; font-size: 16px;
font-style: normal; font-style: normal;
@@ -109,4 +120,4 @@
} }
} }
} }
} }

View File

@@ -186,7 +186,7 @@ export default forwardRef(function GameManagePopup(props, ref) {
.some((item) => item.user.id === userInfo.id); .some((item) => item.user.id === userInfo.id);
const finished = [MATCH_STATUS.FINISHED, MATCH_STATUS.CANCELED].includes( const finished = [MATCH_STATUS.FINISHED, MATCH_STATUS.CANCELED].includes(
detail.match_status detail.match_status,
); );
const inTwoHours = dayjs(detail.start_time).diff(dayjs(), "hour") < 2; const inTwoHours = dayjs(detail.start_time).diff(dayjs(), "hour") < 2;
@@ -207,7 +207,7 @@ export default forwardRef(function GameManagePopup(props, ref) {
style={{ minHeight: "unset" }} style={{ minHeight: "unset" }}
> >
<View className={styles.container}> <View className={styles.container}>
{!inTwoHours && !hasOtherParticiappants && ( {!finished && !inTwoHours && !hasOtherParticiappants && (
<View className={styles.button} onClick={handleEditGame}> <View className={styles.button} onClick={handleEditGame}>
</View> </View>
@@ -217,12 +217,12 @@ export default forwardRef(function GameManagePopup(props, ref) {
</View> </View>
)} )}
{!inTwoHours && !hasOtherParticiappants && ( {!finished && !inTwoHours && !hasOtherParticiappants && (
<View className={styles.button} onClick={handleCancelGame}> <View className={styles.button} onClick={handleCancelGame}>
</View> </View>
)} )}
{hasJoin && ( {!finished && hasJoin && (
<View className={styles.button} onClick={handleQuitGame}> <View className={styles.button} onClick={handleQuitGame}>
退 退
</View> </View>

View File

@@ -45,7 +45,7 @@ const ListCard: React.FC<ListCardProps> = ({
className="image" className="image"
mode="aspectFill" mode="aspectFill"
lazyLoad lazyLoad
defaultSource="https://images.unsplash.com/photo-1554068865-24cecd4e34b8?w=200&h=200&fit=crop&crop=center" defaultSource={require("@/static/emptyStatus/publish-empty-card.png")}
/> />
); );
}; };

View File

@@ -62,7 +62,7 @@ const NTRPEvaluatePopup = (props: NTRPEvaluatePopupProps, ref) => {
showGuide = false, showGuide = false,
} = props; } = props;
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [ntrp, setNtrp] = useState<string>(""); const [ntrp, setNtrp] = useState<string>("1.5");
const [guideShow, setGuideShow] = useState(() => showGuide); const [guideShow, setGuideShow] = useState(() => showGuide);
const { updateUserInfo } = useUserActions(); const { updateUserInfo } = useUserActions();
const userInfo = useUserInfo(); const userInfo = useUserInfo();
@@ -105,10 +105,10 @@ const NTRPEvaluatePopup = (props: NTRPEvaluatePopupProps, ref) => {
if (match) { if (match) {
setNtrp(match[0]); setNtrp(match[0]);
} else { } else {
setNtrp(""); setNtrp("1.5");
} }
} else { } else {
setNtrp(""); setNtrp("1.5");
} }
} }
}, [visible, userInfo?.ntrp_level]); }, [visible, userInfo?.ntrp_level]);

View File

@@ -8,7 +8,7 @@ import {
useLastTestResult, useLastTestResult,
} from "@/store/userStore"; } from "@/store/userStore";
// import { getCurrentFullPath } from "@/utils"; // import { getCurrentFullPath } from "@/utils";
import { OSS_BASE_URL } from "@/config/api"; import { OSS_BASE } from "@/config/api";
import { StageType } from "@/services/evaluateService"; import { StageType } from "@/services/evaluateService";
import { waitForAuthInit } from "@/utils/authInit"; import { waitForAuthInit } from "@/utils/authInit";
import DocCopy from "@/static/ntrp/ntrp_doc_copy.svg"; import DocCopy from "@/static/ntrp/ntrp_doc_copy.svg";
@@ -148,7 +148,7 @@ function NTRPTestEntryCard(props: {
<View <View
className={styles.lines} className={styles.lines}
style={{ style={{
backgroundImage: `url(${OSS_BASE_URL}/images/215f1ce1-be52-4a92-8250-5a4a69e7f2b3.png)`, backgroundImage: `url(${OSS_BASE}/front/ball/images/215f1ce1-be52-4a92-8250-5a4a69e7f2b3.png)`,
}} }}
/> />
<View className={styles.desc}> <View className={styles.desc}>
@@ -188,7 +188,7 @@ function NTRPTestEntryCard(props: {
<View <View
className={styles.lines} className={styles.lines}
style={{ style={{
backgroundImage: `url(${OSS_BASE_URL}/images/215f1ce1-be52-4a92-8250-5a4a69e7f2b3.png)`, backgroundImage: `url(${OSS_BASE}/front/ball/images/215f1ce1-be52-4a92-8250-5a4a69e7f2b3.png)`,
}} }}
/> />
<View className={styles.desc}> <View className={styles.desc}>

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect, useRef } from "react"; import React, { useState, useEffect, useRef } from "react";
import CommonPopup from "@/components/CommonPopup"; import CommonPopup from "@/components/CommonPopup";
import { View } from "@tarojs/components"; import { View } from "@tarojs/components";
import Taro from "@tarojs/taro";
import CalendarUI, { import CalendarUI, {
CalendarUIRef, CalendarUIRef,
} from "@/components/Picker/CalendarUI/CalendarUI"; } from "@/components/Picker/CalendarUI/CalendarUI";
@@ -47,6 +48,13 @@ const DialogCalendarCard: React.FC<DialogCalendarCardProps> = ({
onClose(); onClose();
return; return;
} }
if (!selected) {
Taro.showToast({
title: '请选择日期',
icon: "none",
});
return;
}
// 年份选择完成后,进入月份选择 // 年份选择完成后,进入月份选择
setType("time"); setType("time");
} else if (type === "month") { } else if (type === "month") {

View File

@@ -1,5 +1,6 @@
import React, { useState, useEffect, useRef } from "react"; import React, { useState, useEffect, useRef } from "react";
import CommonPopup from "@/components/CommonPopup"; import CommonPopup from "@/components/CommonPopup";
import Taro from "@tarojs/taro";
import { View } from "@tarojs/components"; import { View } from "@tarojs/components";
import CalendarUI, { import CalendarUI, {
CalendarUIRef, CalendarUIRef,
@@ -32,6 +33,13 @@ const DayDialog: React.FC<DayDialogProps> = ({
} | null>(null); } | null>(null);
const handleConfirm = () => { const handleConfirm = () => {
console.log(selected, 'selectedselected'); console.log(selected, 'selectedselected');
if (!selected) {
Taro.showToast({
title: '请选择日期',
icon: "none",
});
return;
}
const finalDate = dayjs(selected as Date).format("YYYY-MM-DD"); const finalDate = dayjs(selected as Date).format("YYYY-MM-DD");
if (onChange){ if (onChange){
onChange(finalDate) onChange(finalDate)

View File

@@ -89,25 +89,15 @@ const RadarChart: React.FC = forwardRef((props, ref) => {
ctx.lineWidth = 1; ctx.lineWidth = 1;
ctx.stroke(); ctx.stroke();
// 标签 // 标签:沿轴线外侧延伸,文字中心对齐轴线端点
const offset = 10; const labelOffset = 28;
const textX = center.x + (radius + offset) * Math.cos(angle); const textX = center.x + (radius + labelOffset) * Math.cos(angle);
const textY = center.y + (radius + offset) * Math.sin(angle); const textY = center.y + (radius + labelOffset) * Math.sin(angle);
ctx.font = "12px sans-serif"; ctx.font = "12px sans-serif";
ctx.fillStyle = "#333"; ctx.fillStyle = "#333";
ctx.textBaseline = "middle"; ctx.textBaseline = "middle";
ctx.textAlign = "center";
if (
Math.abs(angle) < 0.01 ||
Math.abs(Math.abs(angle) - Math.PI) < 0.01
) {
ctx.textAlign = "center";
} else if (angle > -Math.PI / 2 && angle < Math.PI / 2) {
ctx.textAlign = "left";
} else {
ctx.textAlign = "right";
}
ctx.fillText(label, textX, textY); ctx.fillText(label, textX, textY);
}); });

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { View, Canvas } from '@tarojs/components' import { View, Canvas } from '@tarojs/components'
import Taro from '@tarojs/taro' import Taro from '@tarojs/taro'
import { OSS_BASE_URL } from "@/config/api"; import { OSS_BASE } from "@/config/api";
// 分享卡片数据接口 // 分享卡片数据接口
export interface ShareCardData { export interface ShareCardData {
@@ -506,7 +506,7 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
const textX = iconX + iconSize + 20 const textX = iconX + iconSize + 20
// 绘制网球图标 // 绘制网球图标
const tennisBallPath = await loadImage(`${OSS_BASE_URL}/images/b3eaf45e-ef28-4e45-9195-823b832e0451.jpg`, canvasNode) const tennisBallPath = await loadImage(`${OSS_BASE}/front/ball/images/b3eaf45e-ef28-4e45-9195-823b832e0451.jpg`, canvasNode)
ctx.drawImage(tennisBallPath, iconX, gameInfoY, iconSize, iconSize) ctx.drawImage(tennisBallPath, iconX, gameInfoY, iconSize, iconSize)
// 绘制"单打"标签 // 绘制"单打"标签
@@ -542,7 +542,7 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
const dateX = danDaX const dateX = danDaX
const timeInfoY = infoStartY + infoSpacing const timeInfoY = infoStartY + infoSpacing
const timeInfoFontSize = scale * 24 * dpr const timeInfoFontSize = scale * 24 * dpr
const calendarPath = await loadImage(`${OSS_BASE_URL}/images/ea792a5d-b105-4c95-bfc4-8af558f2b33b.jpg`, canvasNode) const calendarPath = await loadImage(`${OSS_BASE}/front/ball/images/ea792a5d-b105-4c95-bfc4-8af558f2b33b.jpg`, canvasNode)
ctx.drawImage(calendarPath, iconX, timeInfoY, iconSize, iconSize) ctx.drawImage(calendarPath, iconX, timeInfoY, iconSize, iconSize)
// 绘制日期(绿色) // 绘制日期(绿色)
@@ -556,7 +556,7 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
// 绘制地点 // 绘制地点
const locationInfoY = infoStartY + infoSpacing * 2 const locationInfoY = infoStartY + infoSpacing * 2
const locationFontSize = scale * 22 * dpr const locationFontSize = scale * 22 * dpr
const locationPath = await loadImage(`${OSS_BASE_URL}/images/adc9a167-2ea9-4e3b-b963-6a894a1fd91b.jpg`, canvasNode) const locationPath = await loadImage(`${OSS_BASE}/front/ball/images/adc9a167-2ea9-4e3b-b963-6a894a1fd91b.jpg`, canvasNode)
ctx.drawImage(locationPath, iconX, locationInfoY, iconSize, iconSize) ctx.drawImage(locationPath, iconX, locationInfoY, iconSize, iconSize)
drawBoldText(ctx, data.venueName, danDaX, locationInfoY + 10, locationFontSize, '#000000') drawBoldText(ctx, data.venueName, danDaX, locationInfoY + 10, locationFontSize, '#000000')

View File

@@ -79,6 +79,7 @@ const TextareaTag: React.FC<TextareaTagProps> = ({
autoHeight={true} autoHeight={true}
onFocus={onFocus} onFocus={onFocus}
onBlur={onBlur} onBlur={onBlur}
adjustPosition={false}
/> />
<View className={`char-count${isOverflow ? ' char-count--error' : ''}`}> <View className={`char-count${isOverflow ? ' char-count--error' : ''}`}>
{value.description.length}/{maxLength} {value.description.length}/{maxLength}

View File

@@ -8,6 +8,7 @@ import NumberInterval from "./NumberInterval";
import TimeSelector from "./TimeSelector"; import TimeSelector from "./TimeSelector";
import TitleTextarea from "./TitleTextarea"; import TitleTextarea from "./TitleTextarea";
import CommonPopup from "./CommonPopup"; import CommonPopup from "./CommonPopup";
import CustomPopup from "./CustomPopup";
import { CalendarUI, DialogCalendarCard } from "./Picker"; import { CalendarUI, DialogCalendarCard } from "./Picker";
import CommonDialog from "./CommonDialog"; import CommonDialog from "./CommonDialog";
import PublishMenu from "./PublishMenu/PublishMenu"; import PublishMenu from "./PublishMenu/PublishMenu";
@@ -37,6 +38,7 @@ export {
TimeSelector, TimeSelector,
TitleTextarea, TitleTextarea,
CommonPopup, CommonPopup,
CustomPopup,
DialogCalendarCard, DialogCalendarCard,
CalendarUI, CalendarUI,
CommonDialog, CommonDialog,

View File

@@ -1,7 +1,10 @@
import envConfig from './env'// API配置 import envConfig from './env'// API配置
// OSS 基础路径配置 // OSS 配置:仅域名,调用处拼接 /front/ball 及后续路径
export const OSS_BASE_URL = 'https://youchang2026.oss-cn-shanghai.aliyuncs.com/front/ball' // export const OSS_BASE = "https://bimwe-oss.oss-cn-shanghai.aliyuncs.com";
// 因乐驰OSS 配置:仅域名,调用处拼接 /front/ball 及后续路径
export const OSS_BASE = envConfig.ossBaseURL;
export const API_CONFIG = { export const API_CONFIG = {
// 基础URL // 基础URL

61
src/config/env.ts Normal file
View File

@@ -0,0 +1,61 @@
import Taro from "@tarojs/taro";
/**
* 环境配置:从 config/env.config.ts 经 defineConstants 注入
* 构建时由 config/index.ts 根据 APP_ENV 选择并注入
*/
export type EnvType = "dev" | "dev_local" | "sit" | "pr";
export interface EnvConfig {
name: string;
apiBaseURL: string;
ossBaseURL: string;
timeout: number;
enableLog: boolean;
enableMock: boolean;
customerService: {
corpId: string;
serviceUrl: string;
};
}
// 从 defineConstants 注入的编译时常量读取
const getInjectedConfig = (): EnvConfig => ({
name: process.env.APP_ENV || "dev",
apiBaseURL: process.env.API_BASE_URL || "",
ossBaseURL: process.env.OSS_BASE_URL || "",
timeout: Number(process.env.TIMEOUT) || 10000,
enableLog: process.env.ENABLE_LOG === "true",
enableMock: false,
customerService: {
corpId: process.env.CUSTOMER_CORP_ID || "",
serviceUrl: process.env.CUSTOMER_SERVICE_URL || "",
},
});
export const getCurrentEnv = (): EnvType =>
(process.env.APP_ENV as EnvType) || "dev";
export const getCurrentConfig = (): EnvConfig => getInjectedConfig();
export const isDevelopment = (): boolean =>
getCurrentEnv() === "dev" || getCurrentEnv() === "dev_local" || getCurrentEnv() === "sit";
export const isProduction = (): boolean => getCurrentEnv() === "pr";
export const getEnvInfo = () => {
const config = getCurrentConfig();
return {
env: getCurrentEnv(),
config,
taroEnv: (Taro as any).getEnv?.(),
platform:
(Taro as any).getEnv?.() === (Taro as any).ENV_TYPE?.WEAPP
? "微信小程序"
: (Taro as any).getEnv?.() === (Taro as any).ENV_TYPE?.WEB
? "Web"
: "未知",
};
};
export default getCurrentConfig();

View File

@@ -29,6 +29,7 @@ const ListContainer = (props) => {
collapse = false, collapse = false,
defaultShowNum, defaultShowNum,
evaluateFlag, evaluateFlag,
enableHomeCards = false, // 仅首页需要 banner 和 NTRP 测评卡片
listLoadErrorWrapperHeight, listLoadErrorWrapperHeight,
listLoadErrorWidth, listLoadErrorWidth,
listLoadErrorHeight, listLoadErrorHeight,
@@ -94,10 +95,10 @@ const ListContainer = (props) => {
}; };
}, []); }, []);
// 获取测试结果,判断最近一个月是否有测试记录 // 获取测试结果,判断最近一个月是否有测试记录(仅首页需要)
useEffect(() => { useEffect(() => {
const init = async () => { const init = async () => {
if (!evaluateFlag) return; if (!evaluateFlag || !enableHomeCards) return;
// 先等待静默登录完成 // 先等待静默登录完成
await waitForAuthInit(); await waitForAuthInit();
// 然后再获取用户信息 // 然后再获取用户信息
@@ -112,7 +113,7 @@ const ListContainer = (props) => {
} }
}; };
init(); init();
}, [evaluateFlag, userInfo, lastTestResult, fetchLastTestResult]); }, [evaluateFlag, enableHomeCards, userInfo, lastTestResult, fetchLastTestResult]);
// 从全局状态中获取测试状态 // 从全局状态中获取测试状态
const hasTestInLastMonth = lastTestResult?.has_test_in_last_month || false; const hasTestInLastMonth = lastTestResult?.has_test_in_last_month || false;
@@ -134,6 +135,7 @@ const ListContainer = (props) => {
// 插入 banner 卡片 // 插入 banner 卡片
function insertBannerCard(list) { function insertBannerCard(list) {
if (!bannerListImage) return list; if (!bannerListImage) return list;
if (!list || !Array.isArray(list)) return list ?? [];
return [ return [
...list.slice(0, Number(bannerListIndex)), ...list.slice(0, Number(bannerListIndex)),
{ type: "banner", banner_image_url: bannerListImage, banner_detail_url: bannerDetailImage }, { type: "banner", banner_image_url: bannerListImage, banner_detail_url: bannerDetailImage },
@@ -142,61 +144,62 @@ const ListContainer = (props) => {
} }
// 对于没有ntrp等级的用户每个月展示一次, 插在第二个位置后面 // 对于没有ntrp等级的用户每个月展示一次, 插在第二个位置后面
// insertBannerCard 需在最后统一执行,否则前面分支直接 return 时 banner 不会被插入
function insertEvaluateCard(list) { function insertEvaluateCard(list) {
if (!evaluateFlag) let result: any[];
return showNumber !== undefined ? list.slice(0, showNumber) : list;
if (!list || list.length === 0) { if (!evaluateFlag) {
return list; result = showNumber !== undefined ? list.slice(0, showNumber) : list;
} } else if (!list || list.length === 0) {
// 如果最近一个月有测试记录,则不插入 card result = list;
if (hasTestInLastMonth) { } else if (hasTestInLastMonth) {
return showNumber !== undefined ? list.slice(0, showNumber) : list; 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),
];
} }
if (list.length <= 2) { return insertBannerCard(result);
return [...list, { type: "evaluateCard" }];
}
const [item1, item2, ...rest] = list;
let result = [
item1,
item2,
{ type: "evaluateCard" },
...(showNumber !== undefined ? rest.slice(0, showNumber - 3) : rest),
];
if (bannerListImage) {
return insertBannerCard(result);
}
return result;
} }
const memoizedList = useMemo( const memoizedList = useMemo(
() => insertEvaluateCard(data), () => (enableHomeCards ? insertEvaluateCard(data) : data),
[evaluateFlag, data, hasTestInLastMonth, showNumber, bannerListImage, bannerDetailImage, bannerListIndex] [enableHomeCards, evaluateFlag, data, hasTestInLastMonth, showNumber, bannerListImage, bannerDetailImage, bannerListIndex]
); );
// 渲染 banner 卡片 // 渲染 banner 卡片
const renderBanner = (item, index) => { const renderBanner = (item, index) => {
if (!item?.banner_image_url) return null; if (!item?.banner_image_url) {
return null;
}
return ( return (
<View <View
key={item.id || `banner-${index}`} key={item.id || `banner-${index}`}
className="banner-image-wrapper" onClick={() => {
const target = item.banner_detail_url;
if (target) {
(Taro as any).navigateTo({
url: `/other_pages/bannerDetail/index?img=${encodeURIComponent(target)}`,
});
}
}}
style={{
height: "100px",
overflow: "hidden",
borderRadius: "12px",
backgroundImage: `url(${item.banner_image_url})`,
backgroundSize: "cover",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
}}
> >
<Image
src={item.banner_image_url}
mode="widthFix"
className="banner-image"
onClick={() => {
const target = item.banner_detail_url;
if (target) {
(Taro as any).navigateTo({
url: `/other_pages/bannerDetail/index?img=${encodeURIComponent(target)}`,
});
}
}}
/>
</View> </View>
); );
}; };
@@ -224,12 +227,12 @@ const ListContainer = (props) => {
return ( return (
<> <>
{memoizedList.map((match, index) => { {memoizedList.map((match, index) => {
if (match?.type === "banner") { if (enableHomeCards && match?.type === "banner") {
return renderBanner(match, index); return renderBanner(match, index);
} }
if (match?.type === "evaluateCard") { if (enableHomeCards && match?.type === "evaluateCard") {
return ( return (
<NTRPTestEntryCard key="evaluate" type={EvaluateScene.list} /> <NTRPTestEntryCard key={`evaluate-${index}`} type={EvaluateScene.list} />
); );
} }
return <ListCard key={match?.id || index} {...match} />; return <ListCard key={match?.id || index} {...match} />;

View File

@@ -40,7 +40,7 @@ function isFull(counts) {
function matchNtrpRequestment( function matchNtrpRequestment(
target?: string, target?: string,
min?: string, min?: string,
max?: string max?: string,
): boolean { ): boolean {
// 目标值为空或 undefined // 目标值为空或 undefined
if (!target?.trim()) return true; if (!target?.trim()) return true;
@@ -110,7 +110,7 @@ export default function Participants(props) {
user_action_status; user_action_status;
const showApplicationEntry = const showApplicationEntry =
[can_pay, can_substitute, is_substituting, waiting_start].every( [can_pay, can_substitute, is_substituting, waiting_start].every(
(item) => !item (item) => !item,
) && ) &&
can_join && can_join &&
dayjs(start_time).isAfter(dayjs()); dayjs(start_time).isAfter(dayjs());
@@ -138,7 +138,7 @@ export default function Participants(props) {
Taro.navigateTo({ Taro.navigateTo({
url: `/login_pages/index/index?redirect=${encodeURIComponent( url: `/login_pages/index/index?redirect=${encodeURIComponent(
fullPath fullPath,
)}`, )}`,
}); });
} }
@@ -153,7 +153,7 @@ export default function Participants(props) {
const matchNtrpReq = matchNtrpRequestment( const matchNtrpReq = matchNtrpRequestment(
userInfo?.ntrp_level, userInfo?.ntrp_level,
skill_level_min, skill_level_min,
skill_level_max skill_level_max,
); );
function handleSelfEvaluate() { function handleSelfEvaluate() {
@@ -180,7 +180,7 @@ export default function Participants(props) {
} }
function generateTextAndAction( function generateTextAndAction(
user_action_status: null | { [key: string]: boolean } user_action_status: null | { [key: string]: boolean },
): ):
| undefined | undefined
| { text: string | React.FC; action?: () => void; available?: boolean } { | { text: string | React.FC; action?: () => void; available?: boolean } {
@@ -259,7 +259,7 @@ export default function Participants(props) {
const res = await OrderService.getUnpaidOrder(id); const res = await OrderService.getUnpaidOrder(id);
if (res.code === 0) { if (res.code === 0) {
navto( navto(
`/order_pages/orderDetail/index?id=${res.data.order_info.order_id}` `/order_pages/orderDetail/index?id=${res.data.order_info.order_id}`,
); );
} }
}), }),
@@ -296,10 +296,11 @@ export default function Participants(props) {
const { action = () => {} } = generateTextAndAction(user_action_status)!; const { action = () => {} } = generateTextAndAction(user_action_status)!;
const leftCount = max_participants - participant_count; const leftCount = max_participants - participant_count;
const leftSubstituteCount = (max_substitute_players || 0) - (substitute_count || 0); const leftSubstituteCount =
(max_substitute_players || 0) - (substitute_count || 0);
const showSubstituteApplicationEntry = const showSubstituteApplicationEntry =
[can_pay, can_join, is_substituting, waiting_start].every( [can_pay, can_join, is_substituting, waiting_start].every(
(item) => !item (item) => !item,
) && ) &&
can_substitute && can_substitute &&
dayjs(start_time).isAfter(dayjs()); dayjs(start_time).isAfter(dayjs());
@@ -336,7 +337,7 @@ export default function Participants(props) {
refresherBackground="#FAFAFA" refresherBackground="#FAFAFA"
className={classnames( className={classnames(
styles["participants-list-scroll"], styles["participants-list-scroll"],
showApplicationEntry ? styles.withApplication : "" showApplicationEntry ? styles.withApplication : "",
)} )}
scrollX scrollX
> >
@@ -377,14 +378,14 @@ export default function Participants(props) {
src={avatar_url} src={avatar_url}
onClick={handleViewUserInfo.bind( onClick={handleViewUserInfo.bind(
null, null,
participant_user_id participant_user_id,
)} )}
/> />
<Text className={styles["participants-list-item-name"]}> <Text className={styles["participants-list-item-name"]}>
{nickname || "未知"} {nickname || "未知"}
</Text> </Text>
<Text className={styles["participants-list-item-level"]}> <Text className={styles["participants-list-item-level"]}>
{displayNtrp} NTRP {displayNtrp}
</Text> </Text>
<Text className={styles["participants-list-item-role"]}> <Text className={styles["participants-list-item-role"]}>
{role} {role}
@@ -400,97 +401,107 @@ export default function Participants(props) {
)} )}
</View> </View>
{/* 候补区域 */} {/* 候补区域 */}
{max_substitute_players > 0 && (substitute_count > 0 || showSubstituteApplicationEntry) && ( {max_substitute_players > 0 &&
<View className={styles["detail-page-content-participants"]}> (substitute_count > 0 || showSubstituteApplicationEntry) && (
<View className={styles["participants-title"]}> <View className={styles["detail-page-content-participants"]}>
<Text></Text> <View className={styles["participants-title"]}>
<Text>·</Text> <Text></Text>
<Text>{leftSubstituteCount > 0 ? `剩余空位 ${leftSubstituteCount}` : "已满员"}</Text> <Text>·</Text>
</View> <Text>
<View className={styles["participants-list"]}> {leftSubstituteCount > 0
{/* 候补申请入口 */} ? `剩余空位 ${leftSubstituteCount}`
{showSubstituteApplicationEntry && ( : "已满员"}
<View </Text>
className={styles["participants-list-application"]} </View>
onClick={() => { <View className={styles["participants-list"]}>
action?.(); {/* 候补申请入口 */}
}} {showSubstituteApplicationEntry && (
> <View
<Image className={styles["participants-list-application"]}
className={styles["participants-list-application-icon"]} onClick={() => {
src={img.ICON_DETAIL_APPLICATION_ADD} action?.();
/> }}
<Text className={styles["participants-list-application-text"]}> >
<Image
</Text> className={styles["participants-list-application-icon"]}
</View> src={img.ICON_DETAIL_APPLICATION_ADD}
)} />
{/* 候补成员列表 */} <Text
<ScrollView className={styles["participants-list-application-text"]}
refresherBackground="#FAFAFA" >
className={classnames(
styles["participants-list-scroll"], </Text>
showSubstituteApplicationEntry ? styles.withApplication : "" </View>
)} )}
scrollX {/* 候补成员列表 */}
> <ScrollView
<View refresherBackground="#FAFAFA"
className={styles["participants-list-scroll-content"]} className={classnames(
style={{ styles["participants-list-scroll"],
width: `${ showSubstituteApplicationEntry ? styles.withApplication : "",
Math.max(substitute_members.length, 1) * 103 + (Math.max(substitute_members.length, 1) - 1) * 8 )}
}px`, scrollX
}}
> >
{substitute_members.map((substitute) => { <View
const { className={styles["participants-list-scroll-content"]}
is_organizer, style={{
user: { width: `${
avatar_url, Math.max(substitute_members.length, 1) * 103 +
nickname, (Math.max(substitute_members.length, 1) - 1) * 8
level, }px`,
ntrp_level, }}
id: substitute_user_id, >
}, {substitute_members.map((substitute) => {
} = substitute; const {
const role = is_organizer ? "组织者" : "参与者"; is_organizer,
// 优先使用 ntrp_level如果没有则使用 level user: {
const ntrpValue = ntrp_level || level; avatar_url,
// 格式化显示 NTRP如果没有值则显示"初学者" nickname,
const displayNtrp = ntrpValue level,
? formatNtrpDisplay(ntrpValue) ntrp_level,
: "初学者"; id: substitute_user_id,
return ( },
<View } = substitute;
key={substitute.id} const role = is_organizer ? "组织者" : "参与者";
className={styles["participants-list-item"]} // 优先使用 ntrp_level如果没有则使用 level
> const ntrpValue = ntrp_level || level;
<Image // 格式化显示 NTRP如果没有值则显示"初学者"
className={styles["participants-list-item-avatar"]} const displayNtrp = ntrpValue
mode="aspectFill" ? formatNtrpDisplay(ntrpValue)
src={avatar_url} : "初学者";
onClick={handleViewUserInfo.bind( return (
null, <View
substitute_user_id key={substitute.id}
)} className={styles["participants-list-item"]}
/> >
<Text className={styles["participants-list-item-name"]}> <Image
{nickname || "未知"} className={styles["participants-list-item-avatar"]}
</Text> mode="aspectFill"
<Text className={styles["participants-list-item-level"]}> src={avatar_url}
{displayNtrp} onClick={handleViewUserInfo.bind(
</Text> null,
<Text className={styles["participants-list-item-role"]}> substitute_user_id,
{role} )}
</Text> />
</View> <Text className={styles["participants-list-item-name"]}>
); {nickname || "未知"}
})} </Text>
</View> <Text
</ScrollView> className={styles["participants-list-item-level"]}
>
{displayNtrp}
</Text>
<Text className={styles["participants-list-item-role"]}>
{role}
</Text>
</View>
);
})}
</View>
</ScrollView>
</View>
</View> </View>
</View> )}
)}
<NTRPEvaluatePopup type={EvaluateScene.detail} ref={ntrpRef} showGuide /> <NTRPEvaluatePopup type={EvaluateScene.detail} ref={ntrpRef} showGuide />
</> </>
); );

View File

@@ -15,7 +15,7 @@ import CrossIcon from "@/static/detail/cross.svg";
import { genNTRPRequirementText, navto } from "@/utils/helper"; import { genNTRPRequirementText, navto } from "@/utils/helper";
import { waitForAuthInit } from "@/utils/authInit"; import { waitForAuthInit } from "@/utils/authInit";
import { useUserActions } from "@/store/userStore"; import { useUserActions } from "@/store/userStore";
import { OSS_BASE_URL } from "@/config/api"; import { OSS_BASE } from "@/config/api";
import { generatePosterImage, base64ToTempFilePath, delay } from "@/utils"; import { generatePosterImage, base64ToTempFilePath, delay } from "@/utils";
import { DayOfWeekMap } from "../../config"; import { DayOfWeekMap } from "../../config";
import styles from "./index.module.scss"; import styles from "./index.module.scss";
@@ -60,8 +60,10 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
show: async (publish_flag = false) => { show: async (publish_flag = false) => {
setPublishFlag(publish_flag); setPublishFlag(publish_flag);
if (publish_flag) { if (publish_flag) {
const url = await generateShareImageUrl(); try {
setShareImageUrl(url); const url = await generateShareImageUrl();
setShareImageUrl(url);
} catch (e) {}
} }
setVisible(true); setVisible(true);
}, },
@@ -81,13 +83,14 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
const endTime = dayjs(end_time); const endTime = dayjs(end_time);
const dayofWeek = DayOfWeekMap.get(startTime.day()); const dayofWeek = DayOfWeekMap.get(startTime.day());
const gameLength = `${endTime.diff(startTime, "hour")}小时`; const gameLength = `${endTime.diff(startTime, "hour")}小时`;
console.log(userInfo, "userInfo");
const url = await generateShareImage({ const url = await generateShareImage({
userAvatar: userInfo.avatar_url, userAvatar: userInfo.avatar_url,
userNickname: userInfo.nickname, userNickname: userInfo.nickname,
gameType: play_type, gameType: play_type,
skillLevel: `NTRP ${genNTRPRequirementText( skillLevel: `NTRP ${genNTRPRequirementText(
skill_level_min, skill_level_min,
skill_level_max skill_level_max,
)}`, )}`,
gameDate: `${startTime.format("M月D日")} (${dayofWeek})`, gameDate: `${startTime.format("M月D日")} (${dayofWeek})`,
gameTime: `${startTime.format("ah")}${gameLength}`, gameTime: `${startTime.format("ah")}${gameLength}`,
@@ -128,22 +131,24 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
const endTime = dayjs(end_time); const endTime = dayjs(end_time);
const dayofWeek = DayOfWeekMap.get(startTime.day()); const dayofWeek = DayOfWeekMap.get(startTime.day());
const gameLength = `${endTime.diff(startTime, "hour")}小时`; const gameLength = `${endTime.diff(startTime, "hour")}小时`;
Taro.showLoading({ title: "生成中..." }); // Taro.showLoading({ title: "生成中..." });
const qrCodeUrlRes = await DetailService.getQrCodeUrl({ const qrCodeUrlRes = await DetailService.getQrCodeUrl({
page: "game_pages/detail/index", page: "game_pages/detail/index",
scene: `id=${id}`, scene: `id=${id}`,
}); });
const qrCodeUrl = await base64ToTempFilePath( // const qrCodeUrl = await base64ToTempFilePath(
qrCodeUrlRes.data.qr_code_base64 // qrCodeUrlRes.data.qr_code_base64
); // );
const qrCodeUrl = qrCodeUrlRes.data.ossPath;
await delay(100); await delay(100);
// Taro.showLoading({ title: "生成中..." });
const url = await generatePosterImage({ const url = await generatePosterImage({
playType: play_type, playType: play_type,
ntrp: `NTRP ${genNTRPRequirementText(skill_level_min, skill_level_max)}`, ntrp: `NTRP ${genNTRPRequirementText(skill_level_min, skill_level_max)}`,
mainCoursal: mainCoursal:
image_list[0] && image_list[0].startsWith("http") image_list[0] && image_list[0].startsWith("http")
? image_list[0] ? image_list[0]
: `${OSS_BASE_URL}/images/0621b8cf-f7d6-43ad-b852-7dc39f29a782.png`, : `${OSS_BASE}/front/ball/images/0621b8cf-f7d6-43ad-b852-7dc39f29a782.png`,
nickname, nickname,
avatarUrl: avatar_url, avatarUrl: avatar_url,
title, title,
@@ -152,7 +157,7 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
time: `${startTime.format("ah")}${gameLength}`, time: `${startTime.format("ah")}${gameLength}`,
qrCodeUrl, qrCodeUrl,
}); });
Taro.hideLoading(); // Taro.hideLoading();
Taro.showShareImageMenu({ Taro.showShareImageMenu({
path: url, path: url,
}); });
@@ -164,6 +169,18 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
setVisible(false); setVisible(false);
} }
async function handleCopyLink() {
const linkUrlRes = await DetailService.getLinkUrl({
path: "game_pages/detail/index",
query: `id=${id}`,
});
await Taro.setClipboardData({
data: linkUrlRes.data.url_link,
});
Taro.showToast({ title: "链接已复制到剪贴板", icon: "success" });
setVisible(false);
}
function onClose() { function onClose() {
setVisible(false); setVisible(false);
setPublishFlag(false); setPublishFlag(false);
@@ -193,14 +210,14 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
<View <View
className={styles.contentContainer} className={styles.contentContainer}
style={{ style={{
backgroundImage: `url(${OSS_BASE_URL}/images/215f1ce1-be52-4a92-8250-5a4a69e7f2b3.png)`, backgroundImage: `url(${OSS_BASE}/front/ball/images/215f1ce1-be52-4a92-8250-5a4a69e7f2b3.png)`,
}} }}
> >
<View <View
catchMove catchMove
className={classnames( className={classnames(
styles.title, styles.title,
publishFlag ? styles.publishTitle : "" publishFlag ? styles.publishTitle : "",
)} )}
> >
{publishFlag ? ( {publishFlag ? (
@@ -254,7 +271,7 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
</View> </View>
</View> </View>
<View className={styles.customBtnWrapper}> <View className={styles.customBtnWrapper}>
<Button className={styles.button}> <Button className={styles.button} onClick={handleCopyLink}>
<View className={styles.icon}> <View className={styles.icon}>
<Image className={styles.linkIcon} src={LinkIcon} /> <Image className={styles.linkIcon} src={LinkIcon} />
</View> </View>

View File

@@ -23,6 +23,7 @@ import SupplementalNotes from "./components/SupplementalNotes";
import OrganizerInfo from "./components/OrganizerInfo"; import OrganizerInfo from "./components/OrganizerInfo";
import SharePopup from "./components/SharePopup"; import SharePopup from "./components/SharePopup";
import { navto, toast } from "@/utils/helper"; import { navto, toast } from "@/utils/helper";
import { delay } from "@/utils";
import ArrowLeft from "@/static/detail/icon-arrow-left.svg"; import ArrowLeft from "@/static/detail/icon-arrow-left.svg";
// import Logo from "@/static/detail/icon-logo-go.svg"; // import Logo from "@/static/detail/icon-logo-go.svg";
import styles from "./index.module.scss"; import styles from "./index.module.scss";
@@ -53,6 +54,12 @@ function Index() {
await waitForAuthInit(); await waitForAuthInit();
// 然后再获取用户信息 // 然后再获取用户信息
await fetchUserInfo(); await fetchUserInfo();
await delay(1000);
if (from === "publish") {
handleShare(true);
}
}; };
init(); init();
}, []); }, []);
@@ -81,9 +88,9 @@ function Index() {
// 位置更新后,重新获取详情页数据(因为距离等信息可能发生变化) // 位置更新后,重新获取详情页数据(因为距离等信息可能发生变化)
// 注意:这里不调用 fetchDetail避免与 useDidShow 中的调用重复 // 注意:这里不调用 fetchDetail避免与 useDidShow 中的调用重复
// 如果需要更新距离信息,可以在 fetchDetail 成功后根据当前位置重新计算 // 如果需要更新距离信息,可以在 fetchDetail 成功后根据当前位置重新计算
if (from === "publish") { // if (from === "publish") {
handleShare(true); // handleShare(true);
} // }
} catch (error) { } catch (error) {
console.error("用户位置更新失败", error); console.error("用户位置更新失败", error);
} }
@@ -161,7 +168,7 @@ function Index() {
navto( navto(
userId === myInfo.id userId === myInfo.id
? "/user_pages/myself/index" ? "/user_pages/myself/index"
: `/user_pages/other/index?userid=${userId}` : `/user_pages/other/index?userid=${userId}`,
); );
} }
@@ -195,7 +202,7 @@ function Index() {
<View <View
className={classnames( className={classnames(
styles["custom-navbar"], styles["custom-navbar"],
glass ? styles.glass : "" glass ? styles.glass : "",
)} )}
style={{ style={{
height: `${totalHeight}px`, height: `${totalHeight}px`,
@@ -291,7 +298,7 @@ function Index() {
id={id as string} id={id as string}
from={from as string} from={from as string}
detail={detail} detail={detail}
userInfo={userInfo} userInfo={myInfo}
/> />
</View> </View>
</ScrollView> </ScrollView>

View File

@@ -5,7 +5,7 @@ import Taro, { useRouter } from "@tarojs/taro";
import classnames from "classnames"; import classnames from "classnames";
import dayjs from "dayjs"; import dayjs from "dayjs";
import "dayjs/locale/zh-cn"; import "dayjs/locale/zh-cn";
import { generatePosterImage, base64ToTempFilePath, delay } from "@/utils"; import { generatePosterImage, delay } from "@/utils";
import { withAuth } from "@/components"; import { withAuth } from "@/components";
import GeneralNavbar from "@/components/GeneralNavbar"; import GeneralNavbar from "@/components/GeneralNavbar";
import DetailService from "@/services/detailService"; import DetailService from "@/services/detailService";
@@ -16,7 +16,7 @@ import { useUserActions } from "@/store/userStore";
import { DayOfWeekMap } from "../detail/config"; import { DayOfWeekMap } from "../detail/config";
import { genNTRPRequirementText } from "@/utils/helper"; import { genNTRPRequirementText } from "@/utils/helper";
import { waitForAuthInit } from "@/utils/authInit"; import { waitForAuthInit } from "@/utils/authInit";
import { OSS_BASE_URL } from "@/config/api"; import { OSS_BASE } from "@/config/api";
import styles from "./index.module.scss"; import styles from "./index.module.scss";
dayjs.locale("zh-cn"); dayjs.locale("zh-cn");
@@ -59,10 +59,11 @@ function SharePoster(props) {
page: "game_pages/detail/index", page: "game_pages/detail/index",
scene: `id=${id}`, scene: `id=${id}`,
}); });
const qrCodeUrl = await base64ToTempFilePath( const qrCodeUrl = qrCodeUrlRes.data.ossPath;
qrCodeUrlRes.data.qr_code_base64 // const qrCodeUrl = await base64ToTempFilePath(
); // qrCodeUrlRes.data.qr_code_base64
debugger // );
// debugger
await delay(100); await delay(100);
const url = await generatePosterImage({ const url = await generatePosterImage({
playType: play_type, playType: play_type,
@@ -70,7 +71,7 @@ function SharePoster(props) {
mainCoursal: mainCoursal:
image_list[0] && image_list[0].startsWith("http") image_list[0] && image_list[0].startsWith("http")
? image_list[0] ? image_list[0]
: `${OSS_BASE_URL}/images/0621b8cf-f7d6-43ad-b852-7dc39f29a782.png`, : `${OSS_BASE}/front/ball/images/0621b8cf-f7d6-43ad-b852-7dc39f29a782.png`,
nickname, nickname,
avatarUrl: avatar_url, avatarUrl: avatar_url,
title, title,

View File

@@ -9,6 +9,23 @@
} }
.link_button
{
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
height: 52px;
border-radius: 16px;
border: none;
position: relative;
font-size: 16px;
.button_text {
color: #fff;
}
}
// 背景图片和渐变覆盖层 // 背景图片和渐变覆盖层
.background_image { .background_image {
position: absolute; position: absolute;

View File

@@ -155,6 +155,11 @@ const LoginPage: React.FC = () => {
e.stopPropagation(); e.stopPropagation();
}; };
// 返回首页
const handle_return_home = () => {
Taro.navigateTo({ url: "/main_pages/index" });
};
return ( return (
<View className="login_page"> <View className="login_page">
<View className="background_image"> <View className="background_image">
@@ -193,7 +198,7 @@ const LoginPage: React.FC = () => {
/> />
</View> </View>
<Text className="button_text"> <Text className="button_text">
{is_loading ? "登录中..." : "授权登录"} {is_loading ? "登录中..." : "一键登录"}
</Text> </Text>
</Button> </Button>
@@ -211,6 +216,10 @@ const LoginPage: React.FC = () => {
<Text className="button_text"></Text> <Text className="button_text"></Text>
</Button> </Button>
<View className="return_home_button link_button" onClick={handle_return_home}>
<Text className="button_text"></Text>
</View>
{/* 用户协议复选框 */} {/* 用户协议复选框 */}
<View className="terms_checkbox_section"> <View className="terms_checkbox_section">
<View className="checkbox_container" onClick={handle_toggle_terms}> <View className="checkbox_container" onClick={handle_toggle_terms}>

View File

@@ -293,9 +293,9 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
currentProvince, currentProvince,
}); });
// 地址发生变化或不一致,重新加载数据和球局数量 // 延迟刷新,等 tab 切换动画完成后再加载,避免切换时列表重渲染导致抖动
// 先调用列表接口,然后在列表接口完成后调用数量接口 const delayMs = 280;
(async () => { const timer = setTimeout(async () => {
try { try {
if (refreshBothLists) { if (refreshBothLists) {
await refreshBothLists(); await refreshBothLists();
@@ -311,7 +311,9 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
} catch (error) { } catch (error) {
console.error("重新加载数据失败:", error); console.error("重新加载数据失败:", error);
} }
})(); }, delayMs);
prevIsActiveRef.current = isActive;
return () => clearTimeout(timer);
} }
} }
@@ -625,6 +627,7 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
reload={refreshMatches} reload={refreshMatches}
loadMoreMatches={loadMoreMatches} loadMoreMatches={loadMoreMatches}
evaluateFlag evaluateFlag
enableHomeCards
/> />
</ScrollView> </ScrollView>
</View> </View>

View File

@@ -37,6 +37,7 @@ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({ isActive = true }
const [hasLoaded, setHasLoaded] = useState(false); // 记录是否已经加载过数据 const [hasLoaded, setHasLoaded] = useState(false); // 记录是否已经加载过数据
const [collapseProfile, setCollapseProfile] = useState(false); const [collapseProfile, setCollapseProfile] = useState(false);
const [refreshing, setRefreshing] = useState(false);
useEffect(() => { useEffect(() => {
pickerOption.getCities(); pickerOption.getCities();
@@ -169,6 +170,23 @@ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({ isActive = true }
setActiveTab(tab); setActiveTab(tab);
}; };
// 下拉刷新:刷新用户信息和球局数据
const handle_refresh = async () => {
setRefreshing(true);
try {
await Promise.all([fetchUserInfo(), load_game_data()]);
} catch (error) {
console.error("刷新失败:", error);
(Taro as any).showToast({
title: "刷新失败,请重试",
icon: "none",
duration: 2000,
});
} finally {
setRefreshing(false);
}
};
// const handleScroll = (event: any) => { // const handleScroll = (event: any) => {
// const scrollData = event.detail; // const scrollData = event.detail;
// setCollapseProfile(scrollData.scrollTop > 1); // setCollapseProfile(scrollData.scrollTop > 1);
@@ -178,6 +196,9 @@ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({ isActive = true }
<ScrollView <ScrollView
scrollY scrollY
refresherBackground="#FAFAFA" refresherBackground="#FAFAFA"
refresherEnabled
refresherTriggered={refreshing}
onRefresherRefresh={handle_refresh}
className={styles.myselfPage} className={styles.myselfPage}
> >
<View <View

View File

@@ -21,21 +21,17 @@
top: 0; top: 0;
left: 0; left: 0;
opacity: 0; opacity: 0;
transform: scale(0.98); transition: opacity 0.25s ease-out;
transition: opacity 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94),
transform 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94);
overflow-y: auto; overflow-y: auto;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
pointer-events: none; pointer-events: none;
will-change: opacity, transform; visibility: hidden;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
&.active { &.active {
opacity: 1; opacity: 1;
transform: scale(1);
z-index: 1; z-index: 1;
pointer-events: auto; pointer-events: auto;
visibility: visible;
} }
} }

View File

@@ -67,12 +67,6 @@ const MainPage: React.FC = () => {
try { try {
await fetchUserInfo(); await fetchUserInfo();
await checkNicknameChangeStatus(); await checkNicknameChangeStatus();
// 启动时预取 Banner 字典(与业务无强依赖,失败不影响主流程)
try {
await useDictionaryStore.getState().fetchBannerDictionary();
} catch (e) {
console.error("预取 Banner 字典失败:", e);
}
} catch (error) { } catch (error) {
console.error("获取用户信息失败:", error); console.error("获取用户信息失败:", error);
} }

View File

@@ -26,7 +26,7 @@ import { useGlobalStore } from "@/store/global";
import { useOrder } from "@/store/orderStore"; import { useOrder } from "@/store/orderStore";
import detailService, { GameData } from "@/services/detailService"; import detailService, { GameData } from "@/services/detailService";
import { withAuth, RefundPopup, GeneralNavbar } from "@/components"; import { withAuth, RefundPopup, GeneralNavbar } from "@/components";
import { OSS_BASE_URL } from "@/config/api"; import { OSS_BASE } from "@/config/api";
import img from "@/config/images"; import img from "@/config/images";
import CustomerIcon from "@/static/order/customer.svg"; import CustomerIcon from "@/static/order/customer.svg";
import { handleCustomerService } from "@/services/userService"; import { handleCustomerService } from "@/services/userService";
@@ -301,7 +301,7 @@ function GameInfo(props) {
<View className={styles.locationMessageIcon}> <View className={styles.locationMessageIcon}>
<Image <Image
className={styles.locationMessageIconImage} className={styles.locationMessageIconImage}
src={`${OSS_BASE_URL}/images/3ee5c89c-fe58-4a56-9471-1295da09c743.png`} src={`${OSS_BASE}/front/ball/images/3ee5c89c-fe58-4a56-9471-1295da09c743.png`}
/> />
</View> </View>
{/* location message */} {/* location message */}

View File

@@ -69,6 +69,7 @@ function generateTimeMsg(game_info) {
const OrderList = () => { const OrderList = () => {
const [list, setList] = useState<any[][]>([]); const [list, setList] = useState<any[][]>([]);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [refreshing, setRefreshing] = useState(false);
const refundRef = useRef(null); const refundRef = useRef(null);
const end = list.length * PAGESIZE >= total; const end = list.length * PAGESIZE >= total;
@@ -114,6 +115,22 @@ const OrderList = () => {
} }
} }
// 下拉刷新:重新加载第一页订单
async function handle_refresh() {
setRefreshing(true);
try {
await getOrders(1);
} catch (error) {
Taro.showToast({
title: "刷新失败,请重试",
icon: "none",
duration: 2000,
});
} finally {
setRefreshing(false);
}
}
async function handlePayNow(item) { async function handlePayNow(item) {
// 检查登录状态和手机号 // 检查登录状态和手机号
if (!requireLoginWithPhone()) { if (!requireLoginWithPhone()) {
@@ -285,6 +302,10 @@ const OrderList = () => {
scrollWithAnimation scrollWithAnimation
lowerThreshold={20} lowerThreshold={20}
onScrollToLower={handleFetchNext} onScrollToLower={handleFetchNext}
refresherBackground="#FAFAFA"
refresherEnabled
refresherTriggered={refreshing}
onRefresherRefresh={handle_refresh}
enhanced enhanced
showScrollbar={false} showScrollbar={false}
className={styles.list} className={styles.list}

View File

@@ -1,16 +1,20 @@
.banner_detail_page { .banner_detail_page {
min-height: 100vh; min-height: 100vh;
background: #ffffff; background: #ffffff;
display: flex;
flex-direction: column;
} }
.banner_detail_content { .banner_detail_content {
padding: 12px; padding: 12px;
display: flex;
align-items: center;
justify-content: center;
flex: 1;
} }
.banner_detail_image { .banner_detail_image {
width: 100%; width: 100%;
border-radius: 12px; border-radius: 12px;
display: block; display: block;
} }

View File

@@ -1,6 +1,6 @@
export default definePageConfig({ export default definePageConfig({
navigationBarTitleText: '开启消息通知', navigationBarTitleText: '开启消息通知',
navigationStyle: 'custom', navigationStyle: 'custom',
enablePullDownRefresh: false, backgroundColor:"#FAFAFA"
}); });

View File

@@ -1,7 +1,9 @@
.enable_notification_page { .enable_notification_page {
width: 100%; width: 100%;
// min-height: 100vh; height: 100%;
background: radial-gradient(circle at 50% 0%, rgba(191, 255, 239, 1) 0%, rgba(255, 255, 255, 1) 37%); background: radial-gradient(circle at 50% 0%, rgba(191, 255, 239, 1) 0%, rgba(255, 255, 255, 1) 37%);
box-sizing: border-box;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -10,9 +12,8 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
height: calc(100vh - 98px); flex: 1;
position: relative; position: relative;
overflow: hidden;
} }
// 示例消息卡片区域 // 示例消息卡片区域
@@ -30,12 +31,12 @@
align-items: center; align-items: center;
gap: 8px; gap: 8px;
padding: 10px 16px; padding: 10px 16px;
background: #ffffff;
border: 0.5px solid rgba(0, 0, 0, 0.08); border: 0.5px solid rgba(0, 0, 0, 0.08);
border-radius: 20px; border-radius: 20px;
box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.08); box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.08);
box-sizing: border-box; box-sizing: border-box;
position: absolute; position: absolute;
background: #ffffff;
// 第三个卡片(最上面) // 第三个卡片(最上面)
&--3 { &--3 {
@@ -163,7 +164,6 @@
&__qr_image { &__qr_image {
width: 100%; width: 100%;
height: 100%;
} }
&__qr_placeholder { &__qr_placeholder {

View File

@@ -21,10 +21,10 @@ const OrderCheck = () => {
//TODO: get order msg from id //TODO: get order msg from id
const handlePay = async () => { const handlePay = async () => {
Taro.showLoading({ // Taro.showLoading({
title: '支付中...', // title: '支付中...',
mask: true // mask: true
}) // })
const res = await orderService.createOrder(Number(gameId)) const res = await orderService.createOrder(Number(gameId))
if (res.code === 0) { if (res.code === 0) {
const { payment_required, payment_params } = res.data const { payment_required, payment_params } = res.data
@@ -37,7 +37,7 @@ const OrderCheck = () => {
signType, signType,
paySign, paySign,
success: async () => { success: async () => {
Taro.hideLoading() // Taro.hideLoading()
Taro.showToast({ Taro.showToast({
title: '支付成功', title: '支付成功',
icon: 'success' icon: 'success'
@@ -48,7 +48,7 @@ const OrderCheck = () => {
}) })
}, },
fail: () => { fail: () => {
Taro.hideLoading() // Taro.hideLoading()
Taro.showToast({ Taro.showToast({
title: '支付失败', title: '支付失败',
icon: 'none' icon: 'none'

View File

@@ -193,7 +193,7 @@ const NewFollow = () => {
<View className="follow-left" onClick={() => handleUserClick(item.user_id)}> <View className="follow-left" onClick={() => handleUserClick(item.user_id)}>
<Image <Image
className="user-avatar" mode="aspectFill" className="user-avatar" mode="aspectFill"
src={item.user_avatar || "https://img.yzcdn.cn/vant/cat.jpeg"} src={item.user_avatar || require("@/static/userInfo/default_avatar.svg")}
/> />

View File

@@ -16,10 +16,9 @@ import { useGlobalState } from "@/store/global";
import { delay, getCurrentFullPath } from "@/utils"; import { delay, getCurrentFullPath } from "@/utils";
import { formatNtrpDisplay } from "@/utils/helper"; import { formatNtrpDisplay } from "@/utils/helper";
import { waitForAuthInit } from "@/utils/authInit"; import { waitForAuthInit } from "@/utils/authInit";
import httpService from "@/services/httpService"; // import httpService from "@/services/httpService";
import DetailService from "@/services/detailService"; import DetailService from "@/services/detailService";
import { base64ToTempFilePath } from "@/utils/genPoster"; import { OSS_BASE } from "@/config/api";
import { OSS_BASE_URL } from "@/config/api";
import CloseIcon from "@/static/ntrp/ntrp_close_icon.svg"; import CloseIcon from "@/static/ntrp/ntrp_close_icon.svg";
import DocCopy from "@/static/ntrp/ntrp_doc_copy.svg"; import DocCopy from "@/static/ntrp/ntrp_doc_copy.svg";
import ArrowRight from "@/static/ntrp/ntrp_arrow_right.svg"; import ArrowRight from "@/static/ntrp/ntrp_arrow_right.svg";
@@ -38,7 +37,7 @@ const sourceTypeToTextMap = new Map([
function adjustRadarLabels( function adjustRadarLabels(
source: [string, number][], source: [string, number][],
topK: number = 4 // 默认挑前4个最长的标签保护 topK: number = 4, // 默认挑前4个最长的标签保护
): [string, number][] { ): [string, number][] {
if (source.length === 0) return source; if (source.length === 0) return source;
@@ -226,7 +225,7 @@ function Intro() {
<View <View
className={styles.introContainer} className={styles.introContainer}
style={{ style={{
backgroundImage: `url(${OSS_BASE_URL}/images/215f1ce1-be52-4a92-8250-5a4a69e7f2b3.png)`, backgroundImage: `url(${OSS_BASE}/front/ball/images/215f1ce1-be52-4a92-8250-5a4a69e7f2b3.png)`,
}} }}
> >
<CommonGuideBar /> <CommonGuideBar />
@@ -253,7 +252,7 @@ function Intro() {
<View className={styles.tip}> <View className={styles.tip}>
<Image <Image
className={styles.tipImage} className={styles.tipImage}
src={`${OSS_BASE_URL}/images/b7cb47aa-b609-4112-899f-3fde02ed2431.png`} src={`${OSS_BASE}/front/ball/images/b7cb47aa-b609-4112-899f-3fde02ed2431.png`}
mode="aspectFit" mode="aspectFit"
/> />
</View> </View>
@@ -311,7 +310,7 @@ function Intro() {
<View className={styles.tip}> <View className={styles.tip}>
<Image <Image
className={styles.tipImage} className={styles.tipImage}
src={`${OSS_BASE_URL}/images/b7cb47aa-b609-4112-899f-3fde02ed2431.png`} src={`${OSS_BASE}/front/ball/images/b7cb47aa-b609-4112-899f-3fde02ed2431.png`}
mode="aspectFit" mode="aspectFit"
/> />
</View> </View>
@@ -319,7 +318,7 @@ function Intro() {
<View className={styles.radar}> <View className={styles.radar}>
<Image <Image
className={styles.radarImage} className={styles.radarImage}
src={`${OSS_BASE_URL}/images/a2e1b639-82a9-4ab8-b767-8605556eafcb.png`} src={`${OSS_BASE}/front/ball/images/a2e1b639-82a9-4ab8-b767-8605556eafcb.png`}
mode="aspectFit" mode="aspectFit"
/> />
</View> </View>
@@ -381,7 +380,7 @@ function Test() {
prev.map((item, pIndex) => ({ prev.map((item, pIndex) => ({
...item, ...item,
...(pIndex === index ? { choosen: i } : {}), ...(pIndex === index ? { choosen: i } : {}),
})) })),
); );
} }
@@ -428,7 +427,7 @@ function Test() {
<View <View
className={styles.testContainer} className={styles.testContainer}
style={{ style={{
backgroundImage: `url(${OSS_BASE_URL}/images/215f1ce1-be52-4a92-8250-5a4a69e7f2b3.png)`, backgroundImage: `url(${OSS_BASE}/front/ball/images/215f1ce1-be52-4a92-8250-5a4a69e7f2b3.png)`,
}} }}
> >
<CommonGuideBar confirm title={`${index + 1} / ${questions.length}`} /> <CommonGuideBar confirm title={`${index + 1} / ${questions.length}`} />
@@ -523,13 +522,14 @@ function Result() {
page: "other_pages/ntrp-evaluate/index", page: "other_pages/ntrp-evaluate/index",
scene: `stage=${StageType.INTRO}`, scene: `stage=${StageType.INTRO}`,
}); });
if (qrCodeUrlRes.code === 0 && qrCodeUrlRes.data?.qr_code_base64) { setQrCodeUrl(qrCodeUrlRes.data.ossPath);
// 将 base64 转换为临时文件路径 // if (qrCodeUrlRes.code === 0 && qrCodeUrlRes.data?.qr_code_base64) {
const tempFilePath = await base64ToTempFilePath( // // 将 base64 转换为临时文件路径
qrCodeUrlRes.data.qr_code_base64 // const tempFilePath = await base64ToTempFilePath(
); // qrCodeUrlRes.data.qr_code_base64
setQrCodeUrl(tempFilePath); // );
} // setQrCodeUrl(tempFilePath);
// }
} catch (error) { } catch (error) {
console.error("获取二维码失败:", error); console.error("获取二维码失败:", error);
} }
@@ -539,18 +539,25 @@ function Result() {
const res = await evaluateService.getTestResult({ record_id: Number(id) }); const res = await evaluateService.getTestResult({ record_id: Number(id) });
if (res.code === 0) { if (res.code === 0) {
setResult(res.data); setResult(res.data);
// delay(1000);
setRadarData( const sortOrder = res.data.sort || [];
adjustRadarLabels( const abilities = res.data.radar_data.abilities;
Object.entries(res.data.radar_data.abilities).map(([key, value]) => [ const sortedKeys = sortOrder.filter((k) => k in abilities);
key, const remainingKeys = Object.keys(abilities).filter(
Math.min( (k) => !sortOrder.includes(k),
100,
Math.floor((value.current_score / value.max_score) * 100)
),
])
)
); );
const allKeys = [...sortedKeys, ...remainingKeys];
let radarData: [string, number][] = allKeys.map((key) => [
key,
Math.min(
100,
Math.floor(
(abilities[key].current_score / abilities[key].max_score) * 100,
),
),
]);
// 直接使用接口 sort 顺序,不经过 adjustRadarLabels 重新排序
setRadarData(radarData);
updateUserLevel(res.data.record_id, res.data.ntrp_level); updateUserLevel(res.data.record_id, res.data.ntrp_level);
} }
} }
@@ -588,7 +595,7 @@ function Result() {
if (!userInfo?.phone) { if (!userInfo?.phone) {
Taro.redirectTo({ Taro.redirectTo({
url: `/login_pages/index/index?redirect=${encodeURIComponent( url: `/login_pages/index/index?redirect=${encodeURIComponent(
`/main_pages/index` `/main_pages/index`,
)}`, )}`,
}); });
clear(); clear();
@@ -613,11 +620,12 @@ function Result() {
page: "other_pages/ntrp-evaluate/index", page: "other_pages/ntrp-evaluate/index",
scene: `stage=${StageType.INTRO}`, scene: `stage=${StageType.INTRO}`,
}); });
if (qrCodeUrlRes.code === 0 && qrCodeUrlRes.data?.qr_code_base64) { finalQrCodeUrl = qrCodeUrlRes.data.ossPath;
finalQrCodeUrl = await base64ToTempFilePath( // if (qrCodeUrlRes.code === 0 && qrCodeUrlRes.data?.qr_code_base64) {
qrCodeUrlRes.data.qr_code_base64 // finalQrCodeUrl = await base64ToTempFilePath(
); // qrCodeUrlRes.data.qr_code_base64
} // );
// }
} }
// 使用 RadarV2 的 generateFullImage 方法生成完整图片 // 使用 RadarV2 的 generateFullImage 方法生成完整图片
@@ -712,7 +720,7 @@ function Result() {
<View <View
className={styles.card} className={styles.card}
style={{ style={{
backgroundImage: `url(${OSS_BASE_URL}/images/f5b45cea-5015-41d6-aaf4-83b2e76678e1.png)`, backgroundImage: `url(${OSS_BASE}/front/ball/images/f5b45cea-5015-41d6-aaf4-83b2e76678e1.png)`,
}} }}
> >
<View className={styles.avatarWrap}> <View className={styles.avatarWrap}>
@@ -762,7 +770,8 @@ function Result() {
{userInfo?.phone ? ( {userInfo?.phone ? (
<View className={styles.updateTip}> <View className={styles.updateTip}>
<Text> <Text>
NTRP {formatNtrpDisplay(result?.ntrp_level || "")}{" "} NTRP {" "}
{formatNtrpDisplay(result?.ntrp_level || "")}{" "}
</Text> </Text>
<Text className={styles.grayTip}>()</Text> <Text className={styles.grayTip}>()</Text>
</View> </View>

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import { View, Text, Textarea, Image } from '@tarojs/components' import { View, Text, Textarea, Image } from '@tarojs/components'
import Taro from '@tarojs/taro' import Taro from '@tarojs/taro'
import { ConfigProvider, Loading, Popup, Toast } from '@nutui/nutui-react-taro' import { ConfigProvider, Loading, Toast } from '@nutui/nutui-react-taro'
import styles from './index.module.scss' import styles from './index.module.scss'
import uploadFiles from '@/services/uploadFiles' import uploadFiles from '@/services/uploadFiles'
import publishService from '@/services/publishService' import publishService from '@/services/publishService'
@@ -109,7 +109,10 @@ const AiImportPopup: React.FC<AiImportPopupProps> = ({
} }
const handleTextChange = (e: any) => { const handleTextChange = (e: any) => {
setText(e.detail.value) const text = e.detail.value;
const maxAllowedLength = 120;
const truncatedVal = text.length > maxAllowedLength ? text.slice(0, maxAllowedLength) : text
setText(truncatedVal)
} }
// 使用全局键盘状态监听 // 使用全局键盘状态监听
@@ -191,73 +194,90 @@ const AiImportPopup: React.FC<AiImportPopupProps> = ({
} }
const showManualButton = uploadFailCount >= maxFailCount const showManualButton = uploadFailCount >= maxFailCount
if (!visible) {
return null
}
// 阻止弹窗内的触摸事件冒泡
const handleTouchMoveInPopup = (e) => {
if (!isKeyboardVisible) {
e.stopPropagation()
}
}
return ( return (
<Popup <View
visible={visible} className={styles.aiImportPopupOverlay}
position="bottom" >
round={true} <View className={styles.aiImportPopupWrapper} onTouchMove={handleTouchMoveInPopup} catchMove></View>
closeable={false} <View
onClose={closePopupBefore} className={styles.aiImportPopup}
className={styles.aiImportPopup} style={{ paddingBottom: isKeyboardVisible ? `${keyboardHeight}px` : undefined }}
style={{ paddingBottom: isKeyboardVisible ? `${keyboardHeight}px` : undefined }} >
> <View className={styles.popupContent}>
<View className={styles.popupContent}> {/* 头部 */}
{/* 头部 */} <View className={styles.header}>
<View className={styles.header}> <View className={styles.titleContainer}>
<View className={styles.titleContainer}> <Image src={images.ICON_IMPORTANT_BLACK} className={styles.lightningIcon} />
<Image src={images.ICON_IMPORTANT_BLACK} className={styles.lightningIcon} /> <Text className={styles.title}></Text>
<Text className={styles.title}></Text>
</View>
<View className={styles.closeButton} onClick={closePopupBefore}>
<Image src={images.ICON_CLOSE} className={styles.lightningIcon} />
</View>
</View>
{/* 文本域 */}
<View className={styles.textAreaContainer}>
<Textarea
className={styles.textArea}
value={text}
onInput={handleTextChange}
onFocus={() => {}}
onBlur={() => {}}
placeholder="在此「粘贴识别」或输入文本,智能拆分球局时间、费用、地点和其他信息,并帮你智能生成球局标题"
maxlength={-1}
showConfirmBar={false}
placeholderClass={styles.textArea_placeholder}
autoHeight
// 关闭系统自动上推,改为手动根据键盘高度加内边距
adjustPosition={false}
/>
<View className={styles.charCount}>
<Text className={`${styles.charCountText} ${isCharCountExceeded ? styles.charCountTextExceeded : ''}`}>
{text.length}/100
</Text>
</View>
</View>
{/* 图片识别按钮 */}
<View className={styles.imageRecognitionContainer}>
<View className={`${styles.imageRecognitionButton} ${uploadLoading ? styles.uploadLoadingContainer : ''}`} onClick={handleImageRecognition}>
{
uploadLoading ? (<Image src={images.ICON_UPLOAD_SUCCESS} className={styles.cameraIcon} />) : (<Image src={images.ICON_UPLOAD_IMG} className={styles.cameraIcon} />)
}
<Text className={styles.imageRecognitionText}></Text>
<Text className={styles.imageRecognitionDesc}>{uploadLoading ? '已上传 1 张图片' : '支持订场截图/小红书笔记截图等图片'}</Text>
</View>
</View>
{/* 底部按钮 */}
<View className={styles.bottomButtons}>
{showManualButton && (
<View className={styles.manualButton} onClick={handleManualPublish}>
<Text className={styles.manualButtonText}></Text>
</View> </View>
)} <View className={styles.closeButton} onClick={closePopupBefore}>
<View className={styles.pasteButton} onClick={handlePasteAndRecognize}> <Image src={images.ICON_CLOSE} className={styles.lightningIcon} />
{ </View>
loading ? ( </View>
{/* 文本域 */}
<View className={styles.textAreaContainer}>
<Textarea
className={styles.textArea}
value={text}
onInput={handleTextChange}
onFocus={() => {}}
onBlur={() => {}}
placeholder="在此「粘贴识别」或输入文本,智能拆分球局时间、费用、地点和其他信息,并帮你智能生成球局标题"
maxlength={-1}
showConfirmBar={false}
placeholderClass={styles.textArea_placeholder}
autoHeight
// 关闭系统自动上推,改为手动根据键盘高度加内边距
adjustPosition={false}
/>
<View className={styles.charCount}>
<Text className={`${styles.charCountText} ${isCharCountExceeded ? styles.charCountTextExceeded : ''}`}>
{text.length}/100
</Text>
</View>
</View>
{/* 图片识别按钮 */}
<View className={styles.imageRecognitionContainer}>
<View
className={`${styles.imageRecognitionButton} ${
uploadLoading ? styles.uploadLoadingContainer : ''
}`}
onClick={handleImageRecognition}
>
{uploadLoading ? (
<Image src={images.ICON_UPLOAD_SUCCESS} className={styles.cameraIcon} />
) : (
<Image src={images.ICON_UPLOAD_IMG} className={styles.cameraIcon} />
)}
<Text className={styles.imageRecognitionText}></Text>
<Text className={styles.imageRecognitionDesc}>
{uploadLoading ? '已上传 1 张图片' : '支持订场截图/小红书笔记截图等图片'}
</Text>
</View>
</View>
{/* 底部按钮 */}
<View className={styles.bottomButtons}>
{showManualButton && (
<View className={styles.manualButton} onClick={handleManualPublish}>
<Text className={styles.manualButtonText}></Text>
</View>
)}
<View className={styles.pasteButton} onClick={handlePasteAndRecognize}>
{loading ? (
<View className={styles.loadingContainer}> <View className={styles.loadingContainer}>
<ConfigProvider theme={{ nutuiLoadingIconColor: '#fff', nutuiLoadingIconSize: '20px' }}> <ConfigProvider theme={{ nutuiLoadingIconColor: '#fff', nutuiLoadingIconSize: '20px' }}>
<Loading type="circular" /> <Loading type="circular" />
@@ -269,13 +289,13 @@ const AiImportPopup: React.FC<AiImportPopupProps> = ({
<Image src={images.ICON_COPY} className={styles.clipboardIcon} /> <Image src={images.ICON_COPY} className={styles.clipboardIcon} />
<Text className={styles.pasteButtonText}></Text> <Text className={styles.pasteButtonText}></Text>
</> </>
) )}
} </View>
</View> </View>
</View> </View>
<Toast id="toast" />
</View> </View>
<Toast id="toast" /> </View>
</Popup>
) )
} }

View File

@@ -1,14 +1,34 @@
@use '~@/scss/themeColor.scss' as theme; @use '~@/scss/themeColor.scss' as theme;
.aiImportPopupOverlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 9998;
display: flex;
align-items: flex-end;
justify-content: center;
}
.aiImportPopupWrapper {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9998;
}
.aiImportPopup { .aiImportPopup {
background-color: #fff; width: 100%;
&:global(.nut-popup-bottom.nut-popup-round) { background-color:#fafafa;
border-radius: 20px 20px 0 0!important; border-radius: 16px 16px 0 0;
} position: relative;
z-index: 9999;
.popupContent { .popupContent {
width: 100%; width: 100%;
background: #fff;
border-radius: 16px 16px 0 0;
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
max-height: 80vh; max-height: 80vh;

View File

@@ -3,7 +3,7 @@ import { View, Text, Input, ScrollView, Image } from '@tarojs/components'
import Taro from '@tarojs/taro' import Taro from '@tarojs/taro'
import { Loading } from '@nutui/nutui-react-taro' import { Loading } from '@nutui/nutui-react-taro'
import StadiumDetail, { StadiumDetailRef } from './StadiumDetail' import StadiumDetail, { StadiumDetailRef } from './StadiumDetail'
import { CommonPopup } from '../../../../components' import { CommonPopup, CustomPopup } from '../../../../components'
import { getLocation } from '@/utils/locationUtils' import { getLocation } from '@/utils/locationUtils'
import PublishService from '@/services/publishService' import PublishService from '@/services/publishService'
import images from '@/config/images' import images from '@/config/images'
@@ -188,24 +188,20 @@ const SelectStadium: React.FC<SelectStadiumProps> = ({
// 如果显示详情页面 // 如果显示详情页面
if (showDetail && selectedStadium) { if (showDetail && selectedStadium) {
return ( return (
<CommonPopup <CustomPopup
visible={visible} visible={visible}
onClose={handleCancel} onClose={handleCancel}
cancelText="返回" cancelText="返回"
confirmText="确认" confirmText="确认"
className="select-stadium-popup"
onCancel={handleDetailCancel} onCancel={handleDetailCancel}
onConfirm={handleConfirm} onConfirm={handleConfirm}
position="bottom"
//style={{ paddingBottom: keyboardVisible ? `20px` : undefined }}
round
> >
<StadiumDetail {/* 内容区域 */}
ref={stadiumDetailRef} <StadiumDetail
stadium={selectedStadium} ref={stadiumDetailRef}
//onAnyInput={handleAnyInput} stadium={selectedStadium}
/> />
</CommonPopup> </CustomPopup>
) )
} }

View File

@@ -4,7 +4,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.stadium-detail-scroll{ .stadium-detail-scroll{
height:60vh; max-height:60vh;
} }
// 已选球场 // 已选球场
// 场馆列表 // 场馆列表

View File

@@ -1,10 +1,11 @@
import React, { useState, useCallback, forwardRef, useImperativeHandle } from 'react' import React, { useState, useCallback, forwardRef, useImperativeHandle, useEffect } from 'react'
import Taro from '@tarojs/taro' import Taro from '@tarojs/taro'
import { View, Text, Image, ScrollView } from '@tarojs/components' import { View, Text, Image, ScrollView } from '@tarojs/components'
import images from '@/config/images' import images from '@/config/images'
import TextareaTag from '@/components/TextareaTag' import TextareaTag from '@/components/TextareaTag'
// import CoverImageUpload, { type CoverImage } from '@/components/ImageUpload' // import CoverImageUpload, { type CoverImage } from '@/components/ImageUpload'
import UploadCover, { type CoverImageValue } from '@/components/UploadCover' import UploadCover, { type CoverImageValue } from '@/components/UploadCover'
import { useKeyboardHeight } from '@/store/keyboardStore'
import { useDictionaryActions } from '@/store/dictionaryStore' import { useDictionaryActions } from '@/store/dictionaryStore'
import './StadiumDetail.scss' import './StadiumDetail.scss'
@@ -69,12 +70,16 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
stadium, stadium,
onAnyInput onAnyInput
}, ref) => { }, ref) => {
const [openPicker, setOpenPicker] = useState(false); const [openPicker, setOpenPicker] = useState(false); //为了解决上传图片时按钮样式问题
const [scrollTop, setScrollTop] = useState(0); const [scrollTop, setScrollTop] = useState(0);
const { getDictionaryValue } = useDictionaryActions() const { getDictionaryValue } = useDictionaryActions()
const court_type = getDictionaryValue('court_type') || [] const court_type = getDictionaryValue('court_type') || []
const court_surface = getDictionaryValue('court_surface') || [] const court_surface = getDictionaryValue('court_surface') || []
const supplementary_information = getDictionaryValue('supplementary_information') || [] const supplementary_information = getDictionaryValue('supplementary_information') || []
// 使用全局键盘状态
const { keyboardHeight, isKeyboardVisible, addListener, initializeKeyboardListener } = useKeyboardHeight()
const stadiumInfo = [ const stadiumInfo = [
{ {
label: '场地类型', label: '场地类型',
@@ -171,19 +176,47 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
const changeTextarea = (value) => { // 使用全局键盘状态监听
useEffect(() => {
// 初始化全局键盘监听器
initializeKeyboardListener()
// 添加本地监听器
const removeListener = addListener((height, visible) => {
console.log('AiImportPopup 收到键盘变化:', height, visible)
})
return () => {
removeListener()
}
}, [initializeKeyboardListener, addListener])
const changeTextarea = (value: boolean) => {
if (value) { if (value) {
// 先滚动到底部 // 先滚动到底部
setScrollTop(scrollTop ? scrollTop + 1 : 9999); setScrollTop(140);
// 使用 setTimeout 确保滚动后再更新 openPicker // 使用 setTimeout 确保滚动后再更新 openPicker
} }
} }
const changePicker = (value) => { // 当键盘显示时触发 changeTextarea
useEffect(() => {
if (isKeyboardVisible) {
changeTextarea(true)
}
}, [isKeyboardVisible])
const changePicker = (value:boolean) => {
setOpenPicker(value); setOpenPicker(value);
} }
console.log(stadium,'stadiumstadium'); console.log(stadium,'stadiumstadium');
// 计算滚动区域的最大高度
const scrollMaxHeight = isKeyboardVisible
? `calc(100vh - ${keyboardHeight+40}px)`
: '60vh'
return ( return (
<View className='stadium-detail'> <View className='stadium-detail'>
<ScrollView <ScrollView
@@ -191,6 +224,7 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
refresherBackground="#FAFAFA" refresherBackground="#FAFAFA"
scrollY={!openPicker} scrollY={!openPicker}
scrollTop={scrollTop} scrollTop={scrollTop}
style={{ maxHeight: scrollMaxHeight }}
> >
{/* 已选球场 */} {/* 已选球场 */}
<View <View
@@ -235,7 +269,7 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
<TextareaTag <TextareaTag
value={formData[item.prop]} value={formData[item.prop]}
onChange={(value) => { onChange={(value) => {
changeTextarea(true) //changeTextarea(true)
updateFormData(item.prop, value) updateFormData(item.prop, value)
}} }}
// onBlur={() => changeTextarea(false)} // onBlur={() => changeTextarea(false)}

View File

@@ -78,9 +78,8 @@ const PublishBall: React.FC = () => {
} = useKeyboardHeight(); } = useKeyboardHeight();
// 获取页面参数并设置导航标题 // 获取页面参数并设置导航标题
const [optionsConfig, setOptionsConfig] = useState<FormFieldConfig[]>( const [optionsConfig, setOptionsConfig] = useState<FormFieldConfig[]>(
publishBallFormSchema publishBallFormSchema,
); );
console.log(userInfo, "userInfo");
const [formData, setFormData] = useState<PublishBallFormData[]>([ const [formData, setFormData] = useState<PublishBallFormData[]>([
defaultFormData, defaultFormData,
]); ]);
@@ -103,13 +102,11 @@ const PublishBall: React.FC = () => {
const updateFormData = ( const updateFormData = (
key: keyof PublishBallFormData, key: keyof PublishBallFormData,
value: any, value: any,
index: number index: number,
) => { ) => {
console.log(key, value, index, "key, value, index");
setFormData((prev) => { setFormData((prev) => {
const newData = [...prev]; const newData = [...prev];
newData[index] = { ...newData[index], [key]: value }; newData[index] = { ...newData[index], [key]: value };
console.log(newData, "newData");
return newData; return newData;
}); });
}; };
@@ -186,7 +183,7 @@ const PublishBall: React.FC = () => {
const confirmDelete = () => { const confirmDelete = () => {
if (deleteConfirm.index >= 0) { if (deleteConfirm.index >= 0) {
setFormData((prev) => setFormData((prev) =>
prev.filter((_, index) => index !== deleteConfirm.index) prev.filter((_, index) => index !== deleteConfirm.index),
); );
closeDeleteConfirm(); closeDeleteConfirm();
Taro.showToast({ Taro.showToast({
@@ -198,7 +195,7 @@ const PublishBall: React.FC = () => {
const validateFormData = ( const validateFormData = (
formData: PublishBallFormData, formData: PublishBallFormData,
isOnSubmit: boolean = false isOnSubmit: boolean = false,
) => { ) => {
const { const {
activityInfo, activityInfo,
@@ -207,7 +204,7 @@ const PublishBall: React.FC = () => {
image_list, image_list,
players, players,
current_players, current_players,
descriptionInfo descriptionInfo,
} = formData; } = formData;
const { play_type, price, location_name } = activityInfo; const { play_type, price, location_name } = activityInfo;
const { description } = descriptionInfo; const { description } = descriptionInfo;
@@ -225,7 +222,7 @@ const PublishBall: React.FC = () => {
// 判断图片是否上传完成 // 判断图片是否上传完成
if (image_list?.length > 0) { if (image_list?.length > 0) {
const uploadInProgress = image_list.some((item) => const uploadInProgress = image_list.some((item) =>
item.url.startsWith("http://tmp/") item?.url?.startsWith?.("http://tmp/"),
); );
if (uploadInProgress) { if (uploadInProgress) {
Taro.showToast({ Taro.showToast({
@@ -253,7 +250,7 @@ const PublishBall: React.FC = () => {
} }
return false; return false;
} }
if ( if (
!price || !price ||
(typeof price === "number" && price <= 0) || (typeof price === "number" && price <= 0) ||
@@ -368,7 +365,6 @@ const PublishBall: React.FC = () => {
// 提交表单 // 提交表单
const handleSubmit = async () => { const handleSubmit = async () => {
// 基础验证 // 基础验证
console.log(formData, "formData");
const params = getParams(); const params = getParams();
const { republish } = params || {}; const { republish } = params || {};
if (activityType === "individual") { if (activityType === "individual") {
@@ -516,7 +512,7 @@ const PublishBall: React.FC = () => {
const mergeWithDefault = ( const mergeWithDefault = (
data: any, data: any,
isDetail: boolean = false isDetail: boolean = false,
): PublishBallFormData => { ): PublishBallFormData => {
// ai导入与详情数据处理 // ai导入与详情数据处理
const { const {
@@ -741,7 +737,6 @@ const PublishBall: React.FC = () => {
} else { } else {
setIsSubmitDisabled(false); setIsSubmitDisabled(false);
} }
console.log(formData, "formData");
}, [formData]); }, [formData]);
useEffect(() => { useEffect(() => {
@@ -754,9 +749,8 @@ const PublishBall: React.FC = () => {
initializeKeyboardListener(); initializeKeyboardListener();
// 添加本地监听器 // 添加本地监听器
const removeListener = addListener((height, visible) => { const removeListener = addListener(() => {
console.log("PublishBall 收到键盘变化:", height, visible); // 布局是否响应交由 shouldReactToKeyboard 决定
// 这里只记录或用于其他逻辑,布局是否响应交由 shouldReactToKeyboard 决定
}); });
return () => { return () => {
@@ -789,6 +783,7 @@ const PublishBall: React.FC = () => {
> >
<GeneralNavbar <GeneralNavbar
title={titleBar} title={titleBar}
backgroundColor={'#FAFAFA'}
className={styles["publish-ball-navbar"]} className={styles["publish-ball-navbar"]}
/> />
<View <View

View File

@@ -51,7 +51,6 @@ class CommonApiService {
data: results.map(result => result.data) data: results.map(result => result.data)
} }
} catch (error) { } catch (error) {
throw error
} finally { } finally {
Taro.hideLoading() Taro.hideLoading()
} }

View File

@@ -158,14 +158,19 @@ class GameDetailService {
async getQrCodeUrl(req: { page: string, scene: string }): Promise<ApiResponse<{ async getQrCodeUrl(req: { page: string, scene: string }): Promise<ApiResponse<{
qr_code_base64: string, qr_code_base64: string,
image_size: number, image_size: number,
ossPath: string,
page: string, page: string,
scene: string, scene: string,
width: number width: number
}>> { }>> {
return httpService.post('/user/generate_qrcode', req, { return httpService.post('/user/generate_qrcode', req, {
showLoading: false showLoading: true
}) })
} }
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 })
}
} }
// 导出认证服务实例 // 导出认证服务实例

View File

@@ -58,6 +58,7 @@ export interface TestResultData {
level_img?: string; // 等级图片URL level_img?: string; // 等级图片URL
radar_data: RadarData; radar_data: RadarData;
answers: Answer[]; answers: Answer[];
sort?: string[]; // 雷达图能力项排序,如 ["正手球质", "正手控制", ...]
} }
// 单条测试记录 // 单条测试记录

View File

@@ -129,23 +129,30 @@ class HttpService {
// 隐藏loading支持多个并发请求 // 隐藏loading支持多个并发请求
private hideLoading(): void { private hideLoading(): void {
this.loadingCount = Math.max(0, this.loadingCount - 1) try {
this.loadingCount = Math.max(0, this.loadingCount - 1)
// 只有所有请求都完成时才隐藏loading // 只有所有请求都完成时才隐藏loading
if (this.loadingCount === 0) { if (this.loadingCount === 0) {
// 清除之前的延时器 // 清除之前的延时器
if (this.hideLoadingTimer) { if (this.hideLoadingTimer) {
clearTimeout(this.hideLoadingTimer) clearTimeout(this.hideLoadingTimer)
this.hideLoadingTimer = null this.hideLoadingTimer = null
}
// 延时300ms后隐藏loading避免频繁切换
this.hideLoadingTimer = setTimeout(() => {
Taro.hideLoading()
this.currentLoadingText = ''
this.hideLoadingTimer = null
}, 800)
} }
// 延时300ms后隐藏loading避免频繁切换
this.hideLoadingTimer = setTimeout(() => {
Taro.hideLoading()
this.currentLoadingText = ''
this.hideLoadingTimer = null
}, 800)
} }
catch (e) {
console.warn(e)
}
} }
// 处理响应 // 处理响应
@@ -175,7 +182,7 @@ class HttpService {
url: '/login_pages/index/index' url: '/login_pages/index/index'
}) })
reject(new Error('用户不存在')) reject(new Error('用户不存在'))
return response.data return response.data
} }
@@ -187,7 +194,7 @@ class HttpService {
} else { } else {
reject(response.data) reject(response.data)
} }
return response.data return response.data
} }
} }

View File

@@ -263,7 +263,7 @@ export const save_login_state = (token: string, user_info: WechatUserInfo) => {
export const clear_login_state = () => { export const clear_login_state = () => {
try { try {
// 使用 tokenManager 清除令牌 // 使用 tokenManager 清除令牌
tokenManager.clearTokens(); // tokenManager.clearTokens();
// 清除其他登录状态 // 清除其他登录状态
Taro.removeStorageSync("user_info"); Taro.removeStorageSync("user_info");

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -20,7 +20,6 @@ interface DictionaryState {
bannerDetailImage: string bannerDetailImage: string
bannerListIndex: string bannerListIndex: string
} | null } | null
fetchBannerDictionary: () => Promise<void>
} }
// 创建字典Store // 创建字典Store
@@ -36,7 +35,7 @@ export const useDictionaryStore = create<DictionaryState>()((set, get) => ({
set({ isLoading: true, error: null }) set({ isLoading: true, error: null })
try { try {
const keys = 'publishing_requirements,court_type,court_surface,supplementary_information,game_play,fabu_tip,supported_cities'; const keys = 'publishing_requirements,court_type,court_surface,supplementary_information,game_play,fabu_tip,supported_cities,bannerListImage,bannerDetailImage,bannerListIndex';
const response = await commonApi.getDictionaryManyKey(keys) const response = await commonApi.getDictionaryManyKey(keys)
if (response.code === 0 && response.data) { if (response.code === 0 && response.data) {
@@ -53,6 +52,15 @@ export const useDictionaryStore = create<DictionaryState>()((set, get) => ({
dictionaryData: dictionaryData || {}, dictionaryData: dictionaryData || {},
isLoading: false isLoading: false
}) })
set({
bannerDict: {
bannerListImage: response.data.bannerListImage || '',
bannerDetailImage: response.data.bannerDetailImage || '',
bannerListIndex: (response.data.bannerListIndex ?? '').toString(),
}
})
console.log('字典数据获取成功:', response.data) console.log('字典数据获取成功:', response.data)
} else { } else {
throw new Error(response.message || '获取字典数据失败') throw new Error(response.message || '获取字典数据失败')
@@ -67,26 +75,7 @@ export const useDictionaryStore = create<DictionaryState>()((set, get) => ({
} }
}, },
// 获取 Banner 字典(启动时或手动调用)
fetchBannerDictionary: async () => {
try {
const keys = 'bannerListImage,bannerDetailImage,bannerListIndex';
const response = await commonApi.getDictionaryManyKey(keys)
if (response.code === 0 && response.data) {
const data = response.data || {};
set({
bannerDict: {
bannerListImage: data.bannerListImage || '',
bannerDetailImage: data.bannerDetailImage || '',
bannerListIndex: (data.bannerListIndex ?? '').toString(),
}
})
}
} catch (error) {
// 保持静默,避免影响启动流程
console.error('获取 Banner 字典失败:', error)
}
},
// 获取字典值 // 获取字典值
getDictionaryValue: (key: string, defaultValue?: any) => { getDictionaryValue: (key: string, defaultValue?: any) => {

View File

@@ -66,6 +66,34 @@ const DownloadBillRecords: React.FC = () => {
}); });
} }
}; };
const handlePreviewFile = (fileUrl: string) => {
wx.downloadFile({
url: fileUrl,
success: (res) => {
if (res.statusCode === 200) {
// 确保文件路径正确并添加扩展名
const filePath = res.tempFilePath;
wx.openDocument({
filePath: filePath,
fileType: 'xlsx', // 指定文件类型为xlsx
showMenu: true, // 显示右上角菜单按钮
success: (openRes) => {
console.log('打开文档成功');
},
fail: (err) => {
console.error('打开文档失败', err);
}
});
} else {
console.error('下载失败,状态码:', res.statusCode);
}
},
fail: (err) => {
console.error('下载失败', err);
}
});
}
return ( return (
<View className="download-bill-records-page"> <View className="download-bill-records-page">
{/* 导航栏 */} {/* 导航栏 */}
@@ -94,7 +122,7 @@ const DownloadBillRecords: React.FC = () => {
Taro.navigateBack(); Taro.navigateBack();
}} }}
/> />
<View <View
className="records-container" className="records-container"
style={{ marginTop: `${totalHeight}px` }} style={{ marginTop: `${totalHeight}px` }}
> >
@@ -111,7 +139,7 @@ const DownloadBillRecords: React.FC = () => {
</View> </View>
<View className="info-item"> <View className="info-item">
<Text></Text> <Text></Text>
<Text className="btn"></Text> <Text className="btn" onClick={() => handlePreviewFile(record.file_url)}></Text>
</View> </View>
</View> </View>
)) : <EmptyState text="暂无数据" />} )) : <EmptyState text="暂无数据" />}

View File

@@ -37,6 +37,7 @@
.qrcode { .qrcode {
width: 240px; width: 240px;
height: 240px;
margin: 32px 0 -20px; margin: 32px 0 -20px;
} }

View File

@@ -68,6 +68,7 @@ const OtherUserPage: React.FC = () => {
); );
const [collapseProfile, setCollapseProfile] = useState(false); const [collapseProfile, setCollapseProfile] = useState(false);
const [refreshing, setRefreshing] = useState(false);
// 进入页面时检查 user_id只在组件挂载时执行一次 // 进入页面时检查 user_id只在组件挂载时执行一次
useEffect(() => { useEffect(() => {
@@ -82,56 +83,52 @@ const OtherUserPage: React.FC = () => {
} }
}, []); // 空依赖数组,确保只在进入时执行一次 }, []); // 空依赖数组,确保只在进入时执行一次
// 页面加载时获取用户信息 // 加载用户信息(使用 useCallback 便于下拉刷新复用)
useEffect(() => { const load_user_data = useCallback(async () => {
const load_user_data = async () => { if (!user_id) return;
if (user_id) { try {
try { const res = await LoginService.getUserInfoById(user_id);
// const user_data = await UserService.get_user_info(user_id); const { data: userData } = res;
const res = await LoginService.getUserInfoById(user_id); setUserInfo({
const { data: userData } = res; id: parseInt(user_id || "") || 0,
// setUserInfo({...res.data as UserInfo, avatar: data.avatar_url || require("@/static/userInfo/default_avatar.svg")}); nickname: userData.nickname || "",
setUserInfo({ avatar_url: userData.avatar_url || "",
id: parseInt(user_id || "") || 0, join_date: userData.subscribe_time
nickname: userData.nickname || "", ? `${new Date(userData.subscribe_time).getFullYear()}${
avatar_url: userData.avatar_url || "", new Date(userData.subscribe_time).getMonth() + 1
join_date: userData.subscribe_time }月加入`
? `${new Date(userData.subscribe_time).getFullYear()}${ : "",
new Date(userData.subscribe_time).getMonth() + 1 stats: {
}月加入` following_count: userData.stats?.following_count || 0,
: "", followers_count: userData.stats?.followers_count || 0,
stats: { hosted_games_count: userData.stats?.hosted_games_count || 0,
following_count: userData.stats?.following_count || 0, participated_games_count:
followers_count: userData.stats?.followers_count || 0, userData.stats?.participated_games_count || 0,
hosted_games_count: userData.stats?.hosted_games_count || 0, },
participated_games_count: personal_profile: userData.personal_profile || "",
userData.stats?.participated_games_count || 0, province: userData.province || "",
}, city: userData.city || "",
district: userData.district || "",
personal_profile: userData.personal_profile || "", occupation: userData.occupation || "",
province: userData.province || "", ntrp_level: "",
city: userData.city || "", phone: userData.phone || "",
district: userData.district || "", gender: userData.gender || "",
occupation: userData.occupation || "", birthday: userData.birthday || "",
ntrp_level: "", });
phone: userData.phone || "", setIsFollowing(userData.is_following || false);
gender: userData.gender || "", } catch (error) {
birthday: userData.birthday || "", console.error("加载用户数据失败:", error);
}); Taro.showToast({
setIsFollowing(userData.is_following || false); title: "加载失败",
} catch (error) { icon: "none",
console.error("加载用户数据失败:", error); });
Taro.showToast({ }
title: "加载失败",
icon: "none",
});
}
}
};
load_user_data();
}, [user_id]); }, [user_id]);
useEffect(() => {
load_user_data();
}, [load_user_data]);
// 分类球局数据(使用 useCallback 包装,避免每次渲染都创建新函数) // 分类球局数据(使用 useCallback 包装,避免每次渲染都创建新函数)
const classifyGameRecords = useCallback( const classifyGameRecords = useCallback(
( (
@@ -232,6 +229,18 @@ const OtherUserPage: React.FC = () => {
setCollapseProfile(scrollData.scrollTop > 1); setCollapseProfile(scrollData.scrollTop > 1);
}, []); }, []);
// 下拉刷新:刷新用户信息和球局数据
const handle_refresh = useCallback(async () => {
setRefreshing(true);
try {
await Promise.all([load_user_data(), load_game_data()]);
} catch (error) {
console.error("刷新失败:", error);
} finally {
setRefreshing(false);
}
}, [load_user_data, load_game_data]);
// 处理球局详情 // 处理球局详情
// const handle_game_detail = (game_id: string) => { // const handle_game_detail = (game_id: string) => {
// Taro.navigateTo({ // Taro.navigateTo({
@@ -244,6 +253,9 @@ const OtherUserPage: React.FC = () => {
scrollY scrollY
className="other_user_page" className="other_user_page"
refresherBackground="#FAFAFA" refresherBackground="#FAFAFA"
refresherEnabled
refresherTriggered={refreshing}
onRefresherRefresh={handle_refresh}
> >
{/* <CustomNavbar> {/* <CustomNavbar>
<View className="navbar_content"> <View className="navbar_content">

View File

@@ -1,12 +1,19 @@
// @use '../../scss/common.scss' as *; // @use '../../scss/common.scss' as *;
.wallet_page { .wallet_page {
display: flex;
flex-direction: column;
height: 100vh; height: 100vh;
overflow-y: auto; overflow: hidden;
background-color: #fafafa; background-color: #fafafa;
padding-bottom: 5px;
box-sizing: border-box; box-sizing: border-box;
.wallet_scroll {
flex: 1;
height: 0;
padding-bottom: 5px;
}
&::-webkit-scrollbar { &::-webkit-scrollbar {
display: none; display: none;
width: 0; width: 0;

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState, useCallback } from "react";
import { View, Text, Input, Button, Image } from "@tarojs/components"; import { View, Text, Input, Button, Image, ScrollView } from "@tarojs/components";
import Taro, { useDidShow, useReachBottom } from "@tarojs/taro"; import Taro, { useDidShow } from "@tarojs/taro";
import "./index.scss"; import "./index.scss";
import { CommonPopup, EmptyState } from "@/components"; import { CommonPopup, EmptyState } from "@/components";
import httpService from "@/services/httpService"; import httpService from "@/services/httpService";
@@ -109,16 +109,6 @@ const WalletPage: React.FC = () => {
const pageConfig = currentPage.page?.config; const pageConfig = currentPage.page?.config;
const pageTitle = pageConfig?.navigationBarTitleText; const pageTitle = pageConfig?.navigationBarTitleText;
useReachBottom(() => {
if (load_transactions_params.page >= totalPages) return;
// 加载更多方法
set_load_transactions_params((prev) => {
return {
...prev,
page: prev.page + 1,
};
});
});
// 钱包信息状态 // 钱包信息状态
const [wallet_info, set_wallet_info] = useState<WalletInfo>({ const [wallet_info, set_wallet_info] = useState<WalletInfo>({
balance: 0, balance: 0,
@@ -158,6 +148,7 @@ const WalletPage: React.FC = () => {
}); });
const [totalPages, setTotalPages] = useState(1); const [totalPages, setTotalPages] = useState(1);
const [refreshing, setRefreshing] = useState(false);
useEffect(() => { useEffect(() => {
load_transactions(); load_transactions();
@@ -452,6 +443,33 @@ const WalletPage: React.FC = () => {
setShowFilterPopup(true); setShowFilterPopup(true);
}; };
// 下拉刷新:刷新钱包余额和交易记录
const handle_refresh = useCallback(async () => {
setRefreshing(true);
try {
await load_wallet_data();
set_transactions([]);
set_load_transactions_params((prev) => ({ ...prev, page: 1 }));
} catch (error) {
Taro.showToast({
title: "刷新失败,请重试",
icon: "none",
duration: 2000,
});
} finally {
setRefreshing(false);
}
}, []);
// 滚动到底部加载更多交易记录
const handle_scroll_to_lower = useCallback(() => {
if (load_transactions_params.page >= totalPages) return;
set_load_transactions_params((prev) => ({
...prev,
page: prev.page + 1,
}));
}, [load_transactions_params.page, totalPages]);
const handleFilterCancel = () => { const handleFilterCancel = () => {
setShowFilterPopup(false); setShowFilterPopup(false);
setFilterParams({ setFilterParams({
@@ -488,6 +506,16 @@ const WalletPage: React.FC = () => {
Taro.navigateBack(); Taro.navigateBack();
}} }}
/> />
<ScrollView
scrollY
refresherBackground="#FAFAFA"
refresherEnabled
refresherTriggered={refreshing}
onRefresherRefresh={handle_refresh}
lowerThreshold={50}
onScrollToLower={handle_scroll_to_lower}
className="wallet_scroll"
>
{/* 钱包主卡片 */} {/* 钱包主卡片 */}
<View <View
className="wallet_main_card" className="wallet_main_card"
@@ -649,6 +677,7 @@ const WalletPage: React.FC = () => {
)} )}
</View> </View>
</View> </View>
</ScrollView>
{/* 提现弹窗 */} {/* 提现弹窗 */}
<CommonPopup <CommonPopup

View File

@@ -1,17 +1,11 @@
import Taro from "@tarojs/taro"; import Taro from "@tarojs/taro";
import { OSS_BASE_URL } from "@/config/api"; import { OSS_BASE } from "@/config/api";
const bgUrl = `${OSS_BASE_URL}/images/5e2c85ab-fb0c-4026-974d-1e0725181542.png`; const bgUrl = `${OSS_BASE}/front/ball/images/5e2c85ab-fb0c-4026-974d-1e0725181542.png`;
const ringUrl = `${OSS_BASE}/front/ball/images/b635164f-ecec-434a-a00b-69614a918f2f.png`;
const ringUrl = `${OSS_BASE_URL}/images/b635164f-ecec-434a-a00b-69614a918f2f.png`; const dateIcon = `${OSS_BASE}/front/ball/images/1b49476e-0eda-42ff-b08c-002ce510df82.jpg`;
const mapIcon = `${OSS_BASE}/front/ball/images/06b994fa-9227-4708-8555-8a07af8d0c3b.jpg`;
const dateIcon = `${OSS_BASE_URL}/images/1b49476e-0eda-42ff-b08c-002ce510df82.jpg`; const logoText = `${OSS_BASE}/system/youchang_tip_text.png`;
const mapIcon = `${OSS_BASE_URL}/images/06b994fa-9227-4708-8555-8a07af8d0c3b.jpg`;
// const logo = `${OSS_BASE_URL}/images/fb732da6-11b9-4022-a524-a377b17635eb.jpg`
const logoText = `${OSS_BASE_URL}/images/9d8cbc9d-9601-4e2d-ab23-76420a4537d6.png`;
export function base64ToTempFilePath(base64Data: string): Promise<string> { export function base64ToTempFilePath(base64Data: string): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -288,7 +282,9 @@ function drawTextWrap(
/** 核心纯函数:生成海报图片 */ /** 核心纯函数:生成海报图片 */
export async function generatePosterImage(data: any): Promise<string> { export async function generatePosterImage(data: any): Promise<string> {
console.log("start !!!!"); console.log("start !!!!");
const dpr = Taro.getWindowInfo().pixelRatio; // const dpr = Taro.getWindowInfo().pixelRatio;
const dpr = 1;
// console.log(dpr, 'dpr')
const width = 600; const width = 600;
const height = 1000; const height = 1000;
@@ -439,7 +435,7 @@ export async function generatePosterImage(data: any): Promise<string> {
const { tempFilePath } = await Taro.canvasToTempFilePath({ const { tempFilePath } = await Taro.canvasToTempFilePath({
canvas, canvas,
fileType: 'png', fileType: 'png',
quality: 1, quality: 0.7,
}); });
return tempFilePath; return tempFilePath;
} }

View File

@@ -1,5 +1,5 @@
import Taro from '@tarojs/taro' import Taro from '@tarojs/taro'
import { OSS_BASE_URL } from "@/config/api"; import { OSS_BASE } from "@/config/api";
export interface ShareCardData { export interface ShareCardData {
userAvatar: string userAvatar: string
@@ -481,7 +481,7 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro
const textX = iconX + iconSize + 20 const textX = iconX + iconSize + 20
// 绘制网球图标 // 绘制网球图标
const tennisBallPath = await loadImage(`${OSS_BASE_URL}/images/b3eaf45e-ef28-4e45-9195-823b832e0451.jpg`) const tennisBallPath = await loadImage(`${OSS_BASE}/front/ball/images/b3eaf45e-ef28-4e45-9195-823b832e0451.jpg`)
ctx.drawImage(tennisBallPath, iconX, gameInfoY, iconSize, iconSize) ctx.drawImage(tennisBallPath, iconX, gameInfoY, iconSize, iconSize)
// 绘制"单打"标签 // 绘制"单打"标签
@@ -517,7 +517,7 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro
const dateX = danDaX const dateX = danDaX
const timeInfoY = infoStartY + infoSpacing const timeInfoY = infoStartY + infoSpacing
const timeInfoFontSize = scale * 24 * dpr const timeInfoFontSize = scale * 24 * dpr
const calendarPath = await loadImage(`${OSS_BASE_URL}/images/ea792a5d-b105-4c95-bfc4-8af558f2b33b.jpg`) const calendarPath = await loadImage(`${OSS_BASE}/front/ball/images/ea792a5d-b105-4c95-bfc4-8af558f2b33b.jpg`)
ctx.drawImage(calendarPath, iconX, timeInfoY, iconSize, iconSize) ctx.drawImage(calendarPath, iconX, timeInfoY, iconSize, iconSize)
// 绘制日期(绿色,非描边粗体) // 绘制日期(绿色,非描边粗体)
@@ -530,27 +530,27 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro
// 绘制地点 // 绘制地点
const locationInfoY = infoStartY + infoSpacing * 2 const locationInfoY = infoStartY + infoSpacing * 2
const locationFontSize = scale * 22 * dpr const locationFontSize = scale * 22 * dpr
const locationPath = await loadImage(`${OSS_BASE_URL}/images/adc9a167-2ea9-4e3b-b963-6a894a1fd91b.jpg`) const locationPath = await loadImage(`${OSS_BASE}/front/ball/images/adc9a167-2ea9-4e3b-b963-6a894a1fd91b.jpg`)
ctx.drawImage(locationPath, iconX, locationInfoY, iconSize, iconSize) ctx.drawImage(locationPath, iconX, locationInfoY, iconSize, iconSize)
drawBoldText(ctx, data.venueName, danDaX, locationInfoY + 10, locationFontSize, '#000000') drawBoldText(ctx, data.venueName, danDaX, locationInfoY + 10, locationFontSize, '#000000')
try { try {
const wxAny: any = (typeof (globalThis as any) !== 'undefined' && (globalThis as any).wx) ? (globalThis as any).wx : null const wxAny: any = (typeof (globalThis as any) !== 'undefined' && (globalThis as any).wx) ? (globalThis as any).wx : null
if (wxAny && typeof wxAny.canvasToTempFilePath === 'function') { if (wxAny && typeof wxAny.canvasToTempFilePath === 'function') {
wxAny.canvasToTempFilePath({ wxAny.canvasToTempFilePath({
canvas: offscreen, canvas: offscreen,
fileType: 'png', fileType: 'png',
quality: 1, quality: 1,
success: (res: any) => { success: (res: any) => {
console.log('===res666', res) console.log('===res666', res)
resolve(res.tempFilePath) resolve(res.tempFilePath)
}, },
fail: reject fail: reject
}) })
return return
} }
} catch { } } catch { }
reject(new Error('无法导出图片OffscreenCanvas 转文件失败)')) reject(new Error('无法导出图片OffscreenCanvas 转文件失败)'))
console.log('Canvas绘制命令已发送') console.log('Canvas绘制命令已发送')
} catch (error) { } catch (error) {

2
types/global.d.ts vendored
View File

@@ -17,6 +17,8 @@ declare namespace NodeJS {
NODE_ENV: 'development' | 'production', NODE_ENV: 'development' | 'production',
/** 当前构建的平台 */ /** 当前构建的平台 */
TARO_ENV: 'weapp' | 'swan' | 'alipay' | 'h5' | 'rn' | 'tt' | 'quickapp' | 'qq' | 'jd' TARO_ENV: 'weapp' | 'swan' | 'alipay' | 'h5' | 'rn' | 'tt' | 'quickapp' | 'qq' | 'jd'
/** 应用环境标识 */
APP_ENV: 'dev' | 'dev_local' | 'sit' | 'pr'
/** /**
* 当前构建的小程序 appid * 当前构建的小程序 appid
* @description 若不同环境有不同的小程序,可通过在 env 文件中配置环境变量`TARO_APP_ID`来方便快速切换 appid 而不必手动去修改 dist/project.config.json 文件 * @description 若不同环境有不同的小程序,可通过在 env 文件中配置环境变量`TARO_APP_ID`来方便快速切换 appid 而不必手动去修改 dist/project.config.json 文件