Compare commits
41 Commits
af2c472030
...
feat/liuji
| Author | SHA1 | Date | |
|---|---|---|---|
| c47ebce43c | |||
| b0f4b5713d | |||
| f7f10f5d15 | |||
|
|
2bcdd93479 | ||
|
|
8d0ed5b1b3 | ||
|
|
e99986c52a | ||
|
|
4b2f6707cc | ||
|
|
a019fe473b | ||
|
|
1d0d2edaa2 | ||
| 5926e096b5 | |||
|
|
e07f2ad2d1 | ||
|
|
bfc6a149f0 | ||
|
|
6f73bb6d99 | ||
| 54b7a27af5 | |||
| 396ff4a347 | |||
|
|
b732bd361e | ||
|
|
5146894d92 | ||
|
|
07cf8e884e | ||
| 5416ea127c | |||
| a7bc517fae | |||
| 16b38539f6 | |||
|
|
0d46311bbc | ||
| e884b1f258 | |||
| 84159a4835 | |||
| 2acee85dd5 | |||
| ba72e0ec97 | |||
|
|
32f5339cc2 | ||
|
|
2cbbc7f432 | ||
| 694b00e011 | |||
| 87eaa31cef | |||
|
|
f131c9896d | ||
| b08f3325e6 | |||
| ff864fe64d | |||
|
|
da0ae6046c | ||
| 42025d49f8 | |||
|
|
536619ebfc | ||
|
|
5a10c73adf | ||
|
|
b29e000747 | ||
|
|
02841222a2 | ||
|
|
b417b3a4c2 | ||
|
|
8d729a0132 |
2
.env.dev_local
Normal file
2
.env.dev_local
Normal file
@@ -0,0 +1,2 @@
|
||||
APP_ENV=dev_local
|
||||
TARO_APP_ID=wx815b533167eb7b53
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -8,4 +8,4 @@ node_modules/
|
||||
src/config/env.ts
|
||||
.vscode
|
||||
*.http
|
||||
env.ts
|
||||
|
||||
|
||||
79
config/env.config.ts
Normal file
79
config/env.config.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* 统一环境配置(dev/sit/pr)
|
||||
* 构建时通过 APP_ENV 选择,defineConstants 注入业务代码
|
||||
* project.config.json 的 appid 由 scripts/sync-project-config.js 同步
|
||||
*/
|
||||
|
||||
export type EnvType = "dev" | "dev_local" | "sit" | "pr";
|
||||
|
||||
export interface EnvConfig {
|
||||
name: string;
|
||||
apiBaseURL: string;
|
||||
ossBaseURL: string;
|
||||
appid: string;
|
||||
timeout: number;
|
||||
enableLog: boolean;
|
||||
enableMock: boolean;
|
||||
customerService: {
|
||||
corpId: string;
|
||||
serviceUrl: string;
|
||||
};
|
||||
}
|
||||
|
||||
const baseConfig = {
|
||||
apiBaseURL: "https://tennis.bimwe.com",
|
||||
ossBaseURL: "https://bimwe.oss-cn-shanghai.aliyuncs.com",
|
||||
appid: "wx815b533167eb7b53", // 测试号
|
||||
timeout: 15000,
|
||||
enableLog: true,
|
||||
enableMock: false,
|
||||
customerService: {
|
||||
corpId: "ww51fc969e8b76af82",
|
||||
serviceUrl: "https://work.weixin.qq.com/kfid/kfc64085b93243c5c91",
|
||||
},
|
||||
}
|
||||
|
||||
export const envConfigs: Record<EnvType, EnvConfig> = {
|
||||
// 本地开发:API 指向本地或测试服
|
||||
dev: {
|
||||
name: "DEV",
|
||||
// apiBaseURL: "http://localhost:9098",
|
||||
...baseConfig
|
||||
},
|
||||
// 本地联调:API 指向本机
|
||||
dev_local: {
|
||||
name: "DEV_LOCAL",
|
||||
|
||||
...Object.assign(baseConfig, {
|
||||
apiBaseURL: "http://localhost:9098",
|
||||
})
|
||||
|
||||
},
|
||||
|
||||
// SIT 测试环境
|
||||
sit: {
|
||||
name: "SIT",
|
||||
...Object.assign(baseConfig, {
|
||||
apiBaseURL: "https://tennis.bimwe.com",
|
||||
})
|
||||
},
|
||||
|
||||
// PR 生产环境
|
||||
pr: {
|
||||
name: "PR",
|
||||
apiBaseURL: "https://youchang.qiongjingtiyu.com",
|
||||
ossBaseURL: "https://youchang2026.oss-cn-shanghai.aliyuncs.com",
|
||||
appid: "wx915ecf6c01bea4ec", // 生产小程序 appid,按实际填写
|
||||
timeout: 10000,
|
||||
enableLog: false,
|
||||
enableMock: false,
|
||||
customerService: {
|
||||
corpId: "ww9a2d9a5d9410c664",
|
||||
serviceUrl: "https://work.weixin.qq.com/kfid/kfcd355e162e0390684",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function getEnvConfig(env: EnvType): EnvConfig {
|
||||
return envConfigs[env];
|
||||
}
|
||||
@@ -2,11 +2,21 @@ import { defineConfig, type UserConfigExport } from '@tarojs/cli'
|
||||
import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin'
|
||||
import devConfig from './dev'
|
||||
import prodConfig from './prod'
|
||||
// import vitePluginImp from 'vite-plugin-imp'
|
||||
import { getEnvConfig, type EnvType } from './env.config'
|
||||
import path from 'path'
|
||||
|
||||
// 环境:dev(本地) | dev_local(联调) | sit(测试) | pr(生产)
|
||||
const ENV_LIST: EnvType[] = ['dev', 'dev_local', 'sit', 'pr']
|
||||
|
||||
// https://taro-docs.jd.com/docs/next/config#defineconfig-辅助函数
|
||||
export default defineConfig<'webpack5'>(async (merge, { command, mode }) => {
|
||||
const appEnv = (
|
||||
(ENV_LIST.includes(mode as EnvType) ? mode : process.env.APP_ENV) ||
|
||||
(process.env.NODE_ENV === 'production' ? 'pr' : 'dev')
|
||||
) as EnvType
|
||||
|
||||
const envConfig = getEnvConfig(appEnv)
|
||||
|
||||
const baseConfig: UserConfigExport<'webpack5'> = {
|
||||
projectName: 'playBallTogether',
|
||||
date: '2025-8-9',
|
||||
@@ -22,6 +32,13 @@ export default defineConfig<'webpack5'>(async (merge, { command, mode }) => {
|
||||
outputRoot: 'dist',
|
||||
plugins: ['@tarojs/plugin-html'],
|
||||
defineConstants: {
|
||||
'process.env.APP_ENV': JSON.stringify(appEnv),
|
||||
'process.env.API_BASE_URL': JSON.stringify(envConfig.apiBaseURL),
|
||||
'process.env.OSS_BASE_URL': JSON.stringify(envConfig.ossBaseURL),
|
||||
'process.env.ENABLE_LOG': JSON.stringify(envConfig.enableLog),
|
||||
'process.env.TIMEOUT': JSON.stringify(envConfig.timeout),
|
||||
'process.env.CUSTOMER_CORP_ID': JSON.stringify(envConfig.customerService.corpId),
|
||||
'process.env.CUSTOMER_SERVICE_URL': JSON.stringify(envConfig.customerService.serviceUrl),
|
||||
},
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, '..', 'src'),
|
||||
@@ -76,6 +93,9 @@ export default defineConfig<'webpack5'>(async (merge, { command, mode }) => {
|
||||
},
|
||||
// @ts-expect-error: Taro 类型定义缺少 mini.hot
|
||||
hot: true,
|
||||
projectConfig: {
|
||||
appid: envConfig.appid,
|
||||
},
|
||||
},
|
||||
h5: {
|
||||
publicPath: '/',
|
||||
|
||||
35
package.json
35
package.json
@@ -10,32 +10,17 @@
|
||||
"framework": "React"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run build:weapp ",
|
||||
"dev": "npm run dev:weapp ",
|
||||
"build:weapp": "taro build --type weapp --mode production",
|
||||
"build:swan": "taro build --type swan",
|
||||
"build:alipay": "taro build --type alipay",
|
||||
"build:tt": "taro build --type tt",
|
||||
"build:h5": "taro build --type h5",
|
||||
"build:rn": "taro build --type rn",
|
||||
"build:qq": "taro build --type qq",
|
||||
"build:jd": "taro build --type jd",
|
||||
"build:quickapp": "taro build --type quickapp",
|
||||
"dev:weapp": "npm run build:weapp -- --watch",
|
||||
"dev:swan": "npm run build:swan -- --watch",
|
||||
"dev:alipay": "npm run build:alipay -- --watch",
|
||||
"dev:tt": "npm run build:tt -- --watch",
|
||||
"dev:h5": "npm run build:h5 -- --watch",
|
||||
"dev:rn": "npm run build:rn -- --watch",
|
||||
"dev:qq": "npm run build:qq -- --watch",
|
||||
"dev:jd": "npm run build:jd -- --watch",
|
||||
"dev:quickapp": "npm run build:quickapp -- --watch"
|
||||
"dev": "npm run dev:weapp",
|
||||
"dev:local": "npm run dev:weapp:dev_local",
|
||||
"dev:weapp": "node scripts/sync-project-config.js dev && taro build --type weapp --mode dev --watch",
|
||||
"dev:weapp:dev_local": "node scripts/sync-project-config.js dev_local && taro build --type weapp --mode dev_local --watch",
|
||||
"build": "npm run build:weapp",
|
||||
"build:weapp": "node scripts/sync-project-config.js pr && taro build --type weapp --mode pr",
|
||||
"build:sit": "node scripts/sync-project-config.js sit && taro build --type weapp --mode sit",
|
||||
"build:pr": "node scripts/sync-project-config.js pr && taro build --type weapp --mode pr",
|
||||
"dev:h5": "npm run build:h5 -- --watch"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 3 versions",
|
||||
"Android >= 4.1",
|
||||
"ios >= 8"
|
||||
],
|
||||
"browserslist": ["last 3 versions", "Android >= 4.1", "ios >= 8"],
|
||||
"author": "",
|
||||
"dependencies": {
|
||||
"@babel/plugin-transform-runtime": "^7.28.3",
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
"miniprogramRoot": "dist/",
|
||||
"projectname": "playBallTogether",
|
||||
"description": "playBallTogether",
|
||||
"appid": "wx915ecf6c01bea4ec",
|
||||
|
||||
"appid": "wx815b533167eb7b53",
|
||||
"setting": {
|
||||
"urlCheck": true,
|
||||
"es6": true,
|
||||
@@ -47,4 +46,4 @@
|
||||
"simulatorType": "wechat",
|
||||
"simulatorPluginLibVersion": {},
|
||||
"condition": {}
|
||||
}
|
||||
}
|
||||
|
||||
25
scripts/sync-project-config.js
Normal file
25
scripts/sync-project-config.js
Normal file
@@ -0,0 +1,25 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
require('ts-node/register/transpile-only');
|
||||
|
||||
const envArg = process.argv[2];
|
||||
const appEnv = envArg || process.env.APP_ENV || (process.env.NODE_ENV === 'production' ? 'pr' : 'dev');
|
||||
|
||||
const envConfigPath = path.resolve(__dirname, '../config/env.config.ts');
|
||||
const { envConfigs } = require(envConfigPath);
|
||||
|
||||
const config = envConfigs[appEnv];
|
||||
if (!config) {
|
||||
console.error(`[sync-project-config] Unknown APP_ENV: ${appEnv}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const projectConfigPath = path.resolve(__dirname, '../project.config.json');
|
||||
const projectConfigRaw = fs.readFileSync(projectConfigPath, 'utf-8');
|
||||
const projectConfig = JSON.parse(projectConfigRaw);
|
||||
|
||||
projectConfig.appid = config.appid;
|
||||
|
||||
fs.writeFileSync(projectConfigPath, JSON.stringify(projectConfig, null, 2) + '\n', 'utf-8');
|
||||
console.log(`[sync-project-config] project.config.json appid -> ${config.appid} (${appEnv})`);
|
||||
@@ -19,8 +19,7 @@ page {
|
||||
|
||||
@font-face {
|
||||
font-family: "Quicksand";
|
||||
// 注意:此路径来自 @/config/api.ts 中的 OSS_BASE_URL 配置
|
||||
// 如需修改,请更新配置文件中的 OSS_BASE_URL
|
||||
src: url("https://youchang2026.oss-cn-shanghai.aliyuncs.com/front/ball/other/57dc951f-f10e-45b7-9157-0b1e468187fd.ttf") format("truetype");
|
||||
// 注意:此路径对应 @/config/api.ts 中的 OSS_BASE
|
||||
src: url("https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/other/57dc951f-f10e-45b7-9157-0b1e468187fd.ttf") format("truetype");
|
||||
font-display: swap;
|
||||
}
|
||||
@@ -34,7 +34,7 @@ function toast(msg) {
|
||||
|
||||
interface CommentInputProps {
|
||||
onConfirm?: (
|
||||
value: { content: string } & Partial<CommentInputReplyParamsType>
|
||||
value: { content: string } & Partial<CommentInputReplyParamsType>,
|
||||
) => void;
|
||||
}
|
||||
|
||||
@@ -49,119 +49,118 @@ interface CommentInputReplyParamsType {
|
||||
nickname: string;
|
||||
}
|
||||
|
||||
const CommentInput = forwardRef<CommentInputRef, CommentInputProps>(function (
|
||||
props,
|
||||
ref
|
||||
) {
|
||||
const { onConfirm } = props;
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [value, setValue] = useState("");
|
||||
const [params, setParams] = useState<
|
||||
CommentInputReplyParamsType | undefined
|
||||
>();
|
||||
const CommentInput = forwardRef<CommentInputRef, CommentInputProps>(
|
||||
function (props, ref) {
|
||||
const { onConfirm } = props;
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [value, setValue] = useState("");
|
||||
const [params, setParams] = useState<
|
||||
CommentInputReplyParamsType | undefined
|
||||
>();
|
||||
|
||||
const {
|
||||
keyboardHeight,
|
||||
isKeyboardVisible,
|
||||
addListener,
|
||||
initializeKeyboardListener,
|
||||
} = useKeyboardHeight();
|
||||
const {
|
||||
keyboardHeight,
|
||||
isKeyboardVisible,
|
||||
addListener,
|
||||
initializeKeyboardListener,
|
||||
} = useKeyboardHeight();
|
||||
|
||||
// 使用全局键盘状态监听
|
||||
useEffect(() => {
|
||||
// 初始化全局键盘监听器
|
||||
initializeKeyboardListener();
|
||||
// 使用全局键盘状态监听
|
||||
useEffect(() => {
|
||||
// 初始化全局键盘监听器
|
||||
initializeKeyboardListener();
|
||||
|
||||
// 添加本地监听器
|
||||
const removeListener = addListener((height, visible) => {
|
||||
console.log("PublishBall 收到键盘变化:", height, visible);
|
||||
// 这里只记录或用于其他逻辑,布局是否响应交由 shouldReactToKeyboard 决定
|
||||
});
|
||||
// 添加本地监听器
|
||||
const removeListener = addListener(() => {
|
||||
// 布局是否响应交由 shouldReactToKeyboard 决定
|
||||
});
|
||||
|
||||
return () => {
|
||||
removeListener();
|
||||
};
|
||||
}, [initializeKeyboardListener, addListener]);
|
||||
return () => {
|
||||
removeListener();
|
||||
};
|
||||
}, [initializeKeyboardListener, addListener]);
|
||||
|
||||
const inputDomRef = useRef(null);
|
||||
const inputDomRef = useRef(null);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
show: (_params: CommentInputReplyParamsType | undefined) => {
|
||||
setVisible(true);
|
||||
setTimeout(() => {
|
||||
inputDomRef.current && inputDomRef.current?.focus();
|
||||
}, 100);
|
||||
setParams(_params);
|
||||
},
|
||||
}));
|
||||
useImperativeHandle(ref, () => ({
|
||||
show: (_params: CommentInputReplyParamsType | undefined) => {
|
||||
setVisible(true);
|
||||
setTimeout(() => {
|
||||
inputDomRef.current && inputDomRef.current?.focus();
|
||||
}, 100);
|
||||
setParams(_params);
|
||||
},
|
||||
}));
|
||||
|
||||
function handleSend() {
|
||||
if (!value) {
|
||||
toast("评论内容不得为空");
|
||||
return;
|
||||
function handleSend() {
|
||||
if (!value) {
|
||||
toast("评论内容不得为空");
|
||||
return;
|
||||
}
|
||||
if (value.length > 200) {
|
||||
return;
|
||||
}
|
||||
onConfirm?.({ content: value, ...params });
|
||||
onClose();
|
||||
}
|
||||
if (value.length > 200) {
|
||||
return;
|
||||
}
|
||||
onConfirm?.({ content: value, ...params });
|
||||
onClose();
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
setVisible(false);
|
||||
setValue("");
|
||||
inputDomRef.current && inputDomRef.current?.blur();
|
||||
}
|
||||
console.log(keyboardHeight, "keyboardHeight");
|
||||
return (
|
||||
<CommonPopup
|
||||
visible={visible}
|
||||
showHeader={false}
|
||||
hideFooter
|
||||
zIndex={1002}
|
||||
onClose={onClose}
|
||||
style={{
|
||||
// height: "60px!important",
|
||||
minHeight: "unset",
|
||||
bottom:
|
||||
isKeyboardVisible && keyboardHeight > 0 ? `${keyboardHeight}px` : "0",
|
||||
}}
|
||||
enableDragToClose={false}
|
||||
>
|
||||
<View className={styles.inputContainer}>
|
||||
<View className={styles.inputWrapper}>
|
||||
<Textarea
|
||||
adjustPosition={false}
|
||||
ref={inputDomRef}
|
||||
className={styles.input}
|
||||
value={value}
|
||||
onInput={(e) => setValue(e.detail.value)}
|
||||
placeholder={
|
||||
params?.reply_to_user_id ? `回复 @${params.nickname}` : "写评论"
|
||||
}
|
||||
confirmType="send"
|
||||
onConfirm={handleSend}
|
||||
focus
|
||||
maxlength={-1}
|
||||
autoHeight
|
||||
// showCount
|
||||
/>
|
||||
<View
|
||||
className={classnames(
|
||||
styles.limit,
|
||||
value.length > 200 ? styles.red : ""
|
||||
)}
|
||||
>
|
||||
<Text>{value.length}</Text>/<Text>200</Text>
|
||||
function onClose() {
|
||||
setVisible(false);
|
||||
setValue("");
|
||||
inputDomRef.current && inputDomRef.current?.blur();
|
||||
}
|
||||
return (
|
||||
<CommonPopup
|
||||
visible={visible}
|
||||
showHeader={false}
|
||||
hideFooter
|
||||
zIndex={1002}
|
||||
onClose={onClose}
|
||||
style={{
|
||||
// height: "60px!important",
|
||||
minHeight: "unset",
|
||||
bottom:
|
||||
isKeyboardVisible && keyboardHeight > 0
|
||||
? `${keyboardHeight}px`
|
||||
: "0",
|
||||
}}
|
||||
enableDragToClose={false}
|
||||
>
|
||||
<View className={styles.inputContainer}>
|
||||
<View className={styles.inputWrapper}>
|
||||
<Textarea
|
||||
adjustPosition={false}
|
||||
ref={inputDomRef}
|
||||
className={styles.input}
|
||||
value={value}
|
||||
onInput={(e) => setValue(e.detail.value)}
|
||||
placeholder={
|
||||
params?.reply_to_user_id ? `回复 @${params.nickname}` : "写评论"
|
||||
}
|
||||
confirmType="send"
|
||||
onConfirm={handleSend}
|
||||
focus
|
||||
maxlength={-1}
|
||||
autoHeight
|
||||
// showCount
|
||||
/>
|
||||
<View
|
||||
className={classnames(
|
||||
styles.limit,
|
||||
value.length > 200 ? styles.red : "",
|
||||
)}
|
||||
>
|
||||
<Text>{value.length}</Text>/<Text>200</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className={styles.sendIcon} onClick={handleSend}>
|
||||
<Image className={styles.sendImage} src={sendImg} />
|
||||
</View>
|
||||
</View>
|
||||
<View className={styles.sendIcon} onClick={handleSend}>
|
||||
<Image className={styles.sendImage} src={sendImg} />
|
||||
</View>
|
||||
</View>
|
||||
</CommonPopup>
|
||||
);
|
||||
});
|
||||
</CommonPopup>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
function isReplyComment(item: BaseComment<any>): item is ReplyComment {
|
||||
return "reply_to_user" in item;
|
||||
@@ -208,7 +207,7 @@ function CommentItem(props: {
|
||||
className={classnames(
|
||||
styles.commentItem,
|
||||
blink_id === comment.id && styles.blink,
|
||||
styles.weight_super
|
||||
styles.weight_super,
|
||||
)}
|
||||
key={comment.id}
|
||||
id={`comment_id_${comment.id}`}
|
||||
@@ -293,7 +292,8 @@ function CommentItem(props: {
|
||||
/>
|
||||
))}
|
||||
{!isReplyComment(comment) &&
|
||||
comment.replies.length !== comment.reply_count && (
|
||||
comment.replies.length !== comment.reply_count &&
|
||||
comment.replies.length > 3 && (
|
||||
<View
|
||||
className={styles.viewMore}
|
||||
onClick={() => handleLoadMore(comment)}
|
||||
@@ -313,7 +313,7 @@ export default forwardRef(function Comments(
|
||||
message_id?: number;
|
||||
onScrollTo: (id: string) => void;
|
||||
},
|
||||
ref
|
||||
ref,
|
||||
) {
|
||||
const { game_id, publisher_id, message_id, onScrollTo } = props;
|
||||
const [comments, setComments] = useState<Comment[]>([]);
|
||||
@@ -371,7 +371,7 @@ export default forwardRef(function Comments(
|
||||
replies: [res.data, ...item.replies].sort((a, b) =>
|
||||
dayjs(a.create_time).isAfter(dayjs(b.create_time))
|
||||
? 1
|
||||
: -1
|
||||
: -1,
|
||||
),
|
||||
};
|
||||
});
|
||||
@@ -435,7 +435,7 @@ export default forwardRef(function Comments(
|
||||
item.replies.splice(
|
||||
page === 1 ? 0 : page * PAGESIZE - 1,
|
||||
newReplies.length,
|
||||
...newReplies
|
||||
...newReplies,
|
||||
);
|
||||
item.reply_count = res.data.count;
|
||||
}
|
||||
@@ -502,7 +502,7 @@ export default forwardRef(function Comments(
|
||||
return {
|
||||
...item,
|
||||
replies: item.replies.filter(
|
||||
(replyItem) => replyItem.id !== id
|
||||
(replyItem) => replyItem.id !== id,
|
||||
),
|
||||
reply_count: item.reply_count - 1,
|
||||
};
|
||||
|
||||
208
src/components/CustomPopup/CustomPopup.tsx
Normal file
208
src/components/CustomPopup/CustomPopup.tsx
Normal file
@@ -0,0 +1,208 @@
|
||||
import React, { useRef, useState, useEffect } from 'react'
|
||||
import type { CSSProperties, ReactNode } from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Button } from '@nutui/nutui-react-taro'
|
||||
import { useKeyboardHeight } from '@/store/keyboardStore'
|
||||
import styles from './index.module.scss'
|
||||
|
||||
export interface CustomPopupProps {
|
||||
visible: boolean
|
||||
onClose: () => void
|
||||
title?: ReactNode
|
||||
showHeader?: boolean
|
||||
hideFooter?: boolean
|
||||
cancelText?: string
|
||||
confirmText?: string
|
||||
onCancel?: () => void
|
||||
onConfirm?: () => void
|
||||
children?: ReactNode
|
||||
className?: string
|
||||
style?: CSSProperties
|
||||
// 与 CommonPopup 保持入参一致
|
||||
position?: 'center' | 'bottom' | 'top' | 'left' | 'right'
|
||||
round?: boolean
|
||||
zIndex?: number
|
||||
enableDragToClose?: boolean
|
||||
}
|
||||
|
||||
const CustomPopup: React.FC<CustomPopupProps> = ({
|
||||
visible,
|
||||
onClose,
|
||||
title,
|
||||
showHeader = false,
|
||||
hideFooter = false,
|
||||
cancelText = '返回',
|
||||
confirmText = '完成',
|
||||
onCancel,
|
||||
onConfirm,
|
||||
children,
|
||||
className,
|
||||
style,
|
||||
position = 'bottom',
|
||||
round = true,
|
||||
zIndex,
|
||||
enableDragToClose = true,
|
||||
}) => {
|
||||
const [dragOffset, setDragOffset] = useState(0)
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const touchStartY = useRef(0)
|
||||
|
||||
// 使用全局键盘状态
|
||||
const { keyboardHeight, isKeyboardVisible, addListener, initializeKeyboardListener } = useKeyboardHeight()
|
||||
|
||||
// 使用全局键盘状态监听
|
||||
useEffect(() => {
|
||||
// 初始化全局键盘监听器
|
||||
initializeKeyboardListener()
|
||||
|
||||
// 添加本地监听器
|
||||
const removeListener = addListener((height, visible) => {
|
||||
console.log('CustomPopup 收到键盘变化:', height, visible)
|
||||
})
|
||||
|
||||
return () => {
|
||||
removeListener()
|
||||
}
|
||||
}, [initializeKeyboardListener, addListener])
|
||||
|
||||
if (!visible) {
|
||||
return null
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
if (onCancel) {
|
||||
onCancel()
|
||||
} else {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
const handleTouchStart = (e: any) => {
|
||||
if (!enableDragToClose) return
|
||||
|
||||
touchStartY.current = e.touches[0].clientY
|
||||
setIsDragging(true)
|
||||
}
|
||||
|
||||
const handleTouchMove = (e: any) => {
|
||||
if (!enableDragToClose || !isDragging) return
|
||||
|
||||
const currentY = e.touches[0].clientY
|
||||
const deltaY = currentY - touchStartY.current
|
||||
|
||||
if (deltaY > 0) {
|
||||
setDragOffset(Math.min(deltaY, 100))
|
||||
}
|
||||
}
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
if (!enableDragToClose || !isDragging) return
|
||||
|
||||
setIsDragging(false)
|
||||
|
||||
if (dragOffset > 50) {
|
||||
onClose()
|
||||
}
|
||||
|
||||
setDragOffset(0)
|
||||
}
|
||||
|
||||
const overlayAlignItems =
|
||||
position === 'center'
|
||||
? 'center'
|
||||
: position === 'top'
|
||||
? 'flex-start'
|
||||
: 'flex-end'
|
||||
|
||||
const handleOverlayClick = () => {
|
||||
onClose()
|
||||
}
|
||||
|
||||
// 阻止弹窗内的触摸事件冒泡
|
||||
const handleTouchMoveInPopup = (e: any) => {
|
||||
if (!isKeyboardVisible) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<View
|
||||
className={styles['custom-popup-overlay']}
|
||||
style={{ zIndex: zIndex ?? undefined, alignItems: overlayAlignItems }}
|
||||
onClick={handleOverlayClick}
|
||||
>
|
||||
<View className={styles['custom-popup-move']} onTouchMove={handleTouchMoveInPopup} catchMove></View>
|
||||
<View
|
||||
className={`${styles['custom-popup']} ${className ? className : ''}`}
|
||||
style={{
|
||||
...style,
|
||||
paddingBottom: isKeyboardVisible ? `${keyboardHeight}px` : undefined,
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
{enableDragToClose && (
|
||||
<View
|
||||
className={styles['custom-popup__drag-handle-container']}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
>
|
||||
<View
|
||||
className={styles['custom-popup__drag-handle']}
|
||||
style={{
|
||||
transform: `translateX(-50%) translateY(${dragOffset * 0.3}px)`,
|
||||
opacity: isDragging ? 0.8 : 1,
|
||||
transition: isDragging ? 'none' : 'all 0.3s ease-out',
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{showHeader && (
|
||||
<View className={styles['custom-popup__header']}>
|
||||
{typeof title === 'string' ? (
|
||||
<Text className={styles['custom-popup__title']}>{title}</Text>
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
<View className={styles['close_button']} onClick={onClose}>
|
||||
<View className={styles['close_icon']}>
|
||||
<View className={styles['close_line']} />
|
||||
<View className={styles['close_line']} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View className={styles['custom-popup__body']}>{children}</View>
|
||||
|
||||
{!hideFooter && !isKeyboardVisible && (
|
||||
<View className={styles['custom-popup__footer']}>
|
||||
<Button
|
||||
className={`${styles['custom-popup__btn']} ${styles['custom-popup__btn-cancel']}`}
|
||||
type="default"
|
||||
size="small"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
{cancelText}
|
||||
</Button>
|
||||
<Button
|
||||
className={`${styles['custom-popup__btn']} ${styles['custom-popup__btn-confirm']}`}
|
||||
type="primary"
|
||||
size="small"
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{confirmText}
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomPopup
|
||||
|
||||
155
src/components/CustomPopup/index.module.scss
Normal file
155
src/components/CustomPopup/index.module.scss
Normal file
@@ -0,0 +1,155 @@
|
||||
@use "~@/scss/themeColor.scss" as theme;
|
||||
|
||||
.custom-popup-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
}
|
||||
.custom-popup-move{
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 998;
|
||||
}
|
||||
.custom-popup {
|
||||
position: relative;
|
||||
z-index: 999;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: theme.$page-background-color;
|
||||
border-radius: 20px 20px 0 0;
|
||||
overflow: hidden;
|
||||
transition: padding-bottom 0.3s ease;
|
||||
.custom-popup__drag-handle-container {
|
||||
position: relative;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.custom-popup__drag-handle {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 50%;
|
||||
width: 90px;
|
||||
height: 30px;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
width: 32px;
|
||||
height: 4px;
|
||||
background-color: rgba(22, 24, 35, 0.2);
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-popup__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
|
||||
.custom-popup__title {
|
||||
font-family: "PingFang SC";
|
||||
font-weight: 600;
|
||||
font-size: 22px;
|
||||
line-height: 1.27em;
|
||||
color: #000000;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.close_button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #ffffff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
box-shadow: 0px 4px 36px 0px rgba(0, 0, 0, 0.06);
|
||||
|
||||
.close_icon {
|
||||
position: relative;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
|
||||
.close_line {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 17px;
|
||||
height: 3px;
|
||||
border-radius: 3px;
|
||||
background: #000000;
|
||||
transform: translate(-50%, -50%) rotate(45deg);
|
||||
|
||||
&:nth-child(2) {
|
||||
transform: translate(-50%, -50%) rotate(-45deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-popup__body {
|
||||
flex: 1 1 auto;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.custom-popup__footer {
|
||||
padding: 8px 10px 0 10px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
background: #fafafa;
|
||||
padding-bottom: max(10px, env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.custom-popup__btn {
|
||||
flex: 1;
|
||||
font-feature-settings: "liga" off, "clig" off;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.custom-popup__btn-cancel {
|
||||
background: #f5f6f7;
|
||||
color: #1f2329;
|
||||
border: none;
|
||||
width: 154px;
|
||||
height: 44px;
|
||||
border-radius: 12px !important;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.06);
|
||||
background: #fff;
|
||||
padding: 4px 10px;
|
||||
}
|
||||
|
||||
.custom-popup__btn-confirm {
|
||||
width: 154px;
|
||||
height: 44px;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.06);
|
||||
background: #000;
|
||||
border-radius: 12px !important;
|
||||
padding: 4px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
4
src/components/CustomPopup/index.ts
Normal file
4
src/components/CustomPopup/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import CustomPopup from './CustomPopup'
|
||||
export default CustomPopup
|
||||
export * from './CustomPopup'
|
||||
|
||||
@@ -13,7 +13,9 @@
|
||||
align-items: center;
|
||||
color: #000;
|
||||
text-align: center;
|
||||
font-feature-settings: 'liga' off, 'clig' off;
|
||||
font-feature-settings:
|
||||
"liga" off,
|
||||
"clig" off;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
@@ -32,7 +34,9 @@
|
||||
padding-top: 24px;
|
||||
color: #000;
|
||||
text-align: center;
|
||||
font-feature-settings: 'liga' off, 'clig' off;
|
||||
font-feature-settings:
|
||||
"liga" off,
|
||||
"clig" off;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
@@ -48,8 +52,10 @@
|
||||
align-items: center;
|
||||
|
||||
.tips {
|
||||
color: rgba(60, 60, 67, 0.60);
|
||||
font-feature-settings: 'liga' off, 'clig' off;
|
||||
color: rgba(60, 60, 67, 0.6);
|
||||
font-feature-settings:
|
||||
"liga" off,
|
||||
"clig" off;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
@@ -62,13 +68,15 @@
|
||||
margin-top: 8px;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
background: #F0F0F0;
|
||||
background: #f0f0f0;
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
&:placeholder-shown {
|
||||
color: rgba(60, 60, 67, 0.30);
|
||||
font-feature-settings: 'liga' off, 'clig' off;
|
||||
color: rgba(60, 60, 67, 0.3);
|
||||
font-feature-settings:
|
||||
"liga" off,
|
||||
"clig" off;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
@@ -84,11 +92,12 @@
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 44px;
|
||||
border-top: 0.5px solid #CECECE;
|
||||
background: #FFF;
|
||||
border-top: 0.5px solid #cecece;
|
||||
background: #fff;
|
||||
margin-top: 2px;
|
||||
|
||||
.confirm, .cancel {
|
||||
.confirm,
|
||||
.cancel {
|
||||
width: 50%;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
@@ -96,7 +105,9 @@
|
||||
align-items: center;
|
||||
color: #000;
|
||||
text-align: center;
|
||||
font-feature-settings: 'liga' off, 'clig' off;
|
||||
font-feature-settings:
|
||||
"liga" off,
|
||||
"clig" off;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
@@ -109,4 +120,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ export default forwardRef(function GameManagePopup(props, ref) {
|
||||
.some((item) => item.user.id === userInfo.id);
|
||||
|
||||
const finished = [MATCH_STATUS.FINISHED, MATCH_STATUS.CANCELED].includes(
|
||||
detail.match_status
|
||||
detail.match_status,
|
||||
);
|
||||
|
||||
const inTwoHours = dayjs(detail.start_time).diff(dayjs(), "hour") < 2;
|
||||
@@ -207,7 +207,7 @@ export default forwardRef(function GameManagePopup(props, ref) {
|
||||
style={{ minHeight: "unset" }}
|
||||
>
|
||||
<View className={styles.container}>
|
||||
{!inTwoHours && !hasOtherParticiappants && (
|
||||
{!finished && !inTwoHours && !hasOtherParticiappants && (
|
||||
<View className={styles.button} onClick={handleEditGame}>
|
||||
编辑活动
|
||||
</View>
|
||||
@@ -217,12 +217,12 @@ export default forwardRef(function GameManagePopup(props, ref) {
|
||||
重新发布
|
||||
</View>
|
||||
)}
|
||||
{!inTwoHours && !hasOtherParticiappants && (
|
||||
{!finished && !inTwoHours && !hasOtherParticiappants && (
|
||||
<View className={styles.button} onClick={handleCancelGame}>
|
||||
取消活动
|
||||
</View>
|
||||
)}
|
||||
{hasJoin && (
|
||||
{!finished && hasJoin && (
|
||||
<View className={styles.button} onClick={handleQuitGame}>
|
||||
退出活动
|
||||
</View>
|
||||
|
||||
@@ -45,7 +45,7 @@ const ListCard: React.FC<ListCardProps> = ({
|
||||
className="image"
|
||||
mode="aspectFill"
|
||||
lazyLoad
|
||||
defaultSource="https://images.unsplash.com/photo-1554068865-24cecd4e34b8?w=200&h=200&fit=crop&crop=center"
|
||||
defaultSource={require("@/static/emptyStatus/publish-empty-card.png")}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -62,7 +62,7 @@ const NTRPEvaluatePopup = (props: NTRPEvaluatePopupProps, ref) => {
|
||||
showGuide = false,
|
||||
} = props;
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [ntrp, setNtrp] = useState<string>("");
|
||||
const [ntrp, setNtrp] = useState<string>("1.5");
|
||||
const [guideShow, setGuideShow] = useState(() => showGuide);
|
||||
const { updateUserInfo } = useUserActions();
|
||||
const userInfo = useUserInfo();
|
||||
@@ -105,10 +105,10 @@ const NTRPEvaluatePopup = (props: NTRPEvaluatePopupProps, ref) => {
|
||||
if (match) {
|
||||
setNtrp(match[0]);
|
||||
} else {
|
||||
setNtrp("");
|
||||
setNtrp("1.5");
|
||||
}
|
||||
} else {
|
||||
setNtrp("");
|
||||
setNtrp("1.5");
|
||||
}
|
||||
}
|
||||
}, [visible, userInfo?.ntrp_level]);
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
useLastTestResult,
|
||||
} from "@/store/userStore";
|
||||
// import { getCurrentFullPath } from "@/utils";
|
||||
import { OSS_BASE_URL } from "@/config/api";
|
||||
import { OSS_BASE } from "@/config/api";
|
||||
import { StageType } from "@/services/evaluateService";
|
||||
import { waitForAuthInit } from "@/utils/authInit";
|
||||
import DocCopy from "@/static/ntrp/ntrp_doc_copy.svg";
|
||||
@@ -148,7 +148,7 @@ function NTRPTestEntryCard(props: {
|
||||
<View
|
||||
className={styles.lines}
|
||||
style={{
|
||||
backgroundImage: `url(${OSS_BASE_URL}/images/215f1ce1-be52-4a92-8250-5a4a69e7f2b3.png)`,
|
||||
backgroundImage: `url(${OSS_BASE}/front/ball/images/215f1ce1-be52-4a92-8250-5a4a69e7f2b3.png)`,
|
||||
}}
|
||||
/>
|
||||
<View className={styles.desc}>
|
||||
@@ -188,7 +188,7 @@ function NTRPTestEntryCard(props: {
|
||||
<View
|
||||
className={styles.lines}
|
||||
style={{
|
||||
backgroundImage: `url(${OSS_BASE_URL}/images/215f1ce1-be52-4a92-8250-5a4a69e7f2b3.png)`,
|
||||
backgroundImage: `url(${OSS_BASE}/front/ball/images/215f1ce1-be52-4a92-8250-5a4a69e7f2b3.png)`,
|
||||
}}
|
||||
/>
|
||||
<View className={styles.desc}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import CommonPopup from "@/components/CommonPopup";
|
||||
import { View } from "@tarojs/components";
|
||||
import Taro from "@tarojs/taro";
|
||||
import CalendarUI, {
|
||||
CalendarUIRef,
|
||||
} from "@/components/Picker/CalendarUI/CalendarUI";
|
||||
@@ -47,6 +48,13 @@ const DialogCalendarCard: React.FC<DialogCalendarCardProps> = ({
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
if (!selected) {
|
||||
Taro.showToast({
|
||||
title: '请选择日期',
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 年份选择完成后,进入月份选择
|
||||
setType("time");
|
||||
} else if (type === "month") {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import CommonPopup from "@/components/CommonPopup";
|
||||
import Taro from "@tarojs/taro";
|
||||
import { View } from "@tarojs/components";
|
||||
import CalendarUI, {
|
||||
CalendarUIRef,
|
||||
@@ -32,6 +33,13 @@ const DayDialog: React.FC<DayDialogProps> = ({
|
||||
} | null>(null);
|
||||
const handleConfirm = () => {
|
||||
console.log(selected, 'selectedselected');
|
||||
if (!selected) {
|
||||
Taro.showToast({
|
||||
title: '请选择日期',
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const finalDate = dayjs(selected as Date).format("YYYY-MM-DD");
|
||||
if (onChange){
|
||||
onChange(finalDate)
|
||||
|
||||
@@ -89,25 +89,15 @@ const RadarChart: React.FC = forwardRef((props, ref) => {
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
|
||||
// 标签
|
||||
const offset = 10;
|
||||
const textX = center.x + (radius + offset) * Math.cos(angle);
|
||||
const textY = center.y + (radius + offset) * Math.sin(angle);
|
||||
// 标签:沿轴线外侧延伸,文字中心对齐轴线端点
|
||||
const labelOffset = 28;
|
||||
const textX = center.x + (radius + labelOffset) * Math.cos(angle);
|
||||
const textY = center.y + (radius + labelOffset) * Math.sin(angle);
|
||||
|
||||
ctx.font = "12px sans-serif";
|
||||
ctx.fillStyle = "#333";
|
||||
ctx.textBaseline = "middle";
|
||||
|
||||
if (
|
||||
Math.abs(angle) < 0.01 ||
|
||||
Math.abs(Math.abs(angle) - Math.PI) < 0.01
|
||||
) {
|
||||
ctx.textAlign = "center";
|
||||
} else if (angle > -Math.PI / 2 && angle < Math.PI / 2) {
|
||||
ctx.textAlign = "left";
|
||||
} else {
|
||||
ctx.textAlign = "right";
|
||||
}
|
||||
ctx.textAlign = "center";
|
||||
|
||||
ctx.fillText(label, textX, textY);
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { View, Canvas } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { OSS_BASE_URL } from "@/config/api";
|
||||
import { OSS_BASE } from "@/config/api";
|
||||
|
||||
// 分享卡片数据接口
|
||||
export interface ShareCardData {
|
||||
@@ -506,7 +506,7 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
||||
const textX = iconX + iconSize + 20
|
||||
|
||||
// 绘制网球图标
|
||||
const tennisBallPath = await loadImage(`${OSS_BASE_URL}/images/b3eaf45e-ef28-4e45-9195-823b832e0451.jpg`, canvasNode)
|
||||
const tennisBallPath = await loadImage(`${OSS_BASE}/front/ball/images/b3eaf45e-ef28-4e45-9195-823b832e0451.jpg`, canvasNode)
|
||||
ctx.drawImage(tennisBallPath, iconX, gameInfoY, iconSize, iconSize)
|
||||
|
||||
// 绘制"单打"标签
|
||||
@@ -542,7 +542,7 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
||||
const dateX = danDaX
|
||||
const timeInfoY = infoStartY + infoSpacing
|
||||
const timeInfoFontSize = scale * 24 * dpr
|
||||
const calendarPath = await loadImage(`${OSS_BASE_URL}/images/ea792a5d-b105-4c95-bfc4-8af558f2b33b.jpg`, canvasNode)
|
||||
const calendarPath = await loadImage(`${OSS_BASE}/front/ball/images/ea792a5d-b105-4c95-bfc4-8af558f2b33b.jpg`, canvasNode)
|
||||
ctx.drawImage(calendarPath, iconX, timeInfoY, iconSize, iconSize)
|
||||
|
||||
// 绘制日期(绿色)
|
||||
@@ -556,7 +556,7 @@ const ShareCardCanvas: React.FC<ShareCardCanvasProps> = ({
|
||||
// 绘制地点
|
||||
const locationInfoY = infoStartY + infoSpacing * 2
|
||||
const locationFontSize = scale * 22 * dpr
|
||||
const locationPath = await loadImage(`${OSS_BASE_URL}/images/adc9a167-2ea9-4e3b-b963-6a894a1fd91b.jpg`, canvasNode)
|
||||
const locationPath = await loadImage(`${OSS_BASE}/front/ball/images/adc9a167-2ea9-4e3b-b963-6a894a1fd91b.jpg`, canvasNode)
|
||||
ctx.drawImage(locationPath, iconX, locationInfoY, iconSize, iconSize)
|
||||
drawBoldText(ctx, data.venueName, danDaX, locationInfoY + 10, locationFontSize, '#000000')
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ const TextareaTag: React.FC<TextareaTagProps> = ({
|
||||
autoHeight={true}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
adjustPosition={false}
|
||||
/>
|
||||
<View className={`char-count${isOverflow ? ' char-count--error' : ''}`}>
|
||||
{value.description.length}/{maxLength}
|
||||
|
||||
@@ -8,6 +8,7 @@ import NumberInterval from "./NumberInterval";
|
||||
import TimeSelector from "./TimeSelector";
|
||||
import TitleTextarea from "./TitleTextarea";
|
||||
import CommonPopup from "./CommonPopup";
|
||||
import CustomPopup from "./CustomPopup";
|
||||
import { CalendarUI, DialogCalendarCard } from "./Picker";
|
||||
import CommonDialog from "./CommonDialog";
|
||||
import PublishMenu from "./PublishMenu/PublishMenu";
|
||||
@@ -37,6 +38,7 @@ export {
|
||||
TimeSelector,
|
||||
TitleTextarea,
|
||||
CommonPopup,
|
||||
CustomPopup,
|
||||
DialogCalendarCard,
|
||||
CalendarUI,
|
||||
CommonDialog,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import envConfig from './env'// API配置
|
||||
|
||||
// OSS 基础路径配置
|
||||
export const OSS_BASE_URL = 'https://youchang2026.oss-cn-shanghai.aliyuncs.com/front/ball'
|
||||
// OSS 配置:仅域名,调用处拼接 /front/ball 及后续路径
|
||||
// export const OSS_BASE = "https://bimwe-oss.oss-cn-shanghai.aliyuncs.com";
|
||||
|
||||
// 因乐驰OSS 配置:仅域名,调用处拼接 /front/ball 及后续路径
|
||||
export const OSS_BASE = envConfig.ossBaseURL;
|
||||
|
||||
export const API_CONFIG = {
|
||||
// 基础URL
|
||||
|
||||
61
src/config/env.ts
Normal file
61
src/config/env.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import Taro from "@tarojs/taro";
|
||||
|
||||
/**
|
||||
* 环境配置:从 config/env.config.ts 经 defineConstants 注入
|
||||
* 构建时由 config/index.ts 根据 APP_ENV 选择并注入
|
||||
*/
|
||||
export type EnvType = "dev" | "dev_local" | "sit" | "pr";
|
||||
|
||||
export interface EnvConfig {
|
||||
name: string;
|
||||
apiBaseURL: string;
|
||||
ossBaseURL: string;
|
||||
timeout: number;
|
||||
enableLog: boolean;
|
||||
enableMock: boolean;
|
||||
customerService: {
|
||||
corpId: string;
|
||||
serviceUrl: string;
|
||||
};
|
||||
}
|
||||
|
||||
// 从 defineConstants 注入的编译时常量读取
|
||||
const getInjectedConfig = (): EnvConfig => ({
|
||||
name: process.env.APP_ENV || "dev",
|
||||
apiBaseURL: process.env.API_BASE_URL || "",
|
||||
ossBaseURL: process.env.OSS_BASE_URL || "",
|
||||
timeout: Number(process.env.TIMEOUT) || 10000,
|
||||
enableLog: process.env.ENABLE_LOG === "true",
|
||||
enableMock: false,
|
||||
customerService: {
|
||||
corpId: process.env.CUSTOMER_CORP_ID || "",
|
||||
serviceUrl: process.env.CUSTOMER_SERVICE_URL || "",
|
||||
},
|
||||
});
|
||||
|
||||
export const getCurrentEnv = (): EnvType =>
|
||||
(process.env.APP_ENV as EnvType) || "dev";
|
||||
|
||||
export const getCurrentConfig = (): EnvConfig => getInjectedConfig();
|
||||
|
||||
export const isDevelopment = (): boolean =>
|
||||
getCurrentEnv() === "dev" || getCurrentEnv() === "dev_local" || getCurrentEnv() === "sit";
|
||||
|
||||
export const isProduction = (): boolean => getCurrentEnv() === "pr";
|
||||
|
||||
export const getEnvInfo = () => {
|
||||
const config = getCurrentConfig();
|
||||
return {
|
||||
env: getCurrentEnv(),
|
||||
config,
|
||||
taroEnv: (Taro as any).getEnv?.(),
|
||||
platform:
|
||||
(Taro as any).getEnv?.() === (Taro as any).ENV_TYPE?.WEAPP
|
||||
? "微信小程序"
|
||||
: (Taro as any).getEnv?.() === (Taro as any).ENV_TYPE?.WEB
|
||||
? "Web"
|
||||
: "未知",
|
||||
};
|
||||
};
|
||||
|
||||
export default getCurrentConfig();
|
||||
@@ -29,6 +29,7 @@ const ListContainer = (props) => {
|
||||
collapse = false,
|
||||
defaultShowNum,
|
||||
evaluateFlag,
|
||||
enableHomeCards = false, // 仅首页需要 banner 和 NTRP 测评卡片
|
||||
listLoadErrorWrapperHeight,
|
||||
listLoadErrorWidth,
|
||||
listLoadErrorHeight,
|
||||
@@ -94,10 +95,10 @@ const ListContainer = (props) => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 获取测试结果,判断最近一个月是否有测试记录
|
||||
// 获取测试结果,判断最近一个月是否有测试记录(仅首页需要)
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
if (!evaluateFlag) return;
|
||||
if (!evaluateFlag || !enableHomeCards) return;
|
||||
// 先等待静默登录完成
|
||||
await waitForAuthInit();
|
||||
// 然后再获取用户信息
|
||||
@@ -112,7 +113,7 @@ const ListContainer = (props) => {
|
||||
}
|
||||
};
|
||||
init();
|
||||
}, [evaluateFlag, userInfo, lastTestResult, fetchLastTestResult]);
|
||||
}, [evaluateFlag, enableHomeCards, userInfo, lastTestResult, fetchLastTestResult]);
|
||||
|
||||
// 从全局状态中获取测试状态
|
||||
const hasTestInLastMonth = lastTestResult?.has_test_in_last_month || false;
|
||||
@@ -134,6 +135,7 @@ const ListContainer = (props) => {
|
||||
// 插入 banner 卡片
|
||||
function insertBannerCard(list) {
|
||||
if (!bannerListImage) return list;
|
||||
if (!list || !Array.isArray(list)) return list ?? [];
|
||||
return [
|
||||
...list.slice(0, Number(bannerListIndex)),
|
||||
{ type: "banner", banner_image_url: bannerListImage, banner_detail_url: bannerDetailImage },
|
||||
@@ -142,61 +144,62 @@ const ListContainer = (props) => {
|
||||
}
|
||||
|
||||
// 对于没有ntrp等级的用户每个月展示一次, 插在第二个位置后面
|
||||
// insertBannerCard 需在最后统一执行,否则前面分支直接 return 时 banner 不会被插入
|
||||
function insertEvaluateCard(list) {
|
||||
if (!evaluateFlag)
|
||||
return showNumber !== undefined ? list.slice(0, showNumber) : list;
|
||||
if (!list || list.length === 0) {
|
||||
return list;
|
||||
}
|
||||
// 如果最近一个月有测试记录,则不插入 card
|
||||
if (hasTestInLastMonth) {
|
||||
return showNumber !== undefined ? list.slice(0, showNumber) : list;
|
||||
let result: any[];
|
||||
|
||||
if (!evaluateFlag) {
|
||||
result = showNumber !== undefined ? list.slice(0, showNumber) : list;
|
||||
} else if (!list || list.length === 0) {
|
||||
result = list;
|
||||
} else if (hasTestInLastMonth) {
|
||||
result = showNumber !== undefined ? list.slice(0, showNumber) : list;
|
||||
} else if (list.length <= 2) {
|
||||
result = [...list, { type: "evaluateCard" }];
|
||||
} else {
|
||||
const [item1, item2, ...rest] = list;
|
||||
result = [
|
||||
item1,
|
||||
item2,
|
||||
{ type: "evaluateCard" },
|
||||
...(showNumber !== undefined ? rest.slice(0, showNumber - 3) : rest),
|
||||
];
|
||||
}
|
||||
|
||||
if (list.length <= 2) {
|
||||
return [...list, { type: "evaluateCard" }];
|
||||
}
|
||||
const [item1, item2, ...rest] = list;
|
||||
|
||||
let result = [
|
||||
item1,
|
||||
item2,
|
||||
{ type: "evaluateCard" },
|
||||
...(showNumber !== undefined ? rest.slice(0, showNumber - 3) : rest),
|
||||
];
|
||||
|
||||
if (bannerListImage) {
|
||||
return insertBannerCard(result);
|
||||
}
|
||||
return result;
|
||||
return insertBannerCard(result);
|
||||
}
|
||||
|
||||
const memoizedList = useMemo(
|
||||
() => insertEvaluateCard(data),
|
||||
[evaluateFlag, data, hasTestInLastMonth, showNumber, bannerListImage, bannerDetailImage, bannerListIndex]
|
||||
() => (enableHomeCards ? insertEvaluateCard(data) : data),
|
||||
[enableHomeCards, evaluateFlag, data, hasTestInLastMonth, showNumber, bannerListImage, bannerDetailImage, bannerListIndex]
|
||||
);
|
||||
|
||||
// 渲染 banner 卡片
|
||||
const renderBanner = (item, index) => {
|
||||
if (!item?.banner_image_url) return null;
|
||||
if (!item?.banner_image_url) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<View
|
||||
key={item.id || `banner-${index}`}
|
||||
className="banner-image-wrapper"
|
||||
onClick={() => {
|
||||
const target = item.banner_detail_url;
|
||||
if (target) {
|
||||
(Taro as any).navigateTo({
|
||||
url: `/other_pages/bannerDetail/index?img=${encodeURIComponent(target)}`,
|
||||
});
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
height: "100px",
|
||||
overflow: "hidden",
|
||||
borderRadius: "12px",
|
||||
backgroundImage: `url(${item.banner_image_url})`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={item.banner_image_url}
|
||||
mode="widthFix"
|
||||
className="banner-image"
|
||||
onClick={() => {
|
||||
const target = item.banner_detail_url;
|
||||
if (target) {
|
||||
(Taro as any).navigateTo({
|
||||
url: `/other_pages/bannerDetail/index?img=${encodeURIComponent(target)}`,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -224,12 +227,12 @@ const ListContainer = (props) => {
|
||||
return (
|
||||
<>
|
||||
{memoizedList.map((match, index) => {
|
||||
if (match?.type === "banner") {
|
||||
if (enableHomeCards && match?.type === "banner") {
|
||||
return renderBanner(match, index);
|
||||
}
|
||||
if (match?.type === "evaluateCard") {
|
||||
if (enableHomeCards && match?.type === "evaluateCard") {
|
||||
return (
|
||||
<NTRPTestEntryCard key="evaluate" type={EvaluateScene.list} />
|
||||
<NTRPTestEntryCard key={`evaluate-${index}`} type={EvaluateScene.list} />
|
||||
);
|
||||
}
|
||||
return <ListCard key={match?.id || index} {...match} />;
|
||||
|
||||
@@ -40,7 +40,7 @@ function isFull(counts) {
|
||||
function matchNtrpRequestment(
|
||||
target?: string,
|
||||
min?: string,
|
||||
max?: string
|
||||
max?: string,
|
||||
): boolean {
|
||||
// 目标值为空或 undefined
|
||||
if (!target?.trim()) return true;
|
||||
@@ -110,7 +110,7 @@ export default function Participants(props) {
|
||||
user_action_status;
|
||||
const showApplicationEntry =
|
||||
[can_pay, can_substitute, is_substituting, waiting_start].every(
|
||||
(item) => !item
|
||||
(item) => !item,
|
||||
) &&
|
||||
can_join &&
|
||||
dayjs(start_time).isAfter(dayjs());
|
||||
@@ -138,7 +138,7 @@ export default function Participants(props) {
|
||||
|
||||
Taro.navigateTo({
|
||||
url: `/login_pages/index/index?redirect=${encodeURIComponent(
|
||||
fullPath
|
||||
fullPath,
|
||||
)}`,
|
||||
});
|
||||
}
|
||||
@@ -153,7 +153,7 @@ export default function Participants(props) {
|
||||
const matchNtrpReq = matchNtrpRequestment(
|
||||
userInfo?.ntrp_level,
|
||||
skill_level_min,
|
||||
skill_level_max
|
||||
skill_level_max,
|
||||
);
|
||||
|
||||
function handleSelfEvaluate() {
|
||||
@@ -180,7 +180,7 @@ export default function Participants(props) {
|
||||
}
|
||||
|
||||
function generateTextAndAction(
|
||||
user_action_status: null | { [key: string]: boolean }
|
||||
user_action_status: null | { [key: string]: boolean },
|
||||
):
|
||||
| undefined
|
||||
| { text: string | React.FC; action?: () => void; available?: boolean } {
|
||||
@@ -259,7 +259,7 @@ export default function Participants(props) {
|
||||
const res = await OrderService.getUnpaidOrder(id);
|
||||
if (res.code === 0) {
|
||||
navto(
|
||||
`/order_pages/orderDetail/index?id=${res.data.order_info.order_id}`
|
||||
`/order_pages/orderDetail/index?id=${res.data.order_info.order_id}`,
|
||||
);
|
||||
}
|
||||
}),
|
||||
@@ -296,10 +296,11 @@ export default function Participants(props) {
|
||||
const { action = () => {} } = generateTextAndAction(user_action_status)!;
|
||||
|
||||
const leftCount = max_participants - participant_count;
|
||||
const leftSubstituteCount = (max_substitute_players || 0) - (substitute_count || 0);
|
||||
const leftSubstituteCount =
|
||||
(max_substitute_players || 0) - (substitute_count || 0);
|
||||
const showSubstituteApplicationEntry =
|
||||
[can_pay, can_join, is_substituting, waiting_start].every(
|
||||
(item) => !item
|
||||
(item) => !item,
|
||||
) &&
|
||||
can_substitute &&
|
||||
dayjs(start_time).isAfter(dayjs());
|
||||
@@ -336,7 +337,7 @@ export default function Participants(props) {
|
||||
refresherBackground="#FAFAFA"
|
||||
className={classnames(
|
||||
styles["participants-list-scroll"],
|
||||
showApplicationEntry ? styles.withApplication : ""
|
||||
showApplicationEntry ? styles.withApplication : "",
|
||||
)}
|
||||
scrollX
|
||||
>
|
||||
@@ -377,14 +378,14 @@ export default function Participants(props) {
|
||||
src={avatar_url}
|
||||
onClick={handleViewUserInfo.bind(
|
||||
null,
|
||||
participant_user_id
|
||||
participant_user_id,
|
||||
)}
|
||||
/>
|
||||
<Text className={styles["participants-list-item-name"]}>
|
||||
{nickname || "未知"}
|
||||
</Text>
|
||||
<Text className={styles["participants-list-item-level"]}>
|
||||
{displayNtrp}
|
||||
NTRP {displayNtrp}
|
||||
</Text>
|
||||
<Text className={styles["participants-list-item-role"]}>
|
||||
{role}
|
||||
@@ -400,97 +401,107 @@ export default function Participants(props) {
|
||||
)}
|
||||
</View>
|
||||
{/* 候补区域 */}
|
||||
{max_substitute_players > 0 && (substitute_count > 0 || showSubstituteApplicationEntry) && (
|
||||
<View className={styles["detail-page-content-participants"]}>
|
||||
<View className={styles["participants-title"]}>
|
||||
<Text>候补</Text>
|
||||
<Text>·</Text>
|
||||
<Text>{leftSubstituteCount > 0 ? `剩余空位 ${leftSubstituteCount}` : "已满员"}</Text>
|
||||
</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 : ""
|
||||
{max_substitute_players > 0 &&
|
||||
(substitute_count > 0 || showSubstituteApplicationEntry) && (
|
||||
<View className={styles["detail-page-content-participants"]}>
|
||||
<View className={styles["participants-title"]}>
|
||||
<Text>候补</Text>
|
||||
<Text>·</Text>
|
||||
<Text>
|
||||
{leftSubstituteCount > 0
|
||||
? `剩余空位 ${leftSubstituteCount}`
|
||||
: "已满员"}
|
||||
</Text>
|
||||
</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>
|
||||
)}
|
||||
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`,
|
||||
}}
|
||||
{/* 候补成员列表 */}
|
||||
<ScrollView
|
||||
refresherBackground="#FAFAFA"
|
||||
className={classnames(
|
||||
styles["participants-list-scroll"],
|
||||
showSubstituteApplicationEntry ? styles.withApplication : "",
|
||||
)}
|
||||
scrollX
|
||||
>
|
||||
{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
|
||||
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>
|
||||
</View>
|
||||
)}
|
||||
)}
|
||||
<NTRPEvaluatePopup type={EvaluateScene.detail} ref={ntrpRef} showGuide />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -15,7 +15,7 @@ import CrossIcon from "@/static/detail/cross.svg";
|
||||
import { genNTRPRequirementText, navto } from "@/utils/helper";
|
||||
import { waitForAuthInit } from "@/utils/authInit";
|
||||
import { useUserActions } from "@/store/userStore";
|
||||
import { OSS_BASE_URL } from "@/config/api";
|
||||
import { OSS_BASE } from "@/config/api";
|
||||
import { generatePosterImage, base64ToTempFilePath, delay } from "@/utils";
|
||||
import { DayOfWeekMap } from "../../config";
|
||||
import styles from "./index.module.scss";
|
||||
@@ -60,8 +60,10 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
|
||||
show: async (publish_flag = false) => {
|
||||
setPublishFlag(publish_flag);
|
||||
if (publish_flag) {
|
||||
const url = await generateShareImageUrl();
|
||||
setShareImageUrl(url);
|
||||
try {
|
||||
const url = await generateShareImageUrl();
|
||||
setShareImageUrl(url);
|
||||
} catch (e) {}
|
||||
}
|
||||
setVisible(true);
|
||||
},
|
||||
@@ -81,13 +83,14 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
|
||||
const endTime = dayjs(end_time);
|
||||
const dayofWeek = DayOfWeekMap.get(startTime.day());
|
||||
const gameLength = `${endTime.diff(startTime, "hour")}小时`;
|
||||
console.log(userInfo, "userInfo");
|
||||
const url = await generateShareImage({
|
||||
userAvatar: userInfo.avatar_url,
|
||||
userNickname: userInfo.nickname,
|
||||
gameType: play_type,
|
||||
skillLevel: `NTRP ${genNTRPRequirementText(
|
||||
skill_level_min,
|
||||
skill_level_max
|
||||
skill_level_max,
|
||||
)}`,
|
||||
gameDate: `${startTime.format("M月D日")} (${dayofWeek})`,
|
||||
gameTime: `${startTime.format("ah")}点 ${gameLength}`,
|
||||
@@ -128,22 +131,24 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
|
||||
const endTime = dayjs(end_time);
|
||||
const dayofWeek = DayOfWeekMap.get(startTime.day());
|
||||
const gameLength = `${endTime.diff(startTime, "hour")}小时`;
|
||||
Taro.showLoading({ title: "生成中..." });
|
||||
// Taro.showLoading({ title: "生成中..." });
|
||||
const qrCodeUrlRes = await DetailService.getQrCodeUrl({
|
||||
page: "game_pages/detail/index",
|
||||
scene: `id=${id}`,
|
||||
});
|
||||
const qrCodeUrl = await base64ToTempFilePath(
|
||||
qrCodeUrlRes.data.qr_code_base64
|
||||
);
|
||||
// const qrCodeUrl = await base64ToTempFilePath(
|
||||
// qrCodeUrlRes.data.qr_code_base64
|
||||
// );
|
||||
const qrCodeUrl = qrCodeUrlRes.data.ossPath;
|
||||
await delay(100);
|
||||
// Taro.showLoading({ title: "生成中..." });
|
||||
const url = await generatePosterImage({
|
||||
playType: play_type,
|
||||
ntrp: `NTRP ${genNTRPRequirementText(skill_level_min, skill_level_max)}`,
|
||||
mainCoursal:
|
||||
image_list[0] && image_list[0].startsWith("http")
|
||||
? image_list[0]
|
||||
: `${OSS_BASE_URL}/images/0621b8cf-f7d6-43ad-b852-7dc39f29a782.png`,
|
||||
: `${OSS_BASE}/front/ball/images/0621b8cf-f7d6-43ad-b852-7dc39f29a782.png`,
|
||||
nickname,
|
||||
avatarUrl: avatar_url,
|
||||
title,
|
||||
@@ -152,7 +157,7 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
|
||||
time: `${startTime.format("ah")}点 ${gameLength}`,
|
||||
qrCodeUrl,
|
||||
});
|
||||
Taro.hideLoading();
|
||||
// Taro.hideLoading();
|
||||
Taro.showShareImageMenu({
|
||||
path: url,
|
||||
});
|
||||
@@ -164,6 +169,18 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
|
||||
setVisible(false);
|
||||
}
|
||||
|
||||
async function handleCopyLink() {
|
||||
const linkUrlRes = await DetailService.getLinkUrl({
|
||||
path: "game_pages/detail/index",
|
||||
query: `id=${id}`,
|
||||
});
|
||||
await Taro.setClipboardData({
|
||||
data: linkUrlRes.data.url_link,
|
||||
});
|
||||
Taro.showToast({ title: "链接已复制到剪贴板", icon: "success" });
|
||||
setVisible(false);
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
setVisible(false);
|
||||
setPublishFlag(false);
|
||||
@@ -193,14 +210,14 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
|
||||
<View
|
||||
className={styles.contentContainer}
|
||||
style={{
|
||||
backgroundImage: `url(${OSS_BASE_URL}/images/215f1ce1-be52-4a92-8250-5a4a69e7f2b3.png)`,
|
||||
backgroundImage: `url(${OSS_BASE}/front/ball/images/215f1ce1-be52-4a92-8250-5a4a69e7f2b3.png)`,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
catchMove
|
||||
className={classnames(
|
||||
styles.title,
|
||||
publishFlag ? styles.publishTitle : ""
|
||||
publishFlag ? styles.publishTitle : "",
|
||||
)}
|
||||
>
|
||||
{publishFlag ? (
|
||||
@@ -254,7 +271,7 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
|
||||
</View>
|
||||
</View>
|
||||
<View className={styles.customBtnWrapper}>
|
||||
<Button className={styles.button}>
|
||||
<Button className={styles.button} onClick={handleCopyLink}>
|
||||
<View className={styles.icon}>
|
||||
<Image className={styles.linkIcon} src={LinkIcon} />
|
||||
</View>
|
||||
|
||||
@@ -23,6 +23,7 @@ import SupplementalNotes from "./components/SupplementalNotes";
|
||||
import OrganizerInfo from "./components/OrganizerInfo";
|
||||
import SharePopup from "./components/SharePopup";
|
||||
import { navto, toast } from "@/utils/helper";
|
||||
import { delay } from "@/utils";
|
||||
import ArrowLeft from "@/static/detail/icon-arrow-left.svg";
|
||||
// import Logo from "@/static/detail/icon-logo-go.svg";
|
||||
import styles from "./index.module.scss";
|
||||
@@ -53,6 +54,12 @@ function Index() {
|
||||
await waitForAuthInit();
|
||||
// 然后再获取用户信息
|
||||
await fetchUserInfo();
|
||||
|
||||
await delay(1000);
|
||||
|
||||
if (from === "publish") {
|
||||
handleShare(true);
|
||||
}
|
||||
};
|
||||
init();
|
||||
}, []);
|
||||
@@ -81,9 +88,9 @@ function Index() {
|
||||
// 位置更新后,重新获取详情页数据(因为距离等信息可能发生变化)
|
||||
// 注意:这里不调用 fetchDetail,避免与 useDidShow 中的调用重复
|
||||
// 如果需要更新距离信息,可以在 fetchDetail 成功后根据当前位置重新计算
|
||||
if (from === "publish") {
|
||||
handleShare(true);
|
||||
}
|
||||
// if (from === "publish") {
|
||||
// handleShare(true);
|
||||
// }
|
||||
} catch (error) {
|
||||
console.error("用户位置更新失败", error);
|
||||
}
|
||||
@@ -161,7 +168,7 @@ function Index() {
|
||||
navto(
|
||||
userId === myInfo.id
|
||||
? "/user_pages/myself/index"
|
||||
: `/user_pages/other/index?userid=${userId}`
|
||||
: `/user_pages/other/index?userid=${userId}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -195,7 +202,7 @@ function Index() {
|
||||
<View
|
||||
className={classnames(
|
||||
styles["custom-navbar"],
|
||||
glass ? styles.glass : ""
|
||||
glass ? styles.glass : "",
|
||||
)}
|
||||
style={{
|
||||
height: `${totalHeight}px`,
|
||||
@@ -291,7 +298,7 @@ function Index() {
|
||||
id={id as string}
|
||||
from={from as string}
|
||||
detail={detail}
|
||||
userInfo={userInfo}
|
||||
userInfo={myInfo}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
@@ -5,7 +5,7 @@ import Taro, { useRouter } from "@tarojs/taro";
|
||||
import classnames from "classnames";
|
||||
import dayjs from "dayjs";
|
||||
import "dayjs/locale/zh-cn";
|
||||
import { generatePosterImage, base64ToTempFilePath, delay } from "@/utils";
|
||||
import { generatePosterImage, delay } from "@/utils";
|
||||
import { withAuth } from "@/components";
|
||||
import GeneralNavbar from "@/components/GeneralNavbar";
|
||||
import DetailService from "@/services/detailService";
|
||||
@@ -16,7 +16,7 @@ import { useUserActions } from "@/store/userStore";
|
||||
import { DayOfWeekMap } from "../detail/config";
|
||||
import { genNTRPRequirementText } from "@/utils/helper";
|
||||
import { waitForAuthInit } from "@/utils/authInit";
|
||||
import { OSS_BASE_URL } from "@/config/api";
|
||||
import { OSS_BASE } from "@/config/api";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
dayjs.locale("zh-cn");
|
||||
@@ -59,10 +59,11 @@ function SharePoster(props) {
|
||||
page: "game_pages/detail/index",
|
||||
scene: `id=${id}`,
|
||||
});
|
||||
const qrCodeUrl = await base64ToTempFilePath(
|
||||
qrCodeUrlRes.data.qr_code_base64
|
||||
);
|
||||
debugger
|
||||
const qrCodeUrl = qrCodeUrlRes.data.ossPath;
|
||||
// const qrCodeUrl = await base64ToTempFilePath(
|
||||
// qrCodeUrlRes.data.qr_code_base64
|
||||
// );
|
||||
// debugger
|
||||
await delay(100);
|
||||
const url = await generatePosterImage({
|
||||
playType: play_type,
|
||||
@@ -70,7 +71,7 @@ function SharePoster(props) {
|
||||
mainCoursal:
|
||||
image_list[0] && image_list[0].startsWith("http")
|
||||
? image_list[0]
|
||||
: `${OSS_BASE_URL}/images/0621b8cf-f7d6-43ad-b852-7dc39f29a782.png`,
|
||||
: `${OSS_BASE}/front/ball/images/0621b8cf-f7d6-43ad-b852-7dc39f29a782.png`,
|
||||
nickname,
|
||||
avatarUrl: avatar_url,
|
||||
title,
|
||||
|
||||
@@ -9,6 +9,23 @@
|
||||
|
||||
}
|
||||
|
||||
.link_button
|
||||
{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
height: 52px;
|
||||
border-radius: 16px;
|
||||
border: none;
|
||||
position: relative;
|
||||
font-size: 16px;
|
||||
|
||||
.button_text {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
// 背景图片和渐变覆盖层
|
||||
.background_image {
|
||||
position: absolute;
|
||||
|
||||
@@ -155,6 +155,11 @@ const LoginPage: React.FC = () => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
// 返回首页
|
||||
const handle_return_home = () => {
|
||||
Taro.navigateTo({ url: "/main_pages/index" });
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="login_page">
|
||||
<View className="background_image">
|
||||
@@ -193,7 +198,7 @@ const LoginPage: React.FC = () => {
|
||||
/>
|
||||
</View>
|
||||
<Text className="button_text">
|
||||
{is_loading ? "登录中..." : "授权登录"}
|
||||
{is_loading ? "登录中..." : "一键登录"}
|
||||
</Text>
|
||||
</Button>
|
||||
|
||||
@@ -211,6 +216,10 @@ const LoginPage: React.FC = () => {
|
||||
<Text className="button_text">手机号快捷登录</Text>
|
||||
</Button>
|
||||
|
||||
<View className="return_home_button link_button" onClick={handle_return_home}>
|
||||
<Text className="button_text">返回首页</Text>
|
||||
</View>
|
||||
|
||||
{/* 用户协议复选框 */}
|
||||
<View className="terms_checkbox_section">
|
||||
<View className="checkbox_container" onClick={handle_toggle_terms}>
|
||||
|
||||
@@ -293,9 +293,9 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
|
||||
currentProvince,
|
||||
});
|
||||
|
||||
// 地址发生变化或不一致,重新加载数据和球局数量
|
||||
// 先调用列表接口,然后在列表接口完成后调用数量接口
|
||||
(async () => {
|
||||
// 延迟刷新,等 tab 切换动画完成后再加载,避免切换时列表重渲染导致抖动
|
||||
const delayMs = 280;
|
||||
const timer = setTimeout(async () => {
|
||||
try {
|
||||
if (refreshBothLists) {
|
||||
await refreshBothLists();
|
||||
@@ -311,7 +311,9 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
|
||||
} catch (error) {
|
||||
console.error("重新加载数据失败:", error);
|
||||
}
|
||||
})();
|
||||
}, delayMs);
|
||||
prevIsActiveRef.current = isActive;
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -625,6 +627,7 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
|
||||
reload={refreshMatches}
|
||||
loadMoreMatches={loadMoreMatches}
|
||||
evaluateFlag
|
||||
enableHomeCards
|
||||
/>
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
@@ -37,6 +37,7 @@ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({ isActive = true }
|
||||
const [hasLoaded, setHasLoaded] = useState(false); // 记录是否已经加载过数据
|
||||
|
||||
const [collapseProfile, setCollapseProfile] = useState(false);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
pickerOption.getCities();
|
||||
@@ -169,6 +170,23 @@ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({ isActive = true }
|
||||
setActiveTab(tab);
|
||||
};
|
||||
|
||||
// 下拉刷新:刷新用户信息和球局数据
|
||||
const handle_refresh = async () => {
|
||||
setRefreshing(true);
|
||||
try {
|
||||
await Promise.all([fetchUserInfo(), load_game_data()]);
|
||||
} catch (error) {
|
||||
console.error("刷新失败:", error);
|
||||
(Taro as any).showToast({
|
||||
title: "刷新失败,请重试",
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
// const handleScroll = (event: any) => {
|
||||
// const scrollData = event.detail;
|
||||
// setCollapseProfile(scrollData.scrollTop > 1);
|
||||
@@ -178,6 +196,9 @@ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({ isActive = true }
|
||||
<ScrollView
|
||||
scrollY
|
||||
refresherBackground="#FAFAFA"
|
||||
refresherEnabled
|
||||
refresherTriggered={refreshing}
|
||||
onRefresherRefresh={handle_refresh}
|
||||
className={styles.myselfPage}
|
||||
>
|
||||
<View
|
||||
|
||||
@@ -21,21 +21,17 @@
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
transform: scale(0.98);
|
||||
transition: opacity 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94),
|
||||
transform 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
transition: opacity 0.25s ease-out;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
pointer-events: none;
|
||||
will-change: opacity, transform;
|
||||
backface-visibility: hidden;
|
||||
-webkit-backface-visibility: hidden;
|
||||
visibility: hidden;
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
z-index: 1;
|
||||
pointer-events: auto;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -67,12 +67,6 @@ const MainPage: React.FC = () => {
|
||||
try {
|
||||
await fetchUserInfo();
|
||||
await checkNicknameChangeStatus();
|
||||
// 启动时预取 Banner 字典(与业务无强依赖,失败不影响主流程)
|
||||
try {
|
||||
await useDictionaryStore.getState().fetchBannerDictionary();
|
||||
} catch (e) {
|
||||
console.error("预取 Banner 字典失败:", e);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取用户信息失败:", error);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ import { useGlobalStore } from "@/store/global";
|
||||
import { useOrder } from "@/store/orderStore";
|
||||
import detailService, { GameData } from "@/services/detailService";
|
||||
import { withAuth, RefundPopup, GeneralNavbar } from "@/components";
|
||||
import { OSS_BASE_URL } from "@/config/api";
|
||||
import { OSS_BASE } from "@/config/api";
|
||||
import img from "@/config/images";
|
||||
import CustomerIcon from "@/static/order/customer.svg";
|
||||
import { handleCustomerService } from "@/services/userService";
|
||||
@@ -301,7 +301,7 @@ function GameInfo(props) {
|
||||
<View className={styles.locationMessageIcon}>
|
||||
<Image
|
||||
className={styles.locationMessageIconImage}
|
||||
src={`${OSS_BASE_URL}/images/3ee5c89c-fe58-4a56-9471-1295da09c743.png`}
|
||||
src={`${OSS_BASE}/front/ball/images/3ee5c89c-fe58-4a56-9471-1295da09c743.png`}
|
||||
/>
|
||||
</View>
|
||||
{/* location message */}
|
||||
|
||||
@@ -69,6 +69,7 @@ function generateTimeMsg(game_info) {
|
||||
const OrderList = () => {
|
||||
const [list, setList] = useState<any[][]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const refundRef = useRef(null);
|
||||
|
||||
const end = list.length * PAGESIZE >= total;
|
||||
@@ -114,6 +115,22 @@ const OrderList = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 下拉刷新:重新加载第一页订单
|
||||
async function handle_refresh() {
|
||||
setRefreshing(true);
|
||||
try {
|
||||
await getOrders(1);
|
||||
} catch (error) {
|
||||
Taro.showToast({
|
||||
title: "刷新失败,请重试",
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePayNow(item) {
|
||||
// 检查登录状态和手机号
|
||||
if (!requireLoginWithPhone()) {
|
||||
@@ -285,6 +302,10 @@ const OrderList = () => {
|
||||
scrollWithAnimation
|
||||
lowerThreshold={20}
|
||||
onScrollToLower={handleFetchNext}
|
||||
refresherBackground="#FAFAFA"
|
||||
refresherEnabled
|
||||
refresherTriggered={refreshing}
|
||||
onRefresherRefresh={handle_refresh}
|
||||
enhanced
|
||||
showScrollbar={false}
|
||||
className={styles.list}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
.banner_detail_page {
|
||||
min-height: 100vh;
|
||||
background: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.banner_detail_content {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.banner_detail_image {
|
||||
width: 100%;
|
||||
border-radius: 12px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '开启消息通知',
|
||||
navigationStyle: 'custom',
|
||||
enablePullDownRefresh: false,
|
||||
backgroundColor:"#FAFAFA"
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
.enable_notification_page {
|
||||
width: 100%;
|
||||
// min-height: 100vh;
|
||||
height: 100%;
|
||||
background: radial-gradient(circle at 50% 0%, rgba(191, 255, 239, 1) 0%, rgba(255, 255, 255, 1) 37%);
|
||||
box-sizing: border-box;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -10,9 +12,8 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: calc(100vh - 98px);
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// 示例消息卡片区域
|
||||
@@ -30,12 +31,12 @@
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 16px;
|
||||
background: #ffffff;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.08);
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
background: #ffffff;
|
||||
|
||||
// 第三个卡片(最上面)
|
||||
&--3 {
|
||||
@@ -163,7 +164,6 @@
|
||||
|
||||
&__qr_image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__qr_placeholder {
|
||||
|
||||
@@ -21,10 +21,10 @@ const OrderCheck = () => {
|
||||
|
||||
//TODO: get order msg from id
|
||||
const handlePay = async () => {
|
||||
Taro.showLoading({
|
||||
title: '支付中...',
|
||||
mask: true
|
||||
})
|
||||
// Taro.showLoading({
|
||||
// title: '支付中...',
|
||||
// mask: true
|
||||
// })
|
||||
const res = await orderService.createOrder(Number(gameId))
|
||||
if (res.code === 0) {
|
||||
const { payment_required, payment_params } = res.data
|
||||
@@ -37,7 +37,7 @@ const OrderCheck = () => {
|
||||
signType,
|
||||
paySign,
|
||||
success: async () => {
|
||||
Taro.hideLoading()
|
||||
// Taro.hideLoading()
|
||||
Taro.showToast({
|
||||
title: '支付成功',
|
||||
icon: 'success'
|
||||
@@ -48,7 +48,7 @@ const OrderCheck = () => {
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
Taro.hideLoading()
|
||||
// Taro.hideLoading()
|
||||
Taro.showToast({
|
||||
title: '支付失败',
|
||||
icon: 'none'
|
||||
|
||||
@@ -193,7 +193,7 @@ const NewFollow = () => {
|
||||
<View className="follow-left" onClick={() => handleUserClick(item.user_id)}>
|
||||
<Image
|
||||
className="user-avatar" mode="aspectFill"
|
||||
src={item.user_avatar || "https://img.yzcdn.cn/vant/cat.jpeg"}
|
||||
src={item.user_avatar || require("@/static/userInfo/default_avatar.svg")}
|
||||
|
||||
/>
|
||||
|
||||
|
||||
@@ -16,10 +16,9 @@ import { useGlobalState } from "@/store/global";
|
||||
import { delay, getCurrentFullPath } from "@/utils";
|
||||
import { formatNtrpDisplay } from "@/utils/helper";
|
||||
import { waitForAuthInit } from "@/utils/authInit";
|
||||
import httpService from "@/services/httpService";
|
||||
// import httpService from "@/services/httpService";
|
||||
import DetailService from "@/services/detailService";
|
||||
import { base64ToTempFilePath } from "@/utils/genPoster";
|
||||
import { OSS_BASE_URL } from "@/config/api";
|
||||
import { OSS_BASE } from "@/config/api";
|
||||
import CloseIcon from "@/static/ntrp/ntrp_close_icon.svg";
|
||||
import DocCopy from "@/static/ntrp/ntrp_doc_copy.svg";
|
||||
import ArrowRight from "@/static/ntrp/ntrp_arrow_right.svg";
|
||||
@@ -38,7 +37,7 @@ const sourceTypeToTextMap = new Map([
|
||||
|
||||
function adjustRadarLabels(
|
||||
source: [string, number][],
|
||||
topK: number = 4 // 默认挑前4个最长的标签保护
|
||||
topK: number = 4, // 默认挑前4个最长的标签保护
|
||||
): [string, number][] {
|
||||
if (source.length === 0) return source;
|
||||
|
||||
@@ -226,7 +225,7 @@ function Intro() {
|
||||
<View
|
||||
className={styles.introContainer}
|
||||
style={{
|
||||
backgroundImage: `url(${OSS_BASE_URL}/images/215f1ce1-be52-4a92-8250-5a4a69e7f2b3.png)`,
|
||||
backgroundImage: `url(${OSS_BASE}/front/ball/images/215f1ce1-be52-4a92-8250-5a4a69e7f2b3.png)`,
|
||||
}}
|
||||
>
|
||||
<CommonGuideBar />
|
||||
@@ -253,7 +252,7 @@ function Intro() {
|
||||
<View className={styles.tip}>
|
||||
<Image
|
||||
className={styles.tipImage}
|
||||
src={`${OSS_BASE_URL}/images/b7cb47aa-b609-4112-899f-3fde02ed2431.png`}
|
||||
src={`${OSS_BASE}/front/ball/images/b7cb47aa-b609-4112-899f-3fde02ed2431.png`}
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</View>
|
||||
@@ -311,7 +310,7 @@ function Intro() {
|
||||
<View className={styles.tip}>
|
||||
<Image
|
||||
className={styles.tipImage}
|
||||
src={`${OSS_BASE_URL}/images/b7cb47aa-b609-4112-899f-3fde02ed2431.png`}
|
||||
src={`${OSS_BASE}/front/ball/images/b7cb47aa-b609-4112-899f-3fde02ed2431.png`}
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</View>
|
||||
@@ -319,7 +318,7 @@ function Intro() {
|
||||
<View className={styles.radar}>
|
||||
<Image
|
||||
className={styles.radarImage}
|
||||
src={`${OSS_BASE_URL}/images/a2e1b639-82a9-4ab8-b767-8605556eafcb.png`}
|
||||
src={`${OSS_BASE}/front/ball/images/a2e1b639-82a9-4ab8-b767-8605556eafcb.png`}
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</View>
|
||||
@@ -381,7 +380,7 @@ function Test() {
|
||||
prev.map((item, pIndex) => ({
|
||||
...item,
|
||||
...(pIndex === index ? { choosen: i } : {}),
|
||||
}))
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -428,7 +427,7 @@ function Test() {
|
||||
<View
|
||||
className={styles.testContainer}
|
||||
style={{
|
||||
backgroundImage: `url(${OSS_BASE_URL}/images/215f1ce1-be52-4a92-8250-5a4a69e7f2b3.png)`,
|
||||
backgroundImage: `url(${OSS_BASE}/front/ball/images/215f1ce1-be52-4a92-8250-5a4a69e7f2b3.png)`,
|
||||
}}
|
||||
>
|
||||
<CommonGuideBar confirm title={`${index + 1} / ${questions.length}`} />
|
||||
@@ -523,13 +522,14 @@ function Result() {
|
||||
page: "other_pages/ntrp-evaluate/index",
|
||||
scene: `stage=${StageType.INTRO}`,
|
||||
});
|
||||
if (qrCodeUrlRes.code === 0 && qrCodeUrlRes.data?.qr_code_base64) {
|
||||
// 将 base64 转换为临时文件路径
|
||||
const tempFilePath = await base64ToTempFilePath(
|
||||
qrCodeUrlRes.data.qr_code_base64
|
||||
);
|
||||
setQrCodeUrl(tempFilePath);
|
||||
}
|
||||
setQrCodeUrl(qrCodeUrlRes.data.ossPath);
|
||||
// if (qrCodeUrlRes.code === 0 && qrCodeUrlRes.data?.qr_code_base64) {
|
||||
// // 将 base64 转换为临时文件路径
|
||||
// const tempFilePath = await base64ToTempFilePath(
|
||||
// qrCodeUrlRes.data.qr_code_base64
|
||||
// );
|
||||
// setQrCodeUrl(tempFilePath);
|
||||
// }
|
||||
} catch (error) {
|
||||
console.error("获取二维码失败:", error);
|
||||
}
|
||||
@@ -539,18 +539,25 @@ function Result() {
|
||||
const res = await evaluateService.getTestResult({ record_id: Number(id) });
|
||||
if (res.code === 0) {
|
||||
setResult(res.data);
|
||||
// delay(1000);
|
||||
setRadarData(
|
||||
adjustRadarLabels(
|
||||
Object.entries(res.data.radar_data.abilities).map(([key, value]) => [
|
||||
key,
|
||||
Math.min(
|
||||
100,
|
||||
Math.floor((value.current_score / value.max_score) * 100)
|
||||
),
|
||||
])
|
||||
)
|
||||
|
||||
const sortOrder = res.data.sort || [];
|
||||
const abilities = res.data.radar_data.abilities;
|
||||
const sortedKeys = sortOrder.filter((k) => k in abilities);
|
||||
const remainingKeys = Object.keys(abilities).filter(
|
||||
(k) => !sortOrder.includes(k),
|
||||
);
|
||||
const allKeys = [...sortedKeys, ...remainingKeys];
|
||||
let radarData: [string, number][] = allKeys.map((key) => [
|
||||
key,
|
||||
Math.min(
|
||||
100,
|
||||
Math.floor(
|
||||
(abilities[key].current_score / abilities[key].max_score) * 100,
|
||||
),
|
||||
),
|
||||
]);
|
||||
// 直接使用接口 sort 顺序,不经过 adjustRadarLabels 重新排序
|
||||
setRadarData(radarData);
|
||||
updateUserLevel(res.data.record_id, res.data.ntrp_level);
|
||||
}
|
||||
}
|
||||
@@ -588,7 +595,7 @@ function Result() {
|
||||
if (!userInfo?.phone) {
|
||||
Taro.redirectTo({
|
||||
url: `/login_pages/index/index?redirect=${encodeURIComponent(
|
||||
`/main_pages/index`
|
||||
`/main_pages/index`,
|
||||
)}`,
|
||||
});
|
||||
clear();
|
||||
@@ -613,11 +620,12 @@ function Result() {
|
||||
page: "other_pages/ntrp-evaluate/index",
|
||||
scene: `stage=${StageType.INTRO}`,
|
||||
});
|
||||
if (qrCodeUrlRes.code === 0 && qrCodeUrlRes.data?.qr_code_base64) {
|
||||
finalQrCodeUrl = await base64ToTempFilePath(
|
||||
qrCodeUrlRes.data.qr_code_base64
|
||||
);
|
||||
}
|
||||
finalQrCodeUrl = qrCodeUrlRes.data.ossPath;
|
||||
// if (qrCodeUrlRes.code === 0 && qrCodeUrlRes.data?.qr_code_base64) {
|
||||
// finalQrCodeUrl = await base64ToTempFilePath(
|
||||
// qrCodeUrlRes.data.qr_code_base64
|
||||
// );
|
||||
// }
|
||||
}
|
||||
|
||||
// 使用 RadarV2 的 generateFullImage 方法生成完整图片
|
||||
@@ -712,7 +720,7 @@ function Result() {
|
||||
<View
|
||||
className={styles.card}
|
||||
style={{
|
||||
backgroundImage: `url(${OSS_BASE_URL}/images/f5b45cea-5015-41d6-aaf4-83b2e76678e1.png)`,
|
||||
backgroundImage: `url(${OSS_BASE}/front/ball/images/f5b45cea-5015-41d6-aaf4-83b2e76678e1.png)`,
|
||||
}}
|
||||
>
|
||||
<View className={styles.avatarWrap}>
|
||||
@@ -762,7 +770,8 @@ function Result() {
|
||||
{userInfo?.phone ? (
|
||||
<View className={styles.updateTip}>
|
||||
<Text>
|
||||
你的 NTRP 水平已更新为 {formatNtrpDisplay(result?.ntrp_level || "")}{" "}
|
||||
你的 NTRP 水平已更新为{" "}
|
||||
{formatNtrpDisplay(result?.ntrp_level || "")}{" "}
|
||||
</Text>
|
||||
<Text className={styles.grayTip}>(可在个人信息中修改)</Text>
|
||||
</View>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, Textarea, Image } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { ConfigProvider, Loading, Popup, Toast } from '@nutui/nutui-react-taro'
|
||||
import { ConfigProvider, Loading, Toast } from '@nutui/nutui-react-taro'
|
||||
import styles from './index.module.scss'
|
||||
import uploadFiles from '@/services/uploadFiles'
|
||||
import publishService from '@/services/publishService'
|
||||
@@ -109,7 +109,10 @@ const AiImportPopup: React.FC<AiImportPopupProps> = ({
|
||||
}
|
||||
|
||||
const handleTextChange = (e: any) => {
|
||||
setText(e.detail.value)
|
||||
const text = e.detail.value;
|
||||
const maxAllowedLength = 120;
|
||||
const truncatedVal = text.length > maxAllowedLength ? text.slice(0, maxAllowedLength) : text
|
||||
setText(truncatedVal)
|
||||
}
|
||||
|
||||
// 使用全局键盘状态监听
|
||||
@@ -191,73 +194,90 @@ const AiImportPopup: React.FC<AiImportPopupProps> = ({
|
||||
}
|
||||
|
||||
const showManualButton = uploadFailCount >= maxFailCount
|
||||
if (!visible) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 阻止弹窗内的触摸事件冒泡
|
||||
const handleTouchMoveInPopup = (e) => {
|
||||
if (!isKeyboardVisible) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Popup
|
||||
visible={visible}
|
||||
position="bottom"
|
||||
round={true}
|
||||
closeable={false}
|
||||
onClose={closePopupBefore}
|
||||
className={styles.aiImportPopup}
|
||||
style={{ paddingBottom: isKeyboardVisible ? `${keyboardHeight}px` : undefined }}
|
||||
>
|
||||
<View className={styles.popupContent}>
|
||||
{/* 头部 */}
|
||||
<View className={styles.header}>
|
||||
<View className={styles.titleContainer}>
|
||||
<Image src={images.ICON_IMPORTANT_BLACK} className={styles.lightningIcon} />
|
||||
<Text className={styles.title}>智能导入球局信息</Text>
|
||||
</View>
|
||||
<View className={styles.closeButton} onClick={closePopupBefore}>
|
||||
<Image src={images.ICON_CLOSE} className={styles.lightningIcon} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 文本域 */}
|
||||
<View className={styles.textAreaContainer}>
|
||||
<Textarea
|
||||
className={styles.textArea}
|
||||
value={text}
|
||||
onInput={handleTextChange}
|
||||
onFocus={() => {}}
|
||||
onBlur={() => {}}
|
||||
placeholder="在此「粘贴识别」或输入文本,智能拆分球局时间、费用、地点和其他信息,并帮你智能生成球局标题"
|
||||
maxlength={-1}
|
||||
showConfirmBar={false}
|
||||
placeholderClass={styles.textArea_placeholder}
|
||||
autoHeight
|
||||
// 关闭系统自动上推,改为手动根据键盘高度加内边距
|
||||
adjustPosition={false}
|
||||
/>
|
||||
<View className={styles.charCount}>
|
||||
<Text className={`${styles.charCountText} ${isCharCountExceeded ? styles.charCountTextExceeded : ''}`}>
|
||||
{text.length}/100
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 图片识别按钮 */}
|
||||
<View className={styles.imageRecognitionContainer}>
|
||||
<View className={`${styles.imageRecognitionButton} ${uploadLoading ? styles.uploadLoadingContainer : ''}`} onClick={handleImageRecognition}>
|
||||
{
|
||||
uploadLoading ? (<Image src={images.ICON_UPLOAD_SUCCESS} className={styles.cameraIcon} />) : (<Image src={images.ICON_UPLOAD_IMG} className={styles.cameraIcon} />)
|
||||
}
|
||||
<Text className={styles.imageRecognitionText}>图片识别</Text>
|
||||
<Text className={styles.imageRecognitionDesc}>{uploadLoading ? '已上传 1 张图片' : '支持订场截图/小红书笔记截图等图片'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 底部按钮 */}
|
||||
<View className={styles.bottomButtons}>
|
||||
{showManualButton && (
|
||||
<View className={styles.manualButton} onClick={handleManualPublish}>
|
||||
<Text className={styles.manualButtonText}>手动发布球局</Text>
|
||||
<View
|
||||
className={styles.aiImportPopupOverlay}
|
||||
>
|
||||
<View className={styles.aiImportPopupWrapper} onTouchMove={handleTouchMoveInPopup} catchMove></View>
|
||||
<View
|
||||
className={styles.aiImportPopup}
|
||||
style={{ paddingBottom: isKeyboardVisible ? `${keyboardHeight}px` : undefined }}
|
||||
>
|
||||
<View className={styles.popupContent}>
|
||||
{/* 头部 */}
|
||||
<View className={styles.header}>
|
||||
<View className={styles.titleContainer}>
|
||||
<Image src={images.ICON_IMPORTANT_BLACK} className={styles.lightningIcon} />
|
||||
<Text className={styles.title}>智能导入球局信息</Text>
|
||||
</View>
|
||||
)}
|
||||
<View className={styles.pasteButton} onClick={handlePasteAndRecognize}>
|
||||
{
|
||||
loading ? (
|
||||
<View className={styles.closeButton} onClick={closePopupBefore}>
|
||||
<Image src={images.ICON_CLOSE} className={styles.lightningIcon} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 文本域 */}
|
||||
<View className={styles.textAreaContainer}>
|
||||
<Textarea
|
||||
className={styles.textArea}
|
||||
value={text}
|
||||
onInput={handleTextChange}
|
||||
onFocus={() => {}}
|
||||
onBlur={() => {}}
|
||||
placeholder="在此「粘贴识别」或输入文本,智能拆分球局时间、费用、地点和其他信息,并帮你智能生成球局标题"
|
||||
maxlength={-1}
|
||||
showConfirmBar={false}
|
||||
placeholderClass={styles.textArea_placeholder}
|
||||
autoHeight
|
||||
// 关闭系统自动上推,改为手动根据键盘高度加内边距
|
||||
adjustPosition={false}
|
||||
/>
|
||||
<View className={styles.charCount}>
|
||||
<Text className={`${styles.charCountText} ${isCharCountExceeded ? styles.charCountTextExceeded : ''}`}>
|
||||
{text.length}/100
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 图片识别按钮 */}
|
||||
<View className={styles.imageRecognitionContainer}>
|
||||
<View
|
||||
className={`${styles.imageRecognitionButton} ${
|
||||
uploadLoading ? styles.uploadLoadingContainer : ''
|
||||
}`}
|
||||
onClick={handleImageRecognition}
|
||||
>
|
||||
{uploadLoading ? (
|
||||
<Image src={images.ICON_UPLOAD_SUCCESS} className={styles.cameraIcon} />
|
||||
) : (
|
||||
<Image src={images.ICON_UPLOAD_IMG} className={styles.cameraIcon} />
|
||||
)}
|
||||
<Text className={styles.imageRecognitionText}>图片识别</Text>
|
||||
<Text className={styles.imageRecognitionDesc}>
|
||||
{uploadLoading ? '已上传 1 张图片' : '支持订场截图/小红书笔记截图等图片'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 底部按钮 */}
|
||||
<View className={styles.bottomButtons}>
|
||||
{showManualButton && (
|
||||
<View className={styles.manualButton} onClick={handleManualPublish}>
|
||||
<Text className={styles.manualButtonText}>手动发布球局</Text>
|
||||
</View>
|
||||
)}
|
||||
<View className={styles.pasteButton} onClick={handlePasteAndRecognize}>
|
||||
{loading ? (
|
||||
<View className={styles.loadingContainer}>
|
||||
<ConfigProvider theme={{ nutuiLoadingIconColor: '#fff', nutuiLoadingIconSize: '20px' }}>
|
||||
<Loading type="circular" />
|
||||
@@ -269,13 +289,13 @@ const AiImportPopup: React.FC<AiImportPopupProps> = ({
|
||||
<Image src={images.ICON_COPY} className={styles.clipboardIcon} />
|
||||
<Text className={styles.pasteButtonText}>粘贴并识别</Text>
|
||||
</>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<Toast id="toast" />
|
||||
</View>
|
||||
<Toast id="toast" />
|
||||
</Popup>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,34 @@
|
||||
@use '~@/scss/themeColor.scss' as theme;
|
||||
|
||||
.aiImportPopupOverlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 9998;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.aiImportPopupWrapper {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9998;
|
||||
}
|
||||
.aiImportPopup {
|
||||
background-color: #fff;
|
||||
&:global(.nut-popup-bottom.nut-popup-round) {
|
||||
border-radius: 20px 20px 0 0!important;
|
||||
}
|
||||
width: 100%;
|
||||
background-color:#fafafa;
|
||||
border-radius: 16px 16px 0 0;
|
||||
position: relative;
|
||||
z-index: 9999;
|
||||
.popupContent {
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border-radius: 16px 16px 0 0;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
max-height: 80vh;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { View, Text, Input, ScrollView, Image } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { Loading } from '@nutui/nutui-react-taro'
|
||||
import StadiumDetail, { StadiumDetailRef } from './StadiumDetail'
|
||||
import { CommonPopup } from '../../../../components'
|
||||
import { CommonPopup, CustomPopup } from '../../../../components'
|
||||
import { getLocation } from '@/utils/locationUtils'
|
||||
import PublishService from '@/services/publishService'
|
||||
import images from '@/config/images'
|
||||
@@ -188,24 +188,20 @@ const SelectStadium: React.FC<SelectStadiumProps> = ({
|
||||
// 如果显示详情页面
|
||||
if (showDetail && selectedStadium) {
|
||||
return (
|
||||
<CommonPopup
|
||||
<CustomPopup
|
||||
visible={visible}
|
||||
onClose={handleCancel}
|
||||
cancelText="返回"
|
||||
confirmText="确认"
|
||||
className="select-stadium-popup"
|
||||
onCancel={handleDetailCancel}
|
||||
onConfirm={handleConfirm}
|
||||
position="bottom"
|
||||
//style={{ paddingBottom: keyboardVisible ? `20px` : undefined }}
|
||||
round
|
||||
>
|
||||
<StadiumDetail
|
||||
ref={stadiumDetailRef}
|
||||
stadium={selectedStadium}
|
||||
//onAnyInput={handleAnyInput}
|
||||
/>
|
||||
</CommonPopup>
|
||||
{/* 内容区域 */}
|
||||
<StadiumDetail
|
||||
ref={stadiumDetailRef}
|
||||
stadium={selectedStadium}
|
||||
/>
|
||||
</CustomPopup>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.stadium-detail-scroll{
|
||||
height:60vh;
|
||||
max-height:60vh;
|
||||
}
|
||||
// 已选球场
|
||||
// 场馆列表
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React, { useState, useCallback, forwardRef, useImperativeHandle } from 'react'
|
||||
import React, { useState, useCallback, forwardRef, useImperativeHandle, useEffect } from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { View, Text, Image, ScrollView } from '@tarojs/components'
|
||||
import images from '@/config/images'
|
||||
import TextareaTag from '@/components/TextareaTag'
|
||||
// import CoverImageUpload, { type CoverImage } from '@/components/ImageUpload'
|
||||
import UploadCover, { type CoverImageValue } from '@/components/UploadCover'
|
||||
import { useKeyboardHeight } from '@/store/keyboardStore'
|
||||
import { useDictionaryActions } from '@/store/dictionaryStore'
|
||||
|
||||
import './StadiumDetail.scss'
|
||||
@@ -69,12 +70,16 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
|
||||
stadium,
|
||||
onAnyInput
|
||||
}, ref) => {
|
||||
const [openPicker, setOpenPicker] = useState(false);
|
||||
const [openPicker, setOpenPicker] = useState(false); //为了解决上传图片时按钮样式问题
|
||||
const [scrollTop, setScrollTop] = useState(0);
|
||||
const { getDictionaryValue } = useDictionaryActions()
|
||||
const court_type = getDictionaryValue('court_type') || []
|
||||
const court_surface = getDictionaryValue('court_surface') || []
|
||||
const supplementary_information = getDictionaryValue('supplementary_information') || []
|
||||
|
||||
// 使用全局键盘状态
|
||||
const { keyboardHeight, isKeyboardVisible, addListener, initializeKeyboardListener } = useKeyboardHeight()
|
||||
|
||||
const stadiumInfo = [
|
||||
{
|
||||
label: '场地类型',
|
||||
@@ -171,19 +176,47 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
|
||||
|
||||
|
||||
|
||||
const changeTextarea = (value) => {
|
||||
// 使用全局键盘状态监听
|
||||
useEffect(() => {
|
||||
// 初始化全局键盘监听器
|
||||
initializeKeyboardListener()
|
||||
|
||||
// 添加本地监听器
|
||||
const removeListener = addListener((height, visible) => {
|
||||
console.log('AiImportPopup 收到键盘变化:', height, visible)
|
||||
})
|
||||
|
||||
return () => {
|
||||
removeListener()
|
||||
}
|
||||
}, [initializeKeyboardListener, addListener])
|
||||
|
||||
const changeTextarea = (value: boolean) => {
|
||||
if (value) {
|
||||
// 先滚动到底部
|
||||
setScrollTop(scrollTop ? scrollTop + 1 : 9999);
|
||||
setScrollTop(140);
|
||||
// 使用 setTimeout 确保滚动后再更新 openPicker
|
||||
}
|
||||
}
|
||||
|
||||
const changePicker = (value) => {
|
||||
// 当键盘显示时触发 changeTextarea
|
||||
useEffect(() => {
|
||||
if (isKeyboardVisible) {
|
||||
changeTextarea(true)
|
||||
}
|
||||
}, [isKeyboardVisible])
|
||||
|
||||
const changePicker = (value:boolean) => {
|
||||
setOpenPicker(value);
|
||||
}
|
||||
|
||||
console.log(stadium,'stadiumstadium');
|
||||
|
||||
// 计算滚动区域的最大高度
|
||||
const scrollMaxHeight = isKeyboardVisible
|
||||
? `calc(100vh - ${keyboardHeight+40}px)`
|
||||
: '60vh'
|
||||
|
||||
return (
|
||||
<View className='stadium-detail'>
|
||||
<ScrollView
|
||||
@@ -191,6 +224,7 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
|
||||
refresherBackground="#FAFAFA"
|
||||
scrollY={!openPicker}
|
||||
scrollTop={scrollTop}
|
||||
style={{ maxHeight: scrollMaxHeight }}
|
||||
>
|
||||
{/* 已选球场 */}
|
||||
<View
|
||||
@@ -235,7 +269,7 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
|
||||
<TextareaTag
|
||||
value={formData[item.prop]}
|
||||
onChange={(value) => {
|
||||
changeTextarea(true)
|
||||
//changeTextarea(true)
|
||||
updateFormData(item.prop, value)
|
||||
}}
|
||||
// onBlur={() => changeTextarea(false)}
|
||||
|
||||
@@ -78,9 +78,8 @@ const PublishBall: React.FC = () => {
|
||||
} = useKeyboardHeight();
|
||||
// 获取页面参数并设置导航标题
|
||||
const [optionsConfig, setOptionsConfig] = useState<FormFieldConfig[]>(
|
||||
publishBallFormSchema
|
||||
publishBallFormSchema,
|
||||
);
|
||||
console.log(userInfo, "userInfo");
|
||||
const [formData, setFormData] = useState<PublishBallFormData[]>([
|
||||
defaultFormData,
|
||||
]);
|
||||
@@ -103,13 +102,11 @@ const PublishBall: React.FC = () => {
|
||||
const updateFormData = (
|
||||
key: keyof PublishBallFormData,
|
||||
value: any,
|
||||
index: number
|
||||
index: number,
|
||||
) => {
|
||||
console.log(key, value, index, "key, value, index");
|
||||
setFormData((prev) => {
|
||||
const newData = [...prev];
|
||||
newData[index] = { ...newData[index], [key]: value };
|
||||
console.log(newData, "newData");
|
||||
return newData;
|
||||
});
|
||||
};
|
||||
@@ -186,7 +183,7 @@ const PublishBall: React.FC = () => {
|
||||
const confirmDelete = () => {
|
||||
if (deleteConfirm.index >= 0) {
|
||||
setFormData((prev) =>
|
||||
prev.filter((_, index) => index !== deleteConfirm.index)
|
||||
prev.filter((_, index) => index !== deleteConfirm.index),
|
||||
);
|
||||
closeDeleteConfirm();
|
||||
Taro.showToast({
|
||||
@@ -198,7 +195,7 @@ const PublishBall: React.FC = () => {
|
||||
|
||||
const validateFormData = (
|
||||
formData: PublishBallFormData,
|
||||
isOnSubmit: boolean = false
|
||||
isOnSubmit: boolean = false,
|
||||
) => {
|
||||
const {
|
||||
activityInfo,
|
||||
@@ -207,7 +204,7 @@ const PublishBall: React.FC = () => {
|
||||
image_list,
|
||||
players,
|
||||
current_players,
|
||||
descriptionInfo
|
||||
descriptionInfo,
|
||||
} = formData;
|
||||
const { play_type, price, location_name } = activityInfo;
|
||||
const { description } = descriptionInfo;
|
||||
@@ -225,7 +222,7 @@ const PublishBall: React.FC = () => {
|
||||
// 判断图片是否上传完成
|
||||
if (image_list?.length > 0) {
|
||||
const uploadInProgress = image_list.some((item) =>
|
||||
item.url.startsWith("http://tmp/")
|
||||
item?.url?.startsWith?.("http://tmp/"),
|
||||
);
|
||||
if (uploadInProgress) {
|
||||
Taro.showToast({
|
||||
@@ -253,7 +250,7 @@ const PublishBall: React.FC = () => {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
!price ||
|
||||
(typeof price === "number" && price <= 0) ||
|
||||
@@ -368,7 +365,6 @@ const PublishBall: React.FC = () => {
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
// 基础验证
|
||||
console.log(formData, "formData");
|
||||
const params = getParams();
|
||||
const { republish } = params || {};
|
||||
if (activityType === "individual") {
|
||||
@@ -516,7 +512,7 @@ const PublishBall: React.FC = () => {
|
||||
|
||||
const mergeWithDefault = (
|
||||
data: any,
|
||||
isDetail: boolean = false
|
||||
isDetail: boolean = false,
|
||||
): PublishBallFormData => {
|
||||
// ai导入与详情数据处理
|
||||
const {
|
||||
@@ -741,7 +737,6 @@ const PublishBall: React.FC = () => {
|
||||
} else {
|
||||
setIsSubmitDisabled(false);
|
||||
}
|
||||
console.log(formData, "formData");
|
||||
}, [formData]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -754,9 +749,8 @@ const PublishBall: React.FC = () => {
|
||||
initializeKeyboardListener();
|
||||
|
||||
// 添加本地监听器
|
||||
const removeListener = addListener((height, visible) => {
|
||||
console.log("PublishBall 收到键盘变化:", height, visible);
|
||||
// 这里只记录或用于其他逻辑,布局是否响应交由 shouldReactToKeyboard 决定
|
||||
const removeListener = addListener(() => {
|
||||
// 布局是否响应交由 shouldReactToKeyboard 决定
|
||||
});
|
||||
|
||||
return () => {
|
||||
@@ -789,6 +783,7 @@ const PublishBall: React.FC = () => {
|
||||
>
|
||||
<GeneralNavbar
|
||||
title={titleBar}
|
||||
backgroundColor={'#FAFAFA'}
|
||||
className={styles["publish-ball-navbar"]}
|
||||
/>
|
||||
<View
|
||||
|
||||
@@ -51,7 +51,6 @@ class CommonApiService {
|
||||
data: results.map(result => result.data)
|
||||
}
|
||||
} catch (error) {
|
||||
throw error
|
||||
} finally {
|
||||
Taro.hideLoading()
|
||||
}
|
||||
|
||||
@@ -158,14 +158,19 @@ class GameDetailService {
|
||||
async getQrCodeUrl(req: { page: string, scene: string }): Promise<ApiResponse<{
|
||||
qr_code_base64: string,
|
||||
image_size: number,
|
||||
ossPath: string,
|
||||
page: string,
|
||||
scene: string,
|
||||
width: number
|
||||
}>> {
|
||||
return httpService.post('/user/generate_qrcode', req, {
|
||||
showLoading: false
|
||||
showLoading: true
|
||||
})
|
||||
}
|
||||
|
||||
async getLinkUrl(req: { path: string, query: string }): Promise<ApiResponse<{ url_link: string, path: string, query: string }>> {
|
||||
return httpService.post('/user/generate_url_link', req, { showLoading: true })
|
||||
}
|
||||
}
|
||||
|
||||
// 导出认证服务实例
|
||||
|
||||
@@ -58,6 +58,7 @@ export interface TestResultData {
|
||||
level_img?: string; // 等级图片URL
|
||||
radar_data: RadarData;
|
||||
answers: Answer[];
|
||||
sort?: string[]; // 雷达图能力项排序,如 ["正手球质", "正手控制", ...]
|
||||
}
|
||||
|
||||
// 单条测试记录
|
||||
|
||||
@@ -129,23 +129,30 @@ class HttpService {
|
||||
|
||||
// 隐藏loading(支持多个并发请求)
|
||||
private hideLoading(): void {
|
||||
this.loadingCount = Math.max(0, this.loadingCount - 1)
|
||||
try {
|
||||
this.loadingCount = Math.max(0, this.loadingCount - 1)
|
||||
|
||||
// 只有所有请求都完成时才隐藏loading
|
||||
if (this.loadingCount === 0) {
|
||||
// 清除之前的延时器
|
||||
if (this.hideLoadingTimer) {
|
||||
clearTimeout(this.hideLoadingTimer)
|
||||
this.hideLoadingTimer = null
|
||||
// 只有所有请求都完成时才隐藏loading
|
||||
if (this.loadingCount === 0) {
|
||||
// 清除之前的延时器
|
||||
if (this.hideLoadingTimer) {
|
||||
clearTimeout(this.hideLoadingTimer)
|
||||
this.hideLoadingTimer = null
|
||||
}
|
||||
|
||||
// 延时300ms后隐藏loading,避免频繁切换
|
||||
this.hideLoadingTimer = setTimeout(() => {
|
||||
Taro.hideLoading()
|
||||
this.currentLoadingText = ''
|
||||
this.hideLoadingTimer = null
|
||||
}, 800)
|
||||
}
|
||||
|
||||
// 延时300ms后隐藏loading,避免频繁切换
|
||||
this.hideLoadingTimer = setTimeout(() => {
|
||||
Taro.hideLoading()
|
||||
this.currentLoadingText = ''
|
||||
this.hideLoadingTimer = null
|
||||
}, 800)
|
||||
}
|
||||
catch (e) {
|
||||
console.warn(e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 处理响应
|
||||
@@ -175,7 +182,7 @@ class HttpService {
|
||||
url: '/login_pages/index/index'
|
||||
})
|
||||
reject(new Error('用户不存在'))
|
||||
return response.data
|
||||
return response.data
|
||||
}
|
||||
|
||||
|
||||
@@ -187,7 +194,7 @@ class HttpService {
|
||||
} else {
|
||||
reject(response.data)
|
||||
}
|
||||
return response.data
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -263,7 +263,7 @@ export const save_login_state = (token: string, user_info: WechatUserInfo) => {
|
||||
export const clear_login_state = () => {
|
||||
try {
|
||||
// 使用 tokenManager 清除令牌
|
||||
tokenManager.clearTokens();
|
||||
// tokenManager.clearTokens();
|
||||
|
||||
// 清除其他登录状态
|
||||
Taro.removeStorageSync("user_info");
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 18 KiB |
@@ -20,7 +20,6 @@ interface DictionaryState {
|
||||
bannerDetailImage: string
|
||||
bannerListIndex: string
|
||||
} | null
|
||||
fetchBannerDictionary: () => Promise<void>
|
||||
}
|
||||
|
||||
// 创建字典Store
|
||||
@@ -36,7 +35,7 @@ export const useDictionaryStore = create<DictionaryState>()((set, get) => ({
|
||||
set({ isLoading: true, error: null })
|
||||
|
||||
try {
|
||||
const keys = 'publishing_requirements,court_type,court_surface,supplementary_information,game_play,fabu_tip,supported_cities';
|
||||
const keys = 'publishing_requirements,court_type,court_surface,supplementary_information,game_play,fabu_tip,supported_cities,bannerListImage,bannerDetailImage,bannerListIndex';
|
||||
const response = await commonApi.getDictionaryManyKey(keys)
|
||||
|
||||
if (response.code === 0 && response.data) {
|
||||
@@ -53,6 +52,15 @@ export const useDictionaryStore = create<DictionaryState>()((set, get) => ({
|
||||
dictionaryData: dictionaryData || {},
|
||||
isLoading: false
|
||||
})
|
||||
|
||||
set({
|
||||
bannerDict: {
|
||||
bannerListImage: response.data.bannerListImage || '',
|
||||
bannerDetailImage: response.data.bannerDetailImage || '',
|
||||
bannerListIndex: (response.data.bannerListIndex ?? '').toString(),
|
||||
}
|
||||
})
|
||||
|
||||
console.log('字典数据获取成功:', response.data)
|
||||
} else {
|
||||
throw new Error(response.message || '获取字典数据失败')
|
||||
@@ -67,26 +75,7 @@ export const useDictionaryStore = create<DictionaryState>()((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
// 获取 Banner 字典(启动时或手动调用)
|
||||
fetchBannerDictionary: async () => {
|
||||
try {
|
||||
const keys = 'bannerListImage,bannerDetailImage,bannerListIndex';
|
||||
const response = await commonApi.getDictionaryManyKey(keys)
|
||||
if (response.code === 0 && response.data) {
|
||||
const data = response.data || {};
|
||||
set({
|
||||
bannerDict: {
|
||||
bannerListImage: data.bannerListImage || '',
|
||||
bannerDetailImage: data.bannerDetailImage || '',
|
||||
bannerListIndex: (data.bannerListIndex ?? '').toString(),
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
// 保持静默,避免影响启动流程
|
||||
console.error('获取 Banner 字典失败:', error)
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// 获取字典值
|
||||
getDictionaryValue: (key: string, defaultValue?: any) => {
|
||||
|
||||
@@ -66,6 +66,34 @@ const DownloadBillRecords: React.FC = () => {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handlePreviewFile = (fileUrl: string) => {
|
||||
wx.downloadFile({
|
||||
url: fileUrl,
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
// 确保文件路径正确并添加扩展名
|
||||
const filePath = res.tempFilePath;
|
||||
wx.openDocument({
|
||||
filePath: filePath,
|
||||
fileType: 'xlsx', // 指定文件类型为xlsx
|
||||
showMenu: true, // 显示右上角菜单按钮
|
||||
success: (openRes) => {
|
||||
console.log('打开文档成功');
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('打开文档失败', err);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.error('下载失败,状态码:', res.statusCode);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('下载失败', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
return (
|
||||
<View className="download-bill-records-page">
|
||||
{/* 导航栏 */}
|
||||
@@ -94,7 +122,7 @@ const DownloadBillRecords: React.FC = () => {
|
||||
Taro.navigateBack();
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
<View
|
||||
className="records-container"
|
||||
style={{ marginTop: `${totalHeight}px` }}
|
||||
>
|
||||
@@ -111,7 +139,7 @@ const DownloadBillRecords: React.FC = () => {
|
||||
</View>
|
||||
<View className="info-item">
|
||||
<Text></Text>
|
||||
<Text className="btn">查看材料</Text>
|
||||
<Text className="btn" onClick={() => handlePreviewFile(record.file_url)}>查看材料</Text>
|
||||
</View>
|
||||
</View>
|
||||
)) : <EmptyState text="暂无数据" />}
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
|
||||
.qrcode {
|
||||
width: 240px;
|
||||
height: 240px;
|
||||
margin: 32px 0 -20px;
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ const OtherUserPage: React.FC = () => {
|
||||
);
|
||||
|
||||
const [collapseProfile, setCollapseProfile] = useState(false);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
// 进入页面时检查 user_id,只在组件挂载时执行一次
|
||||
useEffect(() => {
|
||||
@@ -82,56 +83,52 @@ const OtherUserPage: React.FC = () => {
|
||||
}
|
||||
}, []); // 空依赖数组,确保只在进入时执行一次
|
||||
|
||||
// 页面加载时获取用户信息
|
||||
useEffect(() => {
|
||||
const load_user_data = async () => {
|
||||
if (user_id) {
|
||||
try {
|
||||
// const user_data = await UserService.get_user_info(user_id);
|
||||
const res = await LoginService.getUserInfoById(user_id);
|
||||
const { data: userData } = res;
|
||||
// setUserInfo({...res.data as UserInfo, avatar: data.avatar_url || require("@/static/userInfo/default_avatar.svg")});
|
||||
setUserInfo({
|
||||
id: parseInt(user_id || "") || 0,
|
||||
nickname: userData.nickname || "",
|
||||
avatar_url: userData.avatar_url || "",
|
||||
join_date: userData.subscribe_time
|
||||
? `${new Date(userData.subscribe_time).getFullYear()}年${
|
||||
new Date(userData.subscribe_time).getMonth() + 1
|
||||
}月加入`
|
||||
: "",
|
||||
stats: {
|
||||
following_count: userData.stats?.following_count || 0,
|
||||
followers_count: userData.stats?.followers_count || 0,
|
||||
hosted_games_count: userData.stats?.hosted_games_count || 0,
|
||||
participated_games_count:
|
||||
userData.stats?.participated_games_count || 0,
|
||||
},
|
||||
|
||||
personal_profile: userData.personal_profile || "",
|
||||
province: userData.province || "",
|
||||
city: userData.city || "",
|
||||
district: userData.district || "",
|
||||
occupation: userData.occupation || "",
|
||||
ntrp_level: "",
|
||||
phone: userData.phone || "",
|
||||
gender: userData.gender || "",
|
||||
birthday: userData.birthday || "",
|
||||
});
|
||||
setIsFollowing(userData.is_following || false);
|
||||
} catch (error) {
|
||||
console.error("加载用户数据失败:", error);
|
||||
Taro.showToast({
|
||||
title: "加载失败",
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
load_user_data();
|
||||
// 加载用户信息(使用 useCallback 便于下拉刷新复用)
|
||||
const load_user_data = useCallback(async () => {
|
||||
if (!user_id) return;
|
||||
try {
|
||||
const res = await LoginService.getUserInfoById(user_id);
|
||||
const { data: userData } = res;
|
||||
setUserInfo({
|
||||
id: parseInt(user_id || "") || 0,
|
||||
nickname: userData.nickname || "",
|
||||
avatar_url: userData.avatar_url || "",
|
||||
join_date: userData.subscribe_time
|
||||
? `${new Date(userData.subscribe_time).getFullYear()}年${
|
||||
new Date(userData.subscribe_time).getMonth() + 1
|
||||
}月加入`
|
||||
: "",
|
||||
stats: {
|
||||
following_count: userData.stats?.following_count || 0,
|
||||
followers_count: userData.stats?.followers_count || 0,
|
||||
hosted_games_count: userData.stats?.hosted_games_count || 0,
|
||||
participated_games_count:
|
||||
userData.stats?.participated_games_count || 0,
|
||||
},
|
||||
personal_profile: userData.personal_profile || "",
|
||||
province: userData.province || "",
|
||||
city: userData.city || "",
|
||||
district: userData.district || "",
|
||||
occupation: userData.occupation || "",
|
||||
ntrp_level: "",
|
||||
phone: userData.phone || "",
|
||||
gender: userData.gender || "",
|
||||
birthday: userData.birthday || "",
|
||||
});
|
||||
setIsFollowing(userData.is_following || false);
|
||||
} catch (error) {
|
||||
console.error("加载用户数据失败:", error);
|
||||
Taro.showToast({
|
||||
title: "加载失败",
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
}, [user_id]);
|
||||
|
||||
useEffect(() => {
|
||||
load_user_data();
|
||||
}, [load_user_data]);
|
||||
|
||||
// 分类球局数据(使用 useCallback 包装,避免每次渲染都创建新函数)
|
||||
const classifyGameRecords = useCallback(
|
||||
(
|
||||
@@ -232,6 +229,18 @@ const OtherUserPage: React.FC = () => {
|
||||
setCollapseProfile(scrollData.scrollTop > 1);
|
||||
}, []);
|
||||
|
||||
// 下拉刷新:刷新用户信息和球局数据
|
||||
const handle_refresh = useCallback(async () => {
|
||||
setRefreshing(true);
|
||||
try {
|
||||
await Promise.all([load_user_data(), load_game_data()]);
|
||||
} catch (error) {
|
||||
console.error("刷新失败:", error);
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
}
|
||||
}, [load_user_data, load_game_data]);
|
||||
|
||||
// 处理球局详情
|
||||
// const handle_game_detail = (game_id: string) => {
|
||||
// Taro.navigateTo({
|
||||
@@ -244,6 +253,9 @@ const OtherUserPage: React.FC = () => {
|
||||
scrollY
|
||||
className="other_user_page"
|
||||
refresherBackground="#FAFAFA"
|
||||
refresherEnabled
|
||||
refresherTriggered={refreshing}
|
||||
onRefresherRefresh={handle_refresh}
|
||||
>
|
||||
{/* <CustomNavbar>
|
||||
<View className="navbar_content">
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
// @use '../../scss/common.scss' as *;
|
||||
|
||||
.wallet_page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
overflow: hidden;
|
||||
background-color: #fafafa;
|
||||
padding-bottom: 5px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.wallet_scroll {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { View, Text, Input, Button, Image } from "@tarojs/components";
|
||||
import Taro, { useDidShow, useReachBottom } from "@tarojs/taro";
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
import { View, Text, Input, Button, Image, ScrollView } from "@tarojs/components";
|
||||
import Taro, { useDidShow } from "@tarojs/taro";
|
||||
import "./index.scss";
|
||||
import { CommonPopup, EmptyState } from "@/components";
|
||||
import httpService from "@/services/httpService";
|
||||
@@ -109,16 +109,6 @@ const WalletPage: React.FC = () => {
|
||||
const pageConfig = currentPage.page?.config;
|
||||
const pageTitle = pageConfig?.navigationBarTitleText;
|
||||
|
||||
useReachBottom(() => {
|
||||
if (load_transactions_params.page >= totalPages) return;
|
||||
// 加载更多方法
|
||||
set_load_transactions_params((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
page: prev.page + 1,
|
||||
};
|
||||
});
|
||||
});
|
||||
// 钱包信息状态
|
||||
const [wallet_info, set_wallet_info] = useState<WalletInfo>({
|
||||
balance: 0,
|
||||
@@ -158,6 +148,7 @@ const WalletPage: React.FC = () => {
|
||||
});
|
||||
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
load_transactions();
|
||||
@@ -452,6 +443,33 @@ const WalletPage: React.FC = () => {
|
||||
setShowFilterPopup(true);
|
||||
};
|
||||
|
||||
// 下拉刷新:刷新钱包余额和交易记录
|
||||
const handle_refresh = useCallback(async () => {
|
||||
setRefreshing(true);
|
||||
try {
|
||||
await load_wallet_data();
|
||||
set_transactions([]);
|
||||
set_load_transactions_params((prev) => ({ ...prev, page: 1 }));
|
||||
} catch (error) {
|
||||
Taro.showToast({
|
||||
title: "刷新失败,请重试",
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 滚动到底部加载更多交易记录
|
||||
const handle_scroll_to_lower = useCallback(() => {
|
||||
if (load_transactions_params.page >= totalPages) return;
|
||||
set_load_transactions_params((prev) => ({
|
||||
...prev,
|
||||
page: prev.page + 1,
|
||||
}));
|
||||
}, [load_transactions_params.page, totalPages]);
|
||||
|
||||
const handleFilterCancel = () => {
|
||||
setShowFilterPopup(false);
|
||||
setFilterParams({
|
||||
@@ -488,6 +506,16 @@ const WalletPage: React.FC = () => {
|
||||
Taro.navigateBack();
|
||||
}}
|
||||
/>
|
||||
<ScrollView
|
||||
scrollY
|
||||
refresherBackground="#FAFAFA"
|
||||
refresherEnabled
|
||||
refresherTriggered={refreshing}
|
||||
onRefresherRefresh={handle_refresh}
|
||||
lowerThreshold={50}
|
||||
onScrollToLower={handle_scroll_to_lower}
|
||||
className="wallet_scroll"
|
||||
>
|
||||
{/* 钱包主卡片 */}
|
||||
<View
|
||||
className="wallet_main_card"
|
||||
@@ -649,6 +677,7 @@ const WalletPage: React.FC = () => {
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{/* 提现弹窗 */}
|
||||
<CommonPopup
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import Taro from "@tarojs/taro";
|
||||
import { OSS_BASE_URL } from "@/config/api";
|
||||
import { OSS_BASE } from "@/config/api";
|
||||
|
||||
const bgUrl = `${OSS_BASE_URL}/images/5e2c85ab-fb0c-4026-974d-1e0725181542.png`;
|
||||
|
||||
const ringUrl = `${OSS_BASE_URL}/images/b635164f-ecec-434a-a00b-69614a918f2f.png`;
|
||||
|
||||
const dateIcon = `${OSS_BASE_URL}/images/1b49476e-0eda-42ff-b08c-002ce510df82.jpg`;
|
||||
|
||||
const mapIcon = `${OSS_BASE_URL}/images/06b994fa-9227-4708-8555-8a07af8d0c3b.jpg`;
|
||||
|
||||
// const logo = `${OSS_BASE_URL}/images/fb732da6-11b9-4022-a524-a377b17635eb.jpg`
|
||||
|
||||
const logoText = `${OSS_BASE_URL}/images/9d8cbc9d-9601-4e2d-ab23-76420a4537d6.png`;
|
||||
const bgUrl = `${OSS_BASE}/front/ball/images/5e2c85ab-fb0c-4026-974d-1e0725181542.png`;
|
||||
const ringUrl = `${OSS_BASE}/front/ball/images/b635164f-ecec-434a-a00b-69614a918f2f.png`;
|
||||
const dateIcon = `${OSS_BASE}/front/ball/images/1b49476e-0eda-42ff-b08c-002ce510df82.jpg`;
|
||||
const mapIcon = `${OSS_BASE}/front/ball/images/06b994fa-9227-4708-8555-8a07af8d0c3b.jpg`;
|
||||
const logoText = `${OSS_BASE}/system/youchang_tip_text.png`;
|
||||
|
||||
export function base64ToTempFilePath(base64Data: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -288,7 +282,9 @@ function drawTextWrap(
|
||||
/** 核心纯函数:生成海报图片 */
|
||||
export async function generatePosterImage(data: any): Promise<string> {
|
||||
console.log("start !!!!");
|
||||
const dpr = Taro.getWindowInfo().pixelRatio;
|
||||
// const dpr = Taro.getWindowInfo().pixelRatio;
|
||||
const dpr = 1;
|
||||
// console.log(dpr, 'dpr')
|
||||
const width = 600;
|
||||
const height = 1000;
|
||||
|
||||
@@ -439,7 +435,7 @@ export async function generatePosterImage(data: any): Promise<string> {
|
||||
const { tempFilePath } = await Taro.canvasToTempFilePath({
|
||||
canvas,
|
||||
fileType: 'png',
|
||||
quality: 1,
|
||||
quality: 0.7,
|
||||
});
|
||||
return tempFilePath;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import { OSS_BASE_URL } from "@/config/api";
|
||||
import { OSS_BASE } from "@/config/api";
|
||||
|
||||
export interface ShareCardData {
|
||||
userAvatar: string
|
||||
@@ -481,7 +481,7 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro
|
||||
const textX = iconX + iconSize + 20
|
||||
|
||||
// 绘制网球图标
|
||||
const tennisBallPath = await loadImage(`${OSS_BASE_URL}/images/b3eaf45e-ef28-4e45-9195-823b832e0451.jpg`)
|
||||
const tennisBallPath = await loadImage(`${OSS_BASE}/front/ball/images/b3eaf45e-ef28-4e45-9195-823b832e0451.jpg`)
|
||||
ctx.drawImage(tennisBallPath, iconX, gameInfoY, iconSize, iconSize)
|
||||
|
||||
// 绘制"单打"标签
|
||||
@@ -517,7 +517,7 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro
|
||||
const dateX = danDaX
|
||||
const timeInfoY = infoStartY + infoSpacing
|
||||
const timeInfoFontSize = scale * 24 * dpr
|
||||
const calendarPath = await loadImage(`${OSS_BASE_URL}/images/ea792a5d-b105-4c95-bfc4-8af558f2b33b.jpg`)
|
||||
const calendarPath = await loadImage(`${OSS_BASE}/front/ball/images/ea792a5d-b105-4c95-bfc4-8af558f2b33b.jpg`)
|
||||
ctx.drawImage(calendarPath, iconX, timeInfoY, iconSize, iconSize)
|
||||
|
||||
// 绘制日期(绿色,非描边粗体)
|
||||
@@ -530,27 +530,27 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro
|
||||
// 绘制地点
|
||||
const locationInfoY = infoStartY + infoSpacing * 2
|
||||
const locationFontSize = scale * 22 * dpr
|
||||
const locationPath = await loadImage(`${OSS_BASE_URL}/images/adc9a167-2ea9-4e3b-b963-6a894a1fd91b.jpg`)
|
||||
const locationPath = await loadImage(`${OSS_BASE}/front/ball/images/adc9a167-2ea9-4e3b-b963-6a894a1fd91b.jpg`)
|
||||
ctx.drawImage(locationPath, iconX, locationInfoY, iconSize, iconSize)
|
||||
drawBoldText(ctx, data.venueName, danDaX, locationInfoY + 10, locationFontSize, '#000000')
|
||||
|
||||
try {
|
||||
const wxAny: any = (typeof (globalThis as any) !== 'undefined' && (globalThis as any).wx) ? (globalThis as any).wx : null
|
||||
if (wxAny && typeof wxAny.canvasToTempFilePath === 'function') {
|
||||
wxAny.canvasToTempFilePath({
|
||||
canvas: offscreen,
|
||||
fileType: 'png',
|
||||
quality: 1,
|
||||
success: (res: any) => {
|
||||
console.log('===res666', res)
|
||||
resolve(res.tempFilePath)
|
||||
},
|
||||
fail: reject
|
||||
})
|
||||
return
|
||||
}
|
||||
} catch { }
|
||||
reject(new Error('无法导出图片(OffscreenCanvas 转文件失败)'))
|
||||
try {
|
||||
const wxAny: any = (typeof (globalThis as any) !== 'undefined' && (globalThis as any).wx) ? (globalThis as any).wx : null
|
||||
if (wxAny && typeof wxAny.canvasToTempFilePath === 'function') {
|
||||
wxAny.canvasToTempFilePath({
|
||||
canvas: offscreen,
|
||||
fileType: 'png',
|
||||
quality: 1,
|
||||
success: (res: any) => {
|
||||
console.log('===res666', res)
|
||||
resolve(res.tempFilePath)
|
||||
},
|
||||
fail: reject
|
||||
})
|
||||
return
|
||||
}
|
||||
} catch { }
|
||||
reject(new Error('无法导出图片(OffscreenCanvas 转文件失败)'))
|
||||
console.log('Canvas绘制命令已发送')
|
||||
|
||||
} catch (error) {
|
||||
|
||||
2
types/global.d.ts
vendored
2
types/global.d.ts
vendored
@@ -17,6 +17,8 @@ declare namespace NodeJS {
|
||||
NODE_ENV: 'development' | 'production',
|
||||
/** 当前构建的平台 */
|
||||
TARO_ENV: 'weapp' | 'swan' | 'alipay' | 'h5' | 'rn' | 'tt' | 'quickapp' | 'qq' | 'jd'
|
||||
/** 应用环境标识 */
|
||||
APP_ENV: 'dev' | 'dev_local' | 'sit' | 'pr'
|
||||
/**
|
||||
* 当前构建的小程序 appid
|
||||
* @description 若不同环境有不同的小程序,可通过在 env 文件中配置环境变量`TARO_APP_ID`来方便快速切换 appid, 而不必手动去修改 dist/project.config.json 文件
|
||||
|
||||
Reference in New Issue
Block a user