50 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
李瑞
af2c472030 处理图片顺序 2026-02-08 12:46:01 +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
李瑞
744169fe34 处理地点展示样式 2026-02-07 22:08:30 +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
李瑞
9dca489aba 处理banner插入 2026-02-07 00:53:40 +08:00
张成
8d729a0132 1 2026-02-07 00:51:30 +08:00
张成
2d68a558da 1 2026-02-06 17:58:38 +08:00
张成
ce0a299b59 1 2026-02-06 10:38:24 +08:00
张成
ca4b52570f 1 2026-02-06 10:37:30 +08:00
张成
cff9afd1e8 1 2026-02-06 00:47:09 +08:00
张成
d149de1f42 1 2026-02-06 00:26:31 +08:00
张成
969066591c 1 2026-02-05 23:23:21 +08:00
87 changed files with 2324 additions and 1315 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
.vscode
*.http
env.ts

View File

@@ -149,3 +149,7 @@ src/
## License
MIT
"appid": "wx915ecf6c01bea4ec",
"appid": "wx815b533167eb7b53",

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 devConfig from './dev'
import prodConfig from './prod'
// import vitePluginImp from 'vite-plugin-imp'
import { getEnvConfig, type EnvType } from './env.config'
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-辅助函数
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'> = {
projectName: 'playBallTogether',
date: '2025-8-9',
@@ -22,6 +32,13 @@ export default defineConfig<'webpack5'>(async (merge, { command, mode }) => {
outputRoot: 'dist',
plugins: ['@tarojs/plugin-html'],
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: {
'@': path.resolve(__dirname, '..', 'src'),
@@ -76,6 +93,9 @@ export default defineConfig<'webpack5'>(async (merge, { command, mode }) => {
},
// @ts-expect-error: Taro 类型定义缺少 mini.hot
hot: true,
projectConfig: {
appid: envConfig.appid,
},
},
h5: {
publicPath: '/',

View File

@@ -10,32 +10,17 @@
"framework": "React"
},
"scripts": {
"build": "npm run build:weapp ",
"dev": "npm run dev:weapp ",
"build:weapp": "taro build --type weapp --mode production",
"build:swan": "taro build --type swan",
"build:alipay": "taro build --type alipay",
"build:tt": "taro build --type tt",
"build:h5": "taro build --type h5",
"build:rn": "taro build --type rn",
"build:qq": "taro build --type qq",
"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"
"dev": "npm run dev:weapp",
"dev:local": "npm run dev:weapp:dev_local",
"dev:weapp": "node scripts/sync-project-config.js dev && taro build --type weapp --mode dev --watch",
"dev:weapp:dev_local": "node scripts/sync-project-config.js dev_local && taro build --type weapp --mode dev_local --watch",
"build": "npm run build:weapp",
"build:weapp": "node scripts/sync-project-config.js pr && taro build --type weapp --mode pr",
"build:sit": "node scripts/sync-project-config.js sit && taro build --type weapp --mode sit",
"build:pr": "node scripts/sync-project-config.js pr && taro build --type weapp --mode pr",
"dev:h5": "npm run build:h5 -- --watch"
},
"browserslist": [
"last 3 versions",
"Android >= 4.1",
"ios >= 8"
],
"browserslist": ["last 3 versions", "Android >= 4.1", "ios >= 8"],
"author": "",
"dependencies": {
"@babel/plugin-transform-runtime": "^7.28.3",
@@ -57,6 +42,7 @@
"@tarojs/shared": "4.1.5",
"@tarojs/taro": "4.1.5",
"babel-plugin-transform-remove-console": "^6.9.4",
"classnames": "^2.5.1",
"dayjs": "^1.11.13",
"qweather-icons": "^1.8.0",
"react": "^18.0.0",

View File

@@ -2,7 +2,7 @@
"miniprogramRoot": "dist/",
"projectname": "playBallTogether",
"description": "playBallTogether",
"appid": "wx915ecf6c01bea4ec",
"appid": "wx815b533167eb7b53",
"setting": {
"urlCheck": true,
"es6": true,

View File

@@ -15,9 +15,10 @@
"useStaticServer": false,
"useLanDebug": false,
"showES6CompileOption": false,
"compileHotReLoad": false,
"compileHotReLoad": true,
"checkInvalidKey": true,
"ignoreDevUnusedFiles": true,
"bigPackageSizeSupport": true
"bigPackageSizeSupport": true,
"useIsolateContext": true
}
}

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

View File

@@ -34,7 +34,7 @@ function toast(msg) {
interface CommentInputProps {
onConfirm?: (
value: { content: string } & Partial<CommentInputReplyParamsType>
value: { content: string } & Partial<CommentInputReplyParamsType>,
) => void;
}
@@ -49,10 +49,8 @@ interface CommentInputReplyParamsType {
nickname: string;
}
const CommentInput = forwardRef<CommentInputRef, CommentInputProps>(function (
props,
ref
) {
const CommentInput = forwardRef<CommentInputRef, CommentInputProps>(
function (props, ref) {
const { onConfirm } = props;
const [visible, setVisible] = useState(false);
const [value, setValue] = useState("");
@@ -73,9 +71,8 @@ const CommentInput = forwardRef<CommentInputRef, CommentInputProps>(function (
initializeKeyboardListener();
// 添加本地监听器
const removeListener = addListener((height, visible) => {
console.log("PublishBall 收到键盘变化:", height, visible);
// 这里只记录或用于其他逻辑,布局是否响应交由 shouldReactToKeyboard 决定
const removeListener = addListener(() => {
// 布局是否响应交由 shouldReactToKeyboard 决定
});
return () => {
@@ -112,7 +109,6 @@ const CommentInput = forwardRef<CommentInputRef, CommentInputProps>(function (
setValue("");
inputDomRef.current && inputDomRef.current?.blur();
}
console.log(keyboardHeight, "keyboardHeight");
return (
<CommonPopup
visible={visible}
@@ -124,7 +120,9 @@ const CommentInput = forwardRef<CommentInputRef, CommentInputProps>(function (
// height: "60px!important",
minHeight: "unset",
bottom:
isKeyboardVisible && keyboardHeight > 0 ? `${keyboardHeight}px` : "0",
isKeyboardVisible && keyboardHeight > 0
? `${keyboardHeight}px`
: "0",
}}
enableDragToClose={false}
>
@@ -149,7 +147,7 @@ const CommentInput = forwardRef<CommentInputRef, CommentInputProps>(function (
<View
className={classnames(
styles.limit,
value.length > 200 ? styles.red : ""
value.length > 200 ? styles.red : "",
)}
>
<Text>{value.length}</Text>/<Text>200</Text>
@@ -161,7 +159,8 @@ const CommentInput = forwardRef<CommentInputRef, CommentInputProps>(function (
</View>
</CommonPopup>
);
});
},
);
function isReplyComment(item: BaseComment<any>): item is ReplyComment {
return "reply_to_user" in item;
@@ -208,7 +207,7 @@ function CommentItem(props: {
className={classnames(
styles.commentItem,
blink_id === comment.id && styles.blink,
styles.weight_super
styles.weight_super,
)}
key={comment.id}
id={`comment_id_${comment.id}`}
@@ -293,7 +292,8 @@ function CommentItem(props: {
/>
))}
{!isReplyComment(comment) &&
comment.replies.length !== comment.reply_count && (
comment.replies.length !== comment.reply_count &&
comment.replies.length > 3 && (
<View
className={styles.viewMore}
onClick={() => handleLoadMore(comment)}
@@ -313,7 +313,7 @@ export default forwardRef(function Comments(
message_id?: number;
onScrollTo: (id: string) => void;
},
ref
ref,
) {
const { game_id, publisher_id, message_id, onScrollTo } = props;
const [comments, setComments] = useState<Comment[]>([]);
@@ -371,7 +371,7 @@ export default forwardRef(function Comments(
replies: [res.data, ...item.replies].sort((a, b) =>
dayjs(a.create_time).isAfter(dayjs(b.create_time))
? 1
: -1
: -1,
),
};
});
@@ -435,7 +435,7 @@ export default forwardRef(function Comments(
item.replies.splice(
page === 1 ? 0 : page * PAGESIZE - 1,
newReplies.length,
...newReplies
...newReplies,
);
item.reply_count = res.data.count;
}
@@ -502,7 +502,7 @@ export default forwardRef(function Comments(
return {
...item,
replies: item.replies.filter(
(replyItem) => replyItem.id !== id
(replyItem) => replyItem.id !== id,
),
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

@@ -85,6 +85,10 @@
font-size: 13px;
font-weight: 400;
color: #3c3c43;
display: flex;
flex-direction: row;
align-items: center;
gap:4px;
}
.distanceWrap {

View File

@@ -1,9 +1,14 @@
import { useRef, useState, useEffect } from "react";
import { Menu } from "@nutui/nutui-react-taro";
import { Image, View, ScrollView } from "@tarojs/components";
import Taro from "@tarojs/taro";
import img from "@/config/images";
import Bubble from "../Bubble";
import { useListState } from "@/store/listStore";
import { useListState, useListStore } from "@/store/listStore";
import { getCurrentLocation } from "@/utils/locationUtils";
import { updateUserLocation } from "@/services/userService";
import { useGlobalState } from "@/store/global";
import { useUserActions } from "@/store/userStore";
import "./index.scss";
const DistanceQuickFilterV2 = (props) => {
@@ -19,15 +24,19 @@ const DistanceQuickFilterV2 = (props) => {
quickValue,
districtValue, // 新增:行政区选中值
onMenuVisibleChange, // 菜单展开/收起回调
onRelocate, // 重新定位回调
} = props;
const cityRef = useRef(null);
const quickRef = useRef(null);
const [changePosition, setChangePosition] = useState<number[]>([]);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [keys, setKeys] = useState(0);
const [isRelocating, setIsRelocating] = useState(false);
// 从 store 获取当前城市信息
const { area } = useListState();
const currentCity = area?.at(-1) || ""; // 获取省份/城市名称
const { updateState } = useGlobalState() || {};
const { fetchUserInfo, updateCache } = useUserActions();
// 全城筛选显示的标题 - 如果选择了行政区,显示行政区名称
const getCityTitle = () => {
@@ -79,6 +88,64 @@ const DistanceQuickFilterV2 = (props) => {
index === 1 && (quickRef.current as any)?.toggle(false);
};
// 重新获取当前位置,调用接口把位置传递后端
const handleRelocate = async () => {
if (isRelocating) return;
setIsRelocating(true);
(Taro as any).showLoading({ title: '定位中...', mask: true });
try {
// 获取当前位置
const location = await getCurrentLocation();
if (location && location.latitude && location.longitude) {
// 更新 store 中的位置信息
updateState?.({ location });
// 调用接口把位置传递给后端,传递一个值代表强制更新
const response = await updateUserLocation(location.latitude, location.longitude, true);
// 如果接口返回成功,重新调用用户信息接口来更新 USER_SELECTED_CITY
if (response?.code === 0) {
// 删除 缓存
(Taro as any).removeStorageSync("USER_SELECTED_CITY");
// 延时一下
await new Promise(resolve => setTimeout(resolve, 600));
// 先清除缓存和 area确保使用最新的用户信息
await updateCache( [ response.data.last_location_province, response.data.last_location_city ]);
}
(Taro as any).showToast({
title: '定位成功',
icon: 'success',
duration: 1500,
});
// 通知父组件位置已更新,可以刷新列表
if (onRelocate) {
onRelocate(location);
}
} else {
throw new Error('获取位置信息失败');
}
} catch (error: any) {
console.error('重新定位失败:', error);
(Taro as any).showToast({
title: error?.message || '定位失败,请检查定位权限',
icon: 'none',
duration: 2000,
});
} finally {
setIsRelocating(false);
(Taro as any).hideLoading();
}
};
// 监听菜单状态变化,通知父组件
useEffect(() => {
onMenuVisibleChange?.(isMenuOpen);
@@ -103,8 +170,11 @@ const DistanceQuickFilterV2 = (props) => {
icon={<Image src={img.ICON_MENU_ITEM_SELECTED} />}
>
<div className="positionWrap">
<p className="title"></p>
<p className="cityName">{currentCity}</p>
<p className="title">{currentCity}</p>
<p className="cityName" onClick={handleRelocate}>
<img src={img.ICON_RELOCATE} style={{ width: '12px', height: "12px" }} />
<span></span>
</p>
</div>
<div className="distanceWrap">
<Bubble

View File

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

View File

@@ -186,7 +186,7 @@ export default forwardRef(function GameManagePopup(props, ref) {
.some((item) => item.user.id === userInfo.id);
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;
@@ -207,7 +207,7 @@ export default forwardRef(function GameManagePopup(props, ref) {
style={{ minHeight: "unset" }}
>
<View className={styles.container}>
{!inTwoHours && !hasOtherParticiappants && (
{!finished && !inTwoHours && !hasOtherParticiappants && (
<View className={styles.button} onClick={handleEditGame}>
</View>
@@ -217,12 +217,12 @@ export default forwardRef(function GameManagePopup(props, ref) {
</View>
)}
{!inTwoHours && !hasOtherParticiappants && (
{!finished && !inTwoHours && !hasOtherParticiappants && (
<View className={styles.button} onClick={handleCancelGame}>
</View>
)}
{hasJoin && (
{!finished && hasJoin && (
<View className={styles.button} onClick={handleQuitGame}>
退
</View>

View File

@@ -105,9 +105,9 @@ const HomeNavbar = (props: IProps) => {
const userInfo = useUserInfo();
// 使用用户详情接口中的 last_location 字段
// USER_SELECTED_CITY 第二个值应该是省份/直辖市,不能是区
const lastLocationProvince = (userInfo as any)?.last_location_province || "";
const lastLocationCity = (userInfo as any)?.last_location_city || "";
// 只使用省份/直辖市,不使用城市(城市可能是区)
const detectedLocation = lastLocationProvince;
const detectedLocation = lastLocationCity;
// 检查是否应该显示定位确认弹窗
const should_show_location_dialog = (): boolean => {
@@ -192,7 +192,7 @@ const HomeNavbar = (props: IProps) => {
} else if (detectedLocation) {
// 只有在完全没有缓存的情况下,才使用用户详情中的位置信息
console.log("[HomeNavbar] 没有缓存,使用用户详情中的位置信息:", detectedLocation);
const newArea: [string, string] = ["中国", detectedLocation];
const newArea: [string, string] = [(userInfo as any)?.last_location_province || "", detectedLocation];
updateArea(newArea);
// 保存定位信息到缓存
(Taro as any).setStorageSync(CITY_CACHE_KEY, newArea);
@@ -266,7 +266,7 @@ const HomeNavbar = (props: IProps) => {
const { detectedProvince } = locationDialogData;
// 用户选择"切换到",使用用户详情中的位置信息
const newArea: [string, string] = ["中国", detectedProvince];
const newArea: [string, string] = [(userInfo as any)?.last_location_province || "", detectedProvince];
updateArea(newArea);
// 更新缓存为新的定位信息
(Taro as any).setStorageSync(CITY_CACHE_KEY, newArea);
@@ -481,8 +481,7 @@ const HomeNavbar = (props: IProps) => {
{/* 搜索导航 */}
{!showTitle && (
<View
className={`inputCustomerNavbarContainer toggleElement secondElement hidden ${
showInput && "visible"
className={`inputCustomerNavbarContainer toggleElement secondElement hidden ${showInput && "visible"
} ${showInput ? "inputCustomerNavbarShowInput" : ""}`}
style={navbarStyle}
>

View File

@@ -53,7 +53,7 @@
}
.location-position {
flex: 1;
// flex: 1;
min-width: 0; // 允许缩小
white-space: nowrap;
overflow: hidden;

View File

@@ -45,7 +45,7 @@ const ListCard: React.FC<ListCardProps> = ({
className="image"
mode="aspectFill"
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")}
/>
);
};
@@ -127,10 +127,10 @@ const ListCard: React.FC<ListCardProps> = ({
return (
<View className="double-image">
<View className="image-container">
{renderItemImage(image_list?.[0])}
{renderItemImage(image_list?.[1])}
</View>
<View className="image-container">
{renderItemImage(image_list?.[1])}
{renderItemImage(image_list?.[0])}
</View>
</View>
);

View File

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

View File

@@ -8,7 +8,7 @@ import {
useLastTestResult,
} from "@/store/userStore";
// import { getCurrentFullPath } from "@/utils";
import { OSS_BASE_URL } from "@/config/api";
import { OSS_BASE } from "@/config/api";
import { StageType } from "@/services/evaluateService";
import { waitForAuthInit } from "@/utils/authInit";
import DocCopy from "@/static/ntrp/ntrp_doc_copy.svg";
@@ -148,7 +148,7 @@ function NTRPTestEntryCard(props: {
<View
className={styles.lines}
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}>
@@ -188,7 +188,7 @@ function NTRPTestEntryCard(props: {
<View
className={styles.lines}
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 File

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

View File

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

View File

@@ -67,7 +67,7 @@ const PublishMenu: React.FC<PublishMenuProps> = (props) => {
};
const handleMenuItemClick = (type: "individual" | "group" | "ai") => {
const [_, address] = area;
if (address !== '上海') {
if (address !== '上海') {
(Taro as any).showModal({
title: '提示',
content: '仅上海地区开放,您可加入社群或切换城市',

View File

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

View File

@@ -3,7 +3,7 @@ import { View, Canvas } from "@tarojs/components";
import { forwardRef, useImperativeHandle } from "react";
import shareLogoSvg from "@/static/ntrp/ntrp_share_logo.png";
import docCopyPng from "@/static/ntrp/ntrp_doc_copy.png";
import { OSS_BASE_URL } from "@/config/api";
import { OSS_BASE } from "@/config/api";
interface RadarChartV2Props {
data: [string, number][];
@@ -29,18 +29,24 @@ export interface RadarChartV2Ref {
}) => Promise<string>;
}
const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref) => {
const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>(
(props, ref) => {
const { data } = props;
const maxValue = 100;
const levels = 5;
// 在 exportCanvasV2 中绘制雷达图的函数
function drawRadarChart(ctx: CanvasRenderingContext2D, radarX: number, radarY: number, radarSize: number) {
function drawRadarChart(
ctx: CanvasRenderingContext2D,
radarX: number,
radarY: number,
radarSize: number,
) {
// 雷达图中心点位置radarSize 已经是2倍图尺寸
const center = {
x: radarX + radarSize / 2,
y: radarY + radarSize / 2
y: radarY + radarSize / 2,
};
// 计算实际半径radarSize 是直径,半径是直径的一半)
@@ -48,9 +54,9 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
// 启用抗锯齿
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.imageSmoothingQuality = "high";
ctx.lineCap = "round";
ctx.lineJoin = "round";
// 解析数据
const { texts, vals } = data.reduce(
@@ -61,7 +67,7 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
vals: [...res.vals, val],
};
},
{ texts: [], vals: [] }
{ texts: [], vals: [] },
);
// === 绘制圆形网格 ===
@@ -102,26 +108,15 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
ctx.lineWidth = 1 * (radarSize / 200); // 根据2倍图调整线宽
ctx.stroke();
// 标签 - 文字显示在圆圈外面
const offset = 10 * (radarSize / 200); // 文字距离圆圈的偏移量2倍图
const textX = center.x + (radius + offset) * Math.cos(angle);
const textY = center.y + (radius + offset) * Math.sin(angle);
// 标签:沿轴线外侧延伸,文字中心对齐轴线端点(与 index.tsx 一致)
const labelOffset = 28 * (radarSize / 200); // 文字距离圆圈的偏移量2倍图
const textX = center.x + (radius + labelOffset) * Math.cos(angle);
const textY = center.y + (radius + labelOffset) * Math.sin(angle);
ctx.font = `${12 * (radarSize / 200)}px sans-serif`; // 根据2倍图调整字体大小
ctx.fillStyle = "#333";
ctx.textBaseline = "middle";
// 调整文字对齐方式
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);
});
@@ -159,25 +154,39 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
}
// 获取图片信息(宽高)
function getImageInfo(src: string): Promise<{ width: number; height: number }> {
function getImageInfo(
src: string,
): Promise<{ width: number; height: number }> {
return new Promise((resolve, reject) => {
(Taro as any).getImageInfo({
src,
success: (res: any) => resolve({ width: res.width, height: res.height }),
success: (res: any) =>
resolve({ width: res.width, height: res.height }),
fail: reject,
});
});
}
// 绘制圆角矩形
function roundRect(ctx: any, x: number, y: number, width: number, height: number, radius: number) {
function roundRect(
ctx: any,
x: number,
y: number,
width: number,
height: number,
radius: number,
) {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
ctx.lineTo(x + width, y + height - radius);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
ctx.quadraticCurveTo(
x + width,
y + height,
x + width - radius,
y + height,
);
ctx.lineTo(x + radius, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
ctx.lineTo(x, y + radius);
@@ -198,8 +207,7 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
useImperativeHandle(ref, () => ({
// 生成原始雷达图(已废弃,现在直接在 exportCanvasV2 中绘制)
generateImage: () =>
Promise.resolve(""),
generateImage: () => Promise.resolve(""),
// 生成完整图片(包含标题、雷达图、底部文字和二维码)
generateFullImage: async (options: {
@@ -240,11 +248,11 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
// 启用抗锯齿
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.imageSmoothingQuality = "high";
// 绘制背景 - 使用 share_bg.png 背景图,撑满整个画布(从 OSS 动态加载)
try {
const shareBgUrl = `${OSS_BASE_URL}/images/share_bg.png`;
const shareBgUrl = `${OSS_BASE}/front/ball/images/share_bg.png`;
const bgImg = await loadImage(canvas, shareBgUrl);
ctx.drawImage(bgImg, 0, 0, width, height);
} catch (error) {
@@ -264,18 +272,28 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
if (options.avatarUrl) {
try {
const avatarSize = 43.46 * scale; // 设计稿头像尺寸
const avatarImg = await loadImage(canvas, options.avatarUrl);
const avatarImg = await loadImage(
canvas,
options.avatarUrl,
);
const avatarInfo = await getImageInfo(options.avatarUrl);
// 头像区域总宽度(头像 + 装饰图片重叠部分)
const avatarWrapWidth = 84.7 * scale; // 设计稿 Frame 1912055063 宽度
const avatarX = sidePadding + (294 * scale - avatarWrapWidth) / 2; // 294 是 Frame 1912055062 宽度,居中
const avatarX =
sidePadding + (294 * scale - avatarWrapWidth) / 2; // 294 是 Frame 1912055062 宽度,居中
const avatarY = currentY;
// 绘制头像圆形背景
ctx.save();
ctx.beginPath();
ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2);
ctx.arc(
avatarX + avatarSize / 2,
avatarY + avatarSize / 2,
avatarSize / 2,
0,
Math.PI * 2,
);
ctx.fillStyle = "#FFFFFF";
ctx.fill();
ctx.strokeStyle = "#EFEFEF";
@@ -284,7 +302,8 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
// 计算头像绘制尺寸,保持宽高比
const innerSize = avatarSize - 1.94 * scale; // 内部可用尺寸
const avatarAspectRatio = avatarInfo.width / avatarInfo.height;
const avatarAspectRatio =
avatarInfo.width / avatarInfo.height;
let drawWidth = innerSize;
let drawHeight = innerSize;
let drawX = avatarX + 0.97 * scale;
@@ -303,9 +322,21 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
// 绘制头像(圆形裁剪)
ctx.beginPath();
ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2 - 0.97 * scale, 0, Math.PI * 2);
ctx.arc(
avatarX + avatarSize / 2,
avatarY + avatarSize / 2,
avatarSize / 2 - 0.97 * scale,
0,
Math.PI * 2,
);
ctx.clip();
ctx.drawImage(avatarImg, drawX, drawY, drawWidth, drawHeight);
ctx.drawImage(
avatarImg,
drawX,
drawY,
drawWidth,
drawHeight,
);
ctx.restore();
// 绘制装饰图片DocCopy- 在头像右侧
@@ -328,7 +359,14 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
const borderRadius = 9.66 * scale; // 设计稿圆角
ctx.fillStyle = "#FFFFFF";
ctx.beginPath();
roundRect(ctx, -addonSize / 2, -addonSize / 2, addonSize, addonSize, borderRadius);
roundRect(
ctx,
-addonSize / 2,
-addonSize / 2,
addonSize,
addonSize,
borderRadius,
);
ctx.fill();
// 添加渐变背景色
@@ -345,7 +383,13 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
const docSize = 26.18 * scale; // 设计稿内部图片尺寸
const docRotation = -7 * (Math.PI / 180); // 内部旋转 -7 度
ctx.rotate(docRotation);
ctx.drawImage(docCopyImg, -docSize / 2, -docSize / 2, docSize, docSize);
ctx.drawImage(
docCopyImg,
-docSize / 2,
-docSize / 2,
docSize,
docSize,
);
ctx.restore();
} catch (error) {
console.error("Failed to load docCopy image:", error);
@@ -420,7 +464,9 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
const qrX = 276 * scale; // 设计稿二维码 x 位置
const qrY = 523 * scale; // 设计稿二维码 y 位置
const bottomTextContent = options.bottomText || "长按识别二维码,快来加入,有你就有场!";
const bottomTextContent =
options.bottomText ||
"长按识别二维码,快来加入,有你就有场!";
// 绘制底部文字 - 设计稿fontSize: 12, fontWeight: 400, line-height: 1.52倍图
ctx.fillStyle = "rgba(0, 0, 0, 0.45)";
@@ -469,7 +515,13 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
const iconImg = await loadImage(canvas, shareLogoSvg);
// 图标位置:文字顶部上方 iconSize + gap
const iconY = textY - iconSize - iconGap;
ctx.drawImage(iconImg, topTitleX, iconY, 235 * scale, iconSize);
ctx.drawImage(
iconImg,
topTitleX,
iconY,
235 * scale,
iconSize,
);
} catch (error) {
console.error("Failed to load icon:", error);
}
@@ -479,7 +531,6 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
ctx.fillText(lineText, textX, textY + index * lineHeight);
});
// 绘制二维码 - 设计稿位置(带白色背景、边框、阴影和圆角)
if (options.qrCodeUrl) {
@@ -528,10 +579,23 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
// 绘制二维码图片(在圆角矩形内)
ctx.save();
// 创建圆角裁剪区域
roundRect(ctx, qrInnerX, qrInnerY, qrInnerSize, qrInnerSize, borderRadius - borderWidth);
roundRect(
ctx,
qrInnerX,
qrInnerY,
qrInnerSize,
qrInnerSize,
borderRadius - borderWidth,
);
ctx.clip();
// 绘制二维码图片
ctx.drawImage(qrImg, qrInnerX, qrInnerY, qrInnerSize, qrInnerSize);
ctx.drawImage(
qrImg,
qrInnerX,
qrInnerY,
qrInnerSize,
qrInnerSize,
);
ctx.restore();
// 恢复上下文状态
@@ -544,8 +608,8 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
// 导出图片
Taro.canvasToTempFilePath({
canvas,
fileType: 'png',
quality: 1,
fileType: "png",
quality: 0.7,
success: (res) => {
resolve(res.tempFilePath);
},
@@ -567,13 +631,19 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
<Canvas
type="2d"
id="exportCanvasV2"
style={{ position: "fixed", top: "-9999px", left: "-9999px", width: "700px", height: "1200px" }}
style={{
position: "fixed",
top: "-9999px",
left: "-9999px",
width: "700px",
height: "1200px",
}}
/>
</View>
);
});
},
);
RadarChartV2.displayName = "RadarChartV2";
export default RadarChartV2;

View File

@@ -45,7 +45,7 @@ const SearchBarComponent = (props: IProps) => {
</View>
}
className={styles.searchBar}
placeholder="搜索上海的球局和场地"
placeholder="搜索球局和场地"
onChange={handleChange}
value={value}
onInputClick={onInputClick}

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react'
import { View, Canvas } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { OSS_BASE_URL } from "@/config/api";
import { OSS_BASE } from "@/config/api";
// 分享卡片数据接口
export interface ShareCardData {
@@ -506,7 +506,7 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
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)
// 绘制"单打"标签
@@ -542,7 +542,7 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
const dateX = danDaX
const timeInfoY = infoStartY + infoSpacing
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)
// 绘制日期(绿色)
@@ -556,7 +556,7 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
// 绘制地点
const locationInfoY = infoStartY + infoSpacing * 2
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)
drawBoldText(ctx, data.venueName, danDaX, locationInfoY + 10, locationFontSize, '#000000')

View File

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

View File

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

View File

@@ -1,7 +1,10 @@
import envConfig from './env'// API配置
// OSS 基础路径配置
export const OSS_BASE_URL = 'https://youchang2026.oss-cn-shanghai.aliyuncs.com/front/ball'
// OSS 配置:仅域名,调用处拼接 /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 = {
// 基础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

@@ -70,4 +70,5 @@ export default {
ICON_LIST_NTPR: require('@/static/list/ntpr.svg'),
ICON_LIST_CHANGDA: require('@/static/list/icon-changda.svg'),
ICON_LIST_CHANGDA_QIuju: require('@/static/list/changdaqiuju.png'),
ICON_RELOCATE: require('@/static/list/icon-relocate.svg'),
}

View File

@@ -10,6 +10,7 @@ import { EvaluateScene } from "@/store/evaluateStore";
import { waitForAuthInit } from "@/utils/authInit";
import "./index.scss";
import { useRef, useEffect, useState, useMemo } from "react";
import { useDictionaryStore } from "@/store/dictionaryStore";
const ListContainer = (props) => {
const {
@@ -28,6 +29,7 @@ const ListContainer = (props) => {
collapse = false,
defaultShowNum,
evaluateFlag,
enableHomeCards = false, // 仅首页需要 banner 和 NTRP 测评卡片
listLoadErrorWrapperHeight,
listLoadErrorWidth,
listLoadErrorHeight,
@@ -44,7 +46,7 @@ const ListContainer = (props) => {
const { fetchUserInfo, fetchLastTestResult } = useUserActions();
// 使用全局状态中的测试结果,避免重复调用接口
const lastTestResult = useLastTestResult();
const { bannerListImage, bannerDetailImage, bannerListIndex = 0 } = useDictionaryStore((s) => s.bannerDict) || {};
useReachBottom(() => {
// 加载更多方法
if (loading) {
@@ -93,10 +95,10 @@ const ListContainer = (props) => {
};
}, []);
// 获取测试结果,判断最近一个月是否有测试记录
// 获取测试结果,判断最近一个月是否有测试记录(仅首页需要)
useEffect(() => {
const init = async () => {
if (!evaluateFlag) return;
if (!evaluateFlag || !enableHomeCards) return;
// 先等待静默登录完成
await waitForAuthInit();
// 然后再获取用户信息
@@ -111,7 +113,7 @@ const ListContainer = (props) => {
}
};
init();
}, [evaluateFlag, userInfo, lastTestResult, fetchLastTestResult]);
}, [evaluateFlag, enableHomeCards, userInfo, lastTestResult, fetchLastTestResult]);
// 从全局状态中获取测试状态
const hasTestInLastMonth = lastTestResult?.has_test_in_last_month || false;
@@ -130,23 +132,33 @@ const ListContainer = (props) => {
);
};
// 对于没有ntrp等级的用户每个月展示一次, 插在第二个位置后面
function insertEvaluateCard(list) {
if (!evaluateFlag)
return showNumber !== undefined ? list.slice(0, showNumber) : list;
if (!list || list.length === 0) {
return list;
}
// 如果最近一个月有测试记录,则不插入 card
if (hasTestInLastMonth) {
return showNumber !== undefined ? list.slice(0, showNumber) : list;
// 插入 banner 卡片
function insertBannerCard(list) {
if (!bannerListImage) return list;
if (!list || !Array.isArray(list)) return list ?? [];
return [
...list.slice(0, Number(bannerListIndex)),
{ type: "banner", banner_image_url: bannerListImage, banner_detail_url: bannerDetailImage },
...list.slice(Number(bannerListIndex))
];
}
if (list.length <= 2) {
return [...list, { type: "evaluateCard" }];
}
// 对于没有ntrp等级的用户每个月展示一次, 插在第二个位置后面
// insertBannerCard 需在最后统一执行,否则前面分支直接 return 时 banner 不会被插入
function insertEvaluateCard(list) {
let result: any[];
if (!evaluateFlag) {
result = showNumber !== undefined ? list.slice(0, showNumber) : list;
} else if (!list || list.length === 0) {
result = list;
} else if (hasTestInLastMonth) {
result = showNumber !== undefined ? list.slice(0, showNumber) : list;
} else if (list.length <= 2) {
result = [...list, { type: "evaluateCard" }];
} else {
const [item1, item2, ...rest] = list;
return [
result = [
item1,
item2,
{ type: "evaluateCard" },
@@ -154,27 +166,22 @@ const ListContainer = (props) => {
];
}
return insertBannerCard(result);
}
const memoizedList = useMemo(
() => insertEvaluateCard(data),
[evaluateFlag, data, hasTestInLastMonth, showNumber]
() => (enableHomeCards ? insertEvaluateCard(data) : data),
[enableHomeCards, evaluateFlag, data, hasTestInLastMonth, showNumber, bannerListImage, bannerDetailImage, bannerListIndex]
);
// 渲染 banner 卡片
const renderBanner = (item, index) => {
if (!item?.banner_image_url) return null;
if (!item?.banner_image_url) {
return null;
}
return (
<View
key={item.id || `banner-${index}`}
style={{
maxHeight: "122px",
overflow: "hidden",
borderRadius: "12px",
}}
>
<Image
src={item.banner_image_url}
mode="widthFix"
style={{ width: "100%", display: "block", maxHeight: "122px" }}
onClick={() => {
const target = item.banner_detail_url;
if (target) {
@@ -183,7 +190,16 @@ const ListContainer = (props) => {
});
}
}}
/>
style={{
height: "100px",
overflow: "hidden",
borderRadius: "12px",
backgroundImage: `url(${item.banner_image_url})`,
backgroundSize: "cover",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
}}
>
</View>
);
};
@@ -211,12 +227,12 @@ const ListContainer = (props) => {
return (
<>
{memoizedList.map((match, index) => {
if (match.type === "banner") {
if (enableHomeCards && match?.type === "banner") {
return renderBanner(match, index);
}
if (match.type === "evaluateCard") {
if (enableHomeCards && match?.type === "evaluateCard") {
return (
<NTRPTestEntryCard key="evaluate" type={EvaluateScene.list} />
<NTRPTestEntryCard key={`evaluate-${index}`} type={EvaluateScene.list} />
);
}
return <ListCard key={match?.id || index} {...match} />;

View File

@@ -40,7 +40,7 @@ function isFull(counts) {
function matchNtrpRequestment(
target?: string,
min?: string,
max?: string
max?: string,
): boolean {
// 目标值为空或 undefined
if (!target?.trim()) return true;
@@ -110,7 +110,7 @@ export default function Participants(props) {
user_action_status;
const showApplicationEntry =
[can_pay, can_substitute, is_substituting, waiting_start].every(
(item) => !item
(item) => !item,
) &&
can_join &&
dayjs(start_time).isAfter(dayjs());
@@ -138,7 +138,7 @@ export default function Participants(props) {
Taro.navigateTo({
url: `/login_pages/index/index?redirect=${encodeURIComponent(
fullPath
fullPath,
)}`,
});
}
@@ -153,7 +153,7 @@ export default function Participants(props) {
const matchNtrpReq = matchNtrpRequestment(
userInfo?.ntrp_level,
skill_level_min,
skill_level_max
skill_level_max,
);
function handleSelfEvaluate() {
@@ -180,7 +180,7 @@ export default function Participants(props) {
}
function generateTextAndAction(
user_action_status: null | { [key: string]: boolean }
user_action_status: null | { [key: string]: boolean },
):
| undefined
| { text: string | React.FC; action?: () => void; available?: boolean } {
@@ -259,7 +259,7 @@ export default function Participants(props) {
const res = await OrderService.getUnpaidOrder(id);
if (res.code === 0) {
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 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 =
[can_pay, can_join, is_substituting, waiting_start].every(
(item) => !item
(item) => !item,
) &&
can_substitute &&
dayjs(start_time).isAfter(dayjs());
@@ -336,7 +337,7 @@ export default function Participants(props) {
refresherBackground="#FAFAFA"
className={classnames(
styles["participants-list-scroll"],
showApplicationEntry ? styles.withApplication : ""
showApplicationEntry ? styles.withApplication : "",
)}
scrollX
>
@@ -377,14 +378,14 @@ export default function Participants(props) {
src={avatar_url}
onClick={handleViewUserInfo.bind(
null,
participant_user_id
participant_user_id,
)}
/>
<Text className={styles["participants-list-item-name"]}>
{nickname || "未知"}
</Text>
<Text className={styles["participants-list-item-level"]}>
{displayNtrp}
NTRP {displayNtrp}
</Text>
<Text className={styles["participants-list-item-role"]}>
{role}
@@ -400,12 +401,17 @@ export default function Participants(props) {
)}
</View>
{/* 候补区域 */}
{max_substitute_players > 0 && (substitute_count > 0 || showSubstituteApplicationEntry) && (
{max_substitute_players > 0 &&
(substitute_count > 0 || showSubstituteApplicationEntry) && (
<View className={styles["detail-page-content-participants"]}>
<View className={styles["participants-title"]}>
<Text></Text>
<Text>·</Text>
<Text>{leftSubstituteCount > 0 ? `剩余空位 ${leftSubstituteCount}` : "已满员"}</Text>
<Text>
{leftSubstituteCount > 0
? `剩余空位 ${leftSubstituteCount}`
: "已满员"}
</Text>
</View>
<View className={styles["participants-list"]}>
{/* 候补申请入口 */}
@@ -420,7 +426,9 @@ export default function Participants(props) {
className={styles["participants-list-application-icon"]}
src={img.ICON_DETAIL_APPLICATION_ADD}
/>
<Text className={styles["participants-list-application-text"]}>
<Text
className={styles["participants-list-application-text"]}
>
</Text>
</View>
@@ -430,7 +438,7 @@ export default function Participants(props) {
refresherBackground="#FAFAFA"
className={classnames(
styles["participants-list-scroll"],
showSubstituteApplicationEntry ? styles.withApplication : ""
showSubstituteApplicationEntry ? styles.withApplication : "",
)}
scrollX
>
@@ -438,7 +446,8 @@ export default function Participants(props) {
className={styles["participants-list-scroll-content"]}
style={{
width: `${
Math.max(substitute_members.length, 1) * 103 + (Math.max(substitute_members.length, 1) - 1) * 8
Math.max(substitute_members.length, 1) * 103 +
(Math.max(substitute_members.length, 1) - 1) * 8
}px`,
}}
>
@@ -471,13 +480,15 @@ export default function Participants(props) {
src={avatar_url}
onClick={handleViewUserInfo.bind(
null,
substitute_user_id
substitute_user_id,
)}
/>
<Text className={styles["participants-list-item-name"]}>
{nickname || "未知"}
</Text>
<Text className={styles["participants-list-item-level"]}>
<Text
className={styles["participants-list-item-level"]}
>
{displayNtrp}
</Text>
<Text className={styles["participants-list-item-role"]}>

View File

@@ -15,7 +15,7 @@ import CrossIcon from "@/static/detail/cross.svg";
import { genNTRPRequirementText, navto } from "@/utils/helper";
import { waitForAuthInit } from "@/utils/authInit";
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 { DayOfWeekMap } from "../../config";
import styles from "./index.module.scss";
@@ -60,8 +60,10 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
show: async (publish_flag = false) => {
setPublishFlag(publish_flag);
if (publish_flag) {
try {
const url = await generateShareImageUrl();
setShareImageUrl(url);
} catch (e) {}
}
setVisible(true);
},
@@ -81,13 +83,14 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
const endTime = dayjs(end_time);
const dayofWeek = DayOfWeekMap.get(startTime.day());
const gameLength = `${endTime.diff(startTime, "hour")}小时`;
console.log(userInfo, "userInfo");
const url = await generateShareImage({
userAvatar: userInfo.avatar_url,
userNickname: userInfo.nickname,
gameType: play_type,
skillLevel: `NTRP ${genNTRPRequirementText(
skill_level_min,
skill_level_max
skill_level_max,
)}`,
gameDate: `${startTime.format("M月D日")} (${dayofWeek})`,
gameTime: `${startTime.format("ah")}${gameLength}`,
@@ -128,22 +131,24 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
const endTime = dayjs(end_time);
const dayofWeek = DayOfWeekMap.get(startTime.day());
const gameLength = `${endTime.diff(startTime, "hour")}小时`;
Taro.showLoading({ title: "生成中..." });
// Taro.showLoading({ title: "生成中..." });
const qrCodeUrlRes = await DetailService.getQrCodeUrl({
page: "game_pages/detail/index",
scene: `id=${id}`,
});
const qrCodeUrl = await base64ToTempFilePath(
qrCodeUrlRes.data.qr_code_base64
);
// const qrCodeUrl = await base64ToTempFilePath(
// qrCodeUrlRes.data.qr_code_base64
// );
const qrCodeUrl = qrCodeUrlRes.data.ossPath;
await delay(100);
// Taro.showLoading({ title: "生成中..." });
const url = await generatePosterImage({
playType: play_type,
ntrp: `NTRP ${genNTRPRequirementText(skill_level_min, skill_level_max)}`,
mainCoursal:
image_list[0] && image_list[0].startsWith("http")
? 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,
avatarUrl: avatar_url,
title,
@@ -152,7 +157,7 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
time: `${startTime.format("ah")}${gameLength}`,
qrCodeUrl,
});
Taro.hideLoading();
// Taro.hideLoading();
Taro.showShareImageMenu({
path: url,
});
@@ -164,6 +169,18 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
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() {
setVisible(false);
setPublishFlag(false);
@@ -193,14 +210,14 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
<View
className={styles.contentContainer}
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
catchMove
className={classnames(
styles.title,
publishFlag ? styles.publishTitle : ""
publishFlag ? styles.publishTitle : "",
)}
>
{publishFlag ? (
@@ -254,7 +271,7 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
</View>
</View>
<View className={styles.customBtnWrapper}>
<Button className={styles.button}>
<Button className={styles.button} onClick={handleCopyLink}>
<View className={styles.icon}>
<Image className={styles.linkIcon} src={LinkIcon} />
</View>

View File

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

View File

@@ -131,7 +131,7 @@ const ListSearch = () => {
<View className="topSearch">
<Image className="searchIcon" src={img.ICON_LIST_SEARCH_SEARCH} />
<Input
placeholder="搜索上海的球局和场地"
placeholder="搜索球局和场地"
value={searchValue}
defaultValue={searchValue}
onChange={handleChange}

View File

@@ -5,7 +5,7 @@ import Taro, { useRouter } from "@tarojs/taro";
import classnames from "classnames";
import dayjs from "dayjs";
import "dayjs/locale/zh-cn";
import { generatePosterImage, base64ToTempFilePath, delay } from "@/utils";
import { generatePosterImage, delay } from "@/utils";
import { withAuth } from "@/components";
import GeneralNavbar from "@/components/GeneralNavbar";
import DetailService from "@/services/detailService";
@@ -16,7 +16,7 @@ import { useUserActions } from "@/store/userStore";
import { DayOfWeekMap } from "../detail/config";
import { genNTRPRequirementText } from "@/utils/helper";
import { waitForAuthInit } from "@/utils/authInit";
import { OSS_BASE_URL } from "@/config/api";
import { OSS_BASE } from "@/config/api";
import styles from "./index.module.scss";
dayjs.locale("zh-cn");
@@ -59,9 +59,11 @@ function SharePoster(props) {
page: "game_pages/detail/index",
scene: `id=${id}`,
});
const qrCodeUrl = await base64ToTempFilePath(
qrCodeUrlRes.data.qr_code_base64
);
const qrCodeUrl = qrCodeUrlRes.data.ossPath;
// const qrCodeUrl = await base64ToTempFilePath(
// qrCodeUrlRes.data.qr_code_base64
// );
// debugger
await delay(100);
const url = await generatePosterImage({
playType: play_type,
@@ -69,7 +71,7 @@ function SharePoster(props) {
mainCoursal:
image_list[0] && image_list[0].startsWith("http")
? 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,
avatarUrl: avatar_url,
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 {
position: absolute;

View File

@@ -155,6 +155,11 @@ const LoginPage: React.FC = () => {
e.stopPropagation();
};
// 返回首页
const handle_return_home = () => {
Taro.navigateTo({ url: "/main_pages/index" });
};
return (
<View className="login_page">
<View className="background_image">
@@ -193,7 +198,7 @@ const LoginPage: React.FC = () => {
/>
</View>
<Text className="button_text">
{is_loading ? "登录中..." : "微信授权登录"}
{is_loading ? "登录中..." : "一键登录"}
</Text>
</Button>
@@ -208,9 +213,13 @@ const LoginPage: React.FC = () => {
src={require("@/static/login/phone_icon.svg")}
/>
</View>
<Text className="button_text"></Text>
<Text className="button_text"></Text>
</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="checkbox_container" onClick={handle_toggle_terms}>
@@ -224,13 +233,13 @@ const LoginPage: React.FC = () => {
className="terms_link"
onClick={() => handle_view_terms("terms")}
>
</Text>
<Text
className="terms_link"
onClick={() => handle_view_terms("binding")}
>
</Text>
<Text
className="terms_link"
@@ -259,13 +268,13 @@ const LoginPage: React.FC = () => {
className="terms_item"
onClick={() => handle_view_terms("terms")}
>
</Text>
<Text
className="terms_item"
onClick={() => handle_view_terms("binding")}
>
</Text>
<Text
className="terms_item"

View File

@@ -1,8 +1,8 @@
# 条款页面 - 场的条款和条件
# 条款页面 - 场的条款和条件
## 功能概述
条款页面展示完整的《场的条款和条件》内容,用户需要仔细阅读并同意后才能继续使用平台服务。
条款页面展示完整的《场的条款和条件》内容,用户需要仔细阅读并同意后才能继续使用平台服务。
## 🎨 设计特点
@@ -54,7 +54,7 @@ TermsPage
## 📋 条款内容
本页面包含完整的《场的条款和条件》,涵盖以下十个主要部分:
本页面包含完整的《场的条款和条件》,涵盖以下十个主要部分:
### 1. 服务内容
- 活动发布、报名、聊天室沟通、活动提醒等服务

View File

@@ -7,7 +7,7 @@ const TermsPage: React.FC = () => {
// 获取页面参数
const [termsType, setTermsType] = React.useState('terms');
const [pageTitle, setPageTitle] = React.useState('条款和条件');
const [termsTitle, setTermsTitle] = React.useState('《场的条款和条件》');
const [termsTitle, setTermsTitle] = React.useState('《场的条款和条件》');
const [termsContent, setTermsContent] = React.useState('');
// 返回上一页
@@ -23,7 +23,7 @@ const TermsPage: React.FC = () => {
switch (type) {
case 'terms':
setPageTitle('条款和条件');
setTermsTitle('《场的条款和条件》');
setTermsTitle('《场的条款和条件》');
setTermsContent(`<span class="terms_first_line">欢迎使用本平台(以下简称"本平台")发布与参与网球活动。为保障您的权益,请您务必仔细阅读并理解以下服务条款。</span>
一、服务内容
@@ -69,7 +69,7 @@ const TermsPage: React.FC = () => {
break;
case 'binding':
setPageTitle('微信号绑定协议');
setTermsTitle('《场与微信号绑定协议》');
setTermsTitle('《场与微信号绑定协议》');
setTermsContent(`<span class="terms_first_line">欢迎使用本平台(以下简称"本平台")的微信绑定服务。为保障您的权益,请您务必仔细阅读并理解以下协议内容。</span>
一、绑定服务说明
@@ -171,7 +171,7 @@ const TermsPage: React.FC = () => {
break;
default:
setPageTitle('条款和条件');
setTermsTitle('《场的条款和条件》');
setTermsTitle('《场的条款和条件》');
setTermsContent('条款内容加载中...');
}
}, []);

View File

@@ -64,7 +64,7 @@ VerificationPage
- **页面跳转**:登录成功后跳转到首页
### 协议支持
- **条款链接**:《场的条款和条件》
- **条款链接**:《场的条款和条件》
- **隐私政策**:《隐私权政策》
- **动态跳转**:支持通过 URL 参数指定协议类型

View File

@@ -66,6 +66,8 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
gamesNum, // 新增:获取球局数量
} = store;
const supportedCitiesList = useDictionaryStore((s) => s.getDictionaryValue('supported_cities', ['上海市'])) || [];
const {
isShowFilterPopup,
data: matches,
@@ -92,6 +94,8 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
// 记录上一次加载数据时的城市,用于检测城市变化
const lastLoadedAreaRef = useRef<[string, string] | null>(null);
const prevIsActiveRef = useRef(isActive);
// 记录是否是进入列表页的第一次调用 updateUserLocation首次传 force: true
const hasUpdatedLocationRef = useRef(false);
// 处理距离筛选显示/隐藏
const handleDistanceFilterVisibleChange = useCallback(
@@ -289,9 +293,9 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
currentProvince,
});
// 地址发生变化或不一致,重新加载数据和球局数量
// 先调用列表接口,然后在列表接口完成后调用数量接口
(async () => {
// 延迟刷新,等 tab 切换动画完成后再加载,避免切换时列表重渲染导致抖动
const delayMs = 280;
const timer = setTimeout(async () => {
try {
if (refreshBothLists) {
await refreshBothLists();
@@ -307,7 +311,9 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
} catch (error) {
console.error("重新加载数据失败:", error);
}
})();
}, delayMs);
prevIsActiveRef.current = isActive;
return () => clearTimeout(timer);
}
}
@@ -364,7 +370,10 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
updateState({ location });
if (location && location.latitude && location.longitude) {
try {
await updateUserLocation(location.latitude, location.longitude);
// 进入列表页的第一次调用传 force: true后续调用传 false
const isFirstCall = !hasUpdatedLocationRef.current;
await updateUserLocation(location.latitude, location.longitude, isFirstCall);
hasUpdatedLocationRef.current = true;
} catch (error) {
console.error("更新用户位置失败:", error);
}
@@ -446,6 +455,17 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
});
};
// 处理重新定位
const handleRelocate = async (location) => {
try {
// 位置已更新到后端,刷新列表数据
await getMatchesData();
await fetchGetGamesCount();
} catch (error) {
console.error("刷新列表失败:", error);
}
};
const handleSearchClick = () => {
navigateTo({
url: "/game_pages/search/index",
@@ -465,7 +485,7 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
initDictionaryData();
}, []);
// 获取省份名称area 格式: ["中国", "省份"]
const province = area?.at(1) || "上海";
function renderCityQrcode() {
@@ -507,8 +527,12 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
}
// 判定是否显示"暂无球局"页面
// 条件:省份不是上海 或 (已加载完成且球局数量为0)
const shouldShowNoGames = province !== "上海";
// 从配置接口 /parameter/many_key 获取 supported_cities格式如 "上海市||北京市"
// 当前省份在有球局城市列表中则显示列表,否则显示暂无球局
const shouldShowNoGames =
supportedCitiesList.length > 0
? !supportedCitiesList.includes(province)
: province !== "上海市"; // 配置未加载时默认按上海判断
return (
<>
@@ -559,6 +583,7 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
quickValue={distanceQuickFilter?.order}
districtValue={distanceQuickFilter?.district}
onMenuVisibleChange={handleDistanceFilterVisibleChange}
onRelocate={handleRelocate}
/>
</View>
</View>
@@ -602,6 +627,7 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
reload={refreshMatches}
loadMoreMatches={loadMoreMatches}
evaluateFlag
enableHomeCards
/>
</ScrollView>
</View>

View File

@@ -37,6 +37,7 @@ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({ isActive = true }
const [hasLoaded, setHasLoaded] = useState(false); // 记录是否已经加载过数据
const [collapseProfile, setCollapseProfile] = useState(false);
const [refreshing, setRefreshing] = useState(false);
useEffect(() => {
pickerOption.getCities();
@@ -169,6 +170,23 @@ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({ isActive = true }
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 scrollData = event.detail;
// setCollapseProfile(scrollData.scrollTop > 1);
@@ -178,6 +196,9 @@ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({ isActive = true }
<ScrollView
scrollY
refresherBackground="#FAFAFA"
refresherEnabled
refresherTriggered={refreshing}
onRefresherRefresh={handle_refresh}
className={styles.myselfPage}
>
<View

View File

@@ -21,21 +21,17 @@
top: 0;
left: 0;
opacity: 0;
transform: scale(0.98);
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);
transition: opacity 0.25s ease-out;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
pointer-events: none;
will-change: opacity, transform;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
visibility: hidden;
&.active {
opacity: 1;
transform: scale(1);
z-index: 1;
pointer-events: auto;
visibility: visible;
}
}

View File

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

View File

@@ -26,7 +26,7 @@ import { useGlobalStore } from "@/store/global";
import { useOrder } from "@/store/orderStore";
import detailService, { GameData } from "@/services/detailService";
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 CustomerIcon from "@/static/order/customer.svg";
import { handleCustomerService } from "@/services/userService";
@@ -301,7 +301,7 @@ function GameInfo(props) {
<View className={styles.locationMessageIcon}>
<Image
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>
{/* location message */}

View File

@@ -69,6 +69,7 @@ function generateTimeMsg(game_info) {
const OrderList = () => {
const [list, setList] = useState<any[][]>([]);
const [total, setTotal] = useState(0);
const [refreshing, setRefreshing] = useState(false);
const refundRef = useRef(null);
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) {
// 检查登录状态和手机号
if (!requireLoginWithPhone()) {
@@ -285,6 +302,10 @@ const OrderList = () => {
scrollWithAnimation
lowerThreshold={20}
onScrollToLower={handleFetchNext}
refresherBackground="#FAFAFA"
refresherEnabled
refresherTriggered={refreshing}
onRefresherRefresh={handle_refresh}
enhanced
showScrollbar={false}
className={styles.list}

View File

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

View File

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

View File

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

View File

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

View File

@@ -193,7 +193,7 @@ const NewFollow = () => {
<View className="follow-left" onClick={() => handleUserClick(item.user_id)}>
<Image
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 { formatNtrpDisplay } from "@/utils/helper";
import { waitForAuthInit } from "@/utils/authInit";
import httpService from "@/services/httpService";
// import httpService from "@/services/httpService";
import DetailService from "@/services/detailService";
import { base64ToTempFilePath } from "@/utils/genPoster";
import { OSS_BASE_URL } from "@/config/api";
import { OSS_BASE } from "@/config/api";
import CloseIcon from "@/static/ntrp/ntrp_close_icon.svg";
import DocCopy from "@/static/ntrp/ntrp_doc_copy.svg";
import ArrowRight from "@/static/ntrp/ntrp_arrow_right.svg";
@@ -38,7 +37,7 @@ const sourceTypeToTextMap = new Map([
function adjustRadarLabels(
source: [string, number][],
topK: number = 4 // 默认挑前4个最长的标签保护
topK: number = 4, // 默认挑前4个最长的标签保护
): [string, number][] {
if (source.length === 0) return source;
@@ -226,7 +225,7 @@ function Intro() {
<View
className={styles.introContainer}
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 />
@@ -253,7 +252,7 @@ function Intro() {
<View className={styles.tip}>
<Image
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"
/>
</View>
@@ -311,7 +310,7 @@ function Intro() {
<View className={styles.tip}>
<Image
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"
/>
</View>
@@ -319,7 +318,7 @@ function Intro() {
<View className={styles.radar}>
<Image
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"
/>
</View>
@@ -381,7 +380,7 @@ function Test() {
prev.map((item, pIndex) => ({
...item,
...(pIndex === index ? { choosen: i } : {}),
}))
})),
);
}
@@ -428,7 +427,7 @@ function Test() {
<View
className={styles.testContainer}
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}`} />
@@ -523,13 +522,14 @@ function Result() {
page: "other_pages/ntrp-evaluate/index",
scene: `stage=${StageType.INTRO}`,
});
if (qrCodeUrlRes.code === 0 && qrCodeUrlRes.data?.qr_code_base64) {
// 将 base64 转换为临时文件路径
const tempFilePath = await base64ToTempFilePath(
qrCodeUrlRes.data.qr_code_base64
);
setQrCodeUrl(tempFilePath);
}
setQrCodeUrl(qrCodeUrlRes.data.ossPath);
// if (qrCodeUrlRes.code === 0 && qrCodeUrlRes.data?.qr_code_base64) {
// // 将 base64 转换为临时文件路径
// const tempFilePath = await base64ToTempFilePath(
// qrCodeUrlRes.data.qr_code_base64
// );
// setQrCodeUrl(tempFilePath);
// }
} catch (error) {
console.error("获取二维码失败:", error);
}
@@ -539,18 +539,25 @@ function Result() {
const res = await evaluateService.getTestResult({ record_id: Number(id) });
if (res.code === 0) {
setResult(res.data);
// delay(1000);
setRadarData(
adjustRadarLabels(
Object.entries(res.data.radar_data.abilities).map(([key, value]) => [
const sortOrder = res.data.sort || [];
const abilities = res.data.radar_data.abilities;
const sortedKeys = sortOrder.filter((k) => k in abilities);
const remainingKeys = Object.keys(abilities).filter(
(k) => !sortOrder.includes(k),
);
const allKeys = [...sortedKeys, ...remainingKeys];
let radarData: [string, number][] = allKeys.map((key) => [
key,
Math.min(
100,
Math.floor((value.current_score / value.max_score) * 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);
}
}
@@ -588,7 +595,7 @@ function Result() {
if (!userInfo?.phone) {
Taro.redirectTo({
url: `/login_pages/index/index?redirect=${encodeURIComponent(
`/main_pages/index`
`/main_pages/index`,
)}`,
});
clear();
@@ -613,11 +620,12 @@ function Result() {
page: "other_pages/ntrp-evaluate/index",
scene: `stage=${StageType.INTRO}`,
});
if (qrCodeUrlRes.code === 0 && qrCodeUrlRes.data?.qr_code_base64) {
finalQrCodeUrl = await base64ToTempFilePath(
qrCodeUrlRes.data.qr_code_base64
);
}
finalQrCodeUrl = qrCodeUrlRes.data.ossPath;
// if (qrCodeUrlRes.code === 0 && qrCodeUrlRes.data?.qr_code_base64) {
// finalQrCodeUrl = await base64ToTempFilePath(
// qrCodeUrlRes.data.qr_code_base64
// );
// }
}
// 使用 RadarV2 的 generateFullImage 方法生成完整图片
@@ -712,7 +720,7 @@ function Result() {
<View
className={styles.card}
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}>
@@ -762,7 +770,8 @@ function Result() {
{userInfo?.phone ? (
<View className={styles.updateTip}>
<Text>
NTRP {formatNtrpDisplay(result?.ntrp_level || "")}{" "}
NTRP {" "}
{formatNtrpDisplay(result?.ntrp_level || "")}{" "}
</Text>
<Text className={styles.grayTip}>()</Text>
</View>

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react'
import { View, Text, Textarea, Image } from '@tarojs/components'
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 uploadFiles from '@/services/uploadFiles'
import publishService from '@/services/publishService'
@@ -109,7 +109,10 @@ const AiImportPopup: React.FC<AiImportPopupProps> = ({
}
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,14 +194,23 @@ const AiImportPopup: React.FC<AiImportPopupProps> = ({
}
const showManualButton = uploadFailCount >= maxFailCount
if (!visible) {
return null
}
// 阻止弹窗内的触摸事件冒泡
const handleTouchMoveInPopup = (e) => {
if (!isKeyboardVisible) {
e.stopPropagation()
}
}
return (
<Popup
visible={visible}
position="bottom"
round={true}
closeable={false}
onClose={closePopupBefore}
<View
className={styles.aiImportPopupOverlay}
>
<View className={styles.aiImportPopupWrapper} onTouchMove={handleTouchMoveInPopup} catchMove></View>
<View
className={styles.aiImportPopup}
style={{ paddingBottom: isKeyboardVisible ? `${keyboardHeight}px` : undefined }}
>
@@ -239,12 +251,21 @@ const AiImportPopup: React.FC<AiImportPopupProps> = ({
{/* 图片识别按钮 */}
<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} />)
}
<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>
<Text className={styles.imageRecognitionDesc}>
{uploadLoading ? '已上传 1 张图片' : '支持订场截图/小红书笔记截图等图片'}
</Text>
</View>
</View>
@@ -256,8 +277,7 @@ const AiImportPopup: React.FC<AiImportPopupProps> = ({
</View>
)}
<View className={styles.pasteButton} onClick={handlePasteAndRecognize}>
{
loading ? (
{loading ? (
<View className={styles.loadingContainer}>
<ConfigProvider theme={{ nutuiLoadingIconColor: '#fff', nutuiLoadingIconSize: '20px' }}>
<Loading type="circular" />
@@ -269,13 +289,13 @@ const AiImportPopup: React.FC<AiImportPopupProps> = ({
<Image src={images.ICON_COPY} className={styles.clipboardIcon} />
<Text className={styles.pasteButtonText}></Text>
</>
)
}
)}
</View>
</View>
</View>
<Toast id="toast" />
</Popup>
</View>
</View>
)
}

View File

@@ -1,14 +1,34 @@
@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 {
background-color: #fff;
&:global(.nut-popup-bottom.nut-popup-round) {
border-radius: 20px 20px 0 0!important;
}
width: 100%;
background-color:#fafafa;
border-radius: 16px 16px 0 0;
position: relative;
z-index: 9999;
.popupContent {
width: 100%;
background: #fff;
border-radius: 16px 16px 0 0;
padding: 0;
box-sizing: border-box;
max-height: 80vh;

View File

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

View File

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

View File

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

View File

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

View File

@@ -158,14 +158,19 @@ class GameDetailService {
async getQrCodeUrl(req: { page: string, scene: string }): Promise<ApiResponse<{
qr_code_base64: string,
image_size: number,
ossPath: string,
page: string,
scene: string,
width: number
}>> {
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
radar_data: RadarData;
answers: Answer[];
sort?: string[]; // 雷达图能力项排序,如 ["正手球质", "正手控制", ...]
}
// 单条测试记录

View File

@@ -129,6 +129,7 @@ class HttpService {
// 隐藏loading支持多个并发请求
private hideLoading(): void {
try {
this.loadingCount = Math.max(0, this.loadingCount - 1)
// 只有所有请求都完成时才隐藏loading
@@ -146,6 +147,12 @@ class HttpService {
this.hideLoadingTimer = null
}, 800)
}
}
catch (e) {
console.warn(e)
}
}
// 处理响应

View File

@@ -134,7 +134,7 @@ export const getCityQrCode = async () => {
}
// 获取行政区列表
export const getDistricts = async (params: { country: string; state: string }) => {
export const getDistricts = async (params: { province: string; city: string }) => {
try {
// 调用HTTP服务获取行政区列表
return httpService.post('/cities/cities', params)

View File

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

View File

@@ -2,7 +2,7 @@ import { UserInfo } from "@/components/UserInfo";
import { API_CONFIG } from "@/config/api";
import httpService, { ApiResponse } from "./httpService";
import uploadFiles from "./uploadFiles";
import Taro from "@tarojs/taro";
import * as Taro from "@tarojs/taro";
import getCurrentConfig from "@/config/env";
import { clear_login_state } from "@/services/loginService";
@@ -740,12 +740,14 @@ export const updateUserProfile = async (payload: Partial<UserInfoType>) => {
// 更新用户坐标位置
export const updateUserLocation = async (
latitude: number,
longitude: number
longitude: number,
force: boolean = false
) => {
try {
const response = await httpService.post("/user/update_location", {
latitude,
longitude,
force
});
return response;
} catch (error) {

View File

@@ -0,0 +1,10 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_8289_53268)">
<path d="M10.9775 4.82657H8.04862C7.97102 4.82657 7.89658 4.7958 7.84163 4.74101C7.78668 4.68622 7.7557 4.61188 7.75547 4.53428V4.36457C7.75497 4.32525 7.76254 4.28625 7.77773 4.24998C7.79292 4.21371 7.81539 4.18094 7.84376 4.15371L8.88604 3.11143C8.50894 2.72825 8.05949 2.4238 7.56377 2.21574C7.06805 2.00768 6.53594 1.90016 5.99833 1.89943C5.1996 1.90138 4.41883 2.13662 3.75196 2.57623C3.08509 3.01584 2.56116 3.64067 2.24453 4.37397C1.92791 5.10727 1.83238 5.91708 1.9697 6.70392C2.10701 7.49077 2.47118 8.22036 3.01746 8.80307C3.56375 9.38578 4.26835 9.79622 5.0447 9.98397C5.82106 10.1717 6.63535 10.1286 7.38753 9.8599C8.13971 9.59121 8.79702 9.10864 9.2787 8.47149C9.76039 7.83435 10.0455 7.07037 10.0989 6.27343C10.1075 6.11828 10.236 5.99743 10.3912 5.99743H10.9775C11.0574 6.00001 11.1331 6.03387 11.1883 6.09171C11.2415 6.15 11.2698 6.22885 11.2638 6.30771C11.1937 7.51221 10.7124 8.6562 9.90046 9.54861C9.08847 10.441 7.99488 11.0278 6.80233 11.211C5.60978 11.3942 4.39049 11.1627 3.34808 10.5551C2.30568 9.94759 1.50328 9.00078 1.0749 7.87285C0.646012 6.74568 0.616794 5.50548 0.992127 4.35935C1.36746 3.21323 2.12463 2.23056 3.13719 1.57543C4.15001 0.920266 5.35687 0.632222 6.55641 0.759351C7.75595 0.88648 8.87562 1.42109 9.72862 2.274L10.6012 1.40143C10.6279 1.37384 10.6599 1.35189 10.6952 1.33687C10.7305 1.32186 10.7685 1.31408 10.8069 1.314H10.9766C11.0541 1.31422 11.1283 1.34509 11.183 1.39986C11.2378 1.45462 11.2687 1.52883 11.2689 1.60628V4.53428C11.2687 4.61173 11.2378 4.68595 11.183 4.74071C11.1283 4.79548 11.0549 4.82634 10.9775 4.82657Z" fill="#A6A6A6"/>
</g>
<defs>
<clipPath id="clip0_8289_53268">
<rect width="12" height="12" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

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
bannerListIndex: string
} | null
fetchBannerDictionary: () => Promise<void>
}
// 创建字典Store
@@ -36,20 +35,32 @@ export const useDictionaryStore = create<DictionaryState>()((set, get) => ({
set({ isLoading: true, error: null })
try {
const keys = 'publishing_requirements,court_type,court_surface,supplementary_information,game_play,fabu_tip';
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)
if (response.code === 0 && response.data) {
const dictionaryData = {};
keys.split(',').forEach(key => {
const list = response.data[key];
const listData = list.split('|');
// supported_cities 格式如 "上海市||北京市",用 || 分割
const listData = key === 'supported_cities'
? (list ? String(list).split('||').map((s) => s.trim()).filter(Boolean) : [])
: (list ? list.split('|') : []);
dictionaryData[key] = listData;
})
set({
dictionaryData: dictionaryData || {},
isLoading: false
})
set({
bannerDict: {
bannerListImage: response.data.bannerListImage || '',
bannerDetailImage: response.data.bannerDetailImage || '',
bannerListIndex: (response.data.bannerListIndex ?? '').toString(),
}
})
console.log('字典数据获取成功:', response.data)
} else {
throw new Error(response.message || '获取字典数据失败')
@@ -64,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) => {

View File

@@ -11,8 +11,6 @@ import {
getCityQrCode,
getDistricts,
} from "../services/listApi";
// 不再在这里请求 banner 字典,统一由 dictionaryStore 启动时获取
import { useDictionaryStore } from "./dictionaryStore";
import {
ListActions,
IFilterOptions,
@@ -20,26 +18,6 @@ import {
IPayload,
} from "../../types/list/types";
// 将 banner 按索引插入到列表的工具方法0基长度不足则插末尾先移除已存在的 banner
function insertBannersToRows(rows: any[], dictData: any) {
if (!Array.isArray(rows) || !dictData) return rows;
const img = (dictData?.bannerListImage || "").trim();
const indexRaw = (dictData?.bannerListIndex || "").toString().trim();
if (!img) return rows;
const parsed = parseInt(indexRaw, 10);
const normalized = Number.isFinite(parsed) ? parsed : 0;
// 先移除已有的 banner确保列表中仅一条 banner
const resultRows = rows?.filter((item) => item?.type !== "banner") || [];
const target = Math.max(0, Math.min(normalized, resultRows.length));
resultRows.splice(target, 0, {
type: "banner",
id: `banner-${target}`,
banner_image_url: img,
banner_detail_url: (dictData?.bannerDetailImage || "").trim(),
} as any);
return resultRows;
}
function translateCityData(dataTree) {
return dataTree.map((item) => {
const { children, ...rest } = item;
@@ -216,18 +194,17 @@ export const useListStore = create<TennisStore>()((set, get) => ({
const { distanceFilter, order, district } = distanceQuickFilter || {};
// 始终使用 state.area确保所有接口使用一致的城市参数
const areaProvince = state.area?.at(1) || "";
const areaProvince = state.area?.at(0) || "";
const areaCity = state.area?.at(1) || "";
const last_location_province = areaProvince;
// city 参数逻辑:
// 1. 如果选择了行政区district 有值使用行政区的名称label
// 2. 如果是"全城"distanceFilter 为空),不传 city
let city: string | undefined = undefined;
let county: string | undefined = undefined;
if (district) {
// 从 districts 数组中查找对应的行政区名称
const selectedDistrict = state.districts.find(item => item.value === district);
if (selectedDistrict) {
city = selectedDistrict.label; // 传递行政区名称,如"静安"
county = selectedDistrict.label; // 传递行政区名称,如"静安"
}
}
// 如果是"全城"distanceFilter 为空city 保持 undefined不会被传递
@@ -246,11 +223,12 @@ export const useListStore = create<TennisStore>()((set, get) => ({
distanceFilter: distanceFilter,
// 显式设置 province确保始终使用 state.area 中的最新值
province: last_location_province, // 始终使用 state.area 中的 province确保城市参数一致
city: areaCity,
};
// 只在有值时添加 city 参数
if (city) {
searchOption.city = city;
if (county) {
searchOption.county = county;
}
const params = {
@@ -272,14 +250,11 @@ export const useListStore = create<TennisStore>()((set, get) => ({
const currentPageState = state.isSearchResult ? state.searchPageState : state.listPageState;
const currentData = currentPageState?.data || [];
const newData = isAppend ? [...currentData, ...(data || [])] : (data || []);
// 从字典缓存获取 banner并将其插入到最终列表指定位置全局索引
const dictData = useDictionaryStore.getState().bannerDict;
const processedData = dictData ? insertBannersToRows(newData, dictData) : newData;
state.updateCurrentPageState({
data: processedData,
data: newData,
isHasMoreData,
// 使用插入后的最终数据判断是否显示空状态,避免有 banner 时仍显示空
isShowNoData: processedData?.length === 0,
isShowNoData: newData?.length === 0,
});
set({
@@ -729,18 +704,18 @@ export const useListStore = create<TennisStore>()((set, get) => ({
async getDistricts() {
try {
const state = get();
// 从 area 中获取省份area 格式: ["中国", 省份, 城市]
const country = "中国";
const province = state.area?.at(1) || "上海"; // area[1] 是省份
// 从 area 中获取省份area 格式: [ 省份, 城市]
const province = state.area?.at(0) || "上海";
const cn_city = state.area?.at(1) || "上海"; // area[1] 是省份
const res = await getDistricts({
country,
state: province
province,
city: cn_city
});
if (res.code === 0 && res.data) {
const districts = res.data.map((item) => ({
label: item.cn_city,
label: item.cn_county,
value: item.id.toString(),
id: item.id,
}));

View File

@@ -36,6 +36,7 @@ const getTimeNextDate = (time: string) => {
// 请求锁,避免重复调用
let isFetchingLastTestResult = false;
let isCheckingNicknameStatus = false;
const CITY_CACHE_KEY = "USER_SELECTED_CITY";
export const useUser = create<UserState>()((set) => ({
user: {},
@@ -47,9 +48,11 @@ export const useUser = create<UserState>()((set) => ({
// 优先使用缓存中的城市,不使用用户信息中的位置
// 检查是否有缓存的城市
const CITY_CACHE_KEY = "USER_SELECTED_CITY";
const cachedCity = (Taro as any).getStorageSync?.(CITY_CACHE_KEY);
if (cachedCity && Array.isArray(cachedCity) && cachedCity.length === 2) {
// 如果有缓存的城市,使用缓存,不更新 area
console.log("[userStore] 检测到缓存的城市,使用缓存,不更新 area");
@@ -60,16 +63,13 @@ export const useUser = create<UserState>()((set) => ({
if (userData?.last_location_province) {
const listStore = useListStore.getState();
const currentArea = listStore.area;
// 只有当 area 不存在时才使用用户信息中的位置
if (!currentArea) {
const newArea: [string, string] = ["中国", userData.last_location_province];
const newArea: [string, string] = [userData.last_location_province||"", userData.last_location_city||""];
listStore.updateArea(newArea);
// 保存到缓存
try {
(Taro as any).setStorageSync?.(CITY_CACHE_KEY, newArea);
} catch (error) {
console.error("保存城市缓存失败:", error);
}
useUser.getState().updateCache(newArea);
}
}
@@ -79,6 +79,16 @@ export const useUser = create<UserState>()((set) => ({
return undefined;
}
},
// 更新缓存
updateCache: async (newArea: [string, string]) => {
try {
(Taro as any).setStorageSync?.(CITY_CACHE_KEY, newArea);
} catch (error) {
console.error("保存城市缓存失败:", error);
}
},
updateUserInfo: async (userInfo: Partial<UserInfoType>) => {
try {
// 先更新后端
@@ -93,7 +103,7 @@ export const useUser = create<UserState>()((set) => ({
const currentArea = listStore.area;
// 只有当 area 不存在或与 userLastLocationProvince 不一致时才更新
if (!currentArea || currentArea[1] !== userInfo.last_location_province) {
const newArea: [string, string] = ["中国", userInfo.last_location_province];
const newArea: [string, string] = [userInfo.last_location_province || "", userInfo.last_location_city || ""];
listStore.updateArea(newArea);
}
}
@@ -195,6 +205,7 @@ export const useNicknameChangeStatus = () =>
export const useUserActions = () =>
useUser((state) => ({
fetchUserInfo: state.fetchUserInfo,
updateCache: state.updateCache,
updateUserInfo: state.updateUserInfo,
checkNicknameChangeStatus: state.checkNicknameChangeStatus,
updateNickname: state.updateNickname,

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 (
<View className="download-bill-records-page">
{/* 导航栏 */}
@@ -111,7 +139,7 @@ const DownloadBillRecords: React.FC = () => {
</View>
<View className="info-item">
<Text></Text>
<Text className="btn"></Text>
<Text className="btn" onClick={() => handlePreviewFile(record.file_url)}></Text>
</View>
</View>
)) : <EmptyState text="暂无数据" />}

View File

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

View File

@@ -68,6 +68,7 @@ const OtherUserPage: React.FC = () => {
);
const [collapseProfile, setCollapseProfile] = useState(false);
const [refreshing, setRefreshing] = useState(false);
// 进入页面时检查 user_id只在组件挂载时执行一次
useEffect(() => {
@@ -82,15 +83,12 @@ const OtherUserPage: React.FC = () => {
}
}, []); // 空依赖数组,确保只在进入时执行一次
// 页面加载时获取用户信息
useEffect(() => {
const load_user_data = async () => {
if (user_id) {
// 加载用户信息(使用 useCallback 便于下拉刷新复用)
const load_user_data = useCallback(async () => {
if (!user_id) return;
try {
// const user_data = await UserService.get_user_info(user_id);
const res = await LoginService.getUserInfoById(user_id);
const { data: userData } = res;
// setUserInfo({...res.data as UserInfo, avatar: data.avatar_url || require("@/static/userInfo/default_avatar.svg")});
setUserInfo({
id: parseInt(user_id || "") || 0,
nickname: userData.nickname || "",
@@ -107,7 +105,6 @@ const OtherUserPage: React.FC = () => {
participated_games_count:
userData.stats?.participated_games_count || 0,
},
personal_profile: userData.personal_profile || "",
province: userData.province || "",
city: userData.city || "",
@@ -126,12 +123,12 @@ const OtherUserPage: React.FC = () => {
icon: "none",
});
}
}
};
load_user_data();
}, [user_id]);
useEffect(() => {
load_user_data();
}, [load_user_data]);
// 分类球局数据(使用 useCallback 包装,避免每次渲染都创建新函数)
const classifyGameRecords = useCallback(
(
@@ -232,6 +229,18 @@ const OtherUserPage: React.FC = () => {
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) => {
// Taro.navigateTo({
@@ -244,6 +253,9 @@ const OtherUserPage: React.FC = () => {
scrollY
className="other_user_page"
refresherBackground="#FAFAFA"
refresherEnabled
refresherTriggered={refreshing}
onRefresherRefresh={handle_refresh}
>
{/* <CustomNavbar>
<View className="navbar_content">

View File

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

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useState } from "react";
import { View, Text, Input, Button, Image } from "@tarojs/components";
import Taro, { useDidShow, useReachBottom } from "@tarojs/taro";
import React, { useEffect, useState, useCallback } from "react";
import { View, Text, Input, Button, Image, ScrollView } from "@tarojs/components";
import Taro, { useDidShow } from "@tarojs/taro";
import "./index.scss";
import { CommonPopup, EmptyState } from "@/components";
import httpService from "@/services/httpService";
@@ -109,16 +109,6 @@ const WalletPage: React.FC = () => {
const pageConfig = currentPage.page?.config;
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>({
balance: 0,
@@ -158,6 +148,7 @@ const WalletPage: React.FC = () => {
});
const [totalPages, setTotalPages] = useState(1);
const [refreshing, setRefreshing] = useState(false);
useEffect(() => {
load_transactions();
@@ -452,6 +443,33 @@ const WalletPage: React.FC = () => {
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 = () => {
setShowFilterPopup(false);
setFilterParams({
@@ -488,6 +506,16 @@ const WalletPage: React.FC = () => {
Taro.navigateBack();
}}
/>
<ScrollView
scrollY
refresherBackground="#FAFAFA"
refresherEnabled
refresherTriggered={refreshing}
onRefresherRefresh={handle_refresh}
lowerThreshold={50}
onScrollToLower={handle_scroll_to_lower}
className="wallet_scroll"
>
{/* 钱包主卡片 */}
<View
className="wallet_main_card"
@@ -649,6 +677,7 @@ const WalletPage: React.FC = () => {
)}
</View>
</View>
</ScrollView>
{/* 提现弹窗 */}
<CommonPopup

View File

@@ -1,17 +1,11 @@
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 ringUrl = `${OSS_BASE_URL}/images/b635164f-ecec-434a-a00b-69614a918f2f.png`;
const dateIcon = `${OSS_BASE_URL}/images/1b49476e-0eda-42ff-b08c-002ce510df82.jpg`;
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`;
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 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 logoText = `${OSS_BASE}/system/youchang_tip_text.png`;
export function base64ToTempFilePath(base64Data: string): Promise<string> {
return new Promise((resolve, reject) => {
@@ -288,7 +282,9 @@ function drawTextWrap(
/** 核心纯函数:生成海报图片 */
export async function generatePosterImage(data: any): Promise<string> {
console.log("start !!!!");
const dpr = Taro.getWindowInfo().pixelRatio;
// const dpr = Taro.getWindowInfo().pixelRatio;
const dpr = 1;
// console.log(dpr, 'dpr')
const width = 600;
const height = 1000;
@@ -439,7 +435,7 @@ export async function generatePosterImage(data: any): Promise<string> {
const { tempFilePath } = await Taro.canvasToTempFilePath({
canvas,
fileType: 'png',
quality: 1,
quality: 0.7,
});
return tempFilePath;
}

View File

@@ -1,5 +1,5 @@
import Taro from '@tarojs/taro'
import { OSS_BASE_URL } from "@/config/api";
import { OSS_BASE } from "@/config/api";
export interface ShareCardData {
userAvatar: string
@@ -481,7 +481,7 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro
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)
// 绘制"单打"标签
@@ -517,7 +517,7 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro
const dateX = danDaX
const timeInfoY = infoStartY + infoSpacing
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)
// 绘制日期(绿色,非描边粗体)
@@ -530,7 +530,7 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro
// 绘制地点
const locationInfoY = infoStartY + infoSpacing * 2
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)
drawBoldText(ctx, data.venueName, danDaX, locationInfoY + 10, locationFontSize, '#000000')

2
types/global.d.ts vendored
View File

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