1 Commits

Author SHA1 Message Date
李瑞
49f53d60ed 处理列表请求 2026-02-06 22:37:35 +08:00
146 changed files with 2321 additions and 4324 deletions

View File

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

View File

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

View File

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

View File

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

4
.gitignore vendored
View File

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

View File

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

View File

@@ -473,7 +473,7 @@ async function safeMarkAsRead(type, ids) {
}) })
} catch (err) { } catch (err) {
// 标记已读失败不影响用户体验,静默处理 // 标记已读失败不影响用户体验,静默处理
console.warn('标记已读失败:', err) console.error('标记已读失败:', err)
} }
} }
``` ```

View File

@@ -28,7 +28,7 @@ function formatSize(bytes) {
function analyze() { function analyze() {
if (!fs.existsSync(DIST_DIR)) { if (!fs.existsSync(DIST_DIR)) {
console.warn('dist 目录不存在,请先执行 taro build --type weapp'); console.error('dist 目录不存在,请先执行 taro build --type weapp');
return; return;
} }

View File

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

View File

@@ -1,79 +0,0 @@
/**
* 统一环境配置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

@@ -1,128 +0,0 @@
import Taro from '@tarojs/taro'
// 环境类型
export type EnvType = 'development' | 'production'
// 环境配置接口
export interface EnvConfig {
name: string
apiBaseURL: string
timeout: number
enableLog: boolean
enableMock: boolean
// 客服配置
customerService: {
corpId: string
serviceUrl: string
phoneNumber?: string
email?: string
}
}
// 各环境配置
const envConfigs: Record<EnvType, EnvConfig> = {
// 开发环境
development: {
name: '开发环境',
// apiBaseURL: 'https://tennis.bimwe.com',
apiBaseURL: 'http://localhost:9098',
timeout: 15000,
enableLog: true,
enableMock: false,
// 客服配置
customerService: {
corpId: 'ww51fc969e8b76af82', // 企业ID
serviceUrl: 'https://work.weixin.qq.com/kfid/kfc64085b93243c5c91',
}
},
// 生产环境1
// production: {
// name: '生产环境1',
// apiBaseURL: 'https://tennis.bimwe.com',
// timeout: 10000,
// enableLog: false,
// enableMock: false,
// // 客服配置
// customerService: {
// corpId: 'ww51fc969e8b76af82', // 企业ID
// serviceUrl: 'https://work.weixin.qq.com/kfid/kfc64085b93243c5c91',
// }
// },
// 生产环境2
production: {
name: '生产环境2',
apiBaseURL: 'https://youchang.qiongjingtiyu.com',
timeout: 10000,
enableLog: false,
enableMock: false,
// 客服配置
customerService: {
corpId: 'ww9a2d9a5d9410c664', // 企业ID
serviceUrl: 'https://work.weixin.qq.com/kfid/kfcd355e162e0390684',
}
}
}
// 获取当前环境
export const getCurrentEnv = (): EnvType => {
// 在小程序环境中,使用默认逻辑判断环境
// 可以根据实际需要配置不同的判断逻辑
// 可以根据实际部署情况添加更多判断逻辑
// 比如通过 Taro.getEnv() 获取当前平台环境
const isProd = process.env.NODE_ENV === 'production'
if (isProd) {
return 'production'
} else {
return 'development'
}
}
// 获取当前环境配置
export const getCurrentConfig = (): EnvConfig => {
const env = getCurrentEnv()
return envConfigs[env]
}
// 获取指定环境配置
export const getEnvConfig = (env: EnvType): EnvConfig => {
return envConfigs[env]
}
// 是否为开发环境
export const isDevelopment = (): boolean => {
return getCurrentEnv() === 'development'
}
// 是否为生产环境
export const isProduction = (): boolean => {
return getCurrentEnv() === 'production'
}
// 环境配置调试信息
export const getEnvInfo = () => {
const config = getCurrentConfig()
return {
env: getCurrentEnv(),
config,
taroEnv: Taro.getEnv(),
platform: Taro.getEnv() === Taro.ENV_TYPE.WEAPP ? '微信小程序' :
Taro.getEnv() === Taro.ENV_TYPE.WEB ? 'Web' :
Taro.getEnv() === Taro.ENV_TYPE.RN ? 'React Native' : '未知'
}
}
// 导出当前环境配置(方便直接使用)
export default getCurrentConfig()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,12 +3,6 @@
.common-popup { .common-popup {
position: fixed; position: fixed;
z-index: 9999 !important; z-index: 9999 !important;
padding: 0;
box-sizing: border-box;
max-height: calc(100vh - 10px);
display: flex;
flex-direction: column;
background-color: theme.$page-background-color;
&:global(.nut-popup-bottom.nut-popup-round) { &:global(.nut-popup-bottom.nut-popup-round) {
border-radius: 20px 20px 0 0 !important; border-radius: 20px 20px 0 0 !important;
} }
@@ -38,7 +32,12 @@
} }
} }
padding: 0;
box-sizing: border-box;
max-height: calc(100vh - 10px);
display: flex;
flex-direction: column;
background-color: theme.$page-background-color;
// .common-popup__header { // .common-popup__header {
// padding: 12px 16px; // padding: 12px 16px;

View File

@@ -1,215 +0,0 @@
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, setKeyboardVisible } = useKeyboardHeight()
// 当弹窗显示时,设置键盘为不可见
useEffect(() => {
if (visible) {
setKeyboardVisible(false)
}
}, [visible, setKeyboardVisible])
// 使用全局键盘状态监听
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

@@ -1,155 +0,0 @@
@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

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

View File

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

View File

@@ -1,14 +1,9 @@
import { useRef, useState, useEffect } from "react"; import { useRef, useState, useEffect } from "react";
import { Menu } from "@nutui/nutui-react-taro"; import { Menu } from "@nutui/nutui-react-taro";
import { Image, View, ScrollView } from "@tarojs/components"; import { Image, View, ScrollView } from "@tarojs/components";
import Taro from "@tarojs/taro";
import img from "@/config/images"; import img from "@/config/images";
import Bubble from "../Bubble"; import Bubble from "../Bubble";
import { useListState, useListStore } from "@/store/listStore"; import { useListState } 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"; import "./index.scss";
const DistanceQuickFilterV2 = (props) => { const DistanceQuickFilterV2 = (props) => {
@@ -24,19 +19,15 @@ const DistanceQuickFilterV2 = (props) => {
quickValue, quickValue,
districtValue, // 新增:行政区选中值 districtValue, // 新增:行政区选中值
onMenuVisibleChange, // 菜单展开/收起回调 onMenuVisibleChange, // 菜单展开/收起回调
onRelocate, // 重新定位回调
} = props; } = props;
const cityRef = useRef(null); const cityRef = useRef(null);
const quickRef = useRef(null); const quickRef = useRef(null);
const [changePosition, setChangePosition] = useState<number[]>([]); const [changePosition, setChangePosition] = useState<number[]>([]);
const [isMenuOpen, setIsMenuOpen] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false);
const [keys, setKeys] = useState(0); const [keys, setKeys] = useState(0);
const [isRelocating, setIsRelocating] = useState(false);
// 从 store 获取当前城市信息 // 从 store 获取当前城市信息
const { area } = useListState(); const { area } = useListState();
const currentCity = area?.at(-1) || ""; // 获取省份/城市名称 const currentCity = area?.at(-1) || ""; // 获取省份/城市名称
const { updateState } = useGlobalState() || {};
const { fetchUserInfo, updateCache } = useUserActions();
// 全城筛选显示的标题 - 如果选择了行政区,显示行政区名称 // 全城筛选显示的标题 - 如果选择了行政区,显示行政区名称
const getCityTitle = () => { const getCityTitle = () => {
@@ -88,64 +79,6 @@ const DistanceQuickFilterV2 = (props) => {
index === 1 && (quickRef.current as any)?.toggle(false); 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.warn('重新定位失败:', error);
(Taro as any).showToast({
title: error?.message || '定位失败,请检查定位权限',
icon: 'none',
duration: 2000,
});
} finally {
setIsRelocating(false);
(Taro as any).hideLoading();
}
};
// 监听菜单状态变化,通知父组件 // 监听菜单状态变化,通知父组件
useEffect(() => { useEffect(() => {
onMenuVisibleChange?.(isMenuOpen); onMenuVisibleChange?.(isMenuOpen);
@@ -170,11 +103,8 @@ const DistanceQuickFilterV2 = (props) => {
icon={<Image src={img.ICON_MENU_ITEM_SELECTED} />} icon={<Image src={img.ICON_MENU_ITEM_SELECTED} />}
> >
<div className="positionWrap"> <div className="positionWrap">
<p className="title">{currentCity}</p> <p className="title"></p>
<p className="cityName" onClick={handleRelocate}> <p className="cityName">{currentCity}</p>
<img src={img.ICON_RELOCATE} style={{ width: '12px', height: "12px" }} />
<span></span>
</p>
</div> </div>
<div className="distanceWrap"> <div className="distanceWrap">
<Bubble <Bubble

View File

@@ -32,52 +32,36 @@ const FilterPopup = (props: FilterPopupProps) => {
const { timeBubbleData, gamesNum } = store; const { timeBubbleData, gamesNum } = store;
/** /**
* @description 日期排序 * @description 处理字典选项
* @param a 日期字符串 * @param dictionaryValue 字典选项
* @param b 日期字符串 * @returns 选项列表
* @returns 日期差值
*/ */
const sortByDate = (a: string, b: string) => { // const [selectedDates, setSelectedDates] = useState<String[]>([])
return new Date(a).getTime() - new Date(b).getTime();
}
const handleDateChange = (dates: Date[]) => { const handleDateChange = (dates: Date[]) => {
// ================================ 日期处理 ================================ let times: String[] = [];
// 默认是是当前日期为开始日期,结束日期为当前日期 + 30天
const defaultDateRange = [dayjs().format('YYYY-MM-DD'), dayjs().add(1, 'M').format('YYYY-MM-DD')];
// 处理空数组的情况
if (!dates.length) {
onChange({ dateRange: defaultDateRange });
return;
}
// 处理多日期范围选择超过1个日期
if (dates.length > 1) { if (dates.length > 1) {
const dateRange = [ times = [dayjs(dates[0]).format('YYYY-MM-DD'), dayjs(dates[dates.length - 1]).format('YYYY-MM-DD')]
dayjs(dates[0]).format('YYYY-MM-DD'), onChange({
dayjs(dates[dates.length - 1]).format('YYYY-MM-DD') 'dateRange': times,
]; })
onChange({ dateRange });
return; return;
} }
// 处理单个日期选择 if (Array.isArray(dates)) {
const currentFilterOptionsDateRange = Array.isArray(filterOptions?.dateRange)
? filterOptions.dateRange const currentDay = dayjs(dates[0]).format('YYYY-MM-DD');
: defaultDateRange; if (filterOptions.dateRange.length === 0 || filterOptions.dateRange.length === 2) {
// 当前选择的日期 times.push(currentDay);
const currentDay = dayjs(dates?.[0]).format('YYYY-MM-DD'); } else {
// 当 dates 每次只返回单个日期时,使用已选范围判断是“第一次点”还是“第二次点” times = [...filterOptions.dateRange, currentDay].sort(
let dateRange: string[]; (a, b) => new Date(a).getTime() - new Date(b).getTime()
if ( )
currentFilterOptionsDateRange.length === 2 && }
currentFilterOptionsDateRange?.[0] === currentFilterOptionsDateRange?.[1]
) {
// 已是单日,点击当前日期扩展为日期范围
dateRange = [currentFilterOptionsDateRange[0], currentDay].sort(sortByDate);
} else {
// 默认区间/已选区间/异常状态,点击当前日期统一收敛为单日
dateRange = [currentDay, currentDay];
} }
onChange({ dateRange });
onChange({
'dateRange': times,
})
} }
const handleOptions = (dictionaryValue: []) => { const handleOptions = (dictionaryValue: []) => {

View File

@@ -42,7 +42,7 @@ const FollowUserCard: React.FC<FollowUserCardProps> = ({ user, tabKey, onFollowC
onFollowChange?.(user.id, new_status); onFollowChange?.(user.id, new_status);
} catch (error) { } catch (error) {
console.warn('关注操作失败:', error); console.error('关注操作失败:', error);
Taro.showToast({ Taro.showToast({
title: '操作失败', title: '操作失败',
icon: 'none' icon: 'none'
@@ -67,7 +67,7 @@ const FollowUserCard: React.FC<FollowUserCardProps> = ({ user, tabKey, onFollowC
onBlockSuccess?.(user.id); onBlockSuccess?.(user.id);
} }
} catch (error) { } catch (error) {
console.warn('删除推荐人员失败:', error); console.error('删除推荐人员失败:', error);
Taro.showToast({ Taro.showToast({
title: '操作失败', title: '操作失败',
icon: 'none' icon: 'none'

View File

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

View File

@@ -16,8 +16,8 @@ const CancelPopup = forwardRef((props, ref) => {
const { detail } = props; const { detail } = props;
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [cancelReason, setCancelReason] = useState(""); const [cancelReason, setCancelReason] = useState("");
const [inputFocus, setInputFocus] = useState(false);
const onFinish = useRef(null); const onFinish = useRef(null);
const inputRef = useRef(null);
const { current_players, participants = [], publisher_id } = detail; const { current_players, participants = [], publisher_id } = detail;
const realParticipants = participants const realParticipants = participants
@@ -32,15 +32,16 @@ const CancelPopup = forwardRef((props, ref) => {
show: (onAct) => { show: (onAct) => {
onFinish.current = onAct; onFinish.current = onAct;
setVisible(true); setVisible(true);
// 使用 Taro.nextTick 确保在下一个渲染周期后聚焦 // 使用 requestAnimationFrame 替代 setTimeout(0),性能更好
Taro.nextTick(() => { requestAnimationFrame(() => {
setInputFocus(true); requestAnimationFrame(() => {
inputRef.current && inputRef.current.focus();
});
}); });
}, },
})); }));
function onClose() { function onClose() {
setInputFocus(false);
setVisible(false); setVisible(false);
setCancelReason(""); setCancelReason("");
} }
@@ -84,13 +85,13 @@ const CancelPopup = forwardRef((props, ref) => {
{hasOtherJoin && ( {hasOtherJoin && (
<View className={styles.cancelReason}> <View className={styles.cancelReason}>
<Input <Input
ref={inputRef}
className={styles.input} className={styles.input}
placeholder="请输入取消理由" placeholder="请输入取消理由"
focus={inputFocus} focus
value={cancelReason} value={cancelReason}
onInput={(e) => setCancelReason(e.detail.value)} onInput={(e) => setCancelReason(e.detail.value)}
maxlength={100} maxlength={100}
onBlur={() => setInputFocus(false)}
/> />
</View> </View>
)} )}
@@ -185,11 +186,10 @@ export default forwardRef(function GameManagePopup(props, ref) {
.some((item) => item.user.id === userInfo.id); .some((item) => item.user.id === userInfo.id);
const finished = [MATCH_STATUS.FINISHED, MATCH_STATUS.CANCELED].includes( const finished = [MATCH_STATUS.FINISHED, MATCH_STATUS.CANCELED].includes(
detail.match_status, detail.match_status
); );
// const inTwoHours = dayjs(detail.start_time).diff(dayjs(), "hour") < 2; const inTwoHours = dayjs(detail.start_time).diff(dayjs(), "hour") < 2;
const beforeStart = dayjs(detail.start_time).isAfter(dayjs());
const hasOtherParticiappants = (detail.participants || []) const hasOtherParticiappants = (detail.participants || [])
.filter((item) => item.status === "joined") .filter((item) => item.status === "joined")
@@ -207,7 +207,7 @@ export default forwardRef(function GameManagePopup(props, ref) {
style={{ minHeight: "unset" }} style={{ minHeight: "unset" }}
> >
<View className={styles.container}> <View className={styles.container}>
{!finished && !hasOtherParticiappants && beforeStart && ( {!inTwoHours && !hasOtherParticiappants && (
<View className={styles.button} onClick={handleEditGame}> <View className={styles.button} onClick={handleEditGame}>
</View> </View>
@@ -217,12 +217,12 @@ export default forwardRef(function GameManagePopup(props, ref) {
</View> </View>
)} )}
{!finished && beforeStart && ( {!inTwoHours && !hasOtherParticiappants && (
<View className={styles.button} onClick={handleCancelGame}> <View className={styles.button} onClick={handleCancelGame}>
</View> </View>
)} )}
{!finished && beforeStart && hasJoin && ( {hasJoin && (
<View className={styles.button} onClick={handleQuitGame}> <View className={styles.button} onClick={handleQuitGame}>
退 退
</View> </View>

View File

@@ -15,7 +15,7 @@ const GamePlayType = (props: IProps) => {
const { name, onChange, value, options } = props; const { name, onChange, value, options } = props;
return ( return (
<View className={styles.gamePlayWrapper}> <View className={styles.gamePlayWrapper}>
<TitleComponent title="玩法" icon={<Image src={img.ICON_GAME_PLAY} />} /> <TitleComponent title="玩法" icon={<Image src={img.ICON_SITE} />} />
<Bubble <Bubble
options={options} options={options}
value={value} value={value}

View File

@@ -105,9 +105,9 @@ const HomeNavbar = (props: IProps) => {
const userInfo = useUserInfo(); const userInfo = useUserInfo();
// 使用用户详情接口中的 last_location 字段 // 使用用户详情接口中的 last_location 字段
// USER_SELECTED_CITY 第二个值应该是省份/直辖市,不能是区 // USER_SELECTED_CITY 第二个值应该是省份/直辖市,不能是区
const lastLocationCity = (userInfo as any)?.last_location_city || ""; const lastLocationProvince = (userInfo as any)?.last_location_province || "";
// 只使用省份/直辖市,不使用城市(城市可能是区) // 只使用省份/直辖市,不使用城市(城市可能是区)
const detectedLocation = lastLocationCity; const detectedLocation = lastLocationProvince;
// 检查是否应该显示定位确认弹窗 // 检查是否应该显示定位确认弹窗
const should_show_location_dialog = (): boolean => { const should_show_location_dialog = (): boolean => {
@@ -146,7 +146,7 @@ const HomeNavbar = (props: IProps) => {
console.log(`[HomeNavbar] 距离上次选择"继续浏览"还不到2小时剩余时间: ${Math.ceil((TWO_HOURS_MS - time_diff) / 1000 / 60)}分钟`); console.log(`[HomeNavbar] 距离上次选择"继续浏览"还不到2小时剩余时间: ${Math.ceil((TWO_HOURS_MS - time_diff) / 1000 / 60)}分钟`);
return false; return false;
} catch (error) { } catch (error) {
console.warn('[HomeNavbar] 检查定位弹窗显示条件失败:', error); console.error('[HomeNavbar] 检查定位弹窗显示条件失败:', error);
return true; // 出错时默认显示 return true; // 出错时默认显示
} }
}; };
@@ -192,7 +192,7 @@ const HomeNavbar = (props: IProps) => {
} else if (detectedLocation) { } else if (detectedLocation) {
// 只有在完全没有缓存的情况下,才使用用户详情中的位置信息 // 只有在完全没有缓存的情况下,才使用用户详情中的位置信息
console.log("[HomeNavbar] 没有缓存,使用用户详情中的位置信息:", detectedLocation); console.log("[HomeNavbar] 没有缓存,使用用户详情中的位置信息:", detectedLocation);
const newArea: [string, string] = [(userInfo as any)?.last_location_province || "", detectedLocation]; const newArea: [string, string] = ["中国", detectedLocation];
updateArea(newArea); updateArea(newArea);
// 保存定位信息到缓存 // 保存定位信息到缓存
(Taro as any).setStorageSync(CITY_CACHE_KEY, newArea); (Taro as any).setStorageSync(CITY_CACHE_KEY, newArea);
@@ -239,7 +239,7 @@ const HomeNavbar = (props: IProps) => {
// console.log(`距离上次选择"继续浏览"还不到2小时剩余时间: ${Math.ceil((TWO_HOURS_MS - time_diff) / 1000 / 60)}分钟`); // console.log(`距离上次选择"继续浏览"还不到2小时剩余时间: ${Math.ceil((TWO_HOURS_MS - time_diff) / 1000 / 60)}分钟`);
// return false; // return false;
// } catch (error) { // } catch (error) {
// console.warn('检查定位弹窗显示条件失败:', error); // console.error('检查定位弹窗显示条件失败:', error);
// return true; // 出错时默认显示 // return true; // 出错时默认显示
// } // }
// }; // };
@@ -266,7 +266,7 @@ const HomeNavbar = (props: IProps) => {
const { detectedProvince } = locationDialogData; const { detectedProvince } = locationDialogData;
// 用户选择"切换到",使用用户详情中的位置信息 // 用户选择"切换到",使用用户详情中的位置信息
const newArea: [string, string] = [(userInfo as any)?.last_location_province || "", detectedProvince]; const newArea: [string, string] = ["中国", detectedProvince];
updateArea(newArea); updateArea(newArea);
// 更新缓存为新的定位信息 // 更新缓存为新的定位信息
(Taro as any).setStorageSync(CITY_CACHE_KEY, newArea); (Taro as any).setStorageSync(CITY_CACHE_KEY, newArea);
@@ -276,7 +276,7 @@ const HomeNavbar = (props: IProps) => {
(Taro as any).setStorageSync(CITY_CHANGE_TIME_KEY, current_time); (Taro as any).setStorageSync(CITY_CHANGE_TIME_KEY, current_time);
console.log(`[LocationDialog] 已记录用户切换城市的时间2小时内不再提示`); console.log(`[LocationDialog] 已记录用户切换城市的时间2小时内不再提示`);
} catch (error) { } catch (error) {
console.warn('保存城市切换时间失败:', error); console.error('保存城市切换时间失败:', error);
} }
console.log("切换到用户详情中的位置信息并更新缓存:", detectedProvince); console.log("切换到用户详情中的位置信息并更新缓存:", detectedProvince);
@@ -304,7 +304,7 @@ const HomeNavbar = (props: IProps) => {
(Taro as any).setStorageSync(LOCATION_DIALOG_DISMISS_TIME_KEY, current_time); (Taro as any).setStorageSync(LOCATION_DIALOG_DISMISS_TIME_KEY, current_time);
console.log(`[LocationDialog] 已记录用户选择"继续浏览"的时间2小时内不再提示`); console.log(`[LocationDialog] 已记录用户选择"继续浏览"的时间2小时内不再提示`);
} catch (error) { } catch (error) {
console.warn('保存定位弹窗关闭时间失败:', error); console.error('保存定位弹窗关闭时间失败:', error);
} }
// 关闭弹窗 // 关闭弹窗
@@ -409,7 +409,7 @@ const HomeNavbar = (props: IProps) => {
(Taro as any).setStorageSync(CITY_CHANGE_TIME_KEY, current_time); (Taro as any).setStorageSync(CITY_CHANGE_TIME_KEY, current_time);
console.log("已保存城市到缓存并记录切换时间:", _newArea, current_time); console.log("已保存城市到缓存并记录切换时间:", _newArea, current_time);
} catch (error) { } catch (error) {
console.warn("保存城市缓存失败:", error); console.error("保存城市缓存失败:", error);
} }
// 先调用列表接口(会使用更新后的 state.area // 先调用列表接口(会使用更新后的 state.area
@@ -481,8 +481,9 @@ const HomeNavbar = (props: IProps) => {
{/* 搜索导航 */} {/* 搜索导航 */}
{!showTitle && ( {!showTitle && (
<View <View
className={`inputCustomerNavbarContainer toggleElement secondElement hidden ${showInput && "visible" className={`inputCustomerNavbarContainer toggleElement secondElement hidden ${
} ${showInput ? "inputCustomerNavbarShowInput" : ""}`} showInput && "visible"
} ${showInput ? "inputCustomerNavbarShowInput" : ""}`}
style={navbarStyle} style={navbarStyle}
> >
<View className="navContent"> <View className="navContent">

View File

@@ -43,7 +43,7 @@ const ImageUpload: React.FC<ImageUploadProps> = ({
onChange([...images, ...newImages]) onChange([...images, ...newImages])
}, },
fail: (err) => { fail: (err) => {
console.warn('选择图片失败:', err) console.error('选择图片失败:', err)
} }
}) })
}, [images.length, maxCount, onChange]) }, [images.length, maxCount, onChange])

View File

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

View File

@@ -5,9 +5,8 @@ import img from "../../config/images";
import { ListCardProps } from "../../../types/list/types"; import { ListCardProps } from "../../../types/list/types";
import { formatGameTime, calculateDuration } from "@/utils/timeUtils"; import { formatGameTime, calculateDuration } from "@/utils/timeUtils";
import { navigateTo } from "@/utils/navigation"; import { navigateTo } from "@/utils/navigation";
import images from "@/config/images"; import images from '@/config/images'
import "./index.scss"; import "./index.scss";
import { OSS_BASE } from "@/config/api";
const ListCard: React.FC<ListCardProps> = ({ const ListCard: React.FC<ListCardProps> = ({
id, id,
@@ -27,7 +26,7 @@ const ListCard: React.FC<ListCardProps> = ({
key, key,
participants, // 参与者图片 participants, // 参与者图片
venue_image_list, // 场馆图片 venue_image_list, // 场馆图片
location_name = '', // 场馆方 venue_description,
game_type, // 球局类型 game_type, // 球局类型
}) => { }) => {
// 参与者要前三个数据 // 参与者要前三个数据
@@ -46,7 +45,7 @@ const ListCard: React.FC<ListCardProps> = ({
className="image" className="image"
mode="aspectFill" mode="aspectFill"
lazyLoad lazyLoad
defaultSource={`${OSS_BASE}/front/ball/images/publish-empty-card.png`} defaultSource="https://images.unsplash.com/photo-1554068865-24cecd4e34b8?w=200&h=200&fit=crop&crop=center"
/> />
); );
}; };
@@ -68,9 +67,7 @@ const ListCard: React.FC<ListCardProps> = ({
const containerWidthPx = screenWidth - 130; const containerWidthPx = screenWidth - 130;
// 计算固定信息宽度 // 计算固定信息宽度
const extraInfo = `${court_type ? `${court_type}` : ""}${ const extraInfo = `${court_type ? `${court_type}` : ''}${distance_km ? `${distance_km}km` : ''}`;
distance_km ? `${distance_km}km` : ""
}`;
// 估算字符宽度(基于 12px 字体) // 估算字符宽度(基于 12px 字体)
const getTextWidth = (text: string) => { const getTextWidth = (text: string) => {
@@ -101,9 +98,7 @@ const ListCard: React.FC<ListCardProps> = ({
let currentWidth = 0; let currentWidth = 0;
for (let i = 0; i < location.length; i++) { for (let i = 0; i < location.length; i++) {
const char = location[i]; const char = location[i];
const charWidth = /[\u4e00-\u9fa5\u3000-\u303f\uff00-\uffef]/.test(char) const charWidth = /[\u4e00-\u9fa5\u3000-\u303f\uff00-\uffef]/.test(char) ? 12 : 6;
? 12
: 6;
if (currentWidth + charWidth > availableWidth) { if (currentWidth + charWidth > availableWidth) {
break; break;
} }
@@ -111,7 +106,7 @@ const ListCard: React.FC<ListCardProps> = ({
maxChars++; maxChars++;
} }
return location.slice(0, maxChars) + "..."; return location.slice(0, maxChars) + '...';
}, [location, court_type, distance_km]); }, [location, court_type, distance_km]);
// 根据图片数量决定展示样式 // 根据图片数量决定展示样式
@@ -132,10 +127,10 @@ const ListCard: React.FC<ListCardProps> = ({
return ( return (
<View className="double-image"> <View className="double-image">
<View className="image-container"> <View className="image-container">
{renderItemImage(image_list?.[1])} {renderItemImage(image_list?.[0])}
</View> </View>
<View className="image-container"> <View className="image-container">
{renderItemImage(image_list?.[0])} {renderItemImage(image_list?.[1])}
</View> </View>
</View> </View>
); );
@@ -225,10 +220,9 @@ const ListCard: React.FC<ListCardProps> = ({
</Text> </Text>
</View> </View>
<View className="tag ntprTag"> <View className="tag ntprTag">
<Image src={images.ICON_LIST_NTPR} className="ntprIcon" /> <Image src={images.ICON_LIST_NTPR} className='ntprIcon' />
<Text className="tag-text"> <Text className="tag-text">
{Number(skill_level_min)?.toFixed(1)} -{" "} {Number(skill_level_min)?.toFixed(1)} - {Number(skill_level_max)?.toFixed(1)}
{Number(skill_level_max)?.toFixed(1)}
</Text> </Text>
{/* 分割线 */} {/* 分割线 */}
<View className="typeLine" /> <View className="typeLine" />
@@ -257,16 +251,22 @@ const ListCard: React.FC<ListCardProps> = ({
/> />
{/* <Text className="smoothTitle">{game_type}</Text> */} {/* <Text className="smoothTitle">{game_type}</Text> */}
</View> </View>
{location_name && <View className="line" />} {
{location_name && ( venue_description && (<View className="line" />)
<View className="localAreaContainer"> }
<View className="localAreaTitle">:</View> {
<View className="localAreaWrapper"> venue_description &&
{venueImage && <Image className="localArea" src={venueImage} />} (
<Text className="localAreaText">{location_name}</Text>
<View className="localAreaContainer">
<View className="localAreaTitle">:</View>
<View className="localAreaWrapper">
<Image className="localArea" src={venueImage} />
<Text className="localAreaText">{venue_description}</Text>
</View>
</View> </View>
</View> )
)} }
</View> </View>
)} )}
</View> </View>

View File

@@ -7,6 +7,7 @@
.listLoadErrorImg { .listLoadErrorImg {
width: 154px; width: 154px;
height: 154px;
} }
.listLoadErrorText { .listLoadErrorText {

View File

@@ -24,6 +24,7 @@ const ListLoadError = (props: IProps) => {
wrapperHeight = "", wrapperHeight = "",
width = "", width = "",
height = "", height = "",
scale = "",
} = props; } = props;
const handleReload = () => { const handleReload = () => {
reload && typeof reload === "function" && reload(); reload && typeof reload === "function" && reload();
@@ -33,7 +34,7 @@ const ListLoadError = (props: IProps) => {
<View className={styles.listLoadError} style={{ height: wrapperHeight }}> <View className={styles.listLoadError} style={{ height: wrapperHeight }}>
<Image <Image
className={styles.listLoadErrorImg} className={styles.listLoadErrorImg}
style={{ width, height }} style={{ width, height, transform: `scale(${scale})` }}
src={errorImg ? img[errorImg] : img.ICON_LIST_LOAD_ERROR} src={errorImg ? img[errorImg] : img.ICON_LIST_LOAD_ERROR}
/> />
{text && <Text className={styles.listLoadErrorText}>{text}</Text>} {text && <Text className={styles.listLoadErrorText}>{text}</Text>}

View File

@@ -62,11 +62,17 @@ const NTRPEvaluatePopup = (props: NTRPEvaluatePopupProps, ref) => {
showGuide = false, showGuide = false,
} = props; } = props;
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [ntrp, setNtrp] = useState<string>("1.5"); const [ntrp, setNtrp] = useState<string>("");
const [guideShow, setGuideShow] = useState(() => showGuide); const [guideShow, setGuideShow] = useState(() => showGuide);
const { updateUserInfo } = useUserActions(); const { updateUserInfo } = useUserActions();
const userInfo = useUserInfo(); const userInfo = useUserInfo();
const ntrpLevels = useNtrpLevels(); const ntrpLevels = useNtrpLevels();
const options = [
ntrpLevels.map((item) => ({
text: item,
value: item,
})),
];
const [evaCallback, setEvaCallback] = useState<EvaluateCallback>({ const [evaCallback, setEvaCallback] = useState<EvaluateCallback>({
type: "", type: "",
next: () => {}, next: () => {},
@@ -99,10 +105,10 @@ const NTRPEvaluatePopup = (props: NTRPEvaluatePopupProps, ref) => {
if (match) { if (match) {
setNtrp(match[0]); setNtrp(match[0]);
} else { } else {
setNtrp("1.5"); setNtrp("");
} }
} else { } else {
setNtrp("1.5"); setNtrp("");
} }
} }
}, [visible, userInfo?.ntrp_level]); }, [visible, userInfo?.ntrp_level]);
@@ -165,7 +171,7 @@ const NTRPEvaluatePopup = (props: NTRPEvaluatePopupProps, ref) => {
{visible && ( {visible && (
<Picker <Picker
visible visible
options={ntrpLevels} options={options}
defaultValue={[ntrp]} defaultValue={[ntrp]}
onChange={(val) => { onChange={(val) => {
console.log(val[0]); console.log(val[0]);

View File

@@ -4,39 +4,10 @@
box-sizing: border-box; box-sizing: border-box;
border-radius: 20px; border-radius: 20px;
border: 0.5px solid rgba(0, 0, 0, 0.08); border: 0.5px solid rgba(0, 0, 0, 0.08);
background: linear-gradient(180deg, #BFFFEF 0%, #F2FFFC 100%), var(--Backgrounds-Primary, #FFF);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
position: relative;
background:
linear-gradient(180deg, #bfffef 0%, #f2fffc 100%),
var(--Backgrounds-Primary, #fff);
.lines {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 20px;
z-index: 1;
background-position-y: 85%;
pointer-events: none;
}
// .gradient {
// inset: 0;
// position: absolute;
// top: 0;
// left: 0;
// width: 100%;
// height: 100%;
// z-index: -2;
// border-radius: 20px;
// background:
// linear-gradient(180deg, #bfffef 0%, #f2fffc 100%),
// var(--Backgrounds-Primary, #fff);
// pointer-events: none;
// }
} }
.higher { .higher {
@@ -47,6 +18,8 @@
.lower { .lower {
height: 80px; height: 80px;
@include commonCardStyle(); @include commonCardStyle();
} }
.desc { .desc {
@@ -57,7 +30,7 @@
gap: 7px; gap: 7px;
.title { .title {
color: #2a4d44; color: #2A4D44;
font-family: "Noto Sans SC"; font-family: "Noto Sans SC";
font-size: 16px; font-size: 16px;
font-style: normal; font-style: normal;
@@ -65,7 +38,7 @@
line-height: 24px; line-height: 24px;
.colorTip { .colorTip {
color: #00e5ad; color: #00E5AD;
font-family: "Noto Sans SC"; font-family: "Noto Sans SC";
font-size: 16px; font-size: 16px;
font-style: normal; font-style: normal;
@@ -74,7 +47,7 @@
} }
.strongTip { .strongTip {
color: #00e5ad; color: #00E5AD;
font-family: "Noto Sans SC"; font-family: "Noto Sans SC";
font-size: 16px; font-size: 16px;
font-style: normal; font-style: normal;
@@ -95,10 +68,8 @@
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
gap: 4px; gap: 4px;
color: #5ca693; color: #5CA693;
font-feature-settings: font-feature-settings: 'liga' off, 'clig' off;
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 12px; font-size: 12px;
font-style: normal; font-style: normal;
@@ -123,9 +94,7 @@
border-radius: 50%; border-radius: 50%;
border: 1px solid #efefef; border: 1px solid #efefef;
overflow: hidden; overflow: hidden;
box-shadow: box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.20), 0 8px 20px 0 rgba(0, 0, 0, 0.12);
0 0 1px 0 rgba(0, 0, 0, 0.2),
0 8px 20px 0 rgba(0, 0, 0, 0.12);
.avatarUrl { .avatarUrl {
width: calc(90px * $multiple); width: calc(90px * $multiple);
@@ -143,14 +112,8 @@
flex-shrink: 0; flex-shrink: 0;
aspect-ratio: 1/1; aspect-ratio: 1/1;
border-radius: calc(20px * $multiple); border-radius: calc(20px * $multiple);
border: 4px solid #fff; border: 4px solid #FFF;
background: background: linear-gradient(0deg, rgba(89, 255, 214, 0.20) 0%, rgba(89, 255, 214, 0.20) 100%), #FFF;
linear-gradient(
0deg,
rgba(89, 255, 214, 0.2) 0%,
rgba(89, 255, 214, 0.2) 100%
),
#fff;
box-shadow: 0 4px 36px 0 rgba(0, 0, 0, 0.12); box-shadow: 0 4px 36px 0 rgba(0, 0, 0, 0.12);
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -2,13 +2,8 @@ import React, { useState, useEffect, useCallback, memo } from "react";
import { View, Image, Text } from "@tarojs/components"; import { View, Image, Text } from "@tarojs/components";
import { requireLoginWithPhone } from "@/utils/helper"; import { requireLoginWithPhone } from "@/utils/helper";
import Taro from "@tarojs/taro"; import Taro from "@tarojs/taro";
import { import { useUserInfo, useUserActions, useLastTestResult } from "@/store/userStore";
useUserInfo,
useUserActions,
useLastTestResult,
} from "@/store/userStore";
// import { getCurrentFullPath } from "@/utils"; // import { getCurrentFullPath } from "@/utils";
import { OSS_BASE } from "@/config/api";
import { StageType } from "@/services/evaluateService"; import { StageType } from "@/services/evaluateService";
import { waitForAuthInit } from "@/utils/authInit"; import { waitForAuthInit } from "@/utils/authInit";
import DocCopy from "@/static/ntrp/ntrp_doc_copy.svg"; import DocCopy from "@/static/ntrp/ntrp_doc_copy.svg";
@@ -31,6 +26,8 @@ function NTRPTestEntryCard(props: {
// 使用全局状态中的测试结果,避免重复调用接口 // 使用全局状态中的测试结果,避免重复调用接口
const lastTestResult = useLastTestResult(); const lastTestResult = useLastTestResult();
console.log(userInfo);
// 从全局状态中获取测试结果,如果不存在则调用接口(使用请求锁避免重复调用) // 从全局状态中获取测试结果,如果不存在则调用接口(使用请求锁避免重复调用)
useEffect(() => { useEffect(() => {
const init = async () => { const init = async () => {
@@ -124,7 +121,7 @@ function NTRPTestEntryCard(props: {
if (!testFlag && !userInfo.phone) { if (!testFlag && !userInfo.phone) {
Taro.navigateTo({ Taro.navigateTo({
url: `/login_pages/index/index?redirect=${encodeURIComponent( url: `/login_pages/index/index?redirect=${encodeURIComponent(
`/other_pages/ntrp-evaluate/index?stage=${StageType.INTRO}`, `/other_pages/ntrp-evaluate/index?stage=${StageType.INTRO}`
)}`, )}`,
}); });
return false; return false;
@@ -135,7 +132,7 @@ function NTRPTestEntryCard(props: {
}`, }`,
}); });
}, },
[setCallback, testFlag, type, evaluateCallback, userInfo.phone], [setCallback, testFlag, type, evaluateCallback, userInfo.phone]
); );
// 如果最近一个月有测试记录,则不展示 // 如果最近一个月有测试记录,则不展示
@@ -145,12 +142,6 @@ function NTRPTestEntryCard(props: {
return type === EvaluateScene.list ? ( return type === EvaluateScene.list ? (
<View className={styles.higher} onClick={handleTest}> <View className={styles.higher} onClick={handleTest}>
<View
className={styles.lines}
style={{
backgroundImage: `url(${OSS_BASE}/front/ball/images/215f1ce1-be52-4a92-8250-5a4a69e7f2b3.png)`,
}}
/>
<View className={styles.desc}> <View className={styles.desc}>
<View> <View>
<View className={styles.title}> <View className={styles.title}>
@@ -185,12 +176,6 @@ function NTRPTestEntryCard(props: {
</View> </View>
) : ( ) : (
<View className={styles.lower} onClick={handleTest}> <View className={styles.lower} onClick={handleTest}>
<View
className={styles.lines}
style={{
backgroundImage: `url(${OSS_BASE}/front/ball/images/215f1ce1-be52-4a92-8250-5a4a69e7f2b3.png)`,
}}
/>
<View className={styles.desc}> <View className={styles.desc}>
<View className={styles.title}> <View className={styles.title}>
<Text></Text> <Text></Text>

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ import { View, Text, Image } from "@tarojs/components";
import Taro from "@tarojs/taro"; import Taro from "@tarojs/taro";
import { useUserInfo } from "@/store/userStore"; import { useUserInfo } from "@/store/userStore";
import { import {
useEvaluate,
EvaluateCallback, EvaluateCallback,
EvaluateScene, EvaluateScene,
} from "@/store/evaluateStore"; } from "@/store/evaluateStore";
@@ -14,7 +15,6 @@ import styles from "./index.module.scss";
import images from "@/config/images"; import images from "@/config/images";
import AiImportPopup from "@/publish_pages/publishBall/components/AiImportPopup"; import AiImportPopup from "@/publish_pages/publishBall/components/AiImportPopup";
import NTRPEvaluatePopup from "../NTRPEvaluatePopup"; import NTRPEvaluatePopup from "../NTRPEvaluatePopup";
import { useDictionaryStore } from "@/store/dictionaryStore";
export interface PublishMenuProps { export interface PublishMenuProps {
onPersonalPublish?: () => void; onPersonalPublish?: () => void;
@@ -30,7 +30,6 @@ const PublishMenu: React.FC<PublishMenuProps> = (props) => {
area area
} = useListState(); } = useListState();
const supportedCitiesList = useDictionaryStore((s) => s.getDictionaryValue('supported_cities')) || [];
// 使用 useEffect 监听 isVisible 变化,确保所有情况都能触发回调 // 使用 useEffect 监听 isVisible 变化,确保所有情况都能触发回调
useEffect(() => { useEffect(() => {
@@ -68,10 +67,10 @@ const PublishMenu: React.FC<PublishMenuProps> = (props) => {
}; };
const handleMenuItemClick = (type: "individual" | "group" | "ai") => { const handleMenuItemClick = (type: "individual" | "group" | "ai") => {
const [_, address] = area; const [_, address] = area;
if (!supportedCitiesList.includes(address)) { if (address !== '上海') {
(Taro as any).showModal({ (Taro as any).showModal({
title: '提示', title: '提示',
content: '该城市尚未开放,您可加入社群或切换城市', content: '仅上海地区开放,您可加入社群或切换城市',
showCancel: false, showCancel: false,
confirmText: '知道了' confirmText: '知道了'
}) })

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -16,7 +16,7 @@ const SubscribeNotificationTip: React.FC<SubscribeNotificationTipProps> = ({
navigateTo({ navigateTo({
url: '/other_pages/enable_notification/index', url: '/other_pages/enable_notification/index',
}).catch((err) => { }).catch((err) => {
console.warn('跳转失败:', err); console.error('跳转失败:', err);
}); });
}; };

View File

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

View File

@@ -86,9 +86,9 @@ async function onChooseImageSuccess(tempFiles) {
...fileRes, ...fileRes,
...(height > IMAGE_MAX_SIZE.height ...(height > IMAGE_MAX_SIZE.height
? { ? {
width: Math.floor(IMAGE_MAX_SIZE.height * image_aspect_ratio), width: Math.floor(IMAGE_MAX_SIZE.height * image_aspect_ratio),
height: IMAGE_MAX_SIZE.height, height: IMAGE_MAX_SIZE.height,
} }
: { width: Math.floor(height * image_aspect_ratio), height }), : { width: Math.floor(height * image_aspect_ratio), height }),
}; };
} else { } else {
@@ -96,9 +96,9 @@ async function onChooseImageSuccess(tempFiles) {
...fileRes, ...fileRes,
...(width > IMAGE_MAX_SIZE.width ...(width > IMAGE_MAX_SIZE.width
? { ? {
width: IMAGE_MAX_SIZE.width, width: IMAGE_MAX_SIZE.width,
height: Math.floor(IMAGE_MAX_SIZE.width / image_aspect_ratio), height: Math.floor(IMAGE_MAX_SIZE.width / image_aspect_ratio),
} }
: { width, height: Math.floor(width / image_aspect_ratio) }), : { width, height: Math.floor(width / image_aspect_ratio) }),
}; };
} }
@@ -119,6 +119,7 @@ export default function UploadFromWx(props: UploadFromWxProps) {
sourceType: ["album", "camera"], sourceType: ["album", "camera"],
}).then(async (res) => { }).then(async (res) => {
const analyzedFiles = await onChooseImageSuccess(res.tempFiles); const analyzedFiles = await onChooseImageSuccess(res.tempFiles);
// cropping image to standard size
const compressedTempFiles = await compressImage(analyzedFiles); const compressedTempFiles = await compressImage(analyzedFiles);
let start = Date.now(); let start = Date.now();
@@ -129,22 +130,19 @@ export default function UploadFromWx(props: UploadFromWxProps) {
is_public: 1 as unknown as 0 | 1, is_public: 1 as unknown as 0 | 1,
id: (start++).toString(), id: (start++).toString(),
})); }));
const onFileUpdate = uploadApi.batchUpload(files).then((res) => {
Taro.showLoading({ title: "上传中..." }); return res.map((item) => ({
try { id: item.id,
const uploadRes = await uploadApi.batchUpload(files); url: item ? item.data.file_url : "",
const successful = uploadRes }));
.filter((item) => item.data != null) });
.map((item) => ({ onAdd(
id: item.id, files.map((item) => ({
url: (item.data as { file_url: string }).file_url, id: item.id,
})); url: item.filePath,
onAdd(successful, Promise.resolve(successful)); })),
} catch (e) { onFileUpdate
console.warn("批量上传失败:", e); );
} finally {
Taro.hideLoading();
}
}); });
}; };
return ( return (

View File

@@ -6,19 +6,14 @@ import "./index.scss";
import { EditModal } from "@/components"; import { EditModal } from "@/components";
import { UserService, PickerOption } from "@/services/userService"; import { UserService, PickerOption } from "@/services/userService";
import { PopupPicker } from "@/components/Picker/index"; import { PopupPicker } from "@/components/Picker/index";
import { import { useUserActions, useNicknameChangeStatus, useLastTestResult } from "@/store/userStore";
useUserActions,
useNicknameChangeStatus,
useLastTestResult,
useUserInfo,
} from "@/store/userStore";
import { UserInfoType } from "@/services/userService"; import { UserInfoType } from "@/services/userService";
import { import {
useCities, useCities,
useProfessions, useProfessions,
useNtrpLevels, useNtrpLevels,
} from "@/store/pickerOptionsStore"; } from "@/store/pickerOptionsStore";
import { formatNtrpDisplay, getBackendErrorMsg } from "@/utils/helper"; import { formatNtrpDisplay } from "@/utils/helper";
import { useGlobalState } from "@/store/global"; import { useGlobalState } from "@/store/global";
// 用户信息接口 // 用户信息接口
@@ -74,7 +69,7 @@ const on_edit = () => {
// 用户信息卡片组件 // 用户信息卡片组件
const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
editable = true, editable = true,
user_info: user_info_prop, user_info,
is_current_user, is_current_user,
is_following = false, is_following = false,
collapseProfile, collapseProfile,
@@ -85,13 +80,9 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
set_user_info, set_user_info,
onTab, onTab,
}) => { }) => {
const global_user_info = useUserInfo();
// 查看别人页面时用传入的 user_info个人页用全局 store
const user_info = is_current_user ? global_user_info : (user_info_prop ?? global_user_info);
const nickname_change_status = useNicknameChangeStatus(); const nickname_change_status = useNicknameChangeStatus();
const { setShowGuideBar } = useGlobalState(); const { setShowGuideBar } = useGlobalState();
const { updateUserInfo, updateNickname, fetchLastTestResult } = const { updateUserInfo, updateNickname, fetchLastTestResult } = useUserActions();
useUserActions();
const ntrpLevels = useNtrpLevels(); const ntrpLevels = useNtrpLevels();
// 使用全局状态中的测试结果,避免重复调用接口 // 使用全局状态中的测试结果,避免重复调用接口
const lastTestResult = useLastTestResult(); const lastTestResult = useLastTestResult();
@@ -100,16 +91,18 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
const prevUserInfoRef = useRef<Partial<UserInfoType>>(); const prevUserInfoRef = useRef<Partial<UserInfoType>>();
useEffect(() => { useEffect(() => {
// 只在 user_info 真正变化时打印(通过 JSON 序列化比较)
const prevStr = JSON.stringify(prevUserInfoRef.current); const prevStr = JSON.stringify(prevUserInfoRef.current);
const currentStr = JSON.stringify(user_info); const currentStr = JSON.stringify(user_info);
if (prevStr !== currentStr) { if (prevStr !== currentStr) {
console.log("UserInfoCard 用户信息变化:", user_info);
prevUserInfoRef.current = user_info; prevUserInfoRef.current = user_info;
} }
// 仅当前用户才拉取 NTRP 测试结果 // 如果全局状态中没有测试结果,则调用接口(使用请求锁,多个组件同时调用时只会请求一次)
if (is_current_user && !lastTestResult && user_info?.id) { if (!lastTestResult && user_info?.id) {
fetchLastTestResult(); fetchLastTestResult();
} }
}, [user_info?.id, lastTestResult, fetchLastTestResult, is_current_user]); }, [user_info?.id, lastTestResult, fetchLastTestResult]);
// 从全局状态中获取测试状态 // 从全局状态中获取测试状态
const ntrpTested = lastTestResult?.has_test_in_last_month || false; const ntrpTested = lastTestResult?.has_test_in_last_month || false;
@@ -124,15 +117,11 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
useState(false); useState(false);
// 表单状态 // 表单状态
const [form_data, set_form_data] = useState<Partial<UserInfoType>>({ ...user_info }); const [form_data, set_form_data] = useState<Partial<UserInfoType>>({});
// useDidShow(() => { useDidShow(() => {
// set_form_data({ ...user_info });
// });
useEffect(() => {
set_form_data({ ...user_info }); set_form_data({ ...user_info });
}, [user_info]) });
useEffect(() => { useEffect(() => {
const visibles = [ const visibles = [
@@ -140,7 +129,6 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
location_picker_visible, location_picker_visible,
ntrp_picker_visible, ntrp_picker_visible,
occupation_picker_visible, occupation_picker_visible,
edit_modal_visible,
]; ];
const allPickersClosed = visibles.every((item) => !item); const allPickersClosed = visibles.every((item) => !item);
// 所有选择器都关闭时,显示 GuideBar否则隐藏 // 所有选择器都关闭时,显示 GuideBar否则隐藏
@@ -150,7 +138,6 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
location_picker_visible, location_picker_visible,
ntrp_picker_visible, ntrp_picker_visible,
occupation_picker_visible, occupation_picker_visible,
edit_modal_visible,
]); ]);
// 职业数据 // 职业数据
@@ -250,10 +237,10 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
icon: "success", icon: "success",
}); });
} catch (error) { } catch (error) {
console.warn("保存失败:", error); console.error("保存失败:", error);
Taro.showToast({ Taro.showToast({
title: getBackendErrorMsg(error, "保存失败"), title: "保存失败",
icon: "none", icon: "error",
}); });
} }
}; };
@@ -293,10 +280,10 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
icon: "success", icon: "success",
}); });
} catch (error) { } catch (error) {
console.warn("保存失败:", error); console.error("保存失败:", error);
Taro.showToast({ Taro.showToast({
title: getBackendErrorMsg(error, "保存失败"), title: "保存失败",
icon: "none", icon: "error",
}); });
} }
}; };
@@ -308,8 +295,8 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
// 处理地区选择 // 处理地区选择
const handle_location_change = (e: any) => { const handle_location_change = (e: any) => {
const [province, city, district] = e; const [country, province, city] = e;
handle_field_edit({ province, city, district }); handle_field_edit({ country, province, city });
}; };
// 处理NTRP水平选择 // 处理NTRP水平选择
@@ -320,8 +307,8 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
// 处理职业选择 // 处理职业选择
const handle_occupation_change = (e: any) => { const handle_occupation_change = (e: any) => {
const [firstVal, secondVal, thirdVal] = e; const [country, province, city] = e;
handle_field_edit("occupation", `${firstVal} ${secondVal} ${thirdVal}`); handle_field_edit("occupation", `${country} ${province} ${city}`);
}; };
const handle_edit_modal_cancel = () => { const handle_edit_modal_cancel = () => {
// 关闭编辑弹窗时显示 GuideBar // 关闭编辑弹窗时显示 GuideBar
@@ -378,6 +365,7 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
urls: [url], urls: [url],
}); });
}; };
return ( return (
<View className="user_info_card"> <View className="user_info_card">
{/* 头像和基本信息 */} {/* 头像和基本信息 */}
@@ -418,11 +406,11 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
<View className="stats_section"> <View className="stats_section">
<View <View
className="stats_container" className="stats_container"
// style={{ // style={{
// marginBottom: `${ // marginBottom: `${
// collapseProfile && setMarginBottom ? "16px" : "unset" // collapseProfile && setMarginBottom ? "16px" : "unset"
// }`, // }`,
// }} // }}
> >
<View <View
className="stat_item clickable" className="stat_item clickable"
@@ -577,12 +565,12 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
<Text></Text> <Text></Text>
</View> </View>
) : null} ) : null}
{user_info.province || user_info.city || user_info.district ? ( {user_info.country || user_info.province || user_info.city ? (
<View <View
className="tag_item" className="tag_item"
onClick={() => editable && handle_open_edit_modal("location")} onClick={() => editable && handle_open_edit_modal("location")}
> >
<Text className="tag_text">{`${user_info.city}${user_info.district}`}</Text> <Text className="tag_text">{`${user_info.province}${user_info.city}`}</Text>
</View> </View>
) : is_current_user ? ( ) : is_current_user ? (
<View <View
@@ -655,16 +643,16 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
<PopupPicker <PopupPicker
showHeader={true} showHeader={true}
title="选择性别" title="选择性别"
options={ options={[
[ [
{ text: "男", value: "0" }, { text: "男", value: "0" },
{ text: "女", value: "1" }, { text: "女", value: "1" },
{ text: "保密", value: "2" }, { text: "保密", value: "2" },
] ],
} ]}
visible={gender_picker_visible} visible={gender_picker_visible}
setvisible={setGenderPickerVisible} setvisible={setGenderPickerVisible}
value={!form_data.gender ? ["0"] : [form_data.gender]} value={form_data.gender === "" ? ["0"] : [form_data.gender]}
onChange={handle_gender_change} onChange={handle_gender_change}
/> />
)} )}
@@ -677,8 +665,8 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
visible={location_picker_visible} visible={location_picker_visible}
setvisible={setLocationPickerVisible} setvisible={setLocationPickerVisible}
value={ value={
form_data.province form_data.country
? [form_data.province, form_data.city, form_data.district] ? [form_data.country, form_data.province, form_data.city]
: getDefaultOption(cities) : getDefaultOption(cities)
} }
onChange={handle_location_change} onChange={handle_location_change}
@@ -690,12 +678,15 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
showHeader={true} showHeader={true}
title="选择 NTRP 自评水平" title="选择 NTRP 自评水平"
ntrpTested={ntrpTested} ntrpTested={ntrpTested}
options={ntrpLevels} options={ntrpLevels.map((level) => ({
text: level,
value: level,
}))}
type="ntrp" type="ntrp"
img={user_info.avatar_url || ""} img={user_info.avatar_url || ""}
visible={ntrp_picker_visible} visible={ntrp_picker_visible}
setvisible={setNtrpPickerVisible} setvisible={setNtrpPickerVisible}
value={!form_data.ntrp_level ? ["2.5"] : [form_data.ntrp_level]} value={[form_data.ntrp_level || "2.5"]}
onChange={handle_ntrp_level_change} onChange={handle_ntrp_level_change}
/> />
)} )}
@@ -873,8 +864,9 @@ export const GameTabs: React.FC<GameTabsProps> = ({
<Text className="tab_text">{hosted_text}</Text> <Text className="tab_text">{hosted_text}</Text>
</View> </View>
<View <View
className={`tab_item ${active_tab === "participated" ? "active" : "" className={`tab_item ${
}`} active_tab === "participated" ? "active" : ""
}`}
onClick={() => on_tab_change("participated")} onClick={() => on_tab_change("participated")}
> >
<Text className="tab_text">{participated_text}</Text> <Text className="tab_text">{participated_text}</Text>

View File

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

View File

@@ -13,7 +13,7 @@ import orderService from "@/services/orderService";
import styles from "./index.module.scss"; import styles from "./index.module.scss";
import closeIcon from "@/static/order/orderListClose.svg"; import closeIcon from "@/static/order/orderListClose.svg";
function genRefundNotice(refund_policy, order_amount) { function genRefundNotice(refund_policy) {
if (refund_policy.length === 0) { if (refund_policy.length === 0) {
return {}; return {};
} }
@@ -23,7 +23,8 @@ function genRefundNotice(refund_policy, order_amount) {
if (matchPolicyIndex === -1) { if (matchPolicyIndex === -1) {
matchPolicyIndex = refund_policy.length - 1; matchPolicyIndex = refund_policy.length - 1;
} }
const { time_range, price, refund_rate } = refund_policy[matchPolicyIndex]; const { deadline_formatted, price, refund_rate } =
refund_policy[matchPolicyIndex];
if (refund_rate === 1) { if (refund_rate === 1) {
return { return {
refundPrice: price, refundPrice: price,
@@ -32,18 +33,20 @@ function genRefundNotice(refund_policy, order_amount) {
} else if (refund_rate === 0) { } else if (refund_rate === 0) {
return { return {
refundPrice: 0, refundPrice: 0,
notice: `当前退出不可退款,¥${order_amount} 将不予退回`, notice: `当前退出不可退款,后续流程未明确,@麻真瑜`,
}; };
} }
// const refundPrice = Number(Math.ceil(price * refund_rate * 100) / 100); const refundPrice = Number(Math.ceil(price * refund_rate * 100) / 100);
// const leftHours = dayjs(deadline_formatted).diff(dayjs(), "hour"); const leftHours = dayjs(deadline_formatted).diff(dayjs(), "hour");
return { return {
refundPrice: price, refundPrice,
notice: `活动开始${time_range},当前退出需扣除${Math.ceil((order_amount - price) * 100) / 100}`, notice: `活动开始已不足${leftHours}h,当前退出需扣除${
Math.floor((price - refundPrice) * 100) / 100
}`,
}; };
} }
function renderCancelContent(refund_policy = [], amount) { function renderCancelContent(refund_policy = []) {
const current = dayjs(); const current = dayjs();
const policyList = [ const policyList = [
{ {
@@ -62,7 +65,7 @@ function renderCancelContent(refund_policy = [], amount) {
}), }),
]; ];
const targetIndex = policyList.findIndex((item) => item.beforeCurrent); const targetIndex = policyList.findIndex((item) => item.beforeCurrent);
const { notice } = genRefundNotice(refund_policy, amount); const { notice } = genRefundNotice(refund_policy);
return ( return (
<View className={styles.refundPolicy}> <View className={styles.refundPolicy}>
{/* <View className={styles.moduleTitle}> {/* <View className={styles.moduleTitle}>
@@ -77,7 +80,7 @@ function renderCancelContent(refund_policy = [], amount) {
className={classnames( className={classnames(
styles.policyItem, styles.policyItem,
targetIndex > index && index !== 0 ? styles.pastItem : "", targetIndex > index && index !== 0 ? styles.pastItem : "",
targetIndex === index ? styles.currentItem : "", targetIndex === index ? styles.currentItem : ""
)} )}
> >
<View className={styles.time}> <View className={styles.time}>
@@ -166,7 +169,7 @@ export default forwardRef<RefundRef>(function RefundPopup(_props, ref) {
onClick={onClose} onClick={onClose}
/> />
</View> </View>
{renderCancelContent(refundPolicy, orderData.amount)} {renderCancelContent(refundPolicy)}
<Button className={styles.action} onClick={handleConfirmQuit}> <Button className={styles.action} onClick={handleConfirmQuit}>
退 退
</Button> </Button>

View File

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

View File

@@ -1,61 +0,0 @@
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

@@ -1,76 +1,73 @@
import { OSS_BASE } from "@/config/api";
export default { export default {
ICON_REMOVE: require("@/static/publishBall/icon-remove.svg"), ICON_REMOVE: require('@/static/publishBall/icon-remove.svg'),
ICON_UPLOAD: require("@/static/publishBall/icon-upload.svg"), ICON_UPLOAD: require('@/static/publishBall/icon-upload.svg'),
ICON_LOCATION: require("@/static/publishBall/icon-location.svg"), ICON_LOCATION: require('@/static/publishBall/icon-location.svg'),
ICON_GAMEPLAY: require("@/static/publishBall/icon-gameplay.svg"), ICON_GAMEPLAY: require('@/static/publishBall/icon-gameplay.svg'),
ICON_PERSONAL: require("@/static/publishBall/icon-personal.svg"), ICON_PERSONAL: require('@/static/publishBall/icon-personal.svg'),
ICON_CHANGDA: require("@/static/publishBall/icon-changda.svg"), ICON_CHANGDA: require('@/static/publishBall/icon-changda.svg'),
ICON_COST: require("@/static/publishBall/icon-cost.svg"), ICON_COST: require('@/static/publishBall/icon-cost.svg'),
ICON_TIPS: require("@/static/publishBall/icon-tips.svg"), ICON_TIPS: require('@/static/publishBall/icon-tips.svg'),
ICON_ARROW_RIGHT: require("@/static/publishBall/icon-arrow-right.svg"), ICON_ARROW_RIGHT: require('@/static/publishBall/icon-arrow-right.svg'),
ICON_FILTER: require("@/static/list/icon-filter.svg"), ICON_FILTER: require('@/static/list/icon-filter.svg'),
ICON_FILTER_SELECTED: require("@/static/list/icon-filter-selected.svg"), ICON_FILTER_SELECTED: require('@/static/list/icon-filter-selected.svg'),
ICON_SEARCH: require("@/static/list/icon-search.svg"), ICON_SEARCH: require('@/static/list/icon-search.svg'),
ICON_PLAY: require("@/static/list/icon-play.svg"), ICON_PLAY: require('@/static/list/icon-play.svg'),
ICON_SITE: require("@/static/list/icon-site.svg"), ICON_SITE: require('@/static/list/icon-site.svg'),
ICON_ARROW_DOWN: require("@/static/list/icon-arrow-down.svg"), ICON_ARROW_DOWN: require('@/static/list/icon-arrow-down.svg'),
ICON_MENU_ITEM_SELECTED: require("@/static/list/icon-menu-item-selected.svg"), ICON_MENU_ITEM_SELECTED: require('@/static/list/icon-menu-item-selected.svg'),
ICON_ARROW_DOWN_WHITE: require("@/static/list/icon-arrow-down-white.svg"), ICON_ARROW_DOWN_WHITE: require('@/static/list/icon-arrow-down-white.svg'),
ICON_LIST_RIGHT_ARROW: require("@/static/list/icon-list-right-arrow.svg"), ICON_LIST_RIGHT_ARROW: require('@/static/list/icon-list-right-arrow.svg'),
ICON_ARROW_LEFT: require("@/static/detail/icon-arrow-left.svg"), ICON_ARROW_LEFT: require('@/static/detail/icon-arrow-left.svg'),
ICON_LOGO_GO: require("@/static/detail/icon-logo-go.svg"), ICON_LOGO_GO: require('@/static/detail/icon-logo-go.svg'),
ICON_MAP: require("@/static/publishBall/icon-map.svg"), ICON_MAP: require('@/static/publishBall/icon-map.svg'),
ICON_STADIUM: require("@/static/publishBall/icon-stadium.svg"), ICON_STADIUM: require('@/static/publishBall/icon-stadium.svg'),
ICON_ARRORW_SMALL: require("@/static/publishBall/icon-arrow-small.svg"), ICON_ARRORW_SMALL: require('@/static/publishBall/icon-arrow-small.svg'),
ICON_MAP_SEARCH: require("@/static/publishBall/icon-map-search.svg"), ICON_MAP_SEARCH: require('@/static/publishBall/icon-map-search.svg'),
ICON_HEART_CIRCLE: require("@/static/publishBall/icon-heartcircle.png"), ICON_HEART_CIRCLE: require('@/static/publishBall/icon-heartcircle.png'),
ICON_ADD: require("@/static/publishBall/icon-add.svg"), ICON_ADD: require('@/static/publishBall/icon-add.svg'),
ICON_COPY: require("@/static/publishBall/icon-arrow-right.svg"), ICON_COPY: require('@/static/publishBall/icon-arrow-right.svg'),
ICON_DELETE: require("@/static/publishBall/icon-delete.svg"), ICON_DELETE: require('@/static/publishBall/icon-delete.svg'),
ICON_RIGHT_MAX: require("@/static/publishBall/icon-right-max.svg"), ICON_RIGHT_MAX: require('@/static/publishBall/icon-right-max.svg'),
ICON_PLUS: require("@/static/publishBall/icon-plus.svg"), ICON_PLUS: require('@/static/publishBall/icon-plus.svg'),
ICON_GROUP: require("@/static/publishBall/icon-group.svg"), ICON_GROUP: require('@/static/publishBall/icon-group.svg'),
ICON_PERSON: require("@/static/publishBall/icon-person.svg"), ICON_PERSON: require('@/static/publishBall/icon-person.svg'),
ICON_PUBLISH: require("@/static/publishBall/icon-publish.png"), ICON_PUBLISH: require('@/static/publishBall/icon-publish.png'),
ICON_CIRCLE_UNSELECT: require("@/static/publishBall/icon-circle-unselect.svg"), ICON_CIRCLE_UNSELECT: require('@/static/publishBall/icon-circle-unselect.svg'),
ICON_CIRCLE_SELECT: require("@/static/publishBall/icon-circle-select-ring.svg"), ICON_CIRCLE_SELECT: require('@/static/publishBall/icon-circle-select-ring.svg'),
ICON_CIRCLE_SELECT_ARROW: require("@/static/publishBall/icon-circle-select-arrow.svg"), ICON_CIRCLE_SELECT_ARROW: require('@/static/publishBall/icon-circle-select-arrow.svg'),
ICON_LOGO: require("@/static/logo.svg"), ICON_LOGO: require('@/static/logo.svg'),
ICON_CHANGE: require("@/static/list/icon-change.svg"), ICON_CHANGE: require('@/static/list/icon-change.svg'),
ICON_DETAIL_MAP: require("@/static/detail/icon-map.svg"), ICON_DETAIL_MAP: require('@/static/detail/icon-map.svg'),
ICON_DETAIL_ARROW_RIGHT: require("@/static/detail/icon-arrow-right.svg"), ICON_DETAIL_ARROW_RIGHT: require('@/static/detail/icon-arrow-right.svg'),
ICON_DETAIL_NOTICE: require("@/static/detail/icon-notice.svg"), ICON_DETAIL_NOTICE: require('@/static/detail/icon-notice.svg'),
ICON_DETAIL_APPLICATION_ADD: require("@/static/detail/icon-application-add.svg"), ICON_DETAIL_APPLICATION_ADD: require('@/static/detail/icon-application-add.svg'),
ICON_DETAIL_COMMENT: require("@/static/detail/icon-comment.svg"), ICON_DETAIL_COMMENT: require('@/static/detail/icon-comment.svg'),
ICON_DETAIL_COMMENT_LIGHT: require("@/static/detail/icon-comment-light.svg"), ICON_DETAIL_COMMENT_LIGHT: require('@/static/detail/icon-comment-light.svg'),
ICON_DETAIL_SHARE: require("@/static/detail/icon-share-light.svg"), ICON_DETAIL_SHARE: require('@/static/detail/icon-share-light.svg'),
ICON_GUIDE_BAR_PUBLISH: require("@/static/common/guide-bar-publish.svg"), ICON_GUIDE_BAR_PUBLISH: require('@/static/common/guide-bar-publish.svg'),
ICON_NAVIGATOR_BACK: require("@/static/common/navigator-back.svg"), ICON_NAVIGATOR_BACK: require('@/static/common/navigator-back.svg'),
ICON_LIST_PLAYING_GAME: require("@/static/list/icon-paying-game.svg"), ICON_LIST_PLAYING_GAME: require('@/static/list/icon-paying-game.svg'),
ICON_LIST_LOAD_ERROR: require("@/static/list/icon-load-error.svg"), ICON_LIST_LOAD_ERROR: require('@/static/list/icon-load-error.svg'),
ICON_LIST_RELOAD: require("@/static/list/icon-reload.svg"), ICON_LIST_RELOAD: require('@/static/list/icon-reload.svg'),
ICON_LIST_EMPTY: require("@/static/emptyStatus/publish-empty.png"), ICON_LIST_EMPTY: require('@/static/emptyStatus/publish-empty.png'),
ICON_LIST_EMPTY_CARD: `${OSS_BASE}/front/ball/images/publish-empty-card.png`, ICON_LIST_EMPTY_CARD: require('@/static/emptyStatus/publish-empty-card.png'),
ICON_LIST_SEARCH_SEARCH: require("@/static/search/icon-search.svg"), ICON_LIST_SEARCH_SEARCH: require('@/static/search/icon-search.svg'),
ICON_LIST_SEARCH_BACK: require("@/static/search/icon-back.svg"), ICON_LIST_SEARCH_BACK: require('@/static/search/icon-back.svg'),
ICON_LIST_SEARCH_CLEAR: require("@/static/search/icon-search-clear.svg"), ICON_LIST_SEARCH_CLEAR: require('@/static/search/icon-search-clear.svg'),
ICON_LIST_SEARCH_CLEAR_HISTORY: require("@/static/search/icon-clear-history.svg"), ICON_LIST_SEARCH_CLEAR_HISTORY: require('@/static/search/icon-clear-history.svg'),
ICON_LIST_SEARCH_SUGGESTION: require("@/static/search/icon-search-suggestion.svg"), ICON_LIST_SEARCH_SUGGESTION: require('@/static/search/icon-search-suggestion.svg'),
ICON_LIST_INPUT_LOGO: require("@/static/list/icon-input-logo.svg"), ICON_LIST_INPUT_LOGO: require('@/static/list/icon-input-logo.svg'),
ICON_IMPORTANT_BTN: require("@/static/publishBall/icon-important-btn.svg"), ICON_IMPORTANT_BTN: require('@/static/publishBall/icon-important-btn.svg'),
ICON_IMPORTANT_BLACK: require("@/static/publishBall/icon-important-black.svg"), ICON_IMPORTANT_BLACK: require('@/static/publishBall/icon-important-black.svg'),
ICON_ARROW_RIGHT_WHITE: require("@/static/publishBall/icon-arrow-right-white.svg"), ICON_ARROW_RIGHT_WHITE: require('@/static/publishBall/icon-arrow-right-white.svg'),
ICON_ARROW_RIGHT_BLACK: require("@/static/publishBall/icon-arrow-right-black.svg"), ICON_ARROW_RIGHT_BLACK: require('@/static/publishBall/icon-arrow-right-black.svg'),
ICON_EXAMINATION: require("@/static/userInfo/examination.svg"), ICON_EXAMINATION: require('@/static/userInfo/examination.svg'),
ICON_ARROW_GREEN: require("@/static/userInfo/arrow-green.svg"), ICON_ARROW_GREEN: require('@/static/userInfo/arrow-green.svg'),
ICON_COPY: require("@/static/publishBall/icon-copy.svg"), ICON_COPY: require('@/static/publishBall/icon-copy.svg'),
ICON_UPLOAD_IMG: require("@/static/publishBall/icon-upload-img.svg"), ICON_UPLOAD_IMG: require('@/static/publishBall/icon-upload-img.svg'),
ICON_UPLOAD_SUCCESS: require("@/static/publishBall/icon-upload-success.svg"), ICON_UPLOAD_SUCCESS: require('@/static/publishBall/icon-upload-success.svg'),
ICON_CLOSE: require("@/static/publishBall/icon-close.svg"), ICON_CLOSE: require('@/static/publishBall/icon-close.svg'),
ICON_LIST_NTPR: require("@/static/list/ntpr.svg"), ICON_LIST_NTPR: require('@/static/list/ntpr.svg'),
ICON_LIST_CHANGDA: require("@/static/list/icon-changda.svg"), ICON_LIST_CHANGDA: require('@/static/list/icon-changda.svg'),
ICON_LIST_CHANGDA_QIuju: require("@/static/list/changdaqiuju.png"), ICON_LIST_CHANGDA_QIuju: require('@/static/list/changdaqiuju.png'),
ICON_RELOCATE: require("@/static/list/icon-relocate.svg"), }
ICON_GAME_PLAY: require("@/static/list/icon_game_type.svg"),
};

View File

@@ -4,17 +4,12 @@ import ListLoadError from "@/components/ListLoadError";
import ListCardSkeleton from "@/components/ListCardSkeleton"; import ListCardSkeleton from "@/components/ListCardSkeleton";
import { useReachBottom } from "@tarojs/taro"; import { useReachBottom } from "@tarojs/taro";
import Taro from "@tarojs/taro"; import Taro from "@tarojs/taro";
import { import { useUserInfo, useUserActions, useLastTestResult } from "@/store/userStore";
useUserInfo,
useUserActions,
useLastTestResult,
} from "@/store/userStore";
import { NTRPTestEntryCard } from "@/components"; import { NTRPTestEntryCard } from "@/components";
import { EvaluateScene } from "@/store/evaluateStore"; import { EvaluateScene } from "@/store/evaluateStore";
import { waitForAuthInit } from "@/utils/authInit"; import { waitForAuthInit } from "@/utils/authInit";
import "./index.scss"; import "./index.scss";
import { useRef, useEffect, useState, useMemo } from "react"; import { useRef, useEffect, useState, useMemo } from "react";
import { useDictionaryStore } from "@/store/dictionaryStore";
const ListContainer = (props) => { const ListContainer = (props) => {
const { const {
@@ -33,7 +28,6 @@ const ListContainer = (props) => {
collapse = false, collapse = false,
defaultShowNum, defaultShowNum,
evaluateFlag, evaluateFlag,
enableHomeCards = false, // 仅首页需要 banner 和 NTRP 测评卡片
listLoadErrorWrapperHeight, listLoadErrorWrapperHeight,
listLoadErrorWidth, listLoadErrorWidth,
listLoadErrorHeight, listLoadErrorHeight,
@@ -50,11 +44,7 @@ const ListContainer = (props) => {
const { fetchUserInfo, fetchLastTestResult } = useUserActions(); const { fetchUserInfo, fetchLastTestResult } = useUserActions();
// 使用全局状态中的测试结果,避免重复调用接口 // 使用全局状态中的测试结果,避免重复调用接口
const lastTestResult = useLastTestResult(); const lastTestResult = useLastTestResult();
const {
bannerListImage,
bannerDetailImage,
bannerListIndex = 0,
} = useDictionaryStore((s) => s.bannerDict) || {};
useReachBottom(() => { useReachBottom(() => {
// 加载更多方法 // 加载更多方法
if (loading) { if (loading) {
@@ -103,14 +93,14 @@ const ListContainer = (props) => {
}; };
}, []); }, []);
// 获取测试结果,判断最近一个月是否有测试记录(仅首页需要) // 获取测试结果,判断最近一个月是否有测试记录
useEffect(() => { useEffect(() => {
const init = async () => { const init = async () => {
if (!evaluateFlag || !enableHomeCards) return; if (!evaluateFlag) return;
// 先等待静默登录完成 // 先等待静默登录完成
await waitForAuthInit(); await waitForAuthInit();
// 然后再获取用户信息 // 然后再获取用户信息
const userInfoId = userInfo && "id" in userInfo ? userInfo.id : null; const userInfoId = userInfo && 'id' in userInfo ? userInfo.id : null;
if (!userInfoId) { if (!userInfoId) {
await fetchUserInfo(); await fetchUserInfo();
return; // 等待下一次 useEffect 触发(此时 userInfo.id 已有值) return; // 等待下一次 useEffect 触发(此时 userInfo.id 已有值)
@@ -121,13 +111,7 @@ const ListContainer = (props) => {
} }
}; };
init(); init();
}, [ }, [evaluateFlag, userInfo, lastTestResult, fetchLastTestResult]);
evaluateFlag,
enableHomeCards,
userInfo,
lastTestResult,
fetchLastTestResult,
]);
// 从全局状态中获取测试状态 // 从全局状态中获取测试状态
const hasTestInLastMonth = lastTestResult?.has_test_in_last_month || false; const hasTestInLastMonth = lastTestResult?.has_test_in_last_month || false;
@@ -146,99 +130,68 @@ const ListContainer = (props) => {
); );
}; };
// showNumber 为 0 表示尚未同步,不参与截断;截断时只限制「数据条数」,插卡不占数据条数 // 对于没有ntrp等级的用户每个月展示一次, 插在第二个位置后面
const shouldLimitByShowNumber = showNumber > 0; function insertEvaluateCard(list) {
if (!evaluateFlag)
// 插入 banner 卡片(在 bannerListIndex 位置插入,不替换数据) return showNumber !== undefined ? list.slice(0, showNumber) : list;
function insertBannerCard(list) { if (!list || list.length === 0) {
if (!bannerListImage) return list; return list;
if (!list || !Array.isArray(list)) {
list = [];
} }
const idx = Number(bannerListIndex); // 如果最近一个月有测试记录,则不插入 card
if (hasTestInLastMonth) {
return showNumber !== undefined ? list.slice(0, showNumber) : list;
}
if (list.length <= 2) {
return [...list, { type: "evaluateCard" }];
}
const [item1, item2, ...rest] = list;
return [ return [
...list.slice(0, idx), item1,
{ item2,
type: "banner", { type: "evaluateCard" },
banner_image_url: bannerListImage, ...(showNumber !== undefined ? rest.slice(0, showNumber - 3) : rest),
banner_detail_url: bannerDetailImage,
},
...list.slice(idx),
]; ];
} }
// 对于没有 ntrp 等级的用户每个月展示一次,插在第 2 条数据后面;插卡是插入不替换,保留全部 showNumber 条数据
function insertEvaluateCard(list) {
if (!list || !Array.isArray(list)) return insertBannerCard(list ?? []);
const limitedList = shouldLimitByShowNumber
? list.slice(0, showNumber)
: list;
if (!evaluateFlag || hasTestInLastMonth) {
return insertBannerCard(limitedList);
}
if (limitedList.length <= 2) {
return insertBannerCard([...limitedList, { type: "evaluateCard" }]);
}
const [item1, item2, ...rest] = limitedList;
const result = [item1, item2, { type: "evaluateCard" }, ...rest];
return insertBannerCard(result);
}
const memoizedList = useMemo( const memoizedList = useMemo(
() => (enableHomeCards ? insertEvaluateCard(data) : data), () => insertEvaluateCard(data),
[ [evaluateFlag, data, hasTestInLastMonth, showNumber]
enableHomeCards,
evaluateFlag,
data,
hasTestInLastMonth,
showNumber,
bannerListImage,
bannerDetailImage,
bannerListIndex,
]
); );
// 渲染 banner 卡片 // 渲染 banner 卡片
const renderBanner = (item, index) => { const renderBanner = (item, index) => {
if (!item?.banner_image_url) { if (!item?.banner_image_url) return null;
return null;
}
return ( return (
<View <View
key={item.id || `banner-${index}`} key={item.id || `banner-${index}`}
onClick={() => {
const target = item.banner_detail_url;
if (target) {
(Taro as any).navigateTo({
url: `/other_pages/bannerDetail/index?img=${encodeURIComponent(
target
)}`,
});
}
}}
style={{ style={{
height: "100px", maxHeight: "122px",
overflow: "hidden", overflow: "hidden",
borderRadius: "12px", borderRadius: "12px",
backgroundImage: `url(${item.banner_image_url})`,
backgroundSize: "cover",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
}} }}
></View> >
<Image
src={item.banner_image_url}
mode="widthFix"
style={{ width: "100%", display: "block", maxHeight: "122px" }}
onClick={() => {
const target = item.banner_detail_url;
if (target) {
(Taro as any).navigateTo({
url: `/other_pages/bannerDetail/index?img=${encodeURIComponent(target)}`,
});
}
}}
/>
</View>
); );
}; };
const showNoData = isShowNoData && !loading && memoizedList?.length === 0;
// 渲染列表 // 渲染列表
const renderList = () => { const renderList = () => {
// 请求数据为空 // 请求数据为空
if (showNoData) { if (isShowNoData) {
return ( return (
<ListLoadError <ListLoadError
reload={reload} reload={reload}
@@ -258,15 +211,12 @@ const ListContainer = (props) => {
return ( return (
<> <>
{memoizedList.map((match, index) => { {memoizedList.map((match, index) => {
if (enableHomeCards && match?.type === "banner") { if (match.type === "banner") {
return renderBanner(match, index); return renderBanner(match, index);
} }
if (enableHomeCards && match?.type === "evaluateCard") { if (match.type === "evaluateCard") {
return ( return (
<NTRPTestEntryCard <NTRPTestEntryCard key="evaluate" type={EvaluateScene.list} />
key={`evaluate-${index}`}
type={EvaluateScene.list}
/>
); );
} }
return <ListCard key={match?.id || index} {...match} />; return <ListCard key={match?.id || index} {...match} />;

View File

@@ -1,13 +1,27 @@
import Taro from "@tarojs/taro"; import Taro from "@tarojs/taro";
import dayjs, { Dayjs } from "dayjs"; import dayjs, { Dayjs } from "dayjs";
import "dayjs/locale/zh-cn"; import "dayjs/locale/zh-cn";
import { calculateDistance, genGameLength } from "@/utils"; import { calculateDistance } from "@/utils";
import { View, Image, Text, Map } from "@tarojs/components"; import { View, Image, Text, Map } from "@tarojs/components";
import img from "@/config/images"; import img from "@/config/images";
import styles from "./index.module.scss"; import styles from "./index.module.scss";
dayjs.locale("zh-cn"); dayjs.locale("zh-cn");
function genGameLength(startTime: Dayjs, endTime: Dayjs) {
if (!startTime || !endTime) {
return "";
}
const hours = endTime.diff(startTime, "hour");
if (Math.floor(hours / 24) >= 1) {
const leftHours = Math.floor(hours % 24);
return `${Math.floor(hours / 24)}${
leftHours !== 0 ? `${leftHours}小时` : ""
}`;
}
return `${hours}小时`;
}
function genGameRange(startTime: Dayjs, endTime: Dayjs) { function genGameRange(startTime: Dayjs, endTime: Dayjs) {
if (!startTime || !endTime) { if (!startTime || !endTime) {
return ""; return "";

View File

@@ -177,32 +177,23 @@
} }
&-title { &-title {
display: flex; display: flex;
align-items: center; align-items: center;
height: 24px; height: 24px;
gap: 2px; gap: 2px;
overflow: hidden; overflow: hidden;
color: rgba(255, 255, 255, 0.85); color: rgba(255, 255, 255, 0.85);
text-overflow: ellipsis; text-overflow: ellipsis;
font-size: 16px; font-size: 16px;
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
line-height: 24px; /* 150% */ line-height: 24px; /* 150% */
&-text { &-arrow {
flex: 1; width: 12px;
min-width: 0; height: 12px;
overflow: hidden; }
text-overflow: ellipsis; }
white-space: nowrap;
}
&-arrow {
flex: 0 0 12px;
width: 12px;
height: 12px;
}
}
&-time-range { &-time-range {
overflow: hidden; overflow: hidden;

View File

@@ -48,8 +48,8 @@ function genRecommendGames(games, location, avatar) {
formatNtrpDisplay(skill_level_max) || "-" formatNtrpDisplay(skill_level_max) || "-"
}` }`
: skill_level_min === "1" : skill_level_min === "1"
? "无要求" ? "无要求"
: `${formatNtrpDisplay(skill_level_min)}以上`, : `${formatNtrpDisplay(skill_level_min)}以上`,
playType: play_type, playType: play_type,
}; };
}); });
@@ -86,12 +86,12 @@ export default function OrganizerInfo(props) {
await LoginService.followUser(id); await LoginService.followUser(id);
} }
onUpdateUserInfo(); onUpdateUserInfo();
(Taro as any).showToast({ Taro.showToast({
title: `${nickname} ${follow ? "已取消关注" : "已关注"}`, title: `${nickname} ${follow ? "已取消关注" : "已关注"}`,
icon: "success", icon: "success",
}); });
} catch (e) { } catch (e) {
(Taro as any).showToast({ Taro.showToast({
title: `${nickname} ${follow ? "取消关注失败" : "关注失败"}`, title: `${nickname} ${follow ? "取消关注失败" : "关注失败"}`,
icon: "error", icon: "error",
}); });
@@ -193,17 +193,13 @@ export default function OrganizerInfo(props) {
className={styles["recommend-games-list-item"]} className={styles["recommend-games-list-item"]}
onClick={handleViewGame.bind(null, game.id)} onClick={handleViewGame.bind(null, game.id)}
> >
{/* game title */} {/* game title */}
<View className={styles["recommend-games-list-item-title"]}> <View className={styles["recommend-games-list-item-title"]}>
<Text <Text>{game.title}</Text>
className={styles["recommend-games-list-item-title-text"]} <Image
> className={
{game.title} styles["recommend-games-list-item-title-arrow"]
</Text> }
<Image
className={
styles["recommend-games-list-item-title-arrow"]
}
src={img.ICON_DETAIL_ARROW_RIGHT} src={img.ICON_DETAIL_ARROW_RIGHT}
/> />
</View> </View>
@@ -224,9 +220,7 @@ export default function OrganizerInfo(props) {
> >
<Text>{game.venue}</Text> <Text>{game.venue}</Text>
<Text>·</Text> <Text>·</Text>
<Text style={{ whiteSpace: "nowrap" }}> <Text>{game.venueType}</Text>
{game.venueType}
</Text>
<Text>·</Text> <Text>·</Text>
<Text>{game.distance}</Text> <Text>{game.distance}</Text>
</View> </View>
@@ -253,7 +247,7 @@ export default function OrganizerInfo(props) {
styles[ styles[
"recommend-games-list-item-addon-message-applications" "recommend-games-list-item-addon-message-applications"
], ],
styles.joinMsg, styles.joinMsg
)} )}
> >
<Text></Text> <Text></Text>

View File

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

View File

@@ -1,7 +1,7 @@
import { forwardRef, useState, useEffect, useImperativeHandle } from "react"; import { forwardRef, useState, useEffect, useImperativeHandle } from "react";
import { View, Button, Image, Text } from "@tarojs/components"; import { View, Button, Image, Text } from "@tarojs/components";
import Taro, { useShareAppMessage } from "@tarojs/taro"; import Taro, { useShareAppMessage } from "@tarojs/taro";
import dayjs, { Dayjs } from "dayjs"; import dayjs from "dayjs";
import "dayjs/locale/zh-cn"; import "dayjs/locale/zh-cn";
import classnames from "classnames"; import classnames from "classnames";
import { generateShareImage } from "@/utils"; import { generateShareImage } from "@/utils";
@@ -12,15 +12,10 @@ import WechatLogo from "@/static/detail/wechat_icon.svg";
// import WechatTimeline from "@/static/detail/wechat_timeline.svg"; // import WechatTimeline from "@/static/detail/wechat_timeline.svg";
import LinkIcon from "@/static/detail/link.svg"; import LinkIcon from "@/static/detail/link.svg";
import CrossIcon from "@/static/detail/cross.svg"; import CrossIcon from "@/static/detail/cross.svg";
import { import { genNTRPRequirementText, navto } from "@/utils/helper";
genNTRPRequirementText,
navto,
genGameLength,
formatGameStartTime,
} from "@/utils/helper";
import { waitForAuthInit } from "@/utils/authInit"; import { waitForAuthInit } from "@/utils/authInit";
import { useUserActions } from "@/store/userStore"; import { useUserActions } from "@/store/userStore";
import { OSS_BASE } from "@/config/api"; import { OSS_BASE_URL } from "@/config/api";
import { generatePosterImage, base64ToTempFilePath, delay } from "@/utils"; import { generatePosterImage, base64ToTempFilePath, delay } from "@/utils";
import { DayOfWeekMap } from "../../config"; import { DayOfWeekMap } from "../../config";
import styles from "./index.module.scss"; import styles from "./index.module.scss";
@@ -30,22 +25,9 @@ dayjs.locale("zh-cn");
// 分享弹窗 // 分享弹窗
export default forwardRef(({ id, from, detail, userInfo }, ref) => { export default forwardRef(({ id, from, detail, userInfo }, ref) => {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [publishFlag, setPublishFlag] = useState(false);
const [shareImageUrl, setShareImageUrl] = useState(""); const [shareImageUrl, setShareImageUrl] = useState("");
const { fetchUserInfo } = useUserActions(); const { fetchUserInfo } = useUserActions();
async function ensureUserInfo() {
if (userInfo?.avatar_url && userInfo?.nickname) {
return userInfo;
}
const fetchedUserInfo = await fetchUserInfo();
return {
avatar_url: fetchedUserInfo?.avatar_url || userInfo?.avatar_url || "",
nickname: fetchedUserInfo?.nickname || userInfo?.nickname || "",
};
}
const publishFlag = from === "publish";
// const posterRef = useRef(); // const posterRef = useRef();
const { max_participants, participant_count } = detail || {}; const { max_participants, participant_count } = detail || {};
@@ -67,16 +49,6 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
withShareTicket: false, // 是否需要返回 shareTicket withShareTicket: false, // 是否需要返回 shareTicket
isUpdatableMessage: true, // 是否是动态消息(需要服务端配置过模版) isUpdatableMessage: true, // 是否是动态消息(需要服务端配置过模版)
activityId: res.data.activity_id, // 动态消息的活动 id activityId: res.data.activity_id, // 动态消息的活动 id
templateInfo: {
parameterList: [
{
name: "member_count",
value: (participant_count ?? 0).toString(),
},
{ name: "room_limit", value: (max_participants ?? 0).toString() },
],
templateId: "666F374D69D16C932E45D7E7D9F10CEF6177F5F5",
},
}); });
} }
} catch (e) { } catch (e) {
@@ -85,20 +57,16 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
} }
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
show: async () => { show: async (publish_flag = false) => {
setPublishFlag(publish_flag);
if (publish_flag) {
const url = await generateShareImageUrl();
setShareImageUrl(url);
}
setVisible(true); setVisible(true);
}, },
})); }));
useEffect(() => {
if (from === "publish") {
generateShareImageUrl().then((url) => {
setShareImageUrl(url);
setVisible(true);
});
}
}, [from]);
async function generateShareImageUrl() { async function generateShareImageUrl() {
const { const {
play_type, play_type,
@@ -112,37 +80,30 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
const startTime = dayjs(start_time); const startTime = dayjs(start_time);
const endTime = dayjs(end_time); const endTime = dayjs(end_time);
const dayofWeek = DayOfWeekMap.get(startTime.day()); const dayofWeek = DayOfWeekMap.get(startTime.day());
const gameLength = genGameLength(startTime, endTime); const gameLength = `${endTime.diff(startTime, "hour")}小时`;
const currentUserInfo = await ensureUserInfo(); const url = await generateShareImage({
try { userAvatar: userInfo.avatar_url,
const url = await generateShareImage({ userNickname: userInfo.nickname,
userAvatar: currentUserInfo.avatar_url, gameType: play_type,
userNickname: currentUserInfo.nickname, skillLevel: `NTRP ${genNTRPRequirementText(
gameType: play_type, skill_level_min,
skillLevel: `NTRP ${genNTRPRequirementText( skill_level_max
skill_level_min, )}`,
skill_level_max, gameDate: `${startTime.format("M月D日")} (${dayofWeek})`,
)}`, gameTime: `${startTime.format("ah")}${gameLength}`,
gameDate: `${startTime.format("M月D日")} (${dayofWeek})`, venueName: location_name,
gameTime: `${formatGameStartTime(startTime)} ${gameLength}`, venueImages: image_list ? image_list : [],
venueName: location_name, });
venueImages: image_list ? image_list : [], return url;
});
if (!url) {
throw new Error("生成分享图片失败URL 为空");
}
return url;
} catch (e) {
console.error("生成分享卡片失败", e);
return `${OSS_BASE}/system/game_dou_di_tu.png`;
}
} }
useShareAppMessage(async () => { useShareAppMessage(async (res) => {
const url = shareImageUrl || (await generateShareImageUrl()); await changeMessageType();
const url = await generateShareImageUrl();
// console.log(res, "res");
return { return {
title: detail.title, title: detail.title,
imageUrl: url, imageUrl: url || "https://img.yzcdn.cn/vant/cat.jpeg",
path: `/game_pages/detail/index?id=${id}&from=share`, path: `/game_pages/detail/index?id=${id}&from=share`,
}; };
}); });
@@ -161,49 +122,37 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
} = detail || {}; } = detail || {};
// 先等待静默登录完成 // 先等待静默登录完成
await waitForAuthInit(); await waitForAuthInit();
const currentUserInfo = await ensureUserInfo(); const userInfo = await fetchUserInfo();
const { avatar_url, nickname } = currentUserInfo; const { avatar_url, nickname } = userInfo;
const startTime = dayjs(start_time); const startTime = dayjs(start_time);
const endTime = dayjs(end_time); const endTime = dayjs(end_time);
const dayofWeek = DayOfWeekMap.get(startTime.day()); const dayofWeek = DayOfWeekMap.get(startTime.day());
const game_length = genGameLength(startTime, endTime); const gameLength = `${endTime.diff(startTime, "hour")}小时`;
let qrCodeUrl = ""; Taro.showLoading({ title: "生成中..." });
try { const qrCodeUrlRes = await DetailService.getQrCodeUrl({
const qrCodeUrlRes = await DetailService.getQrCodeUrl({ page: "game_pages/detail/index",
page: "game_pages/detail/index", scene: `id=${id}`,
scene: `id=${id}`, });
}); const qrCodeUrl = await base64ToTempFilePath(
qrCodeUrl = qrCodeUrlRes.data.ossPath; qrCodeUrlRes.data.qr_code_base64
} catch (e) { );
Taro.showToast({ title: "获取二维码失败", icon: "error" });
return;
}
await delay(100); await delay(100);
console.log("url", qrCodeUrl); const url = await generatePosterImage({
let url = ""; playType: play_type,
try { ntrp: `NTRP ${genNTRPRequirementText(skill_level_min, skill_level_max)}`,
url = await generatePosterImage({ mainCoursal:
playType: play_type, image_list[0] && image_list[0].startsWith("http")
ntrp: `NTRP ${genNTRPRequirementText(skill_level_min, skill_level_max)}`, ? image_list[0]
mainCoursal: : `${OSS_BASE_URL}/images/0621b8cf-f7d6-43ad-b852-7dc39f29a782.png`,
image_list[0] && image_list[0].startsWith("http") nickname,
? image_list[0] avatarUrl: avatar_url,
: `${OSS_BASE}/front/ball/images/0621b8cf-f7d6-43ad-b852-7dc39f29a782.png`, title,
nickname, locationName: location_name,
avatarUrl: avatar_url, date: `${startTime.format("M月D日")} (${dayofWeek})`,
title, time: `${startTime.format("ah")}${gameLength}`,
locationName: location_name, qrCodeUrl,
date: `${startTime.format("M月D日")} (${dayofWeek})`, });
time: `${formatGameStartTime(startTime)} ${game_length}`, Taro.hideLoading();
qrCodeUrl,
});
} catch (e) {
Taro.showToast({ title: "生成海报失败,请重试", icon: "error" });
return;
}
console.log("urlend", url);
// Taro.hideLoading();
Taro.showShareImageMenu({ Taro.showShareImageMenu({
path: url, path: url,
}); });
@@ -215,20 +164,9 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
setVisible(false); setVisible(false);
} }
async function handleCopyLink() {
const linkUrlRes = await DetailService.getLinkUrl({
path: "game_pages/detail/index",
query: `id=${id}`,
});
await Taro.setClipboardData({
data: linkUrlRes.data.url_link,
});
Taro.showToast({ title: "链接已复制到剪贴板", icon: "success" });
setVisible(false);
}
function onClose() { function onClose() {
setVisible(false); setVisible(false);
setPublishFlag(false);
} }
return ( return (
@@ -255,14 +193,14 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
<View <View
className={styles.contentContainer} className={styles.contentContainer}
style={{ style={{
backgroundImage: `url(${OSS_BASE}/front/ball/images/215f1ce1-be52-4a92-8250-5a4a69e7f2b3.png)`, backgroundImage: `url(${OSS_BASE_URL}/images/215f1ce1-be52-4a92-8250-5a4a69e7f2b3.png)`,
}} }}
> >
<View <View
catchMove catchMove
className={classnames( className={classnames(
styles.title, styles.title,
publishFlag ? styles.publishTitle : "", publishFlag ? styles.publishTitle : ""
)} )}
> >
{publishFlag ? ( {publishFlag ? (
@@ -316,7 +254,7 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
</View> </View>
</View> </View>
<View className={styles.customBtnWrapper}> <View className={styles.customBtnWrapper}>
<Button className={styles.button} onClick={handleCopyLink}> <Button className={styles.button}>
<View className={styles.icon}> <View className={styles.icon}>
<Image className={styles.linkIcon} src={LinkIcon} /> <Image className={styles.linkIcon} src={LinkIcon} />
</View> </View>

View File

@@ -24,12 +24,12 @@ function isFull(counts) {
} = counts; } = counts;
if ( if (
current_players >= max_players && max_players === current_players &&
is_substitute_supported === IsSubstituteSupported.NOTSUPPORT is_substitute_supported === IsSubstituteSupported.NOTSUPPORT
) { ) {
return true; return true;
} else if ( } else if (
current_players >= max_players && max_players === current_players &&
is_substitute_supported === IsSubstituteSupported.SUPPORT is_substitute_supported === IsSubstituteSupported.SUPPORT
) { ) {
return max_substitute_players === current_substitute_count; return max_substitute_players === current_substitute_count;
@@ -45,7 +45,7 @@ function RmbIcon() {
function matchNtrpRequestment( function matchNtrpRequestment(
target?: string, target?: string,
min?: string, min?: string,
max?: string, max?: string
): boolean { ): boolean {
// 目标值为空或 undefined // 目标值为空或 undefined
if (!target?.trim()) return true; if (!target?.trim()) return true;
@@ -123,7 +123,7 @@ export default function StickyButton(props) {
Taro.navigateTo({ Taro.navigateTo({
url: `/login_pages/index/index?redirect=${encodeURIComponent( url: `/login_pages/index/index?redirect=${encodeURIComponent(
fullPath, fullPath
)}`, )}`,
}); });
} }
@@ -138,7 +138,7 @@ export default function StickyButton(props) {
const matchNtrpReq = matchNtrpRequestment( const matchNtrpReq = matchNtrpRequestment(
ntrp_level, ntrp_level,
skill_level_min, skill_level_min,
skill_level_max, skill_level_max
); );
const gameManageRef = useRef(); const gameManageRef = useRef();
@@ -173,7 +173,7 @@ export default function StickyButton(props) {
}, [getCommentCount]); }, [getCommentCount]);
function generateTextAndAction( function generateTextAndAction(
user_action_status: null | { [key: string]: boolean }, user_action_status: null | { [key: string]: boolean }
): ):
| undefined | undefined
| { text: string | React.FC; action?: () => void; available?: boolean } { | { text: string | React.FC; action?: () => void; available?: boolean } {
@@ -271,7 +271,7 @@ export default function StickyButton(props) {
const res = await OrderService.getUnpaidOrder(id); const res = await OrderService.getUnpaidOrder(id);
if (res.code === 0) { if (res.code === 0) {
navto( navto(
`/order_pages/orderDetail/index?id=${res.data.order_info.order_id}`, `/order_pages/orderDetail/index?id=${res.data.order_info.order_id}`
); );
} }
}), }),
@@ -387,7 +387,7 @@ export default function StickyButton(props) {
<View <View
className={classnames( className={classnames(
styles["detail-main-action"], styles["detail-main-action"],
available ? "" : styles.disabled, available ? "" : styles.disabled
)} )}
> >
<View <View

View File

@@ -16,11 +16,6 @@ export default function VenueInfo(props) {
venue_image_list = [], venue_image_list = [],
} = detail; } = detail;
// 统一为 URL 数组:接口可能是 { id, url }[] 或 string[]
const screenshot_urls = (venue_image_list || []).map((item) =>
typeof item === "string" ? item : (item?.url ?? "")
).filter(Boolean);
function showScreenShot() { function showScreenShot() {
setVisible(true); setVisible(true);
} }
@@ -28,10 +23,10 @@ export default function VenueInfo(props) {
setVisible(false); setVisible(false);
} }
function previewImage(current_url: string) { function previewImage(current_url) {
Taro.previewImage({ Taro.previewImage({
current: current_url, current: current_url,
urls: screenshot_urls, urls: venue_image_list || [],
}); });
} }
return ( return (
@@ -39,14 +34,14 @@ export default function VenueInfo(props) {
{/* venue detail title and venue ordered status */} {/* venue detail title and venue ordered status */}
<View className={styles["venue-detail-title"]}> <View className={styles["venue-detail-title"]}>
<Text></Text> <Text></Text>
{screenshot_urls.length > 0 ? ( {venue_image_list?.length > 0 ? (
<> <>
<Text>·</Text> <Text>·</Text>
<View <View
className={styles["venue-reserve-status"]} className={styles["venue-reserve-status"]}
onClick={showScreenShot} onClick={showScreenShot}
> >
<Text></Text> <Text></Text>
<Image <Image
className={styles["venue-reserve-screenshot"]} className={styles["venue-reserve-screenshot"]}
src={img.ICON_DETAIL_ARROW_RIGHT} src={img.ICON_DETAIL_ARROW_RIGHT}
@@ -86,20 +81,22 @@ export default function VenueInfo(props) {
<View className={styles["venue-screenshot-title"]}></View> <View className={styles["venue-screenshot-title"]}></View>
<ScrollView scrollY className={styles["venue-screenshot-scroll-view"]}> <ScrollView scrollY className={styles["venue-screenshot-scroll-view"]}>
<View className={styles["venue-screenshot-image-list"]}> <View className={styles["venue-screenshot-image-list"]}>
{screenshot_urls.length > 0 && {venue_image_list?.length > 0 &&
screenshot_urls.map((url, index) => ( venue_image_list.map((url, index) => {
<View return (
className={styles["venue-screenshot-image-item"]} <View
onClick={() => previewImage(url)} className={styles["venue-screenshot-image-item"]}
key={index} onClick={previewImage.bind(null, url)}
> key={index}
<Image >
className={styles["venue-screenshot-image-item-image"]} <Image
mode="aspectFill" className={styles["venue-screenshot-image-item-image"]}
src={url} mode="aspectFill"
/> src={url}
</View> />
))} </View>
);
})}
</View> </View>
</ScrollView> </ScrollView>
</CommonPopup> </CommonPopup>

View File

@@ -23,7 +23,6 @@ import SupplementalNotes from "./components/SupplementalNotes";
import OrganizerInfo from "./components/OrganizerInfo"; import OrganizerInfo from "./components/OrganizerInfo";
import SharePopup from "./components/SharePopup"; import SharePopup from "./components/SharePopup";
import { navto, toast } from "@/utils/helper"; import { navto, toast } from "@/utils/helper";
import { delay } from "@/utils";
import ArrowLeft from "@/static/detail/icon-arrow-left.svg"; import ArrowLeft from "@/static/detail/icon-arrow-left.svg";
// import Logo from "@/static/detail/icon-logo-go.svg"; // import Logo from "@/static/detail/icon-logo-go.svg";
import styles from "./index.module.scss"; import styles from "./index.module.scss";
@@ -82,11 +81,11 @@ function Index() {
// 位置更新后,重新获取详情页数据(因为距离等信息可能发生变化) // 位置更新后,重新获取详情页数据(因为距离等信息可能发生变化)
// 注意:这里不调用 fetchDetail避免与 useDidShow 中的调用重复 // 注意:这里不调用 fetchDetail避免与 useDidShow 中的调用重复
// 如果需要更新距离信息,可以在 fetchDetail 成功后根据当前位置重新计算 // 如果需要更新距离信息,可以在 fetchDetail 成功后根据当前位置重新计算
// if (from === "publish") { if (from === "publish") {
// handleShare(true); handleShare(true);
// } }
} catch (error) { } catch (error) {
console.warn("用户位置更新失败", error); console.error("用户位置更新失败", error);
} }
}; };
@@ -120,12 +119,8 @@ function Index() {
} }
} }
function handleShare() { function handleShare(flag = false) {
if (!detail.id) { sharePopupRef.current.show(flag);
toast("球局未加载完成,请稍后再试");
return false;
}
sharePopupRef.current.show();
} }
const handleJoinGame = async () => { const handleJoinGame = async () => {
@@ -166,7 +161,7 @@ function Index() {
navto( navto(
userId === myInfo.id userId === myInfo.id
? "/user_pages/myself/index" ? "/user_pages/myself/index"
: `/user_pages/other/index?userid=${userId}`, : `/user_pages/other/index?userid=${userId}`
); );
} }
@@ -200,7 +195,7 @@ function Index() {
<View <View
className={classnames( className={classnames(
styles["custom-navbar"], styles["custom-navbar"],
glass ? styles.glass : "", glass ? styles.glass : ""
)} )}
style={{ style={{
height: `${totalHeight}px`, height: `${totalHeight}px`,
@@ -291,15 +286,13 @@ function Index() {
currentUserInfo={myInfo} currentUserInfo={myInfo}
/> />
{/* share popup */} {/* share popup */}
{detail.id && myInfo.id && ( <SharePopup
<SharePopup ref={sharePopupRef}
ref={sharePopupRef} id={id as string}
id={id as string} from={from as string}
from={from as string} detail={detail}
detail={detail} userInfo={userInfo}
userInfo={myInfo} />
/>
)}
</View> </View>
</ScrollView> </ScrollView>
); );

View File

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

View File

@@ -5,7 +5,7 @@ import Taro, { useRouter } from "@tarojs/taro";
import classnames from "classnames"; import classnames from "classnames";
import dayjs from "dayjs"; import dayjs from "dayjs";
import "dayjs/locale/zh-cn"; import "dayjs/locale/zh-cn";
import { generatePosterImage, delay } from "@/utils"; import { generatePosterImage, base64ToTempFilePath, delay } from "@/utils";
import { withAuth } from "@/components"; import { withAuth } from "@/components";
import GeneralNavbar from "@/components/GeneralNavbar"; import GeneralNavbar from "@/components/GeneralNavbar";
import DetailService from "@/services/detailService"; import DetailService from "@/services/detailService";
@@ -14,13 +14,9 @@ import WechatLogo from "@/static/detail/wechat_icon.svg";
import WechatTimeline from "@/static/detail/wechat_timeline.svg"; import WechatTimeline from "@/static/detail/wechat_timeline.svg";
import { useUserActions } from "@/store/userStore"; import { useUserActions } from "@/store/userStore";
import { DayOfWeekMap } from "../detail/config"; import { DayOfWeekMap } from "../detail/config";
import { import { genNTRPRequirementText } from "@/utils/helper";
genNTRPRequirementText,
genGameLength,
formatGameStartTime,
} from "@/utils/helper";
import { waitForAuthInit } from "@/utils/authInit"; import { waitForAuthInit } from "@/utils/authInit";
import { OSS_BASE } from "@/config/api"; import { OSS_BASE_URL } from "@/config/api";
import styles from "./index.module.scss"; import styles from "./index.module.scss";
dayjs.locale("zh-cn"); dayjs.locale("zh-cn");
@@ -57,17 +53,15 @@ function SharePoster(props) {
const startTime = dayjs(start_time); const startTime = dayjs(start_time);
const endTime = dayjs(end_time); const endTime = dayjs(end_time);
const dayofWeek = DayOfWeekMap.get(startTime.day()); const dayofWeek = DayOfWeekMap.get(startTime.day());
const gameLength = genGameLength(startTime, endTime); const gameLength = `${endTime.diff(startTime, "hour")}小时`;
Taro.showLoading({ title: "生成中..." }); Taro.showLoading({ title: "生成中..." });
const qrCodeUrlRes = await DetailService.getQrCodeUrl({ const qrCodeUrlRes = await DetailService.getQrCodeUrl({
page: "game_pages/detail/index", page: "game_pages/detail/index",
scene: `id=${id}`, scene: `id=${id}`,
}); });
const qrCodeUrl = qrCodeUrlRes.data.ossPath; const qrCodeUrl = await base64ToTempFilePath(
// const qrCodeUrl = await base64ToTempFilePath( qrCodeUrlRes.data.qr_code_base64
// qrCodeUrlRes.data.qr_code_base64 );
// );
// debugger
await delay(100); await delay(100);
const url = await generatePosterImage({ const url = await generatePosterImage({
playType: play_type, playType: play_type,
@@ -75,13 +69,13 @@ function SharePoster(props) {
mainCoursal: mainCoursal:
image_list[0] && image_list[0].startsWith("http") image_list[0] && image_list[0].startsWith("http")
? image_list[0] ? image_list[0]
: `${OSS_BASE}/front/ball/images/0621b8cf-f7d6-43ad-b852-7dc39f29a782.png`, : `${OSS_BASE_URL}/images/0621b8cf-f7d6-43ad-b852-7dc39f29a782.png`,
nickname, nickname,
avatarUrl: avatar_url, avatarUrl: avatar_url,
title, title,
locationName: location_name, locationName: location_name,
date: `${startTime.format("M月D日")} (${dayofWeek})`, date: `${startTime.format("M月D日")} (${dayofWeek})`,
time: `${formatGameStartTime(startTime)} ${gameLength}`, time: `${startTime.format("ah")} ${gameLength}`,
qrCodeUrl, qrCodeUrl,
}); });
Taro.hideLoading(); Taro.hideLoading();

View File

@@ -17,14 +17,14 @@ const HomePage: React.FC = () => {
if (loginResult.success) { if (loginResult.success) {
// 静默登录成功,获取用户信息 // 静默登录成功,获取用户信息
fetchUserInfo().catch((error) => { fetchUserInfo().catch((error) => {
console.warn("获取用户信息失败:", error); console.error("获取用户信息失败:", error);
}); });
checkNicknameChangeStatus().catch((error) => { checkNicknameChangeStatus().catch((error) => {
console.warn("检查昵称变更状态失败:", error); console.error("检查昵称变更状态失败:", error);
}); });
} }
} catch (error) { } catch (error) {
console.warn("静默登录失败:", error); console.error("静默登录失败:", error);
// 静默登录失败不影响使用 // 静默登录失败不影响使用
} }

View File

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

View File

@@ -1,7 +1,6 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { View, Text, Button, Image } from "@tarojs/components"; import { View, Text, Button, Image } from "@tarojs/components";
import Taro, { useRouter } from "@tarojs/taro"; import Taro, { useRouter } from "@tarojs/taro";
import { GeneralNavbar } from "@/components";
import { import {
wechat_auth_login, wechat_auth_login,
save_login_state, save_login_state,
@@ -156,11 +155,6 @@ const LoginPage: React.FC = () => {
e.stopPropagation(); e.stopPropagation();
}; };
// 返回首页
const handle_return_home = () => {
Taro.navigateTo({ url: "/main_pages/index" });
};
return ( return (
<View className="login_page"> <View className="login_page">
<View className="background_image"> <View className="background_image">
@@ -172,8 +166,6 @@ const LoginPage: React.FC = () => {
<View className="bg_overlay"></View> <View className="bg_overlay"></View>
</View> </View>
<GeneralNavbar title="" showBack={true} showAvatar={false} onBack={handle_return_home} />
{/* 主要内容 */} {/* 主要内容 */}
<View className="login_main_content"> <View className="login_main_content">
{/* 品牌区域 */} {/* 品牌区域 */}
@@ -201,7 +193,7 @@ const LoginPage: React.FC = () => {
/> />
</View> </View>
<Text className="button_text"> <Text className="button_text">
{is_loading ? "登录中..." : "一键登录"} {is_loading ? "登录中..." : "微信授权登录"}
</Text> </Text>
</Button> </Button>
@@ -216,13 +208,9 @@ const LoginPage: React.FC = () => {
src={require("@/static/login/phone_icon.svg")} src={require("@/static/login/phone_icon.svg")}
/> />
</View> </View>
<Text className="button_text"></Text> <Text className="button_text"></Text>
</Button> </Button>
{/* <View className="return_home_button link_button" onClick={handle_return_home}>
<Text className="button_text">返回首页</Text>
</View> */}
{/* 用户协议复选框 */} {/* 用户协议复选框 */}
<View className="terms_checkbox_section"> <View className="terms_checkbox_section">
<View className="checkbox_container" onClick={handle_toggle_terms}> <View className="checkbox_container" onClick={handle_toggle_terms}>
@@ -236,13 +224,13 @@ const LoginPage: React.FC = () => {
className="terms_link" className="terms_link"
onClick={() => handle_view_terms("terms")} onClick={() => handle_view_terms("terms")}
> >
</Text> </Text>
<Text <Text
className="terms_link" className="terms_link"
onClick={() => handle_view_terms("binding")} onClick={() => handle_view_terms("binding")}
> >
</Text> </Text>
<Text <Text
className="terms_link" className="terms_link"
@@ -271,13 +259,13 @@ const LoginPage: React.FC = () => {
className="terms_item" className="terms_item"
onClick={() => handle_view_terms("terms")} onClick={() => handle_view_terms("terms")}
> >
</Text> </Text>
<Text <Text
className="terms_item" className="terms_item"
onClick={() => handle_view_terms("binding")} onClick={() => handle_view_terms("binding")}
> >
</Text> </Text>
<Text <Text
className="terms_item" className="terms_item"

View File

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

View File

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

View File

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

View File

@@ -139,7 +139,7 @@ const VerificationPage: React.FC = () => {
}); });
} }
} catch (error) { } catch (error) {
console.warn("发送验证码异常:", error); console.error("发送验证码异常:", error);
Taro.showToast({ Taro.showToast({
title: "发送失败,请重试", title: "发送失败,请重试",
icon: "none", icon: "none",

View File

@@ -63,13 +63,10 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
area, area,
cityQrCode, cityQrCode,
districts, districts,
fetchMatches,
gamesNum, // 新增:获取球局数量 gamesNum, // 新增:获取球局数量
} = store; } = store;
const supportedCitiesList = useDictionaryStore((s) => s.getDictionaryValue('supported_cities', ['上海市'])) || [];
// 首页是否展示二维码,由 getDictionaryManyKey 的 show_home_qrcode 控制,默认 true 保持原样
const showHomeQrcode = useDictionaryStore((s) => s.getDictionaryValue('show_home_qrcode', true));
const { const {
isShowFilterPopup, isShowFilterPopup,
data: matches, data: matches,
@@ -81,6 +78,7 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
pageOption, pageOption,
isShowNoData, isShowNoData,
} = listPageState || {}; } = listPageState || {};
console.log('===matches', matches)
const scrollContextRef = useRef(null); const scrollContextRef = useRef(null);
const scrollViewRef = useRef(null); const scrollViewRef = useRef(null);
@@ -96,8 +94,8 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
// 记录上一次加载数据时的城市,用于检测城市变化 // 记录上一次加载数据时的城市,用于检测城市变化
const lastLoadedAreaRef = useRef<[string, string] | null>(null); const lastLoadedAreaRef = useRef<[string, string] | null>(null);
const prevIsActiveRef = useRef(isActive); const prevIsActiveRef = useRef(isActive);
// 记录是否是进入列表页的第一次调用 updateUserLocation首次传 force: true // 首次加载标记:避免切回 tab 时使用 isRefresh 导致智能排序顺序抖动
const hasUpdatedLocationRef = useRef(false); const hasLoadedOnceRef = useRef(false);
// 处理距离筛选显示/隐藏 // 处理距离筛选显示/隐藏
const handleDistanceFilterVisibleChange = useCallback( const handleDistanceFilterVisibleChange = useCallback(
@@ -229,16 +227,25 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
useEffect(() => { useEffect(() => {
// 分批异步执行初始化操作,避免阻塞首屏渲染 // 分批异步执行初始化操作,避免阻塞首屏渲染
// 1. 立即执行:获取城市、二维码和行政区列表(轻量操作)
getCities(); getCities();
if (showHomeQrcode) getCityQrCode(); getCityQrCode();
getDistricts(); getDistricts(); // 新增:获取行政区列表
// 只有当页面激活时才加载位置和列表数据
if (isActive) { if (isActive) {
getLocation().catch((error) => { const firstLoad = !hasLoadedOnceRef.current;
console.warn('获取位置信息失败:', error); getLocation(firstLoad)
}); .then(() => {
if (firstLoad) {
hasLoadedOnceRef.current = true;
}
})
.catch((error) => {
console.error('获取位置信息失败:', error);
});
} }
}, [isActive, showHomeQrcode]); }, [isActive]);
// 记录上一次的城市,用于检测城市变化 // 记录上一次的城市,用于检测城市变化
const prevAreaRef = useRef<[string, string] | null>(null); const prevAreaRef = useRef<[string, string] | null>(null);
@@ -293,9 +300,9 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
currentProvince, currentProvince,
}); });
// 延迟刷新,等 tab 切换动画完成后再加载,避免切换时列表重渲染导致抖动 // 地址发生变化或不一致,重新加载数据和球局数量
const delayMs = 280; // 先调用列表接口,然后在列表接口完成后调用数量接口
const timer = setTimeout(async () => { (async () => {
try { try {
if (refreshBothLists) { if (refreshBothLists) {
await refreshBothLists(); await refreshBothLists();
@@ -309,11 +316,9 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
lastLoadedAreaRef.current = [...currentArea] as [string, string]; lastLoadedAreaRef.current = [...currentArea] as [string, string];
} }
} catch (error) { } catch (error) {
console.warn("重新加载数据失败:", error); console.error("重新加载数据失败:", error);
} }
}, delayMs); })();
prevIsActiveRef.current = isActive;
return () => clearTimeout(timer);
} }
} }
@@ -365,21 +370,18 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
}; };
}, []); }, []);
const getLocation = async () => { const getLocation = async (useRefresh = true) => {
const location = await getCurrentLocationInfo(); const location = await getCurrentLocationInfo();
updateState({ location }); updateState({ location });
if (location && location.latitude && location.longitude) { if (location && location.latitude && location.longitude) {
try { try {
// 进入列表页的第一次调用传 force: true后续调用传 false await updateUserLocation(location.latitude, location.longitude);
const isFirstCall = !hasUpdatedLocationRef.current;
await updateUserLocation(location.latitude, location.longitude, isFirstCall);
hasUpdatedLocationRef.current = true;
} catch (error) { } catch (error) {
console.warn("更新用户位置失败:", error); console.error("更新用户位置失败:", error);
} }
} }
// 先调用列表接口 // 先调用列表接口
await getMatchesData(); await fetchMatches({}, useRefresh);
// 列表接口完成后,再调用数量接口 // 列表接口完成后,再调用数量接口
await fetchGetGamesCount(); await fetchGetGamesCount();
// 初始数据加载完成后,记录当前城市 // 初始数据加载完成后,记录当前城市
@@ -455,17 +457,6 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
}); });
}; };
// 处理重新定位
const handleRelocate = async (location) => {
try {
// 位置已更新到后端,刷新列表数据
await getMatchesData();
await fetchGetGamesCount();
} catch (error) {
console.warn("刷新列表失败:", error);
}
};
const handleSearchClick = () => { const handleSearchClick = () => {
navigateTo({ navigateTo({
url: "/game_pages/search/index", url: "/game_pages/search/index",
@@ -477,7 +468,7 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
const { fetchDictionary } = useDictionaryStore.getState(); const { fetchDictionary } = useDictionaryStore.getState();
await fetchDictionary(); await fetchDictionary();
} catch (error) { } catch (error) {
console.warn("初始化字典数据失败:", error); console.error("初始化字典数据失败:", error);
} }
}; };
@@ -485,7 +476,7 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
initDictionaryData(); initDictionaryData();
}, []); }, []);
// 获取省份名称area 格式: ["中国", "省份"]
const province = area?.at(1) || "上海"; const province = area?.at(1) || "上海";
function renderCityQrcode() { function renderCityQrcode() {
@@ -527,23 +518,13 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
} }
// 判定是否显示"暂无球局"页面 // 判定是否显示"暂无球局"页面
// 从配置接口 /parameter/many_key 获取 supported_cities格式如 "上海市||北京市" // 条件:省份不是上海 或 (已加载完成且球局数量为0)
// 当前省份在有球局城市列表中则显示列表,否则显示暂无球局 const shouldShowNoGames = province !== "上海";
const shouldShowNoGames =
supportedCitiesList.length > 0
? !supportedCitiesList.includes(province)
: province !== "上海市"; // 配置未加载时默认按上海判断
return ( return (
<> <>
{shouldShowNoGames ? ( {shouldShowNoGames ? (
showHomeQrcode ? ( renderCityQrcode()
renderCityQrcode()
) : (
<View className={styles.cqContainer}>
<Text></Text>
</View>
)
) : ( ) : (
<View ref={scrollContextRef}> <View ref={scrollContextRef}>
<View className={styles.listPage} style={{ paddingTop: totalHeight }}> <View className={styles.listPage} style={{ paddingTop: totalHeight }}>
@@ -589,7 +570,6 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
quickValue={distanceQuickFilter?.order} quickValue={distanceQuickFilter?.order}
districtValue={distanceQuickFilter?.district} districtValue={distanceQuickFilter?.district}
onMenuVisibleChange={handleDistanceFilterVisibleChange} onMenuVisibleChange={handleDistanceFilterVisibleChange}
onRelocate={handleRelocate}
/> />
</View> </View>
</View> </View>
@@ -616,7 +596,7 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
try { try {
await loadMoreMatches(); await loadMoreMatches();
} catch (error) { } catch (error) {
console.warn("加载更多失败:", error); console.error("加载更多失败:", error);
} finally { } finally {
loadingMoreRef.current = false; loadingMoreRef.current = false;
} }
@@ -633,7 +613,6 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
reload={refreshMatches} reload={refreshMatches}
loadMoreMatches={loadMoreMatches} loadMoreMatches={loadMoreMatches}
evaluateFlag evaluateFlag
enableHomeCards
/> />
</ScrollView> </ScrollView>
</View> </View>

View File

@@ -11,16 +11,12 @@ import { EvaluateScene } from "@/store/evaluateStore";
import { useUserInfo, useUserActions } from "@/store/userStore"; import { useUserInfo, useUserActions } from "@/store/userStore";
import { usePickerOption } from "@/store/pickerOptionsStore"; import { usePickerOption } from "@/store/pickerOptionsStore";
import { useGlobalState } from "@/store/global"; import { useGlobalState } from "@/store/global";
import { useListState } from "@/store/listStore";
import { useDictionaryStore } from "@/store/dictionaryStore";
interface MyselfPageContentProps { interface MyselfPageContentProps {
isActive?: boolean; isActive?: boolean;
} }
const MyselfPageContent: React.FC<MyselfPageContentProps> = ({ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({ isActive = true }) => {
isActive = true,
}) => {
const pickerOption = usePickerOption(); const pickerOption = usePickerOption();
const { statusNavbarHeightInfo } = useGlobalState() || {}; const { statusNavbarHeightInfo } = useGlobalState() || {};
const { totalHeight = 98 } = statusNavbarHeightInfo || {}; const { totalHeight = 98 } = statusNavbarHeightInfo || {};
@@ -41,11 +37,6 @@ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({
const [hasLoaded, setHasLoaded] = useState(false); // 记录是否已经加载过数据 const [hasLoaded, setHasLoaded] = useState(false); // 记录是否已经加载过数据
const [collapseProfile, setCollapseProfile] = useState(false); const [collapseProfile, setCollapseProfile] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const { area } = useListState();
const supportedCitiesList =
useDictionaryStore((s) => s.getDictionaryValue("supported_cities")) || [];
useEffect(() => { useEffect(() => {
pickerOption.getCities(); pickerOption.getCities();
@@ -73,21 +64,20 @@ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({
game_records: TennisMatch[] game_records: TennisMatch[]
): { notEndGames: TennisMatch[]; finishedGames: TennisMatch[] } => { ): { notEndGames: TennisMatch[]; finishedGames: TennisMatch[] } => {
const now = new Date().getTime(); const now = new Date().getTime();
return game_records.reduce(
// 使用for (result, cur) => {
const notEndGames: TennisMatch[] = []; let { end_time } = cur;
const finishedGames: TennisMatch[] = []; end_time = end_time.replace(/\s/, "T");
for (const game of game_records) { new Date(end_time).getTime() > now
const { end_time } = game; ? result.notEndGames.push(cur)
const end_time_str = end_time.replace(/\s/, "T"); : result.finishedGames.push(cur);
new Date(end_time_str).getTime() > now return result;
? notEndGames.push(game) },
: finishedGames.unshift(game); {
} notEndGames: [] as TennisMatch[],
finishedGames: [] as TennisMatch[],
console.log("notEndGames", notEndGames); }
);
return { notEndGames, finishedGames };
}, },
[] []
); );
@@ -104,7 +94,6 @@ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({
} else { } else {
games_data = await UserService.get_participated_games(user_info.id); games_data = await UserService.get_participated_games(user_info.id);
} }
const sorted_games = games_data.sort((a, b) => { const sorted_games = games_data.sort((a, b) => {
return ( return (
new Date(a.original_start_time.replace(/\s/, "T")).getTime() - new Date(a.original_start_time.replace(/\s/, "T")).getTime() -
@@ -112,12 +101,10 @@ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({
); );
}); });
const { notEndGames, finishedGames } = classifyGameRecords(sorted_games); const { notEndGames, finishedGames } = classifyGameRecords(sorted_games);
console.log("notEndGames", notEndGames);
set_game_records(notEndGames); set_game_records(notEndGames);
setEndedGameRecords(finishedGames); setEndedGameRecords(finishedGames);
} catch (error) { } catch (error) {
console.warn("加载球局数据失败:", error); console.error("加载球局数据失败:", error);
} }
}, [active_tab, user_info, classifyGameRecords]); }, [active_tab, user_info, classifyGameRecords]);
@@ -150,7 +137,7 @@ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({
duration: 1500, duration: 1500,
}); });
} catch (error) { } catch (error) {
console.warn("关注操作失败:", error); console.error("关注操作失败:", error);
(Taro as any).showToast({ (Taro as any).showToast({
title: "操作失败,请重试", title: "操作失败,请重试",
icon: "error", icon: "error",
@@ -160,16 +147,6 @@ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({
}; };
const goPublish = () => { const goPublish = () => {
const [_, address] = area;
if (!supportedCitiesList.includes(address)) {
(Taro as any).showModal({
title: "提示",
content: "该城市尚未开放,您可加入社群或切换城市",
showCancel: false,
confirmText: "知道了",
});
return;
}
(Taro as any).navigateTo({ (Taro as any).navigateTo({
url: "/publish_pages/publishBall/index", url: "/publish_pages/publishBall/index",
}); });
@@ -192,23 +169,6 @@ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({
setActiveTab(tab); setActiveTab(tab);
}; };
// 下拉刷新:刷新用户信息和球局数据
const handle_refresh = async () => {
setRefreshing(true);
try {
await Promise.all([fetchUserInfo(), load_game_data()]);
} catch (error) {
console.warn("刷新失败:", error);
(Taro as any).showToast({
title: "刷新失败,请重试",
icon: "none",
duration: 2000,
});
} finally {
setRefreshing(false);
}
};
// const handleScroll = (event: any) => { // const handleScroll = (event: any) => {
// const scrollData = event.detail; // const scrollData = event.detail;
// setCollapseProfile(scrollData.scrollTop > 1); // setCollapseProfile(scrollData.scrollTop > 1);
@@ -218,9 +178,6 @@ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({
<ScrollView <ScrollView
scrollY scrollY
refresherBackground="#FAFAFA" refresherBackground="#FAFAFA"
refresherEnabled
refresherTriggered={refreshing}
onRefresherRefresh={handle_refresh}
className={styles.myselfPage} className={styles.myselfPage}
> >
<View <View
@@ -310,8 +267,9 @@ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({
overflow: "hidden", overflow: "hidden",
}} }}
listLoadErrorWrapperHeight="fit-content" listLoadErrorWrapperHeight="fit-content"
listLoadErrorWidth="410px" listLoadErrorWidth="320px"
listLoadErrorHeight="185px" listLoadErrorHeight="152px"
listLoadErrorScale="1.2"
defaultShowNum={3} defaultShowNum={3}
/> />
</ScrollView> </ScrollView>
@@ -333,8 +291,9 @@ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({
collapse={true} collapse={true}
style={{ paddingBottom: "90px", overflow: "hidden" }} style={{ paddingBottom: "90px", overflow: "hidden" }}
listLoadErrorWrapperHeight="fit-content" listLoadErrorWrapperHeight="fit-content"
listLoadErrorWidth="410px" listLoadErrorWidth="320px"
listLoadErrorHeight="185px" listLoadErrorHeight="152px"
listLoadErrorScale="1.2"
defaultShowNum={3} defaultShowNum={3}
/> />
</ScrollView> </ScrollView>

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
import React, { useState, useEffect, useCallback } from "react"; import React, { useState, useEffect, useCallback } from "react";
import { View } from "@tarojs/components"; import { View } from "@tarojs/components";
import Taro, { useRouter, useShareAppMessage } from "@tarojs/taro"; import Taro from "@tarojs/taro";
import { OSS_BASE } from "@/config/api";
import { wechat_auth_login, save_login_state } from "@/services/loginService"; import { wechat_auth_login, save_login_state } from "@/services/loginService";
import { useUserActions } from "@/store/userStore"; import { useUserActions } from "@/store/userStore";
import { useGlobalState } from "@/store/global"; import { useGlobalState } from "@/store/global";
@@ -19,11 +18,7 @@ import { useDictionaryStore } from "@/store/dictionaryStore";
type TabType = "list" | "message" | "personal"; type TabType = "list" | "message" | "personal";
const MainPage: React.FC = () => { const MainPage: React.FC = () => {
const { params } = useRouter(); const [currentTab, setCurrentTab] = useState<TabType>("list");
const [currentTab, setCurrentTab] = useState<TabType>(() => {
const tab = params?.tab as TabType | undefined;
return tab === "list" || tab === "message" || tab === "personal" ? tab : "list";
});
const [isPublishMenuVisible, setIsPublishMenuVisible] = useState(false); const [isPublishMenuVisible, setIsPublishMenuVisible] = useState(false);
const [isDistanceFilterVisible, setIsDistanceFilterVisible] = useState(false); const [isDistanceFilterVisible, setIsDistanceFilterVisible] = useState(false);
const [isCityPickerVisible, setIsCityPickerVisible] = useState(false); const [isCityPickerVisible, setIsCityPickerVisible] = useState(false);
@@ -40,14 +35,6 @@ const MainPage: React.FC = () => {
const { showGuideBar, guideBarZIndex, setShowGuideBar, setGuideBarZIndex } = const { showGuideBar, guideBarZIndex, setShowGuideBar, setGuideBarZIndex } =
useGlobalState(); useGlobalState();
// 从分享链接进入时根据 ?tab= 定位到对应 tab
useEffect(() => {
const tab = params?.tab as TabType | undefined;
if (tab === "list" || tab === "message" || tab === "personal") {
setCurrentTab(tab);
}
}, [params?.tab]);
// 初始化:自动微信授权并获取用户信息 // 初始化:自动微信授权并获取用户信息
useEffect(() => { useEffect(() => {
const init = async () => { const init = async () => {
@@ -68,7 +55,7 @@ const MainPage: React.FC = () => {
return; return;
} }
} catch (error) { } catch (error) {
console.warn("微信授权异常:", error); console.error("微信授权异常:", error);
setAuthErrorMessage("微信授权失败,请重试"); setAuthErrorMessage("微信授权失败,请重试");
setShowAuthError(true); setShowAuthError(true);
return; return;
@@ -80,8 +67,14 @@ const MainPage: React.FC = () => {
try { try {
await fetchUserInfo(); await fetchUserInfo();
await checkNicknameChangeStatus(); await checkNicknameChangeStatus();
// 启动时预取 Banner 字典(与业务无强依赖,失败不影响主流程)
try {
await useDictionaryStore.getState().fetchBannerDictionary();
} catch (e) {
console.error("预取 Banner 字典失败:", e);
}
} catch (error) { } catch (error) {
console.warn("获取用户信息失败:", error); console.error("获取用户信息失败:", error);
} }
} }
}; };
@@ -166,43 +159,6 @@ const MainPage: React.FC = () => {
[] []
); );
// 分享:按 tab 用 map 对应文案与分享图
const share_config: Record<
TabType,
{ title: string; image_path: string; query: string }
> = {
list: {
title: "有你就有场,发现身边好球友和好球局",
image_path: "system/share_home.png",
query: "?tab=list",
},
personal: {
title: "快来有场,约我一起打网球~",
image_path: "system/share_self.png",
query: "?tab=personal",
},
message: {
title: "查看球友动态",
image_path: "system/share_home.png",
query: "?tab=message",
},
};
useShareAppMessage(() => {
const config = share_config[currentTab] ?? {
title: "约球",
image_path: "system/share_home.png",
query: "",
};
// const imageUrl = OSS_BASE
// ? `${OSS_BASE.replace(/\/$/, "")}/${config.image_path}`
// : "";
return {
title: config.title,
path: "/main_pages/index" + config.query,
// imageUrl,
};
});
// 滚动到顶部 // 滚动到顶部
const scrollToTop = useCallback(() => { const scrollToTop = useCallback(() => {
// 如果当前是列表页,触发列表页内部滚动 // 如果当前是列表页,触发列表页内部滚动

View File

@@ -22,135 +22,3 @@ export const DECLAIMER = `
发起人临时失联/爽约发起人恶意删除队员GO支持全额退款 发起人临时失联/爽约发起人恶意删除队员GO支持全额退款
参与者爽约不通知,不可退款但鼓励用户评分机制中反馈,平台将限制其部分功能使用(如发起权限、报名权限等)。 参与者爽约不通知,不可退款但鼓励用户评分机制中反馈,平台将限制其部分功能使用(如发起权限、报名权限等)。
`; `;
interface RegInsChildTipType {
text: string
strong?: boolean
}
interface RegInsChildTableType {
refundApplicationTime: string
participantRefundableAmount: string
liquidatedDamages: string
}
interface RegInsChildType {
title: string
desc: string
table?: RegInsChildTableType[]
tips: RegInsChildTipType[]
}
interface RegInsType {
title: string,
desc: string,
children: RegInsChildType[]
}
export const RegistrationInstructions: RegInsType = {
title: '报名须知',
desc: '请在确认支付前仔细阅读以下内容,完成支付即视为您已同意本须知全部内容。',
children: [
{
title: '一、退款规则',
desc: '',
table: [
{
refundApplicationTime: '申请退款时间',
participantRefundableAmount: '参与者可退',
liquidatedDamages: '违约金',
},
{
refundApplicationTime: '活动开始前24小时',
participantRefundableAmount: '报名费 100%',
liquidatedDamages: '无',
},
{
refundApplicationTime: '活动开始前1224小时',
participantRefundableAmount: '报名费 50%',
liquidatedDamages: '报名费 50%',
},
{
refundApplicationTime: '活动开始前12小时内',
participantRefundableAmount: '报名费 20%',
liquidatedDamages: '报名费 80%',
},
{
refundApplicationTime: '未申请 / 直接缺席',
participantRefundableAmount: '0%',
liquidatedDamages: '视为放弃,全归组织者',
},
],
tips: [
{
text: '以上时间节点以提交申请时间为准,非活动开始时间;',
strong: false,
},
{
text: '退款申请入口:活动详情页 > 退出活动',
},
{
text: '退款原路退回至微信支付账户,到账时间 15 个工作日;',
},
{
text: '违约金由组织者95%与平台5%)按比例分配。其中组织者所得部分用于补偿其因人数临时变动产生的场地费损失,平台所得部分用于覆盖违约事务的处理成本;',
},
{
text: '未申请退款直接缺席的,报名费于活动结束后自动结算给组织者,平台不参与分配',
},
],
},
{
title: '二、特殊情形退款',
desc: '以下特殊情形可申请全额退款,需联系客服并提供相关证明材料:',
tips: [
{
text: '活动当天遭遇极端恶劣天气(台风、暴雨红色预警等);',
},
{
text: '球场临时关闭或其他不可抗力导致活动无法进行;',
},
{
text: '参与者本人突发疾病或意外(需提供医院证明)。',
},
],
},
{
title: '三、活动取消规则',
desc: '',
tips: [
{
text: '到达活动开始时间时,报名人数仍未达到最低成局人数,活动自动取消,已付款参与者全额退款;',
},
{
text: '组织者主动取消活动,所有已付款参与者全额退款;',
},
{
text: '以上退款均由系统自动处理,无需申请。',
},
],
},
{
title: '四、免责声明',
desc: '',
tips: [
{
text: '本平台仅为网球约球信息撮合平台,不直接提供场地或运动服务,不对活动中的人身安全及财物损失承担责任;',
},
{
text: '网球运动存在固有运动风险,请在参与前评估自身身体状况,患有心脏病、高血压等基础疾病者请在医生许可下参与;',
},
{
text: '平台强烈建议参与者购买运动意外保险;',
strong: true,
},
{
text: '因组织者或场地方原因导致活动变更或取消,平台将协助处理但不承担连带责任;',
},
{
text: '本平台不对因网络故障、系统维护或不可抗力导致的服务中断承担责任。',
},
],
},
],
}

View File

@@ -40,9 +40,7 @@
border-bottom: 0.5px solid rgba(0, 0, 0, 0.06); border-bottom: 0.5px solid rgba(0, 0, 0, 0.06);
padding: 8px 12px; padding: 8px 12px;
color: #000; color: #000;
font-feature-settings: font-feature-settings: "liga" off, "clig" off;
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 14px; font-size: 14px;
font-style: normal; font-style: normal;
@@ -59,9 +57,7 @@
align-items: flex-start; align-items: flex-start;
color: rgba(60, 60, 67, 0.6); color: rgba(60, 60, 67, 0.6);
font-feature-settings: font-feature-settings: "liga" off, "clig" off;
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 12px; font-size: 12px;
font-style: normal; font-style: normal;
@@ -121,9 +117,7 @@
align-items: center; align-items: center;
background: #ff3b30; background: #ff3b30;
color: #fff; color: #fff;
font-feature-settings: font-feature-settings: "liga" off, "clig" off;
"liga" off,
"clig" off;
font-family: "SF Compact Rounded"; font-family: "SF Compact Rounded";
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
@@ -139,9 +133,7 @@
box-sizing: border-box; box-sizing: border-box;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
font-feature-settings: font-feature-settings: "liga" off, "clig" off;
"liga" off,
"clig" off;
font-family: "SF Compact Rounded"; font-family: "SF Compact Rounded";
font-size: 22px; font-size: 22px;
font-style: normal; font-style: normal;
@@ -162,9 +154,7 @@
.date { .date {
color: #000; color: #000;
font-feature-settings: font-feature-settings: "liga" off, "clig" off;
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 16px; font-size: 16px;
font-style: normal; font-style: normal;
@@ -174,9 +164,7 @@
.venueTime { .venueTime {
color: rgba(0, 0, 0, 0.8); color: rgba(0, 0, 0, 0.8);
font-feature-settings: font-feature-settings: "liga" off, "clig" off;
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 12px; font-size: 12px;
font-style: normal; font-style: normal;
@@ -203,9 +191,7 @@
align-items: center; align-items: center;
gap: 12px; gap: 12px;
color: rgba(0, 0, 0, 0.8); color: rgba(0, 0, 0, 0.8);
font-feature-settings: font-feature-settings: "liga" off, "clig" off;
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 12px; font-size: 12px;
font-style: normal; font-style: normal;
@@ -249,9 +235,7 @@
gap: 4px; gap: 4px;
color: #000; color: #000;
text-align: center; text-align: center;
font-feature-settings: font-feature-settings: "liga" off, "clig" off;
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 16px; font-size: 16px;
font-style: normal; font-style: normal;
@@ -267,9 +251,7 @@
&Address { &Address {
color: rgba(0, 0, 0, 0.8); color: rgba(0, 0, 0, 0.8);
text-align: center; text-align: center;
font-feature-settings: font-feature-settings: "liga" off, "clig" off;
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 12px; font-size: 12px;
font-style: normal; font-style: normal;
@@ -288,9 +270,7 @@
justify-content: flex-start; justify-content: flex-start;
gap: 4px; gap: 4px;
color: var(--Labels-Secondary, rgba(60, 60, 67, 0.6)); color: var(--Labels-Secondary, rgba(60, 60, 67, 0.6));
font-feature-settings: font-feature-settings: "liga" off, "clig" off;
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 12px; font-size: 12px;
font-style: normal; font-style: normal;
@@ -327,9 +307,7 @@
& > .buttonText { & > .buttonText {
color: #000; color: #000;
font-feature-settings: font-feature-settings: "liga" off, "clig" off;
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 12px; font-size: 12px;
font-style: normal; font-style: normal;
@@ -369,9 +347,7 @@
align-items: center; align-items: center;
align-self: stretch; align-self: stretch;
color: #000; color: #000;
font-feature-settings: font-feature-settings: "liga" off, "clig" off;
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 14px; font-size: 14px;
font-style: normal; font-style: normal;
@@ -397,9 +373,7 @@
width: 120px; width: 120px;
display: inline-block; display: inline-block;
color: rgba(60, 60, 67, 0.6); color: rgba(60, 60, 67, 0.6);
font-feature-settings: font-feature-settings: "liga" off, "clig" off;
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 14px; font-size: 14px;
font-style: normal; font-style: normal;
@@ -409,9 +383,7 @@
.content { .content {
color: #000; color: #000;
font-feature-settings: font-feature-settings: "liga" off, "clig" off;
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 14px; font-size: 14px;
font-style: normal; font-style: normal;
@@ -424,15 +396,13 @@
align-items: flex-end; align-items: flex-end;
justify-content: center; justify-content: center;
gap: 8px; gap: 8px;
word-break: break-all;
} }
.orderNo { .orderNo {
display: flex; display: flex;
flex-direction: column; justify-content: flex-end;
justify-content: center; align-items: center;
align-items: flex-end; gap: 8px;
gap: 0px;
.copy { .copy {
color: #007aff; color: #007aff;
@@ -451,9 +421,7 @@
align-items: center; align-items: center;
align-self: stretch; align-self: stretch;
color: #000; color: #000;
font-feature-settings: font-feature-settings: "liga" off, "clig" off;
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 14px; font-size: 14px;
font-style: normal; font-style: normal;
@@ -474,9 +442,7 @@
align-items: center; align-items: center;
color: #000; color: #000;
text-align: center; text-align: center;
font-feature-settings: font-feature-settings: "liga" off, "clig" off;
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 12px; font-size: 12px;
font-style: normal; font-style: normal;
@@ -525,9 +491,7 @@
&:nth-child(1) { &:nth-child(1) {
color: #000; color: #000;
text-align: center; text-align: center;
font-feature-settings: font-feature-settings: "liga" off, "clig" off;
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 14px; font-size: 14px;
font-style: normal; font-style: normal;
@@ -545,160 +509,21 @@
.time { .time {
text-align: left; text-align: left;
padding-left: 30px; padding-left: 30px;
border-right: 1px solid rgba(0, 0, 0, 0.06);
} }
// .rule { .rule {
// border-left: 1px solid rgba(0, 0, 0, 0.06); border-left: 1px solid rgba(0, 0, 0, 0.06);
// } }
} }
} }
.refundTip {
margin-top: 16px;
color: rgba(60, 60, 67, 0.6);
text-align: center;
font-feature-settings:
"liga" off,
"clig" off;
font-family: "PingFang SC";
font-size: 12px;
font-style: normal;
font-weight: 400;
}
} }
.disclaimer { .declaimer {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; align-items: flex-start;
margin-top: 16px; gap: 8px;
padding-bottom: 100px;
.disclaimerTitle {
font-size: 14px;
font-weight: bold;
color: #000;
line-height: 20px;
}
.disclaimerDesc {
font-size: 12px;
color: rgba(0, 0, 0, 0.65);
line-height: 18px;
}
.disclaimerSection {
display: flex;
flex-direction: column;
gap: 8px;
.sectionTitle {
font-size: 14px;
font-weight: bold;
color: #000;
line-height: 20px;
}
.sectionDesc {
font-size: 12px;
color: rgba(0, 0, 0, 0.65);
line-height: 18px;
}
.tableContainer {
display: flex;
flex-direction: column;
// gap: 8px;
margin: 8px 0;
border-radius: 12px;
border: 1px solid rgba(0, 0, 0, 0.06);
background: #fff;
box-shadow: 0 4px 36px 0 rgba(0, 0, 0, 0.06);
overflow: hidden;
.tableRow {
display: flex;
min-height: 44px;
&:first-child {
.tableCell {
color: #000;
font-weight: 600;
}
}
&:not(:last-child) {
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
.tableCell {
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: rgba(0, 0, 0, 0.65);
line-height: 18px;
word-break: break-word;
padding: 4px 0;
&:not(:last-child) {
border-right: 1px solid rgba(0, 0, 0, 0.06);
}
&:nth-child(1) {
flex: 1;
}
&:nth-child(2) {
flex: 0.6;
}
&:nth-child(3) {
flex: 1;
}
.tipText {
text-align: center;
}
}
}
}
.tipsList {
display: flex;
flex-direction: column;
gap: 6px;
margin: 8px 0;
.tipItem {
display: flex;
align-items: flex-start;
&::before {
content: "";
margin-right: 6px;
margin-top: -2px;
font-size: 12px;
color: rgba(0, 0, 0, 0.65);
flex-shrink: 0;
line-height: 18px;
}
.tipText {
font-size: 12px;
color: rgba(0, 0, 0, 0.65);
line-height: 18px;
}
.tipTextStrong {
font-size: 12px;
color: #000;
font-weight: bold;
line-height: 18px;
}
}
}
}
.title { .title {
display: flex; display: flex;
padding: 15px 0 0; padding: 15px 0 0;
@@ -706,9 +531,7 @@
align-items: center; align-items: center;
align-self: stretch; align-self: stretch;
color: #000; color: #000;
font-feature-settings: font-feature-settings: "liga" off, "clig" off;
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 14px; font-size: 14px;
font-style: normal; font-style: normal;
@@ -744,9 +567,7 @@
background: #000; background: #000;
box-shadow: 0 8px 64px 0 rgba(0, 0, 0, 0.1); box-shadow: 0 8px 64px 0 rgba(0, 0, 0, 0.1);
backdrop-filter: blur(16px); backdrop-filter: blur(16px);
font-feature-settings: font-feature-settings: "liga" off, "clig" off;
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 16px; font-size: 16px;
font-style: normal; font-style: normal;
@@ -779,9 +600,7 @@
text-align: center; text-align: center;
// border: 0.5px solid rgba(0, 0, 0, 0.06); // border: 0.5px solid rgba(0, 0, 0, 0.06);
color: #000; color: #000;
font-feature-settings: font-feature-settings: "liga" off, "clig" off;
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 16px; font-size: 16px;
font-style: normal; font-style: normal;
@@ -807,9 +626,7 @@
padding: 12px 15px; padding: 12px 15px;
color: rgba(60, 60, 67, 0.6); color: rgba(60, 60, 67, 0.6);
text-align: center; text-align: center;
font-feature-settings: font-feature-settings: "liga" off, "clig" off;
"liga" off,
"clig" off;
font-family: "PingFang SC"; font-family: "PingFang SC";
font-size: 16px; font-size: 16px;
font-style: normal; font-style: normal;

View File

@@ -2,7 +2,7 @@ import React, { useState, useRef } from "react";
import { View, Text, Button, Image } from "@tarojs/components"; import { View, Text, Button, Image } from "@tarojs/components";
import { Dialog } from "@nutui/nutui-react-taro"; import { Dialog } from "@nutui/nutui-react-taro";
import Taro, { useDidShow, useRouter } from "@tarojs/taro"; import Taro, { useDidShow, useRouter } from "@tarojs/taro";
import dayjs, { Dayjs } from "dayjs"; import dayjs from "dayjs";
import "dayjs/locale/zh-cn"; import "dayjs/locale/zh-cn";
import classnames from "classnames"; import classnames from "classnames";
import orderService, { import orderService, {
@@ -10,7 +10,6 @@ import orderService, {
GameOrderRes, GameOrderRes,
OrderStatus, OrderStatus,
refundTextMap, refundTextMap,
RefundStatus,
} from "@/services/orderService"; } from "@/services/orderService";
import { debounce } from "@tarojs/runtime"; import { debounce } from "@tarojs/runtime";
import { import {
@@ -21,19 +20,18 @@ import {
getOrderStatus, getOrderStatus,
generateOrderActions, generateOrderActions,
isPhoneNumber, isPhoneNumber,
genGameLength,
} from "@/utils"; } from "@/utils";
import { getStorage, setStorage } from "@/store/storage"; import { getStorage, setStorage } from "@/store/storage";
import { useGlobalStore } from "@/store/global"; import { useGlobalStore } from "@/store/global";
import { useOrder } from "@/store/orderStore"; import { useOrder } from "@/store/orderStore";
import detailService, { GameData } from "@/services/detailService"; import detailService, { GameData } from "@/services/detailService";
import { withAuth, RefundPopup, GeneralNavbar } from "@/components"; import { withAuth, RefundPopup, GeneralNavbar } from "@/components";
import { OSS_BASE } from "@/config/api"; import { OSS_BASE_URL } from "@/config/api";
import img from "@/config/images"; import img from "@/config/images";
import CustomerIcon from "@/static/order/customer.svg"; import CustomerIcon from "@/static/order/customer.svg";
import { handleCustomerService } from "@/services/userService"; import { handleCustomerService } from "@/services/userService";
import { requireLoginWithPhone } from "@/utils/helper"; import { requireLoginWithPhone } from "@/utils/helper";
import { RegistrationInstructions } from "./config"; import { DECLAIMER } from "./config";
import styles from "./index.module.scss"; import styles from "./index.module.scss";
dayjs.locale("zh-cn"); dayjs.locale("zh-cn");
@@ -76,20 +74,9 @@ function genGameNotice(order_status, start_time) {
return gameNoticeMap.get(key) || {}; return gameNoticeMap.get(key) || {};
} }
function genGameRange(startTime: Dayjs, endTime: Dayjs) {
if (!startTime || !endTime) {
return "";
}
// 如果跨天(自然日)
if (!startTime.isSame(endTime, "day")) {
return `${startTime.format("HH:mm")} - ${endTime.format("MM月DD日 HH:mm")}`;
}
return `${startTime.format("HH:mm")} - ${endTime.format("HH:mm")}`;
}
function GameInfo(props) { function GameInfo(props) {
const { detail, currentLocation, orderDetail, init } = props; const { detail, currentLocation, orderDetail, init } = props;
const { order_status, refund_status, amount, refund_amount } = orderDetail; const { order_status, refund_status, amount } = orderDetail;
const { const {
latitude, latitude,
longitude, longitude,
@@ -123,17 +110,15 @@ function GameInfo(props) {
const startTime = dayjs(start_time); const startTime = dayjs(start_time);
const endTime = dayjs(end_time); const endTime = dayjs(end_time);
// const game_length = Number( const game_length = Number(
// (endTime.diff(startTime, "minutes") / 60).toFixed(), (endTime.diff(startTime, "minutes") / 60).toFixed()
// ); );
const game_length = genGameLength(startTime, endTime);
const startMonth = startTime.format("M"); const startMonth = startTime.format("M");
const startDay = startTime.format("D"); const startDay = startTime.format("D");
const theDayOfWeek = startTime.format("dddd"); const theDayOfWeek = startTime.format("dddd");
const startDate = `${startMonth}${startDay}${theDayOfWeek}`; const startDate = `${startMonth}${startDay}${theDayOfWeek}`;
// const gameRange = `${startTime.format("HH:mm")} - ${endTime.format("HH:mm")}`; const gameRange = `${startTime.format("HH:mm")} - ${endTime.format("HH:mm")}`;
const gameRange = genGameRange(startTime, endTime);
const orderStatus = getOrderStatus(orderDetail); const orderStatus = getOrderStatus(orderDetail);
@@ -259,10 +244,7 @@ function GameInfo(props) {
<View className={styles.gameInfoContainer}> <View className={styles.gameInfoContainer}>
{["refund", "progress", "expired"].includes(orderStatus) && ( {["refund", "progress", "expired"].includes(orderStatus) && (
<View className={styles.paidInfo}> <View className={styles.paidInfo}>
{refundTextMap.get(refund_status)} ¥{" "} {refundTextMap.get(refund_status)} ¥ {amount}
{[RefundStatus.PENDING, RefundStatus.SUCCESS].includes(refund_status)
? refund_amount
: amount}
</View> </View>
)} )}
{["progress", "expired"].includes(orderStatus) && {["progress", "expired"].includes(orderStatus) &&
@@ -291,7 +273,7 @@ function GameInfo(props) {
<View className={styles.gameInfoDateWeatherCalendarDateDate}> <View className={styles.gameInfoDateWeatherCalendarDateDate}>
<View className={styles.date}>{startDate}</View> <View className={styles.date}>{startDate}</View>
<View className={styles.venueTime}> <View className={styles.venueTime}>
{gameRange} {game_length} {gameRange} {game_length}
</View> </View>
</View> </View>
</View> </View>
@@ -319,7 +301,7 @@ function GameInfo(props) {
<View className={styles.locationMessageIcon}> <View className={styles.locationMessageIcon}>
<Image <Image
className={styles.locationMessageIconImage} className={styles.locationMessageIconImage}
src={`${OSS_BASE}/front/ball/images/3ee5c89c-fe58-4a56-9471-1295da09c743.png`} src={`${OSS_BASE_URL}/images/3ee5c89c-fe58-4a56-9471-1295da09c743.png`}
/> />
</View> </View>
{/* location message */} {/* location message */}
@@ -362,7 +344,7 @@ function GameInfo(props) {
handlePayNow: () => {}, handlePayNow: () => {},
handleViewGame, handleViewGame,
}, },
"detail", "detail"
)?.map((obj) => ( )?.map((obj) => (
<View className={classnames(styles.button, styles[obj.className])}> <View className={classnames(styles.button, styles[obj.className])}>
<Text className={styles.buttonText}>{obj.text}</Text> <Text className={styles.buttonText}>{obj.text}</Text>
@@ -522,7 +504,7 @@ function RefundPolicy(props) {
const theTimeObj = dayjs( const theTimeObj = dayjs(
isLast isLast
? refund_policy.at(-2).deadline_formatted ? refund_policy.at(-2).deadline_formatted
: item.deadline_formatted, : item.deadline_formatted
); );
const year = theTimeObj.format("YYYY"); const year = theTimeObj.format("YYYY");
const month = theTimeObj.format("M"); const month = theTimeObj.format("M");
@@ -549,7 +531,7 @@ function RefundPolicy(props) {
className={classnames( className={classnames(
styles.policyItem, styles.policyItem,
targetIndex > index && index !== 0 ? styles.pastItem : "", targetIndex > index && index !== 0 ? styles.pastItem : "",
targetIndex === index ? styles.currentItem : "", targetIndex === index ? styles.currentItem : ""
)} )}
> >
<View className={styles.time}> <View className={styles.time}>
@@ -564,66 +546,15 @@ function RefundPolicy(props) {
</View> </View>
))} ))}
</View> </View>
<Text className={styles.refundTip}>
95%5%481退
</Text>
</View> </View>
); );
} }
function Disclaimer() { function Disclaimer() {
return ( return (
<View className={styles.disclaimer}> <View className={styles.declaimer}>
<View className={styles.disclaimerTitle}> <Text className={styles.title}></Text>
<Text>{RegistrationInstructions.title}</Text> <Text className={styles.content}>{DECLAIMER}</Text>
</View>
<View className={styles.disclaimerDesc}>
<Text>{RegistrationInstructions.desc}</Text>
</View>
{RegistrationInstructions.children.map((section, sectionIndex) => (
<View key={sectionIndex} className={styles.disclaimerSection}>
<View className={styles.sectionTitle}>
<Text>{section.title}</Text>
</View>
{section.desc && (
<View className={styles.sectionDesc}>
<Text>{section.desc}</Text>
</View>
)}
{section.table && (
<View className={styles.tableContainer}>
{section.table.map((row, rowIndex) => (
<View key={rowIndex} className={styles.tableRow}>
<View className={styles.tableCell}>
<Text>{row.refundApplicationTime}</Text>
</View>
<View className={styles.tableCell}>
<Text>{row.participantRefundableAmount}</Text>
</View>
<View className={styles.tableCell}>
<Text>{row.liquidatedDamages}</Text>
</View>
</View>
))}
</View>
)}
{section.tips && (
<View className={styles.tipsList}>
{section.tips.map((tip, tipIndex) => (
<View key={tipIndex} className={styles.tipItem}>
<Text
className={
tip.strong ? styles.tipTextStrong : styles.tipText
}
>
{tip.text}
</Text>
</View>
))}
</View>
)}
</View>
))}
</View> </View>
); );
} }

View File

@@ -9,7 +9,6 @@ import orderService, {
OrderStatus, OrderStatus,
CancelType, CancelType,
refundTextMap, refundTextMap,
RefundStatus,
} from "@/services/orderService"; } from "@/services/orderService";
import { getStorage, removeStorage, setStorage } from "@/store/storage"; import { getStorage, removeStorage, setStorage } from "@/store/storage";
import { useGlobalStore } from "@/store/global"; import { useGlobalStore } from "@/store/global";
@@ -70,7 +69,6 @@ function generateTimeMsg(game_info) {
const OrderList = () => { const OrderList = () => {
const [list, setList] = useState<any[][]>([]); const [list, setList] = useState<any[][]>([]);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [refreshing, setRefreshing] = useState(false);
const refundRef = useRef(null); const refundRef = useRef(null);
const end = list.length * PAGESIZE >= total; const end = list.length * PAGESIZE >= total;
@@ -102,7 +100,7 @@ const OrderList = () => {
newList.splice( newList.splice(
index, index,
clear ? newList.length - index : 1, clear ? newList.length - index : 1,
addPageInfo(res.data.rows, page), addPageInfo(res.data.rows, page)
); );
return newList; return newList;
}); });
@@ -116,22 +114,6 @@ const OrderList = () => {
} }
} }
// 下拉刷新:重新加载第一页订单
async function handle_refresh() {
setRefreshing(true);
try {
await getOrders(1);
} catch (error) {
Taro.showToast({
title: "刷新失败,请重试",
icon: "none",
duration: 2000,
});
} finally {
setRefreshing(false);
}
}
async function handlePayNow(item) { async function handlePayNow(item) {
// 检查登录状态和手机号 // 检查登录状态和手机号
if (!requireLoginWithPhone()) { if (!requireLoginWithPhone()) {
@@ -265,17 +247,13 @@ const OrderList = () => {
}); });
} }
async function handleQuit(item) { function handleQuit(item) {
if (refundRef.current) { if (refundRef.current) {
const res = await orderService.getRefundPolicy({ order_id: item.id }); refundRef.current.show(item, (result) => {
refundRef.current.show( if (result) {
{ ...item, refund_policy: res.data.refund_policy }, getOrders(item.page);
(result) => { }
if (result) { });
getOrders(item.page);
}
},
);
} }
} }
@@ -298,7 +276,7 @@ const OrderList = () => {
> >
<GeneralNavbar <GeneralNavbar
title="球局订单" title="球局订单"
backgroundColor="#ffffff" backgroundColor="transparent"
titleClassName={styles.titleClassName} titleClassName={styles.titleClassName}
className={styles.navbar} className={styles.navbar}
/> />
@@ -307,10 +285,6 @@ const OrderList = () => {
scrollWithAnimation scrollWithAnimation
lowerThreshold={20} lowerThreshold={20}
onScrollToLower={handleFetchNext} onScrollToLower={handleFetchNext}
refresherBackground="#FAFAFA"
refresherEnabled
refresherTriggered={refreshing}
onRefresherRefresh={handle_refresh}
enhanced enhanced
showScrollbar={false} showScrollbar={false}
className={styles.list} className={styles.list}
@@ -321,7 +295,7 @@ const OrderList = () => {
item.order_status === OrderStatus.PENDING && item.order_status === OrderStatus.PENDING &&
item.cancel_type === CancelType.NONE; item.cancel_type === CancelType.NONE;
const canceled = [CancelType.USER, CancelType.TIMEOUT].includes( const canceled = [CancelType.USER, CancelType.TIMEOUT].includes(
item.cancel_type, item.cancel_type
); );
const { game_info } = item; const { game_info } = item;
@@ -354,7 +328,7 @@ const OrderList = () => {
<View <View
className={classnames( className={classnames(
styles.payNum, styles.payNum,
styles[unPay ? "pending" : "paid"], styles[unPay ? "pending" : "paid"]
)} )}
> >
<Text> <Text>
@@ -363,15 +337,7 @@ const OrderList = () => {
: refundTextMap.get(item.refund_status)} : refundTextMap.get(item.refund_status)}
</Text>{" "} </Text>{" "}
<View className={styles.amount}> <View className={styles.amount}>
¥{" "} ¥ <Text>{item.amount}</Text>
<Text>
{[
RefundStatus.PENDING,
RefundStatus.SUCCESS,
].includes(item.refund_status)
? item.refund_amount
: item.amount}
</Text>
</View> </View>
</View> </View>
)} )}
@@ -383,7 +349,7 @@ const OrderList = () => {
{insertDotInTags([location_name, court_type, "3.5km"]).map( {insertDotInTags([location_name, court_type, "3.5km"]).map(
(text, index) => ( (text, index) => (
<Text key={index}>{text}</Text> <Text key={index}>{text}</Text>
), )
)} )}
</View> </View>
<View className={styles.gameOtherInfo}> <View className={styles.gameOtherInfo}>
@@ -439,12 +405,12 @@ const OrderList = () => {
handlePayNow, handlePayNow,
handleViewGame, handleViewGame,
}, },
"list", "list"
)?.map((obj) => ( )?.map((obj) => (
<View <View
className={classnames( className={classnames(
styles.button, styles.button,
styles[obj.className], styles[obj.className]
)} )}
> >
<Text className={styles.buttonText}>{obj.text}</Text> <Text className={styles.buttonText}>{obj.text}</Text>

View File

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

View File

@@ -77,7 +77,7 @@ const CommentReply = () => {
if (allCommentIds.length > 0) { if (allCommentIds.length > 0) {
// 使用统一接口标记已读传入所有评论ID // 使用统一接口标记已读传入所有评论ID
messageService.markAsRead('comment', allCommentIds).catch(e => { messageService.markAsRead('comment', allCommentIds).catch(e => {
console.warn("标记评论已读失败:", e); console.error("标记评论已读失败:", e);
}); });
} }
} }
@@ -221,7 +221,7 @@ const CommentReply = () => {
if (allCommentIds.length > 0) { if (allCommentIds.length > 0) {
messageService.markAsRead('comment', allCommentIds).catch(e => { messageService.markAsRead('comment', allCommentIds).catch(e => {
console.warn("标记评论已读失败:", e); console.error("标记评论已读失败:", e);
}); });
} }
} }
@@ -249,7 +249,7 @@ const CommentReply = () => {
<View className="comment-left"> <View className="comment-left">
<Image <Image
className="user-avatar" className="user-avatar"
src={item.user_avatar } src={item.user_avatar || "https://img.yzcdn.cn/vant/cat.jpeg"}
mode="aspectFill" mode="aspectFill"
onClick={(e) => handleUserClick(e, item.user_id)} onClick={(e) => handleUserClick(e, item.user_id)}
/> />

View File

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

View File

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

View File

@@ -21,7 +21,7 @@ const EnableNotificationPage: React.FC = () => {
setQrCodeUrl(res.data.ServiceAccountQRCode); setQrCodeUrl(res.data.ServiceAccountQRCode);
} }
} catch (error) { } catch (error) {
console.warn('获取二维码失败:', error); console.error('获取二维码失败:', error);
} }
}; };
fetchQRCode(); fetchQRCode();

View File

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

View File

@@ -62,7 +62,7 @@ const NewFollow = () => {
if (allFanIds.length > 0) { if (allFanIds.length > 0) {
// 使用统一接口标记已读传入所有关注者ID // 使用统一接口标记已读传入所有关注者ID
messageService.markAsRead('follow', allFanIds).catch(e => { messageService.markAsRead('follow', allFanIds).catch(e => {
console.warn("标记关注已读失败:", e); console.error("标记关注已读失败:", e);
}); });
} }
} else { } else {
@@ -168,7 +168,7 @@ const NewFollow = () => {
if (allFanIds.length > 0) { if (allFanIds.length > 0) {
messageService.markAsRead('follow', allFanIds).catch(e => { messageService.markAsRead('follow', allFanIds).catch(e => {
console.warn("标记关注已读失败:", e); console.error("标记关注已读失败:", e);
}); });
} }
} else { } else {
@@ -193,7 +193,7 @@ const NewFollow = () => {
<View className="follow-left" onClick={() => handleUserClick(item.user_id)}> <View className="follow-left" onClick={() => handleUserClick(item.user_id)}>
<Image <Image
className="user-avatar" mode="aspectFill" className="user-avatar" mode="aspectFill"
src={item.user_avatar || require("@/static/userInfo/default_avatar.svg")} src={item.user_avatar || "https://img.yzcdn.cn/vant/cat.jpeg"}
/> />

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,6 @@ import img from '@/config/images';
import { FormFieldConfig } from '@/config/formSchema/publishBallFormSchema'; import { FormFieldConfig } from '@/config/formSchema/publishBallFormSchema';
import SelectStadium from '../SelectStadium/SelectStadium' import SelectStadium from '../SelectStadium/SelectStadium'
import { Stadium } from '../SelectStadium/StadiumDetail' import { Stadium } from '../SelectStadium/StadiumDetail'
import { normalize_address } from '@/utils/locationUtils'
import './FormBasicInfo.scss' import './FormBasicInfo.scss'
type PlayGame = { type PlayGame = {
@@ -55,7 +54,7 @@ const FormBasicInfo: React.FC<FormBasicInfoProps> = ({
onChange({...value, onChange({...value,
venue_id, venue_id,
location_name: name, location_name: name,
location: normalize_address(address || ''), location: address,
latitude, latitude,
longitude, longitude,
court_type, court_type,

View File

@@ -3,8 +3,8 @@ import { View, Text, Input, ScrollView, Image } from '@tarojs/components'
import Taro from '@tarojs/taro' import Taro from '@tarojs/taro'
import { Loading } from '@nutui/nutui-react-taro' import { Loading } from '@nutui/nutui-react-taro'
import StadiumDetail, { StadiumDetailRef } from './StadiumDetail' import StadiumDetail, { StadiumDetailRef } from './StadiumDetail'
import { CommonPopup, CustomPopup } from '../../../../components' import { CommonPopup } from '../../../../components'
import { getLocation, normalize_address } from '@/utils/locationUtils' import { getLocation } from '@/utils/locationUtils'
import PublishService from '@/services/publishService' import PublishService from '@/services/publishService'
import images from '@/config/images' import images from '@/config/images'
import './SelectStadium.scss' import './SelectStadium.scss'
@@ -53,7 +53,7 @@ const SelectStadium: React.FC<SelectStadiumProps> = ({
} }
} }
} catch (error) { } catch (error) {
console.warn('获取场馆列表失败:', error) console.error('获取场馆列表失败:', error)
} finally { } finally {
setLoading(false) setLoading(false)
} }
@@ -100,14 +100,14 @@ const SelectStadium: React.FC<SelectStadiumProps> = ({
success: (res) => { success: (res) => {
setSelectedStadium({ setSelectedStadium({
name: res.name, name: res.name,
address: normalize_address(res.address || ''), address: res.address,
longitude: res.longitude, longitude: res.longitude,
latitude: res.latitude latitude: res.latitude
}) })
setShowDetail(true) setShowDetail(true)
}, },
fail: (err: { errMsg: string }) => { fail: (err: { errMsg: string }) => {
console.warn('选择位置失败:', err) console.error('选择位置失败:', err)
const { errMsg } = err || {}; const { errMsg } = err || {};
if (!errMsg.includes('fail cancel')) { if (!errMsg.includes('fail cancel')) {
Taro.showToast({ Taro.showToast({
@@ -188,20 +188,24 @@ const SelectStadium: React.FC<SelectStadiumProps> = ({
// 如果显示详情页面 // 如果显示详情页面
if (showDetail && selectedStadium) { if (showDetail && selectedStadium) {
return ( return (
<CustomPopup <CommonPopup
visible={visible} visible={visible}
onClose={handleCancel} onClose={handleCancel}
cancelText="返回" cancelText="返回"
confirmText="确认" confirmText="确认"
className="select-stadium-popup"
onCancel={handleDetailCancel} onCancel={handleDetailCancel}
onConfirm={handleConfirm} onConfirm={handleConfirm}
position="bottom"
//style={{ paddingBottom: keyboardVisible ? `20px` : undefined }}
round
> >
{/* 内容区域 */} <StadiumDetail
<StadiumDetail ref={stadiumDetailRef}
ref={stadiumDetailRef} stadium={selectedStadium}
stadium={selectedStadium} //onAnyInput={handleAnyInput}
/> />
</CustomPopup> </CommonPopup>
) )
} }

View File

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

View File

@@ -5,9 +5,7 @@ import images from '@/config/images'
import TextareaTag from '@/components/TextareaTag' import TextareaTag from '@/components/TextareaTag'
// import CoverImageUpload, { type CoverImage } from '@/components/ImageUpload' // import CoverImageUpload, { type CoverImage } from '@/components/ImageUpload'
import UploadCover, { type CoverImageValue } from '@/components/UploadCover' import UploadCover, { type CoverImageValue } from '@/components/UploadCover'
import { useKeyboardHeight } from '@/store/keyboardStore'
import { useDictionaryActions } from '@/store/dictionaryStore' import { useDictionaryActions } from '@/store/dictionaryStore'
import { normalize_address } from '@/utils/locationUtils'
import './StadiumDetail.scss' import './StadiumDetail.scss'
@@ -71,16 +69,11 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
stadium, stadium,
onAnyInput onAnyInput
}, ref) => { }, ref) => {
const [openPicker, setOpenPicker] = useState(false); //为了解决上传图片时按钮样式问题 const [openPicker, setOpenPicker] = useState(false);
const [scrollTop, setScrollTop] = useState(0);
const { getDictionaryValue } = useDictionaryActions() const { getDictionaryValue } = useDictionaryActions()
const court_type = getDictionaryValue('court_type') || [] const court_type = getDictionaryValue('court_type') || []
const court_surface = getDictionaryValue('court_surface') || [] const court_surface = getDictionaryValue('court_surface') || []
const supplementary_information = getDictionaryValue('supplementary_information') || [] const supplementary_information = getDictionaryValue('supplementary_information') || []
// 使用全局键盘状态
const { keyboardHeight, isKeyboardVisible, addListener, initializeKeyboardListener } = useKeyboardHeight()
const stadiumInfo = [ const stadiumInfo = [
{ {
label: '场地类型', label: '场地类型',
@@ -146,14 +139,14 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
setFormData({ setFormData({
...formData, ...formData,
name: res.name, name: res.name,
address: normalize_address(res.address || ''), address: res.address,
latitude: res.latitude, latitude: res.latitude,
longitude: res.longitude, longitude: res.longitude,
istance: null istance: null
}) })
}, },
fail: (err: { errMsg: string }) => { fail: (err: { errMsg: string }) => {
console.warn('选择位置失败:', err) console.error('选择位置失败:', err)
const { errMsg } = err || {}; const { errMsg } = err || {};
if (!errMsg.includes('fail cancel')) { if (!errMsg.includes('fail cancel')) {
Taro.showToast({ Taro.showToast({
@@ -177,49 +170,14 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
// 使用全局键盘状态监听 const changePicker = (value) => {
useEffect(() => { setOpenPicker(value)
// 初始化全局键盘监听器
initializeKeyboardListener()
// 添加本地监听器
const removeListener = addListener((height, visible) => {
console.log('AiImportPopup 收到键盘变化:', height, visible)
})
return () => {
removeListener()
}
}, [initializeKeyboardListener, addListener])
const changeTextarea = (value: boolean) => {
if (value) {
// 先滚动到底部
setScrollTop(140);
// 使用 setTimeout 确保滚动后再更新 openPicker
}
}
const changePicker = (value:boolean) => {
setOpenPicker(value);
} }
console.log(stadium,'stadiumstadium'); console.log(stadium,'stadiumstadium');
// 计算滚动区域的最大高度
const scrollMaxHeight = isKeyboardVisible
? `calc(100vh - ${keyboardHeight+40}px)`
: '60vh'
return ( return (
<View className='stadium-detail'> <View className='stadium-detail'>
<ScrollView <ScrollView className='stadium-detail-scroll' refresherBackground="#FAFAFA" scrollY={!openPicker}>
className='stadium-detail-scroll'
refresherBackground="#FAFAFA"
scrollY={!openPicker}
scrollTop={scrollTop}
style={{ maxHeight: scrollMaxHeight }}
>
{/* 已选球场 */} {/* 已选球场 */}
<View <View
className={`stadium-item`} className={`stadium-item`}
@@ -231,7 +189,7 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
<View className='stadium-item-right'> <View className='stadium-item-right'>
<View className='stadium-name'>{formData.name}</View> <View className='stadium-name'>{formData.name}</View>
<View className='stadium-address'> <View className='stadium-address'>
<Text>{calculateDistance(formData.istance || null) + ' · '}</Text> <Text>{calculateDistance(formData.istance || null)} · </Text>
<Text>{formData.address}</Text> <Text>{formData.address}</Text>
<Image src={images.ICON_ARRORW_SMALL} className='stadium-map-icon' /> <Image src={images.ICON_ARRORW_SMALL} className='stadium-map-icon' />
</View> </View>
@@ -262,14 +220,9 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
<View className='textarea-tag-container'> <View className='textarea-tag-container'>
<TextareaTag <TextareaTag
value={formData[item.prop]} value={formData[item.prop]}
onChange={(value) => { onChange={(value) => updateFormData(item.prop, value)}
updateFormData(item.prop, value) onBlur={() => changePicker(false)}
}} onFocus={() => changePicker(true)}
// onBlur={() => {
// }}
onFocus={() => {
changeTextarea(true)
}}
placeholder='有其他场地信息可备注' placeholder='有其他场地信息可备注'
options={(item.options || []).map((o) => ({ label: o, value: o }))} options={(item.options || []).map((o) => ({ label: o, value: o }))}
/> />

View File

@@ -13,7 +13,7 @@ import {
} from "../../config/formSchema/publishBallFormSchema"; } from "../../config/formSchema/publishBallFormSchema";
import { PublishBallFormData } from "../../../types/publishBall"; import { PublishBallFormData } from "../../../types/publishBall";
import PublishService from "@/services/publishService"; import PublishService from "@/services/publishService";
import { getNextHourTime, getEndTime, delay, getBackendErrorMsg } from "@/utils"; import { getNextHourTime, getEndTime, delay } from "@/utils";
import { useGlobalState } from "@/store/global"; import { useGlobalState } from "@/store/global";
import GeneralNavbar from "@/components/GeneralNavbar"; import GeneralNavbar from "@/components/GeneralNavbar";
import images from "@/config/images"; import images from "@/config/images";
@@ -78,8 +78,9 @@ const PublishBall: React.FC = () => {
} = useKeyboardHeight(); } = useKeyboardHeight();
// 获取页面参数并设置导航标题 // 获取页面参数并设置导航标题
const [optionsConfig, setOptionsConfig] = useState<FormFieldConfig[]>( const [optionsConfig, setOptionsConfig] = useState<FormFieldConfig[]>(
publishBallFormSchema, publishBallFormSchema
); );
console.log(userInfo, "userInfo");
const [formData, setFormData] = useState<PublishBallFormData[]>([ const [formData, setFormData] = useState<PublishBallFormData[]>([
defaultFormData, defaultFormData,
]); ]);
@@ -102,11 +103,13 @@ const PublishBall: React.FC = () => {
const updateFormData = ( const updateFormData = (
key: keyof PublishBallFormData, key: keyof PublishBallFormData,
value: any, value: any,
index: number, index: number
) => { ) => {
console.log(key, value, index, "key, value, index");
setFormData((prev) => { setFormData((prev) => {
const newData = [...prev]; const newData = [...prev];
newData[index] = { ...newData[index], [key]: value }; newData[index] = { ...newData[index], [key]: value };
console.log(newData, "newData");
return newData; return newData;
}); });
}; };
@@ -183,7 +186,7 @@ const PublishBall: React.FC = () => {
const confirmDelete = () => { const confirmDelete = () => {
if (deleteConfirm.index >= 0) { if (deleteConfirm.index >= 0) {
setFormData((prev) => setFormData((prev) =>
prev.filter((_, index) => index !== deleteConfirm.index), prev.filter((_, index) => index !== deleteConfirm.index)
); );
closeDeleteConfirm(); closeDeleteConfirm();
Taro.showToast({ Taro.showToast({
@@ -195,7 +198,7 @@ const PublishBall: React.FC = () => {
const validateFormData = ( const validateFormData = (
formData: PublishBallFormData, formData: PublishBallFormData,
isOnSubmit: boolean = false, isOnSubmit: boolean = false
) => { ) => {
const { const {
activityInfo, activityInfo,
@@ -204,7 +207,7 @@ const PublishBall: React.FC = () => {
image_list, image_list,
players, players,
current_players, current_players,
descriptionInfo, descriptionInfo
} = formData; } = formData;
const { play_type, price, location_name } = activityInfo; const { play_type, price, location_name } = activityInfo;
const { description } = descriptionInfo; const { description } = descriptionInfo;
@@ -222,7 +225,7 @@ const PublishBall: React.FC = () => {
// 判断图片是否上传完成 // 判断图片是否上传完成
if (image_list?.length > 0) { if (image_list?.length > 0) {
const uploadInProgress = image_list.some((item) => const uploadInProgress = image_list.some((item) =>
item?.url?.startsWith?.("http://tmp/"), item.url.startsWith("http://tmp/")
); );
if (uploadInProgress) { if (uploadInProgress) {
Taro.showToast({ Taro.showToast({
@@ -364,79 +367,85 @@ const PublishBall: React.FC = () => {
}; };
// 提交表单 // 提交表单
const handleSubmit = async () => { const handleSubmit = async () => {
// 基础验证
console.log(formData, "formData");
const params = getParams(); const params = getParams();
const { republish } = params || {}; const { republish } = params || {};
if (activityType === "individual") { if (activityType === "individual") {
const isValid = validateFormData(formData[0]); const isValid = validateFormData(formData[0]);
if (!isValid || publishLoading) return; if (!isValid || publishLoading) {
return;
}
setPublishLoading(true); setPublishLoading(true);
try { const {
const { activityInfo,
activityInfo, descriptionInfo,
descriptionInfo, is_substitute_supported,
is_substitute_supported, timeRange,
timeRange, players,
players, skill_level,
skill_level, image_list,
image_list, wechat,
wechat, id,
id, ...rest
title, } = formData[0];
...rest const { min, max, organizer_joined } = players;
} = formData[0]; const options = {
const { min, max, organizer_joined } = players; ...rest,
const options = { ...activityInfo,
...rest, ...descriptionInfo,
...activityInfo, ...timeRange,
...descriptionInfo, max_players: max,
...timeRange, min_players: min,
title: title?.replace(/\n/g, ''), organizer_joined: organizer_joined === true ? 1 : 0,
max_players: max, skill_level_min: skill_level[0],
min_players: min, skill_level_max: skill_level[1],
organizer_joined: organizer_joined === true ? 1 : 0, image_list: image_list.map((item) => item.url),
skill_level_min: skill_level[0], is_wechat_contact: wechat.is_wechat_contact ? 1 : 0,
skill_level_max: skill_level[1], wechat_contact: wechat.wechat_contact || wechat.default_wechat_contact,
image_list: image_list.map((item) => item.url), is_substitute_supported: is_substitute_supported ? "1" : "0",
is_wechat_contact: wechat.is_wechat_contact ? 1 : 0, ...(republish === "0" ? { id } : {}),
wechat_contact: wechat.wechat_contact || wechat.default_wechat_contact, };
is_substitute_supported: is_substitute_supported ? "1" : "0", const res =
...(republish === "0" ? { id } : {}), republish === "0"
}; ? await PublishService.gamesUpdate(options)
const res = : await PublishService.createPersonal(options);
republish === "0" const successText = republish === "0" ? "更新成功" : "发布成功";
? await PublishService.gamesUpdate(options) if (res.code === 0 && res.data) {
: await PublishService.createPersonal(options);
const successText = republish === "0" ? "更新成功" : "发布成功";
if (res.code === 0 && res.data) {
Taro.showToast({ title: successText, icon: "success" });
delay(1000);
const id = (res as any).data?.id;
if (republish === "0") {
Taro.navigateBack();
} else {
Taro.redirectTo({
url: `/game_pages/detail/index?id=${id || 1}&from=publish&autoShare=1`,
});
}
} else {
Taro.showToast({
title: getBackendErrorMsg(res, "发布失败"),
icon: "none",
});
setPublishLoading(false);
}
} catch (error) {
Taro.showToast({ Taro.showToast({
title: getBackendErrorMsg(error, "发布失败"), title: successText,
icon: "success",
});
delay(1000);
// 如果是个人球局,则跳转到详情页,并自动分享
// 如果是畅打,则跳转第一个球局详情页,并自动分享 @刘杰
const id = (res as any).data?.id;
// 如果是编辑,就返回,否则就是新发布
if (republish === "0") {
Taro.navigateBack();
} else {
// 使用 redirectTo 替换当前页面,避免返回时回到发布页面
Taro.redirectTo({
// @ts-expect-error: id
url: `/game_pages/detail/index?id=${
id || 1
}&from=publish&autoShare=1`,
});
}
} else {
Taro.showToast({
title: res.message,
icon: "none", icon: "none",
}); });
setPublishLoading(false); setPublishLoading(false);
} }
return;
} }
if (activityType === "group") { if (activityType === "group") {
const isValid = formData.every((item) => validateFormData(item)); const isValid = formData.every((item) => validateFormData(item));
if (!isValid || publishLoading) return; if (!isValid || publishLoading) {
return;
}
setPublishLoading(true);
if (checkAdjacentDataSame(formData)) { if (checkAdjacentDataSame(formData)) {
Taro.showToast({ Taro.showToast({
title: "信息不可与前序场完全一致", title: "信息不可与前序场完全一致",
@@ -444,62 +453,60 @@ const PublishBall: React.FC = () => {
}); });
return; return;
} }
setPublishLoading(true); const options = formData.map((item) => {
try { const {
const options = formData.map((item) => { activityInfo,
const { descriptionInfo,
activityInfo, timeRange,
descriptionInfo, players,
timeRange, skill_level,
players, is_substitute_supported,
skill_level, id,
is_substitute_supported, ...rest
id, } = item;
title, const { min, max, organizer_joined } = players;
...rest return {
} = item; ...rest,
const { min, max, organizer_joined } = players; ...activityInfo,
return { ...descriptionInfo,
...rest, ...timeRange,
...activityInfo, max_players: max,
...descriptionInfo, min_players: min,
...timeRange, organizer_joined: organizer_joined === true ? 1 : 0,
title: title?.replace(/\n/g, ' '), skill_level_min: skill_level[0],
max_players: max, skill_level_max: skill_level[1],
min_players: min, is_substitute_supported: is_substitute_supported ? "1" : "0",
organizer_joined: organizer_joined === true ? 1 : 0, image_list: item.image_list.map((img) => img.url),
skill_level_min: skill_level[0], ...(republish === "0" ? { id } : {}),
skill_level_max: skill_level[1], };
is_substitute_supported: is_substitute_supported ? "1" : "0", });
image_list: item.image_list.map((img) => img.url), const successText = republish === "0" ? "更新成功" : "发布成功";
...(republish === "0" ? { id } : {}), const res =
}; republish === "0"
}); ? await PublishService.gamesUpdate(options[0])
const successText = republish === "0" ? "更新成功" : "发布成功"; : await PublishService.create_play_pmoothlys({ rows: options });
const res = if (res.code === 0 && res.data) {
republish === "0"
? await PublishService.gamesUpdate(options[0])
: await PublishService.create_play_pmoothlys({ rows: options });
if (res.code === 0 && res.data) {
Taro.showToast({ title: successText, icon: "success" });
delay(1000);
const id =
republish === "0"
? (res as any).data?.id
: (res as any).data?.[0]?.id;
Taro.redirectTo({
url: `/game_pages/detail/index?id=${id || 1}&from=publish&autoShare=1`,
});
} else {
Taro.showToast({
title: getBackendErrorMsg(res, "发布失败"),
icon: "none",
});
setPublishLoading(false);
}
} catch (error) {
Taro.showToast({ Taro.showToast({
title: getBackendErrorMsg(error, "发布失败"), title: successText,
icon: "success",
});
delay(1000);
// 如果是个人球局,则跳转到详情页,并自动分享
// 如果是畅打,则跳转第一个球局详情页,并自动分享 @刘杰
const id =
republish === "0"
? (res as any).data?.id
: (res as any).data?.[0]?.id;
// 使用 redirectTo 替换当前页面,避免返回时回到发布页面
Taro.redirectTo({
// @ts-expect-error: id
url: `/game_pages/detail/index?id=${
id || 1
}&from=publish&autoShare=1`,
});
} else {
Taro.showToast({
title: res.message,
icon: "none", icon: "none",
}); });
setPublishLoading(false); setPublishLoading(false);
@@ -509,7 +516,7 @@ const PublishBall: React.FC = () => {
const mergeWithDefault = ( const mergeWithDefault = (
data: any, data: any,
isDetail: boolean = false, isDetail: boolean = false
): PublishBallFormData => { ): PublishBallFormData => {
// ai导入与详情数据处理 // ai导入与详情数据处理
const { const {
@@ -734,6 +741,7 @@ const PublishBall: React.FC = () => {
} else { } else {
setIsSubmitDisabled(false); setIsSubmitDisabled(false);
} }
console.log(formData, "formData");
}, [formData]); }, [formData]);
useEffect(() => { useEffect(() => {
@@ -746,8 +754,9 @@ const PublishBall: React.FC = () => {
initializeKeyboardListener(); initializeKeyboardListener();
// 添加本地监听器 // 添加本地监听器
const removeListener = addListener(() => { const removeListener = addListener((height, visible) => {
// 布局是否响应交由 shouldReactToKeyboard 决定 console.log("PublishBall 收到键盘变化:", height, visible);
// 这里只记录或用于其他逻辑,布局是否响应交由 shouldReactToKeyboard 决定
}); });
return () => { return () => {
@@ -780,7 +789,6 @@ const PublishBall: React.FC = () => {
> >
<GeneralNavbar <GeneralNavbar
title={titleBar} title={titleBar}
backgroundColor={'#FAFAFA'}
className={styles["publish-ball-navbar"]} className={styles["publish-ball-navbar"]}
/> />
<View <View

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More