Compare commits
34 Commits
54b7a27af5
...
feat/juguo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99c8026f61 | ||
| 05966b2acb | |||
|
|
4cf2b959b5 | ||
|
|
43610dcf99 | ||
|
|
05aa820466 | ||
|
|
b154e31f8f | ||
|
|
669ee2fe4e | ||
|
|
281ee2b746 | ||
|
|
132c74d27c | ||
|
|
6b6a4c9480 | ||
|
|
0f8dd44f5a | ||
| 82ba753b8b | |||
| 159d81ed12 | |||
| 22965eedf3 | |||
| 49935dd049 | |||
|
|
cab90aa1cb | ||
| 632da5112d | |||
| 28955e9da1 | |||
| 70a66fabdc | |||
| c47ebce43c | |||
| b0f4b5713d | |||
| f7f10f5d15 | |||
|
|
2bcdd93479 | ||
|
|
af2c472030 | ||
|
|
8d0ed5b1b3 | ||
|
|
e99986c52a | ||
|
|
4b2f6707cc | ||
|
|
a019fe473b | ||
|
|
1d0d2edaa2 | ||
| 5926e096b5 | |||
|
|
e07f2ad2d1 | ||
|
|
bfc6a149f0 | ||
|
|
6f73bb6d99 | ||
|
|
744169fe34 |
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,6 @@ node_modules/
|
||||
src/config/env.ts
|
||||
.vscode
|
||||
*.http
|
||||
.cursor
|
||||
.codewiz
|
||||
|
||||
|
||||
@@ -4,7 +4,11 @@ export default {
|
||||
quiet: false,
|
||||
stats: true
|
||||
},
|
||||
mini: {},
|
||||
mini: {
|
||||
webpackChain(chain) {
|
||||
chain.devtool('source-map')
|
||||
}
|
||||
},
|
||||
h5: {},
|
||||
// 添加这个配置来显示完整错误信息
|
||||
compiler: {
|
||||
|
||||
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];
|
||||
}
|
||||
128
config/env.ts
Normal file
128
config/env.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
// 环境类型
|
||||
export type EnvType = 'development' | 'production'
|
||||
|
||||
// 环境配置接口
|
||||
export interface EnvConfig {
|
||||
name: string
|
||||
apiBaseURL: string
|
||||
timeout: number
|
||||
enableLog: boolean
|
||||
enableMock: boolean
|
||||
// 客服配置
|
||||
customerService: {
|
||||
corpId: string
|
||||
serviceUrl: string
|
||||
phoneNumber?: string
|
||||
email?: string
|
||||
}
|
||||
}
|
||||
|
||||
// 各环境配置
|
||||
const envConfigs: Record<EnvType, EnvConfig> = {
|
||||
|
||||
|
||||
// 开发环境
|
||||
development: {
|
||||
name: '开发环境',
|
||||
// apiBaseURL: 'https://tennis.bimwe.com',
|
||||
apiBaseURL: 'http://localhost:9098',
|
||||
timeout: 15000,
|
||||
enableLog: true,
|
||||
enableMock: false,
|
||||
// 客服配置
|
||||
customerService: {
|
||||
corpId: 'ww51fc969e8b76af82', // 企业ID
|
||||
serviceUrl: 'https://work.weixin.qq.com/kfid/kfc64085b93243c5c91',
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
|
||||
// 生产环境1
|
||||
// production: {
|
||||
// name: '生产环境1',
|
||||
// apiBaseURL: 'https://tennis.bimwe.com',
|
||||
// timeout: 10000,
|
||||
// enableLog: false,
|
||||
// enableMock: false,
|
||||
// // 客服配置
|
||||
// customerService: {
|
||||
// corpId: 'ww51fc969e8b76af82', // 企业ID
|
||||
// serviceUrl: 'https://work.weixin.qq.com/kfid/kfc64085b93243c5c91',
|
||||
|
||||
// }
|
||||
// },
|
||||
|
||||
// 生产环境2
|
||||
production: {
|
||||
name: '生产环境2',
|
||||
apiBaseURL: 'https://youchang.qiongjingtiyu.com',
|
||||
timeout: 10000,
|
||||
enableLog: false,
|
||||
enableMock: false,
|
||||
// 客服配置
|
||||
customerService: {
|
||||
corpId: 'ww9a2d9a5d9410c664', // 企业ID
|
||||
serviceUrl: 'https://work.weixin.qq.com/kfid/kfcd355e162e0390684',
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前环境
|
||||
export const getCurrentEnv = (): EnvType => {
|
||||
// 在小程序环境中,使用默认逻辑判断环境
|
||||
// 可以根据实际需要配置不同的判断逻辑
|
||||
|
||||
// 可以根据实际部署情况添加更多判断逻辑
|
||||
// 比如通过 Taro.getEnv() 获取当前平台环境
|
||||
|
||||
const isProd = process.env.NODE_ENV === 'production'
|
||||
if (isProd) {
|
||||
return 'production'
|
||||
} else {
|
||||
return 'development'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前环境配置
|
||||
export const getCurrentConfig = (): EnvConfig => {
|
||||
const env = getCurrentEnv()
|
||||
return envConfigs[env]
|
||||
}
|
||||
|
||||
// 获取指定环境配置
|
||||
export const getEnvConfig = (env: EnvType): EnvConfig => {
|
||||
return envConfigs[env]
|
||||
}
|
||||
|
||||
// 是否为开发环境
|
||||
export const isDevelopment = (): boolean => {
|
||||
return getCurrentEnv() === 'development'
|
||||
}
|
||||
|
||||
// 是否为生产环境
|
||||
export const isProduction = (): boolean => {
|
||||
return getCurrentEnv() === 'production'
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 环境配置调试信息
|
||||
export const getEnvInfo = () => {
|
||||
const config = getCurrentConfig()
|
||||
return {
|
||||
env: getCurrentEnv(),
|
||||
config,
|
||||
taroEnv: Taro.getEnv(),
|
||||
platform: Taro.getEnv() === Taro.ENV_TYPE.WEAPP ? '微信小程序' :
|
||||
Taro.getEnv() === Taro.ENV_TYPE.WEB ? 'Web' :
|
||||
Taro.getEnv() === Taro.ENV_TYPE.RN ? 'React Native' : '未知'
|
||||
}
|
||||
}
|
||||
|
||||
// 导出当前环境配置(方便直接使用)
|
||||
export default getCurrentConfig()
|
||||
@@ -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: '/',
|
||||
|
||||
33
package.json
33
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: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",
|
||||
|
||||
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})`);
|
||||
@@ -3,6 +3,12 @@
|
||||
.common-popup {
|
||||
position: fixed;
|
||||
z-index: 9999 !important;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
max-height: calc(100vh - 10px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: theme.$page-background-color;
|
||||
&:global(.nut-popup-bottom.nut-popup-round) {
|
||||
border-radius: 20px 20px 0 0 !important;
|
||||
}
|
||||
@@ -32,12 +38,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
max-height: calc(100vh - 10px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: theme.$page-background-color;
|
||||
|
||||
|
||||
// .common-popup__header {
|
||||
// padding: 12px 16px;
|
||||
|
||||
215
src/components/CustomPopup/CustomPopup.tsx
Normal file
215
src/components/CustomPopup/CustomPopup.tsx
Normal file
@@ -0,0 +1,215 @@
|
||||
import React, { useRef, useState, useEffect } from 'react'
|
||||
import type { CSSProperties, ReactNode } from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Button } from '@nutui/nutui-react-taro'
|
||||
import { useKeyboardHeight } from '@/store/keyboardStore'
|
||||
import styles from './index.module.scss'
|
||||
|
||||
export interface CustomPopupProps {
|
||||
visible: boolean
|
||||
onClose: () => void
|
||||
title?: ReactNode
|
||||
showHeader?: boolean
|
||||
hideFooter?: boolean
|
||||
cancelText?: string
|
||||
confirmText?: string
|
||||
onCancel?: () => void
|
||||
onConfirm?: () => void
|
||||
children?: ReactNode
|
||||
className?: string
|
||||
style?: CSSProperties
|
||||
// 与 CommonPopup 保持入参一致
|
||||
position?: 'center' | 'bottom' | 'top' | 'left' | 'right'
|
||||
round?: boolean
|
||||
zIndex?: number
|
||||
enableDragToClose?: boolean
|
||||
}
|
||||
|
||||
const CustomPopup: React.FC<CustomPopupProps> = ({
|
||||
visible,
|
||||
onClose,
|
||||
title,
|
||||
showHeader = false,
|
||||
hideFooter = false,
|
||||
cancelText = '返回',
|
||||
confirmText = '完成',
|
||||
onCancel,
|
||||
onConfirm,
|
||||
children,
|
||||
className,
|
||||
style,
|
||||
position = 'bottom',
|
||||
round = true,
|
||||
zIndex,
|
||||
enableDragToClose = true,
|
||||
}) => {
|
||||
const [dragOffset, setDragOffset] = useState(0)
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const touchStartY = useRef(0)
|
||||
|
||||
// 使用全局键盘状态
|
||||
const { keyboardHeight, isKeyboardVisible, addListener, initializeKeyboardListener, setKeyboardVisible } = useKeyboardHeight()
|
||||
|
||||
// 当弹窗显示时,设置键盘为不可见
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
setKeyboardVisible(false)
|
||||
}
|
||||
}, [visible, setKeyboardVisible])
|
||||
|
||||
// 使用全局键盘状态监听
|
||||
useEffect(() => {
|
||||
// 初始化全局键盘监听器
|
||||
initializeKeyboardListener()
|
||||
|
||||
// 添加本地监听器
|
||||
const removeListener = addListener((height, visible) => {
|
||||
console.log('CustomPopup 收到键盘变化:', height, visible)
|
||||
})
|
||||
|
||||
return () => {
|
||||
removeListener()
|
||||
}
|
||||
}, [initializeKeyboardListener, addListener])
|
||||
|
||||
if (!visible) {
|
||||
return null
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
if (onCancel) {
|
||||
onCancel()
|
||||
} else {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
const handleTouchStart = (e: any) => {
|
||||
if (!enableDragToClose) return
|
||||
|
||||
touchStartY.current = e.touches[0].clientY
|
||||
setIsDragging(true)
|
||||
}
|
||||
|
||||
const handleTouchMove = (e: any) => {
|
||||
if (!enableDragToClose || !isDragging) return
|
||||
|
||||
const currentY = e.touches[0].clientY
|
||||
const deltaY = currentY - touchStartY.current
|
||||
|
||||
if (deltaY > 0) {
|
||||
setDragOffset(Math.min(deltaY, 100))
|
||||
}
|
||||
}
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
if (!enableDragToClose || !isDragging) return
|
||||
|
||||
setIsDragging(false)
|
||||
|
||||
if (dragOffset > 50) {
|
||||
onClose()
|
||||
}
|
||||
|
||||
setDragOffset(0)
|
||||
}
|
||||
|
||||
const overlayAlignItems =
|
||||
position === 'center'
|
||||
? 'center'
|
||||
: position === 'top'
|
||||
? 'flex-start'
|
||||
: 'flex-end'
|
||||
|
||||
const handleOverlayClick = () => {
|
||||
onClose()
|
||||
}
|
||||
|
||||
// 阻止弹窗内的触摸事件冒泡
|
||||
const handleTouchMoveInPopup = (e: any) => {
|
||||
if (!isKeyboardVisible) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<View
|
||||
className={styles['custom-popup-overlay']}
|
||||
style={{ zIndex: zIndex ?? undefined, alignItems: overlayAlignItems }}
|
||||
onClick={handleOverlayClick}
|
||||
>
|
||||
<View className={styles['custom-popup-move']} onTouchMove={handleTouchMoveInPopup} catchMove></View>
|
||||
<View
|
||||
className={`${styles['custom-popup']} ${className ? className : ''}`}
|
||||
style={{
|
||||
...style,
|
||||
paddingBottom: isKeyboardVisible ? `${keyboardHeight}px` : undefined,
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
{enableDragToClose && (
|
||||
<View
|
||||
className={styles['custom-popup__drag-handle-container']}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
>
|
||||
<View
|
||||
className={styles['custom-popup__drag-handle']}
|
||||
style={{
|
||||
transform: `translateX(-50%) translateY(${dragOffset * 0.3}px)`,
|
||||
opacity: isDragging ? 0.8 : 1,
|
||||
transition: isDragging ? 'none' : 'all 0.3s ease-out',
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{showHeader && (
|
||||
<View className={styles['custom-popup__header']}>
|
||||
{typeof title === 'string' ? (
|
||||
<Text className={styles['custom-popup__title']}>{title}</Text>
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
<View className={styles['close_button']} onClick={onClose}>
|
||||
<View className={styles['close_icon']}>
|
||||
<View className={styles['close_line']} />
|
||||
<View className={styles['close_line']} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View className={styles['custom-popup__body']}>{children}</View>
|
||||
|
||||
{!hideFooter && !isKeyboardVisible && (
|
||||
<View className={styles['custom-popup__footer']}>
|
||||
<Button
|
||||
className={`${styles['custom-popup__btn']} ${styles['custom-popup__btn-cancel']}`}
|
||||
type="default"
|
||||
size="small"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
{cancelText}
|
||||
</Button>
|
||||
<Button
|
||||
className={`${styles['custom-popup__btn']} ${styles['custom-popup__btn-confirm']}`}
|
||||
type="primary"
|
||||
size="small"
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{confirmText}
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomPopup
|
||||
|
||||
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;
|
||||
|
||||
@@ -186,10 +186,11 @@ 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;
|
||||
// const inTwoHours = dayjs(detail.start_time).diff(dayjs(), "hour") < 2;
|
||||
const beforeStart = dayjs(detail.start_time).isAfter(dayjs());
|
||||
|
||||
const hasOtherParticiappants = (detail.participants || [])
|
||||
.filter((item) => item.status === "joined")
|
||||
@@ -207,7 +208,7 @@ export default forwardRef(function GameManagePopup(props, ref) {
|
||||
style={{ minHeight: "unset" }}
|
||||
>
|
||||
<View className={styles.container}>
|
||||
{!inTwoHours && !hasOtherParticiappants && (
|
||||
{!finished && !hasOtherParticiappants && beforeStart && (
|
||||
<View className={styles.button} onClick={handleEditGame}>
|
||||
编辑活动
|
||||
</View>
|
||||
@@ -217,12 +218,12 @@ export default forwardRef(function GameManagePopup(props, ref) {
|
||||
重新发布
|
||||
</View>
|
||||
)}
|
||||
{!inTwoHours && !hasOtherParticiappants && (
|
||||
{!finished && beforeStart && (
|
||||
<View className={styles.button} onClick={handleCancelGame}>
|
||||
取消活动
|
||||
</View>
|
||||
)}
|
||||
{hasJoin && (
|
||||
{!finished && beforeStart && hasJoin && (
|
||||
<View className={styles.button} onClick={handleQuitGame}>
|
||||
退出活动
|
||||
</View>
|
||||
|
||||
@@ -15,7 +15,7 @@ const GamePlayType = (props: IProps) => {
|
||||
const { name, onChange, value, options } = props;
|
||||
return (
|
||||
<View className={styles.gamePlayWrapper}>
|
||||
<TitleComponent title="玩法" icon={<Image src={img.ICON_SITE} />} />
|
||||
<TitleComponent title="玩法" icon={<Image src={img.ICON_GAME_PLAY} />} />
|
||||
<Bubble
|
||||
options={options}
|
||||
value={value}
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
}
|
||||
|
||||
.location-position {
|
||||
flex: 1;
|
||||
// flex: 1;
|
||||
min-width: 0; // 允许缩小
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -5,8 +5,9 @@ import img from "../../config/images";
|
||||
import { ListCardProps } from "../../../types/list/types";
|
||||
import { formatGameTime, calculateDuration } from "@/utils/timeUtils";
|
||||
import { navigateTo } from "@/utils/navigation";
|
||||
import images from '@/config/images'
|
||||
import images from "@/config/images";
|
||||
import "./index.scss";
|
||||
import { OSS_BASE } from "@/config/api";
|
||||
|
||||
const ListCard: React.FC<ListCardProps> = ({
|
||||
id,
|
||||
@@ -45,7 +46,7 @@ const ListCard: React.FC<ListCardProps> = ({
|
||||
className="image"
|
||||
mode="aspectFill"
|
||||
lazyLoad
|
||||
defaultSource={require("@/static/emptyStatus/publish-empty-card.png")}
|
||||
defaultSource={`${OSS_BASE}/front/ball/images/publish-empty-card.svg`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -67,7 +68,9 @@ const ListCard: React.FC<ListCardProps> = ({
|
||||
const containerWidthPx = screenWidth - 130;
|
||||
|
||||
// 计算固定信息宽度
|
||||
const extraInfo = `${court_type ? `・${court_type}` : ''}${distance_km ? `・${distance_km}km` : ''}`;
|
||||
const extraInfo = `${court_type ? `・${court_type}` : ""}${
|
||||
distance_km ? `・${distance_km}km` : ""
|
||||
}`;
|
||||
|
||||
// 估算字符宽度(基于 12px 字体)
|
||||
const getTextWidth = (text: string) => {
|
||||
@@ -98,7 +101,9 @@ const ListCard: React.FC<ListCardProps> = ({
|
||||
let currentWidth = 0;
|
||||
for (let i = 0; i < location.length; i++) {
|
||||
const char = location[i];
|
||||
const charWidth = /[\u4e00-\u9fa5\u3000-\u303f\uff00-\uffef]/.test(char) ? 12 : 6;
|
||||
const charWidth = /[\u4e00-\u9fa5\u3000-\u303f\uff00-\uffef]/.test(char)
|
||||
? 12
|
||||
: 6;
|
||||
if (currentWidth + charWidth > availableWidth) {
|
||||
break;
|
||||
}
|
||||
@@ -106,7 +111,7 @@ const ListCard: React.FC<ListCardProps> = ({
|
||||
maxChars++;
|
||||
}
|
||||
|
||||
return location.slice(0, maxChars) + '...';
|
||||
return location.slice(0, maxChars) + "...";
|
||||
}, [location, court_type, distance_km]);
|
||||
|
||||
// 根据图片数量决定展示样式
|
||||
@@ -127,10 +132,10 @@ const ListCard: React.FC<ListCardProps> = ({
|
||||
return (
|
||||
<View className="double-image">
|
||||
<View className="image-container">
|
||||
{renderItemImage(image_list?.[0])}
|
||||
{renderItemImage(image_list?.[1])}
|
||||
</View>
|
||||
<View className="image-container">
|
||||
{renderItemImage(image_list?.[1])}
|
||||
{renderItemImage(image_list?.[0])}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
@@ -220,9 +225,10 @@ const ListCard: React.FC<ListCardProps> = ({
|
||||
</Text>
|
||||
</View>
|
||||
<View className="tag ntprTag">
|
||||
<Image src={images.ICON_LIST_NTPR} className='ntprIcon' />
|
||||
<Image src={images.ICON_LIST_NTPR} className="ntprIcon" />
|
||||
<Text className="tag-text">
|
||||
{Number(skill_level_min)?.toFixed(1)} - {Number(skill_level_max)?.toFixed(1)}
|
||||
{Number(skill_level_min)?.toFixed(1)} -{" "}
|
||||
{Number(skill_level_max)?.toFixed(1)}
|
||||
</Text>
|
||||
{/* 分割线 */}
|
||||
<View className="typeLine" />
|
||||
@@ -251,13 +257,8 @@ const ListCard: React.FC<ListCardProps> = ({
|
||||
/>
|
||||
{/* <Text className="smoothTitle">{game_type}</Text> */}
|
||||
</View>
|
||||
{
|
||||
venue_description && (<View className="line" />)
|
||||
}
|
||||
{
|
||||
venue_description &&
|
||||
(
|
||||
|
||||
{venue_description && <View className="line" />}
|
||||
{venue_description && (
|
||||
<View className="localAreaContainer">
|
||||
<View className="localAreaTitle">场馆方:</View>
|
||||
<View className="localAreaWrapper">
|
||||
@@ -265,8 +266,7 @@ const ListCard: React.FC<ListCardProps> = ({
|
||||
<Text className="localAreaText">{venue_description}</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -24,7 +24,6 @@ const ListLoadError = (props: IProps) => {
|
||||
wrapperHeight = "",
|
||||
width = "",
|
||||
height = "",
|
||||
scale = "",
|
||||
} = props;
|
||||
const handleReload = () => {
|
||||
reload && typeof reload === "function" && reload();
|
||||
@@ -34,7 +33,7 @@ const ListLoadError = (props: IProps) => {
|
||||
<View className={styles.listLoadError} style={{ height: wrapperHeight }}>
|
||||
<Image
|
||||
className={styles.listLoadErrorImg}
|
||||
style={{ width, height, transform: `scale(${scale})` }}
|
||||
style={{ width, height }}
|
||||
src={errorImg ? img[errorImg] : img.ICON_LIST_LOAD_ERROR}
|
||||
/>
|
||||
{text && <Text className={styles.listLoadErrorText}>{text}</Text>}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -52,7 +52,7 @@ const PopupPicker = ({
|
||||
ntrpTested,
|
||||
}: PickerProps) => {
|
||||
const [defaultValue, setDefaultValue] = useState<(string | number)[]>([]);
|
||||
const [defaultOptions, setDefaultOptions] = useState<PickerOption[][]>([]);
|
||||
const [defaultOptions, setDefaultOptions] = useState<PickerOption[][]>([...options]);
|
||||
const [pickerCurrentValue, setPickerCurrentValue] =
|
||||
useState<(string | number)[]>(value);
|
||||
|
||||
|
||||
@@ -29,18 +29,24 @@ export interface RadarChartV2Ref {
|
||||
}) => Promise<string>;
|
||||
}
|
||||
|
||||
const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref) => {
|
||||
const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>(
|
||||
(props, ref) => {
|
||||
const { data } = props;
|
||||
|
||||
const maxValue = 100;
|
||||
const levels = 5;
|
||||
|
||||
// 在 exportCanvasV2 中绘制雷达图的函数
|
||||
function drawRadarChart(ctx: CanvasRenderingContext2D, radarX: number, radarY: number, radarSize: number) {
|
||||
function drawRadarChart(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
radarX: number,
|
||||
radarY: number,
|
||||
radarSize: number,
|
||||
) {
|
||||
// 雷达图中心点位置(radarSize 已经是2倍图尺寸)
|
||||
const center = {
|
||||
x: radarX + radarSize / 2,
|
||||
y: radarY + radarSize / 2
|
||||
y: radarY + radarSize / 2,
|
||||
};
|
||||
|
||||
// 计算实际半径(radarSize 是直径,半径是直径的一半)
|
||||
@@ -48,9 +54,9 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
|
||||
|
||||
// 启用抗锯齿
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.imageSmoothingQuality = 'high';
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.imageSmoothingQuality = "high";
|
||||
ctx.lineCap = "round";
|
||||
ctx.lineJoin = "round";
|
||||
|
||||
// 解析数据
|
||||
const { texts, vals } = data.reduce(
|
||||
@@ -61,7 +67,7 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
|
||||
vals: [...res.vals, val],
|
||||
};
|
||||
},
|
||||
{ texts: [], vals: [] }
|
||||
{ texts: [], vals: [] },
|
||||
);
|
||||
|
||||
// === 绘制圆形网格 ===
|
||||
@@ -148,25 +154,39 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
|
||||
}
|
||||
|
||||
// 获取图片信息(宽高)
|
||||
function getImageInfo(src: string): Promise<{ width: number; height: number }> {
|
||||
function getImageInfo(
|
||||
src: string,
|
||||
): Promise<{ width: number; height: number }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
(Taro as any).getImageInfo({
|
||||
src,
|
||||
success: (res: any) => resolve({ width: res.width, height: res.height }),
|
||||
success: (res: any) =>
|
||||
resolve({ width: res.width, height: res.height }),
|
||||
fail: reject,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 绘制圆角矩形
|
||||
function roundRect(ctx: any, x: number, y: number, width: number, height: number, radius: number) {
|
||||
function roundRect(
|
||||
ctx: any,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
radius: number,
|
||||
) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + radius, y);
|
||||
ctx.lineTo(x + width - radius, y);
|
||||
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
||||
ctx.lineTo(x + width, y + height - radius);
|
||||
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
||||
ctx.quadraticCurveTo(
|
||||
x + width,
|
||||
y + height,
|
||||
x + width - radius,
|
||||
y + height,
|
||||
);
|
||||
ctx.lineTo(x + radius, y + height);
|
||||
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
||||
ctx.lineTo(x, y + radius);
|
||||
@@ -187,8 +207,7 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
// 生成原始雷达图(已废弃,现在直接在 exportCanvasV2 中绘制)
|
||||
generateImage: () =>
|
||||
Promise.resolve(""),
|
||||
generateImage: () => Promise.resolve(""),
|
||||
|
||||
// 生成完整图片(包含标题、雷达图、底部文字和二维码)
|
||||
generateFullImage: async (options: {
|
||||
@@ -229,7 +248,7 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
|
||||
|
||||
// 启用抗锯齿
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.imageSmoothingQuality = 'high';
|
||||
ctx.imageSmoothingQuality = "high";
|
||||
|
||||
// 绘制背景 - 使用 share_bg.png 背景图,撑满整个画布(从 OSS 动态加载)
|
||||
try {
|
||||
@@ -253,18 +272,28 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
|
||||
if (options.avatarUrl) {
|
||||
try {
|
||||
const avatarSize = 43.46 * scale; // 设计稿头像尺寸
|
||||
const avatarImg = await loadImage(canvas, options.avatarUrl);
|
||||
const avatarImg = await loadImage(
|
||||
canvas,
|
||||
options.avatarUrl,
|
||||
);
|
||||
const avatarInfo = await getImageInfo(options.avatarUrl);
|
||||
|
||||
// 头像区域总宽度(头像 + 装饰图片重叠部分)
|
||||
const avatarWrapWidth = 84.7 * scale; // 设计稿 Frame 1912055063 宽度
|
||||
const avatarX = sidePadding + (294 * scale - avatarWrapWidth) / 2; // 294 是 Frame 1912055062 宽度,居中
|
||||
const avatarX =
|
||||
sidePadding + (294 * scale - avatarWrapWidth) / 2; // 294 是 Frame 1912055062 宽度,居中
|
||||
const avatarY = currentY;
|
||||
|
||||
// 绘制头像圆形背景
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2);
|
||||
ctx.arc(
|
||||
avatarX + avatarSize / 2,
|
||||
avatarY + avatarSize / 2,
|
||||
avatarSize / 2,
|
||||
0,
|
||||
Math.PI * 2,
|
||||
);
|
||||
ctx.fillStyle = "#FFFFFF";
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = "#EFEFEF";
|
||||
@@ -273,7 +302,8 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
|
||||
|
||||
// 计算头像绘制尺寸,保持宽高比
|
||||
const innerSize = avatarSize - 1.94 * scale; // 内部可用尺寸
|
||||
const avatarAspectRatio = avatarInfo.width / avatarInfo.height;
|
||||
const avatarAspectRatio =
|
||||
avatarInfo.width / avatarInfo.height;
|
||||
let drawWidth = innerSize;
|
||||
let drawHeight = innerSize;
|
||||
let drawX = avatarX + 0.97 * scale;
|
||||
@@ -292,9 +322,21 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
|
||||
|
||||
// 绘制头像(圆形裁剪)
|
||||
ctx.beginPath();
|
||||
ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2 - 0.97 * scale, 0, Math.PI * 2);
|
||||
ctx.arc(
|
||||
avatarX + avatarSize / 2,
|
||||
avatarY + avatarSize / 2,
|
||||
avatarSize / 2 - 0.97 * scale,
|
||||
0,
|
||||
Math.PI * 2,
|
||||
);
|
||||
ctx.clip();
|
||||
ctx.drawImage(avatarImg, drawX, drawY, drawWidth, drawHeight);
|
||||
ctx.drawImage(
|
||||
avatarImg,
|
||||
drawX,
|
||||
drawY,
|
||||
drawWidth,
|
||||
drawHeight,
|
||||
);
|
||||
ctx.restore();
|
||||
|
||||
// 绘制装饰图片(DocCopy)- 在头像右侧
|
||||
@@ -317,7 +359,14 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
|
||||
const borderRadius = 9.66 * scale; // 设计稿圆角
|
||||
ctx.fillStyle = "#FFFFFF";
|
||||
ctx.beginPath();
|
||||
roundRect(ctx, -addonSize / 2, -addonSize / 2, addonSize, addonSize, borderRadius);
|
||||
roundRect(
|
||||
ctx,
|
||||
-addonSize / 2,
|
||||
-addonSize / 2,
|
||||
addonSize,
|
||||
addonSize,
|
||||
borderRadius,
|
||||
);
|
||||
ctx.fill();
|
||||
|
||||
// 添加渐变背景色
|
||||
@@ -334,7 +383,13 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
|
||||
const docSize = 26.18 * scale; // 设计稿内部图片尺寸
|
||||
const docRotation = -7 * (Math.PI / 180); // 内部旋转 -7 度
|
||||
ctx.rotate(docRotation);
|
||||
ctx.drawImage(docCopyImg, -docSize / 2, -docSize / 2, docSize, docSize);
|
||||
ctx.drawImage(
|
||||
docCopyImg,
|
||||
-docSize / 2,
|
||||
-docSize / 2,
|
||||
docSize,
|
||||
docSize,
|
||||
);
|
||||
ctx.restore();
|
||||
} catch (error) {
|
||||
console.error("Failed to load docCopy image:", error);
|
||||
@@ -409,7 +464,9 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
|
||||
const qrX = 276 * scale; // 设计稿二维码 x 位置
|
||||
const qrY = 523 * scale; // 设计稿二维码 y 位置
|
||||
|
||||
const bottomTextContent = options.bottomText || "长按识别二维码,快来加入,有你就有场!";
|
||||
const bottomTextContent =
|
||||
options.bottomText ||
|
||||
"长按识别二维码,快来加入,有你就有场!";
|
||||
|
||||
// 绘制底部文字 - 设计稿:fontSize: 12, fontWeight: 400, line-height: 1.5(2倍图)
|
||||
ctx.fillStyle = "rgba(0, 0, 0, 0.45)";
|
||||
@@ -458,7 +515,13 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
|
||||
const iconImg = await loadImage(canvas, shareLogoSvg);
|
||||
// 图标位置:文字顶部上方 iconSize + gap
|
||||
const iconY = textY - iconSize - iconGap;
|
||||
ctx.drawImage(iconImg, topTitleX, iconY, 235 * scale, iconSize);
|
||||
ctx.drawImage(
|
||||
iconImg,
|
||||
topTitleX,
|
||||
iconY,
|
||||
235 * scale,
|
||||
iconSize,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to load icon:", error);
|
||||
}
|
||||
@@ -468,7 +531,6 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
|
||||
ctx.fillText(lineText, textX, textY + index * lineHeight);
|
||||
});
|
||||
|
||||
|
||||
// 绘制二维码 - 设计稿位置(带白色背景、边框、阴影和圆角)
|
||||
|
||||
if (options.qrCodeUrl) {
|
||||
@@ -517,10 +579,23 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
|
||||
// 绘制二维码图片(在圆角矩形内)
|
||||
ctx.save();
|
||||
// 创建圆角裁剪区域
|
||||
roundRect(ctx, qrInnerX, qrInnerY, qrInnerSize, qrInnerSize, borderRadius - borderWidth);
|
||||
roundRect(
|
||||
ctx,
|
||||
qrInnerX,
|
||||
qrInnerY,
|
||||
qrInnerSize,
|
||||
qrInnerSize,
|
||||
borderRadius - borderWidth,
|
||||
);
|
||||
ctx.clip();
|
||||
// 绘制二维码图片
|
||||
ctx.drawImage(qrImg, qrInnerX, qrInnerY, qrInnerSize, qrInnerSize);
|
||||
ctx.drawImage(
|
||||
qrImg,
|
||||
qrInnerX,
|
||||
qrInnerY,
|
||||
qrInnerSize,
|
||||
qrInnerSize,
|
||||
);
|
||||
ctx.restore();
|
||||
|
||||
// 恢复上下文状态
|
||||
@@ -533,8 +608,8 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
|
||||
// 导出图片
|
||||
Taro.canvasToTempFilePath({
|
||||
canvas,
|
||||
fileType: 'png',
|
||||
quality: 1,
|
||||
fileType: "png",
|
||||
quality: 0.7,
|
||||
success: (res) => {
|
||||
resolve(res.tempFilePath);
|
||||
},
|
||||
@@ -556,13 +631,19 @@ const RadarChartV2 = forwardRef<RadarChartV2Ref, RadarChartV2Props>((props, ref)
|
||||
<Canvas
|
||||
type="2d"
|
||||
id="exportCanvasV2"
|
||||
style={{ position: "fixed", top: "-9999px", left: "-9999px", width: "700px", height: "1200px" }}
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: "-9999px",
|
||||
left: "-9999px",
|
||||
width: "700px",
|
||||
height: "1200px",
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
RadarChartV2.displayName = "RadarChartV2";
|
||||
|
||||
export default RadarChartV2;
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -6,7 +6,12 @@ import "./index.scss";
|
||||
import { EditModal } from "@/components";
|
||||
import { UserService, PickerOption } from "@/services/userService";
|
||||
import { PopupPicker } from "@/components/Picker/index";
|
||||
import { useUserActions, useNicknameChangeStatus, useLastTestResult } from "@/store/userStore";
|
||||
import {
|
||||
useUserActions,
|
||||
useNicknameChangeStatus,
|
||||
useLastTestResult,
|
||||
useUserInfo,
|
||||
} from "@/store/userStore";
|
||||
import { UserInfoType } from "@/services/userService";
|
||||
import {
|
||||
useCities,
|
||||
@@ -69,7 +74,6 @@ const on_edit = () => {
|
||||
// 用户信息卡片组件
|
||||
const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
|
||||
editable = true,
|
||||
user_info,
|
||||
is_current_user,
|
||||
is_following = false,
|
||||
collapseProfile,
|
||||
@@ -80,9 +84,12 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
|
||||
set_user_info,
|
||||
onTab,
|
||||
}) => {
|
||||
|
||||
const user_info = useUserInfo();
|
||||
const nickname_change_status = useNicknameChangeStatus();
|
||||
const { setShowGuideBar } = useGlobalState();
|
||||
const { updateUserInfo, updateNickname, fetchLastTestResult } = useUserActions();
|
||||
const { updateUserInfo, updateNickname, fetchLastTestResult } =
|
||||
useUserActions();
|
||||
const ntrpLevels = useNtrpLevels();
|
||||
// 使用全局状态中的测试结果,避免重复调用接口
|
||||
const lastTestResult = useLastTestResult();
|
||||
@@ -117,11 +124,15 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
|
||||
useState(false);
|
||||
|
||||
// 表单状态
|
||||
const [form_data, set_form_data] = useState<Partial<UserInfoType>>({});
|
||||
const [form_data, set_form_data] = useState<Partial<UserInfoType>>({ ...user_info });
|
||||
|
||||
useDidShow(() => {
|
||||
// useDidShow(() => {
|
||||
// set_form_data({ ...user_info });
|
||||
// });
|
||||
|
||||
useEffect(() => {
|
||||
set_form_data({ ...user_info });
|
||||
});
|
||||
}, [user_info])
|
||||
|
||||
useEffect(() => {
|
||||
const visibles = [
|
||||
@@ -129,6 +140,7 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
|
||||
location_picker_visible,
|
||||
ntrp_picker_visible,
|
||||
occupation_picker_visible,
|
||||
edit_modal_visible,
|
||||
];
|
||||
const allPickersClosed = visibles.every((item) => !item);
|
||||
// 所有选择器都关闭时,显示 GuideBar;否则隐藏
|
||||
@@ -138,6 +150,7 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
|
||||
location_picker_visible,
|
||||
ntrp_picker_visible,
|
||||
occupation_picker_visible,
|
||||
edit_modal_visible,
|
||||
]);
|
||||
|
||||
// 职业数据
|
||||
@@ -295,8 +308,8 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
|
||||
|
||||
// 处理地区选择
|
||||
const handle_location_change = (e: any) => {
|
||||
const [country, province, city] = e;
|
||||
handle_field_edit({ country, province, city });
|
||||
const [province, city, district] = e;
|
||||
handle_field_edit({ province, city, district });
|
||||
};
|
||||
|
||||
// 处理NTRP水平选择
|
||||
@@ -307,8 +320,8 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
|
||||
|
||||
// 处理职业选择
|
||||
const handle_occupation_change = (e: any) => {
|
||||
const [country, province, city] = e;
|
||||
handle_field_edit("occupation", `${country} ${province} ${city}`);
|
||||
const [firstVal, secondVal, thirdVal] = e;
|
||||
handle_field_edit("occupation", `${firstVal} ${secondVal} ${thirdVal}`);
|
||||
};
|
||||
const handle_edit_modal_cancel = () => {
|
||||
// 关闭编辑弹窗时显示 GuideBar
|
||||
@@ -365,7 +378,6 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
|
||||
urls: [url],
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="user_info_card">
|
||||
{/* 头像和基本信息 */}
|
||||
@@ -565,12 +577,12 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
|
||||
<Text>选择职业</Text>
|
||||
</View>
|
||||
) : null}
|
||||
{user_info.country || user_info.province || user_info.city ? (
|
||||
{user_info.province || user_info.city || user_info.district ? (
|
||||
<View
|
||||
className="tag_item"
|
||||
onClick={() => editable && handle_open_edit_modal("location")}
|
||||
>
|
||||
<Text className="tag_text">{`${user_info.province}${user_info.city}`}</Text>
|
||||
<Text className="tag_text">{`${user_info.city}${user_info.district}`}</Text>
|
||||
</View>
|
||||
) : is_current_user ? (
|
||||
<View
|
||||
@@ -643,16 +655,16 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
|
||||
<PopupPicker
|
||||
showHeader={true}
|
||||
title="选择性别"
|
||||
options={[
|
||||
options={
|
||||
[
|
||||
{ text: "男", value: "0" },
|
||||
{ text: "女", value: "1" },
|
||||
{ text: "保密", value: "2" },
|
||||
],
|
||||
]}
|
||||
]
|
||||
}
|
||||
visible={gender_picker_visible}
|
||||
setvisible={setGenderPickerVisible}
|
||||
value={form_data.gender === "" ? ["0"] : [form_data.gender]}
|
||||
value={!form_data.gender ? ["0"] : [form_data.gender]}
|
||||
onChange={handle_gender_change}
|
||||
/>
|
||||
)}
|
||||
@@ -665,8 +677,8 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
|
||||
visible={location_picker_visible}
|
||||
setvisible={setLocationPickerVisible}
|
||||
value={
|
||||
form_data.country
|
||||
? [form_data.country, form_data.province, form_data.city]
|
||||
form_data.province
|
||||
? [form_data.province, form_data.city, form_data.district]
|
||||
: getDefaultOption(cities)
|
||||
}
|
||||
onChange={handle_location_change}
|
||||
@@ -678,15 +690,12 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
|
||||
showHeader={true}
|
||||
title="选择 NTRP 自评水平"
|
||||
ntrpTested={ntrpTested}
|
||||
options={ntrpLevels.map((level) => ({
|
||||
text: level,
|
||||
value: level,
|
||||
}))}
|
||||
options={ntrpLevels}
|
||||
type="ntrp"
|
||||
img={user_info.avatar_url || ""}
|
||||
visible={ntrp_picker_visible}
|
||||
setvisible={setNtrpPickerVisible}
|
||||
value={[form_data.ntrp_level || "2.5"]}
|
||||
value={!form_data.ntrp_level ? ["2.5"] : [form_data.ntrp_level]}
|
||||
onChange={handle_ntrp_level_change}
|
||||
/>
|
||||
)}
|
||||
@@ -864,8 +873,7 @@ export const GameTabs: React.FC<GameTabsProps> = ({
|
||||
<Text className="tab_text">{hosted_text}</Text>
|
||||
</View>
|
||||
<View
|
||||
className={`tab_item ${
|
||||
active_tab === "participated" ? "active" : ""
|
||||
className={`tab_item ${active_tab === "participated" ? "active" : ""
|
||||
}`}
|
||||
onClick={() => on_tab_change("participated")}
|
||||
>
|
||||
|
||||
@@ -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,135 +1,61 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import Taro from "@tarojs/taro";
|
||||
|
||||
// 环境类型
|
||||
export type EnvType = 'development' | 'production'
|
||||
/**
|
||||
* 环境配置:从 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
|
||||
// 客服配置
|
||||
name: string;
|
||||
apiBaseURL: string;
|
||||
ossBaseURL: string;
|
||||
timeout: number;
|
||||
enableLog: boolean;
|
||||
enableMock: boolean;
|
||||
customerService: {
|
||||
corpId: string
|
||||
serviceUrl: string
|
||||
phoneNumber?: string
|
||||
email?: string
|
||||
}
|
||||
corpId: string;
|
||||
serviceUrl: string;
|
||||
};
|
||||
}
|
||||
|
||||
// 各环境配置
|
||||
const envConfigs: Record<EnvType, EnvConfig> = {
|
||||
|
||||
|
||||
// 开发环境
|
||||
development: {
|
||||
name: '开发环境',
|
||||
apiBaseURL: 'https://tennis.bimwe.com',
|
||||
ossBaseURL: 'https://bimwe.oss-cn-shanghai.aliyuncs.com',
|
||||
//apiBaseURL: 'http://localhost:9098',
|
||||
timeout: 15000,
|
||||
enableLog: true,
|
||||
// 从 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: 'ww51fc969e8b76af82', // 企业ID
|
||||
serviceUrl: 'https://work.weixin.qq.com/kfid/kfc64085b93243c5c91',
|
||||
|
||||
}
|
||||
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";
|
||||
|
||||
|
||||
|
||||
// 生产环境1
|
||||
production: {
|
||||
name: '生产环境1',
|
||||
apiBaseURL: 'https://tennis.bimwe.com',
|
||||
ossBaseURL: 'https://bimwe.oss-cn-shanghai.aliyuncs.com',
|
||||
timeout: 10000,
|
||||
enableLog: false,
|
||||
enableMock: false,
|
||||
// 客服配置
|
||||
customerService: {
|
||||
corpId: 'ww51fc969e8b76af82', // 企业ID
|
||||
serviceUrl: 'https://work.weixin.qq.com/kfid/kfc64085b93243c5c91',
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
//生产环境2
|
||||
// production: {
|
||||
// name: '生产环境2',
|
||||
// apiBaseURL: 'https://youchang.qiongjingtiyu.com',
|
||||
// ossBaseURL: 'https://youchang2026.oss-cn-shanghai.aliyuncs.com',
|
||||
// timeout: 10000,
|
||||
// enableLog: false,
|
||||
// enableMock: false,
|
||||
// // 客服配置
|
||||
// customerService: {
|
||||
// corpId: 'ww9a2d9a5d9410c664', // 企业ID
|
||||
// serviceUrl: 'https://work.weixin.qq.com/kfid/kfcd355e162e0390684',
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
// 获取当前环境
|
||||
export const getCurrentEnv = (): EnvType => {
|
||||
// 在小程序环境中,使用默认逻辑判断环境
|
||||
// 可以根据实际需要配置不同的判断逻辑
|
||||
|
||||
// 可以根据实际部署情况添加更多判断逻辑
|
||||
// 比如通过 Taro.getEnv() 获取当前平台环境
|
||||
|
||||
const isProd = process.env.NODE_ENV === 'production'
|
||||
if (isProd) {
|
||||
return 'production'
|
||||
} else {
|
||||
return 'development'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前环境配置
|
||||
export const getCurrentConfig = (): EnvConfig => {
|
||||
const env = getCurrentEnv()
|
||||
return envConfigs[env]
|
||||
}
|
||||
|
||||
// 获取指定环境配置
|
||||
export const getEnvConfig = (env: EnvType): EnvConfig => {
|
||||
return envConfigs[env]
|
||||
}
|
||||
|
||||
// 是否为开发环境
|
||||
export const isDevelopment = (): boolean => {
|
||||
return getCurrentEnv() === 'development'
|
||||
}
|
||||
|
||||
// 是否为生产环境
|
||||
export const isProduction = (): boolean => {
|
||||
return getCurrentEnv() === 'production'
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 环境配置调试信息
|
||||
export const getEnvInfo = () => {
|
||||
const config = getCurrentConfig()
|
||||
const config = getCurrentConfig();
|
||||
return {
|
||||
env: getCurrentEnv(),
|
||||
config,
|
||||
taroEnv: Taro.getEnv(),
|
||||
platform: Taro.getEnv() === Taro.ENV_TYPE.WEAPP ? '微信小程序' :
|
||||
Taro.getEnv() === Taro.ENV_TYPE.WEB ? 'Web' :
|
||||
Taro.getEnv() === Taro.ENV_TYPE.RN ? 'React Native' : '未知'
|
||||
}
|
||||
}
|
||||
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()
|
||||
export default getCurrentConfig();
|
||||
|
||||
@@ -1,74 +1,76 @@
|
||||
import { OSS_BASE } from "@/config/api";
|
||||
export default {
|
||||
ICON_REMOVE: require('@/static/publishBall/icon-remove.svg'),
|
||||
ICON_UPLOAD: require('@/static/publishBall/icon-upload.svg'),
|
||||
ICON_LOCATION: require('@/static/publishBall/icon-location.svg'),
|
||||
ICON_GAMEPLAY: require('@/static/publishBall/icon-gameplay.svg'),
|
||||
ICON_PERSONAL: require('@/static/publishBall/icon-personal.svg'),
|
||||
ICON_CHANGDA: require('@/static/publishBall/icon-changda.svg'),
|
||||
ICON_COST: require('@/static/publishBall/icon-cost.svg'),
|
||||
ICON_TIPS: require('@/static/publishBall/icon-tips.svg'),
|
||||
ICON_ARROW_RIGHT: require('@/static/publishBall/icon-arrow-right.svg'),
|
||||
ICON_FILTER: require('@/static/list/icon-filter.svg'),
|
||||
ICON_FILTER_SELECTED: require('@/static/list/icon-filter-selected.svg'),
|
||||
ICON_SEARCH: require('@/static/list/icon-search.svg'),
|
||||
ICON_PLAY: require('@/static/list/icon-play.svg'),
|
||||
ICON_SITE: require('@/static/list/icon-site.svg'),
|
||||
ICON_ARROW_DOWN: require('@/static/list/icon-arrow-down.svg'),
|
||||
ICON_MENU_ITEM_SELECTED: require('@/static/list/icon-menu-item-selected.svg'),
|
||||
ICON_ARROW_DOWN_WHITE: require('@/static/list/icon-arrow-down-white.svg'),
|
||||
ICON_LIST_RIGHT_ARROW: require('@/static/list/icon-list-right-arrow.svg'),
|
||||
ICON_ARROW_LEFT: require('@/static/detail/icon-arrow-left.svg'),
|
||||
ICON_LOGO_GO: require('@/static/detail/icon-logo-go.svg'),
|
||||
ICON_MAP: require('@/static/publishBall/icon-map.svg'),
|
||||
ICON_STADIUM: require('@/static/publishBall/icon-stadium.svg'),
|
||||
ICON_ARRORW_SMALL: require('@/static/publishBall/icon-arrow-small.svg'),
|
||||
ICON_MAP_SEARCH: require('@/static/publishBall/icon-map-search.svg'),
|
||||
ICON_HEART_CIRCLE: require('@/static/publishBall/icon-heartcircle.png'),
|
||||
ICON_ADD: require('@/static/publishBall/icon-add.svg'),
|
||||
ICON_COPY: require('@/static/publishBall/icon-arrow-right.svg'),
|
||||
ICON_DELETE: require('@/static/publishBall/icon-delete.svg'),
|
||||
ICON_RIGHT_MAX: require('@/static/publishBall/icon-right-max.svg'),
|
||||
ICON_PLUS: require('@/static/publishBall/icon-plus.svg'),
|
||||
ICON_GROUP: require('@/static/publishBall/icon-group.svg'),
|
||||
ICON_PERSON: require('@/static/publishBall/icon-person.svg'),
|
||||
ICON_PUBLISH: require('@/static/publishBall/icon-publish.png'),
|
||||
ICON_CIRCLE_UNSELECT: require('@/static/publishBall/icon-circle-unselect.svg'),
|
||||
ICON_CIRCLE_SELECT: require('@/static/publishBall/icon-circle-select-ring.svg'),
|
||||
ICON_CIRCLE_SELECT_ARROW: require('@/static/publishBall/icon-circle-select-arrow.svg'),
|
||||
ICON_LOGO: require('@/static/logo.svg'),
|
||||
ICON_CHANGE: require('@/static/list/icon-change.svg'),
|
||||
ICON_DETAIL_MAP: require('@/static/detail/icon-map.svg'),
|
||||
ICON_DETAIL_ARROW_RIGHT: require('@/static/detail/icon-arrow-right.svg'),
|
||||
ICON_DETAIL_NOTICE: require('@/static/detail/icon-notice.svg'),
|
||||
ICON_DETAIL_APPLICATION_ADD: require('@/static/detail/icon-application-add.svg'),
|
||||
ICON_DETAIL_COMMENT: require('@/static/detail/icon-comment.svg'),
|
||||
ICON_DETAIL_COMMENT_LIGHT: require('@/static/detail/icon-comment-light.svg'),
|
||||
ICON_DETAIL_SHARE: require('@/static/detail/icon-share-light.svg'),
|
||||
ICON_GUIDE_BAR_PUBLISH: require('@/static/common/guide-bar-publish.svg'),
|
||||
ICON_NAVIGATOR_BACK: require('@/static/common/navigator-back.svg'),
|
||||
ICON_LIST_PLAYING_GAME: require('@/static/list/icon-paying-game.svg'),
|
||||
ICON_LIST_LOAD_ERROR: require('@/static/list/icon-load-error.svg'),
|
||||
ICON_LIST_RELOAD: require('@/static/list/icon-reload.svg'),
|
||||
ICON_LIST_EMPTY: require('@/static/emptyStatus/publish-empty.png'),
|
||||
ICON_LIST_EMPTY_CARD: require('@/static/emptyStatus/publish-empty-card.png'),
|
||||
ICON_LIST_SEARCH_SEARCH: require('@/static/search/icon-search.svg'),
|
||||
ICON_LIST_SEARCH_BACK: require('@/static/search/icon-back.svg'),
|
||||
ICON_LIST_SEARCH_CLEAR: require('@/static/search/icon-search-clear.svg'),
|
||||
ICON_LIST_SEARCH_CLEAR_HISTORY: require('@/static/search/icon-clear-history.svg'),
|
||||
ICON_LIST_SEARCH_SUGGESTION: require('@/static/search/icon-search-suggestion.svg'),
|
||||
ICON_LIST_INPUT_LOGO: require('@/static/list/icon-input-logo.svg'),
|
||||
ICON_IMPORTANT_BTN: require('@/static/publishBall/icon-important-btn.svg'),
|
||||
ICON_IMPORTANT_BLACK: require('@/static/publishBall/icon-important-black.svg'),
|
||||
ICON_ARROW_RIGHT_WHITE: require('@/static/publishBall/icon-arrow-right-white.svg'),
|
||||
ICON_ARROW_RIGHT_BLACK: require('@/static/publishBall/icon-arrow-right-black.svg'),
|
||||
ICON_EXAMINATION: require('@/static/userInfo/examination.svg'),
|
||||
ICON_ARROW_GREEN: require('@/static/userInfo/arrow-green.svg'),
|
||||
ICON_COPY: require('@/static/publishBall/icon-copy.svg'),
|
||||
ICON_UPLOAD_IMG: require('@/static/publishBall/icon-upload-img.svg'),
|
||||
ICON_UPLOAD_SUCCESS: require('@/static/publishBall/icon-upload-success.svg'),
|
||||
ICON_CLOSE: require('@/static/publishBall/icon-close.svg'),
|
||||
ICON_LIST_NTPR: require('@/static/list/ntpr.svg'),
|
||||
ICON_LIST_CHANGDA: require('@/static/list/icon-changda.svg'),
|
||||
ICON_LIST_CHANGDA_QIuju: require('@/static/list/changdaqiuju.png'),
|
||||
ICON_RELOCATE: require('@/static/list/icon-relocate.svg'),
|
||||
}
|
||||
ICON_REMOVE: require("@/static/publishBall/icon-remove.svg"),
|
||||
ICON_UPLOAD: require("@/static/publishBall/icon-upload.svg"),
|
||||
ICON_LOCATION: require("@/static/publishBall/icon-location.svg"),
|
||||
ICON_GAMEPLAY: require("@/static/publishBall/icon-gameplay.svg"),
|
||||
ICON_PERSONAL: require("@/static/publishBall/icon-personal.svg"),
|
||||
ICON_CHANGDA: require("@/static/publishBall/icon-changda.svg"),
|
||||
ICON_COST: require("@/static/publishBall/icon-cost.svg"),
|
||||
ICON_TIPS: require("@/static/publishBall/icon-tips.svg"),
|
||||
ICON_ARROW_RIGHT: require("@/static/publishBall/icon-arrow-right.svg"),
|
||||
ICON_FILTER: require("@/static/list/icon-filter.svg"),
|
||||
ICON_FILTER_SELECTED: require("@/static/list/icon-filter-selected.svg"),
|
||||
ICON_SEARCH: require("@/static/list/icon-search.svg"),
|
||||
ICON_PLAY: require("@/static/list/icon-play.svg"),
|
||||
ICON_SITE: require("@/static/list/icon-site.svg"),
|
||||
ICON_ARROW_DOWN: require("@/static/list/icon-arrow-down.svg"),
|
||||
ICON_MENU_ITEM_SELECTED: require("@/static/list/icon-menu-item-selected.svg"),
|
||||
ICON_ARROW_DOWN_WHITE: require("@/static/list/icon-arrow-down-white.svg"),
|
||||
ICON_LIST_RIGHT_ARROW: require("@/static/list/icon-list-right-arrow.svg"),
|
||||
ICON_ARROW_LEFT: require("@/static/detail/icon-arrow-left.svg"),
|
||||
ICON_LOGO_GO: require("@/static/detail/icon-logo-go.svg"),
|
||||
ICON_MAP: require("@/static/publishBall/icon-map.svg"),
|
||||
ICON_STADIUM: require("@/static/publishBall/icon-stadium.svg"),
|
||||
ICON_ARRORW_SMALL: require("@/static/publishBall/icon-arrow-small.svg"),
|
||||
ICON_MAP_SEARCH: require("@/static/publishBall/icon-map-search.svg"),
|
||||
ICON_HEART_CIRCLE: require("@/static/publishBall/icon-heartcircle.png"),
|
||||
ICON_ADD: require("@/static/publishBall/icon-add.svg"),
|
||||
ICON_COPY: require("@/static/publishBall/icon-arrow-right.svg"),
|
||||
ICON_DELETE: require("@/static/publishBall/icon-delete.svg"),
|
||||
ICON_RIGHT_MAX: require("@/static/publishBall/icon-right-max.svg"),
|
||||
ICON_PLUS: require("@/static/publishBall/icon-plus.svg"),
|
||||
ICON_GROUP: require("@/static/publishBall/icon-group.svg"),
|
||||
ICON_PERSON: require("@/static/publishBall/icon-person.svg"),
|
||||
ICON_PUBLISH: require("@/static/publishBall/icon-publish.png"),
|
||||
ICON_CIRCLE_UNSELECT: require("@/static/publishBall/icon-circle-unselect.svg"),
|
||||
ICON_CIRCLE_SELECT: require("@/static/publishBall/icon-circle-select-ring.svg"),
|
||||
ICON_CIRCLE_SELECT_ARROW: require("@/static/publishBall/icon-circle-select-arrow.svg"),
|
||||
ICON_LOGO: require("@/static/logo.svg"),
|
||||
ICON_CHANGE: require("@/static/list/icon-change.svg"),
|
||||
ICON_DETAIL_MAP: require("@/static/detail/icon-map.svg"),
|
||||
ICON_DETAIL_ARROW_RIGHT: require("@/static/detail/icon-arrow-right.svg"),
|
||||
ICON_DETAIL_NOTICE: require("@/static/detail/icon-notice.svg"),
|
||||
ICON_DETAIL_APPLICATION_ADD: require("@/static/detail/icon-application-add.svg"),
|
||||
ICON_DETAIL_COMMENT: require("@/static/detail/icon-comment.svg"),
|
||||
ICON_DETAIL_COMMENT_LIGHT: require("@/static/detail/icon-comment-light.svg"),
|
||||
ICON_DETAIL_SHARE: require("@/static/detail/icon-share-light.svg"),
|
||||
ICON_GUIDE_BAR_PUBLISH: require("@/static/common/guide-bar-publish.svg"),
|
||||
ICON_NAVIGATOR_BACK: require("@/static/common/navigator-back.svg"),
|
||||
ICON_LIST_PLAYING_GAME: require("@/static/list/icon-paying-game.svg"),
|
||||
ICON_LIST_LOAD_ERROR: require("@/static/list/icon-load-error.svg"),
|
||||
ICON_LIST_RELOAD: require("@/static/list/icon-reload.svg"),
|
||||
ICON_LIST_EMPTY: require("@/static/emptyStatus/publish-empty.png"),
|
||||
ICON_LIST_EMPTY_CARD: `${OSS_BASE}/front/ball/images/publish-empty-card.svg`,
|
||||
ICON_LIST_SEARCH_SEARCH: require("@/static/search/icon-search.svg"),
|
||||
ICON_LIST_SEARCH_BACK: require("@/static/search/icon-back.svg"),
|
||||
ICON_LIST_SEARCH_CLEAR: require("@/static/search/icon-search-clear.svg"),
|
||||
ICON_LIST_SEARCH_CLEAR_HISTORY: require("@/static/search/icon-clear-history.svg"),
|
||||
ICON_LIST_SEARCH_SUGGESTION: require("@/static/search/icon-search-suggestion.svg"),
|
||||
ICON_LIST_INPUT_LOGO: require("@/static/list/icon-input-logo.svg"),
|
||||
ICON_IMPORTANT_BTN: require("@/static/publishBall/icon-important-btn.svg"),
|
||||
ICON_IMPORTANT_BLACK: require("@/static/publishBall/icon-important-black.svg"),
|
||||
ICON_ARROW_RIGHT_WHITE: require("@/static/publishBall/icon-arrow-right-white.svg"),
|
||||
ICON_ARROW_RIGHT_BLACK: require("@/static/publishBall/icon-arrow-right-black.svg"),
|
||||
ICON_EXAMINATION: require("@/static/userInfo/examination.svg"),
|
||||
ICON_ARROW_GREEN: require("@/static/userInfo/arrow-green.svg"),
|
||||
ICON_COPY: require("@/static/publishBall/icon-copy.svg"),
|
||||
ICON_UPLOAD_IMG: require("@/static/publishBall/icon-upload-img.svg"),
|
||||
ICON_UPLOAD_SUCCESS: require("@/static/publishBall/icon-upload-success.svg"),
|
||||
ICON_CLOSE: require("@/static/publishBall/icon-close.svg"),
|
||||
ICON_LIST_NTPR: require("@/static/list/ntpr.svg"),
|
||||
ICON_LIST_CHANGDA: require("@/static/list/icon-changda.svg"),
|
||||
ICON_LIST_CHANGDA_QIuju: require("@/static/list/changdaqiuju.png"),
|
||||
ICON_RELOCATE: require("@/static/list/icon-relocate.svg"),
|
||||
ICON_GAME_PLAY: require("@/static/list/icon_game_type.svg"),
|
||||
};
|
||||
|
||||
@@ -132,40 +132,39 @@ const ListContainer = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
// 插入 banner 卡片
|
||||
// showNumber 为 0 表示尚未同步,不参与截断;截断时只限制「数据条数」,插卡不占数据条数
|
||||
const shouldLimitByShowNumber = showNumber > 0;
|
||||
|
||||
// 插入 banner 卡片(在 bannerListIndex 位置插入,不替换数据)
|
||||
function insertBannerCard(list) {
|
||||
if (!bannerListImage) return list;
|
||||
if (!list || !Array.isArray(list)) return list ?? [];
|
||||
if (!list || !Array.isArray(list)) {
|
||||
list = [];
|
||||
}
|
||||
const idx = Number(bannerListIndex);
|
||||
return [
|
||||
...list.slice(0, Number(bannerListIndex)),
|
||||
...list.slice(0, idx),
|
||||
{ type: "banner", banner_image_url: bannerListImage, banner_detail_url: bannerDetailImage },
|
||||
...list.slice(Number(bannerListIndex))
|
||||
...list.slice(idx),
|
||||
];
|
||||
}
|
||||
|
||||
// 对于没有ntrp等级的用户每个月展示一次, 插在第二个位置后面
|
||||
// insertBannerCard 需在最后统一执行,否则前面分支直接 return 时 banner 不会被插入
|
||||
// 对于没有 ntrp 等级的用户每个月展示一次,插在第 2 条数据后面;插卡是插入不替换,保留全部 showNumber 条数据
|
||||
function insertEvaluateCard(list) {
|
||||
let result: any[];
|
||||
if (!list || !Array.isArray(list)) return insertBannerCard(list ?? []);
|
||||
|
||||
if (!evaluateFlag) {
|
||||
result = showNumber !== undefined ? list.slice(0, showNumber) : list;
|
||||
} else if (!list || list.length === 0) {
|
||||
result = list;
|
||||
} else if (hasTestInLastMonth) {
|
||||
result = showNumber !== undefined ? list.slice(0, showNumber) : list;
|
||||
} else if (list.length <= 2) {
|
||||
result = [...list, { type: "evaluateCard" }];
|
||||
} else {
|
||||
const [item1, item2, ...rest] = list;
|
||||
result = [
|
||||
item1,
|
||||
item2,
|
||||
{ type: "evaluateCard" },
|
||||
...(showNumber !== undefined ? rest.slice(0, showNumber - 3) : rest),
|
||||
];
|
||||
const limitedList = shouldLimitByShowNumber ? list.slice(0, showNumber) : list;
|
||||
|
||||
if (!evaluateFlag || hasTestInLastMonth) {
|
||||
return insertBannerCard(limitedList);
|
||||
}
|
||||
|
||||
if (limitedList.length <= 2) {
|
||||
return insertBannerCard([...limitedList, { type: "evaluateCard" }]);
|
||||
}
|
||||
|
||||
const [item1, item2, ...rest] = limitedList;
|
||||
const result = [item1, item2, { type: "evaluateCard" }, ...rest];
|
||||
return insertBannerCard(result);
|
||||
}
|
||||
|
||||
@@ -204,10 +203,12 @@ const ListContainer = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const showNoData = isShowNoData && !loading && memoizedList?.length === 0;
|
||||
|
||||
// 渲染列表
|
||||
const renderList = () => {
|
||||
// 请求数据为空
|
||||
if (isShowNoData) {
|
||||
if (showNoData) {
|
||||
return (
|
||||
<ListLoadError
|
||||
reload={reload}
|
||||
|
||||
@@ -40,7 +40,7 @@ function isFull(counts) {
|
||||
function matchNtrpRequestment(
|
||||
target?: string,
|
||||
min?: string,
|
||||
max?: string
|
||||
max?: string,
|
||||
): boolean {
|
||||
// 目标值为空或 undefined
|
||||
if (!target?.trim()) return true;
|
||||
@@ -110,7 +110,7 @@ export default function Participants(props) {
|
||||
user_action_status;
|
||||
const showApplicationEntry =
|
||||
[can_pay, can_substitute, is_substituting, waiting_start].every(
|
||||
(item) => !item
|
||||
(item) => !item,
|
||||
) &&
|
||||
can_join &&
|
||||
dayjs(start_time).isAfter(dayjs());
|
||||
@@ -138,7 +138,7 @@ export default function Participants(props) {
|
||||
|
||||
Taro.navigateTo({
|
||||
url: `/login_pages/index/index?redirect=${encodeURIComponent(
|
||||
fullPath
|
||||
fullPath,
|
||||
)}`,
|
||||
});
|
||||
}
|
||||
@@ -153,7 +153,7 @@ export default function Participants(props) {
|
||||
const matchNtrpReq = matchNtrpRequestment(
|
||||
userInfo?.ntrp_level,
|
||||
skill_level_min,
|
||||
skill_level_max
|
||||
skill_level_max,
|
||||
);
|
||||
|
||||
function handleSelfEvaluate() {
|
||||
@@ -180,7 +180,7 @@ export default function Participants(props) {
|
||||
}
|
||||
|
||||
function generateTextAndAction(
|
||||
user_action_status: null | { [key: string]: boolean }
|
||||
user_action_status: null | { [key: string]: boolean },
|
||||
):
|
||||
| undefined
|
||||
| { text: string | React.FC; action?: () => void; available?: boolean } {
|
||||
@@ -259,7 +259,7 @@ export default function Participants(props) {
|
||||
const res = await OrderService.getUnpaidOrder(id);
|
||||
if (res.code === 0) {
|
||||
navto(
|
||||
`/order_pages/orderDetail/index?id=${res.data.order_info.order_id}`
|
||||
`/order_pages/orderDetail/index?id=${res.data.order_info.order_id}`,
|
||||
);
|
||||
}
|
||||
}),
|
||||
@@ -296,10 +296,11 @@ export default function Participants(props) {
|
||||
const { action = () => {} } = generateTextAndAction(user_action_status)!;
|
||||
|
||||
const leftCount = max_participants - participant_count;
|
||||
const leftSubstituteCount = (max_substitute_players || 0) - (substitute_count || 0);
|
||||
const leftSubstituteCount =
|
||||
(max_substitute_players || 0) - (substitute_count || 0);
|
||||
const showSubstituteApplicationEntry =
|
||||
[can_pay, can_join, is_substituting, waiting_start].every(
|
||||
(item) => !item
|
||||
(item) => !item,
|
||||
) &&
|
||||
can_substitute &&
|
||||
dayjs(start_time).isAfter(dayjs());
|
||||
@@ -336,7 +337,7 @@ export default function Participants(props) {
|
||||
refresherBackground="#FAFAFA"
|
||||
className={classnames(
|
||||
styles["participants-list-scroll"],
|
||||
showApplicationEntry ? styles.withApplication : ""
|
||||
showApplicationEntry ? styles.withApplication : "",
|
||||
)}
|
||||
scrollX
|
||||
>
|
||||
@@ -377,14 +378,14 @@ export default function Participants(props) {
|
||||
src={avatar_url}
|
||||
onClick={handleViewUserInfo.bind(
|
||||
null,
|
||||
participant_user_id
|
||||
participant_user_id,
|
||||
)}
|
||||
/>
|
||||
<Text className={styles["participants-list-item-name"]}>
|
||||
{nickname || "未知"}
|
||||
</Text>
|
||||
<Text className={styles["participants-list-item-level"]}>
|
||||
{displayNtrp}
|
||||
NTRP {displayNtrp}
|
||||
</Text>
|
||||
<Text className={styles["participants-list-item-role"]}>
|
||||
{role}
|
||||
@@ -400,12 +401,17 @@ export default function Participants(props) {
|
||||
)}
|
||||
</View>
|
||||
{/* 候补区域 */}
|
||||
{max_substitute_players > 0 && (substitute_count > 0 || showSubstituteApplicationEntry) && (
|
||||
{max_substitute_players > 0 &&
|
||||
(substitute_count > 0 || showSubstituteApplicationEntry) && (
|
||||
<View className={styles["detail-page-content-participants"]}>
|
||||
<View className={styles["participants-title"]}>
|
||||
<Text>候补</Text>
|
||||
<Text>·</Text>
|
||||
<Text>{leftSubstituteCount > 0 ? `剩余空位 ${leftSubstituteCount}` : "已满员"}</Text>
|
||||
<Text>
|
||||
{leftSubstituteCount > 0
|
||||
? `剩余空位 ${leftSubstituteCount}`
|
||||
: "已满员"}
|
||||
</Text>
|
||||
</View>
|
||||
<View className={styles["participants-list"]}>
|
||||
{/* 候补申请入口 */}
|
||||
@@ -420,7 +426,9 @@ export default function Participants(props) {
|
||||
className={styles["participants-list-application-icon"]}
|
||||
src={img.ICON_DETAIL_APPLICATION_ADD}
|
||||
/>
|
||||
<Text className={styles["participants-list-application-text"]}>
|
||||
<Text
|
||||
className={styles["participants-list-application-text"]}
|
||||
>
|
||||
申请候补
|
||||
</Text>
|
||||
</View>
|
||||
@@ -430,7 +438,7 @@ export default function Participants(props) {
|
||||
refresherBackground="#FAFAFA"
|
||||
className={classnames(
|
||||
styles["participants-list-scroll"],
|
||||
showSubstituteApplicationEntry ? styles.withApplication : ""
|
||||
showSubstituteApplicationEntry ? styles.withApplication : "",
|
||||
)}
|
||||
scrollX
|
||||
>
|
||||
@@ -438,7 +446,8 @@ export default function Participants(props) {
|
||||
className={styles["participants-list-scroll-content"]}
|
||||
style={{
|
||||
width: `${
|
||||
Math.max(substitute_members.length, 1) * 103 + (Math.max(substitute_members.length, 1) - 1) * 8
|
||||
Math.max(substitute_members.length, 1) * 103 +
|
||||
(Math.max(substitute_members.length, 1) - 1) * 8
|
||||
}px`,
|
||||
}}
|
||||
>
|
||||
@@ -471,13 +480,15 @@ export default function Participants(props) {
|
||||
src={avatar_url}
|
||||
onClick={handleViewUserInfo.bind(
|
||||
null,
|
||||
substitute_user_id
|
||||
substitute_user_id,
|
||||
)}
|
||||
/>
|
||||
<Text className={styles["participants-list-item-name"]}>
|
||||
{nickname || "未知"}
|
||||
</Text>
|
||||
<Text className={styles["participants-list-item-level"]}>
|
||||
<Text
|
||||
className={styles["participants-list-item-level"]}
|
||||
>
|
||||
{displayNtrp}
|
||||
</Text>
|
||||
<Text className={styles["participants-list-item-role"]}>
|
||||
|
||||
@@ -25,9 +25,10 @@ dayjs.locale("zh-cn");
|
||||
// 分享弹窗
|
||||
export default forwardRef(({ id, from, detail, userInfo }, ref) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [publishFlag, setPublishFlag] = useState(false);
|
||||
const [shareImageUrl, setShareImageUrl] = useState("");
|
||||
const { fetchUserInfo } = useUserActions();
|
||||
|
||||
const publishFlag = from === "publish";
|
||||
// const posterRef = useRef();
|
||||
const { max_participants, participant_count } = detail || {};
|
||||
|
||||
@@ -57,18 +58,20 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
show: async (publish_flag = false) => {
|
||||
setPublishFlag(publish_flag);
|
||||
if (publish_flag) {
|
||||
try {
|
||||
const url = await generateShareImageUrl();
|
||||
setShareImageUrl(url);
|
||||
} catch (e) {}
|
||||
}
|
||||
show: async () => {
|
||||
setVisible(true);
|
||||
},
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (from === "publish") {
|
||||
generateShareImageUrl().then((url) => {
|
||||
setShareImageUrl(url);
|
||||
setVisible(true);
|
||||
});
|
||||
}
|
||||
}, [from]);
|
||||
|
||||
async function generateShareImageUrl() {
|
||||
const {
|
||||
play_type,
|
||||
@@ -106,7 +109,7 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
|
||||
// console.log(res, "res");
|
||||
return {
|
||||
title: detail.title,
|
||||
imageUrl: url || "https://img.yzcdn.cn/vant/cat.jpeg",
|
||||
imageUrl: url,
|
||||
path: `/game_pages/detail/index?id=${id}&from=share`,
|
||||
};
|
||||
});
|
||||
@@ -142,6 +145,7 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
|
||||
const qrCodeUrl = qrCodeUrlRes.data.ossPath;
|
||||
await delay(100);
|
||||
// Taro.showLoading({ title: "生成中..." });
|
||||
console.log('url', qrCodeUrl)
|
||||
const url = await generatePosterImage({
|
||||
playType: play_type,
|
||||
ntrp: `NTRP ${genNTRPRequirementText(skill_level_min, skill_level_max)}`,
|
||||
@@ -157,6 +161,8 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
|
||||
time: `${startTime.format("ah")}点 ${gameLength}`,
|
||||
qrCodeUrl,
|
||||
});
|
||||
|
||||
console.log('urlend', url)
|
||||
// Taro.hideLoading();
|
||||
Taro.showShareImageMenu({
|
||||
path: url,
|
||||
@@ -183,7 +189,6 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
|
||||
|
||||
function onClose() {
|
||||
setVisible(false);
|
||||
setPublishFlag(false);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -54,12 +54,6 @@ function Index() {
|
||||
await waitForAuthInit();
|
||||
// 然后再获取用户信息
|
||||
await fetchUserInfo();
|
||||
|
||||
await delay(1000);
|
||||
|
||||
if (from === "publish") {
|
||||
handleShare(true);
|
||||
}
|
||||
};
|
||||
init();
|
||||
}, []);
|
||||
@@ -126,8 +120,12 @@ function Index() {
|
||||
}
|
||||
}
|
||||
|
||||
function handleShare(flag = false) {
|
||||
sharePopupRef.current.show(flag);
|
||||
function handleShare() {
|
||||
if (!detail.id) {
|
||||
toast("球局未加载完成,请稍后再试");
|
||||
return false;
|
||||
}
|
||||
sharePopupRef.current.show();
|
||||
}
|
||||
|
||||
const handleJoinGame = async () => {
|
||||
@@ -293,6 +291,7 @@ function Index() {
|
||||
currentUserInfo={myInfo}
|
||||
/>
|
||||
{/* share popup */}
|
||||
{detail.id && myInfo.id && (
|
||||
<SharePopup
|
||||
ref={sharePopupRef}
|
||||
id={id as string}
|
||||
@@ -300,6 +299,7 @@ function Index() {
|
||||
detail={detail}
|
||||
userInfo={myInfo}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState } from "react";
|
||||
import { View, Text, Button, Image } from "@tarojs/components";
|
||||
import Taro, { useRouter } from "@tarojs/taro";
|
||||
import { GeneralNavbar } from "@/components";
|
||||
import {
|
||||
wechat_auth_login,
|
||||
save_login_state,
|
||||
@@ -155,6 +156,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">
|
||||
@@ -166,6 +172,8 @@ const LoginPage: React.FC = () => {
|
||||
<View className="bg_overlay"></View>
|
||||
</View>
|
||||
|
||||
<GeneralNavbar title="" showBack={true} showAvatar={false} onBack={handle_return_home} />
|
||||
|
||||
{/* 主要内容 */}
|
||||
<View className="login_main_content">
|
||||
{/* 品牌区域 */}
|
||||
@@ -211,6 +219,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}>
|
||||
|
||||
@@ -16,7 +16,9 @@ interface MyselfPageContentProps {
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
const MyselfPageContent: React.FC<MyselfPageContentProps> = ({ isActive = true }) => {
|
||||
const MyselfPageContent: React.FC<MyselfPageContentProps> = ({
|
||||
isActive = true,
|
||||
}) => {
|
||||
const pickerOption = usePickerOption();
|
||||
const { statusNavbarHeightInfo } = useGlobalState() || {};
|
||||
const { totalHeight = 98 } = statusNavbarHeightInfo || {};
|
||||
@@ -65,20 +67,22 @@ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({ isActive = true }
|
||||
game_records: TennisMatch[]
|
||||
): { notEndGames: TennisMatch[]; finishedGames: TennisMatch[] } => {
|
||||
const now = new Date().getTime();
|
||||
return game_records.reduce(
|
||||
(result, cur) => {
|
||||
let { end_time } = cur;
|
||||
end_time = end_time.replace(/\s/, "T");
|
||||
new Date(end_time).getTime() > now
|
||||
? result.notEndGames.push(cur)
|
||||
: result.finishedGames.push(cur);
|
||||
return result;
|
||||
},
|
||||
{
|
||||
notEndGames: [] as TennisMatch[],
|
||||
finishedGames: [] as TennisMatch[],
|
||||
|
||||
// 使用for
|
||||
const notEndGames: TennisMatch[] = [];
|
||||
const finishedGames: TennisMatch[] = [];
|
||||
for (const game of game_records) {
|
||||
const { end_time } = game;
|
||||
const end_time_str = end_time.replace(/\s/, "T");
|
||||
new Date(end_time_str).getTime() > now
|
||||
? notEndGames.push(game)
|
||||
: finishedGames.push(game);
|
||||
}
|
||||
);
|
||||
|
||||
console.log("notEndGames", notEndGames);
|
||||
|
||||
return { notEndGames, finishedGames };
|
||||
|
||||
},
|
||||
[]
|
||||
);
|
||||
@@ -95,6 +99,8 @@ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({ isActive = true }
|
||||
} else {
|
||||
games_data = await UserService.get_participated_games(user_info.id);
|
||||
}
|
||||
|
||||
|
||||
const sorted_games = games_data.sort((a, b) => {
|
||||
return (
|
||||
new Date(a.original_start_time.replace(/\s/, "T")).getTime() -
|
||||
@@ -102,6 +108,8 @@ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({ isActive = true }
|
||||
);
|
||||
});
|
||||
const { notEndGames, finishedGames } = classifyGameRecords(sorted_games);
|
||||
console.log("notEndGames", notEndGames);
|
||||
|
||||
set_game_records(notEndGames);
|
||||
setEndedGameRecords(finishedGames);
|
||||
} catch (error) {
|
||||
@@ -250,16 +258,14 @@ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({ isActive = true }
|
||||
<View className={styles.gameTabsSection}>
|
||||
<View className={styles.tabContainer}>
|
||||
<View
|
||||
className={`${styles.tabItem} ${
|
||||
active_tab === "hosted" ? styles.active : ""
|
||||
className={`${styles.tabItem} ${active_tab === "hosted" ? styles.active : ""
|
||||
}`}
|
||||
onClick={() => setActiveTab("hosted")}
|
||||
>
|
||||
<Text className={styles.tabText}>我主办的</Text>
|
||||
</View>
|
||||
<View
|
||||
className={`${styles.tabItem} ${
|
||||
active_tab === "participated" ? styles.active : ""
|
||||
className={`${styles.tabItem} ${active_tab === "participated" ? styles.active : ""
|
||||
}`}
|
||||
onClick={() => setActiveTab("participated")}
|
||||
>
|
||||
@@ -288,9 +294,8 @@ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({ isActive = true }
|
||||
overflow: "hidden",
|
||||
}}
|
||||
listLoadErrorWrapperHeight="fit-content"
|
||||
listLoadErrorWidth="320px"
|
||||
listLoadErrorHeight="152px"
|
||||
listLoadErrorScale="1.2"
|
||||
listLoadErrorWidth="410px"
|
||||
listLoadErrorHeight="185px"
|
||||
defaultShowNum={3}
|
||||
/>
|
||||
</ScrollView>
|
||||
@@ -312,9 +317,8 @@ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({ isActive = true }
|
||||
collapse={true}
|
||||
style={{ paddingBottom: "90px", overflow: "hidden" }}
|
||||
listLoadErrorWrapperHeight="fit-content"
|
||||
listLoadErrorWidth="320px"
|
||||
listLoadErrorHeight="152px"
|
||||
listLoadErrorScale="1.2"
|
||||
listLoadErrorWidth="410px"
|
||||
listLoadErrorHeight="185px"
|
||||
defaultShowNum={3}
|
||||
/>
|
||||
</ScrollView>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '首页',
|
||||
navigationStyle: 'custom',
|
||||
navigationBarBackgroundColor: '#FAFAFA'
|
||||
navigationBarBackgroundColor: '#FAFAFA',
|
||||
enableShareAppMessage: true,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { View } from "@tarojs/components";
|
||||
import Taro from "@tarojs/taro";
|
||||
import Taro, { useRouter, useShareAppMessage } from "@tarojs/taro";
|
||||
import { OSS_BASE } from "@/config/api";
|
||||
import { wechat_auth_login, save_login_state } from "@/services/loginService";
|
||||
import { useUserActions } from "@/store/userStore";
|
||||
import { useGlobalState } from "@/store/global";
|
||||
@@ -18,7 +19,11 @@ import { useDictionaryStore } from "@/store/dictionaryStore";
|
||||
type TabType = "list" | "message" | "personal";
|
||||
|
||||
const MainPage: React.FC = () => {
|
||||
const [currentTab, setCurrentTab] = useState<TabType>("list");
|
||||
const { params } = useRouter();
|
||||
const [currentTab, setCurrentTab] = useState<TabType>(() => {
|
||||
const tab = params?.tab as TabType | undefined;
|
||||
return tab === "list" || tab === "message" || tab === "personal" ? tab : "list";
|
||||
});
|
||||
const [isPublishMenuVisible, setIsPublishMenuVisible] = useState(false);
|
||||
const [isDistanceFilterVisible, setIsDistanceFilterVisible] = useState(false);
|
||||
const [isCityPickerVisible, setIsCityPickerVisible] = useState(false);
|
||||
@@ -35,6 +40,14 @@ const MainPage: React.FC = () => {
|
||||
const { showGuideBar, guideBarZIndex, setShowGuideBar, setGuideBarZIndex } =
|
||||
useGlobalState();
|
||||
|
||||
// 从分享链接进入时根据 ?tab= 定位到对应 tab
|
||||
useEffect(() => {
|
||||
const tab = params?.tab as TabType | undefined;
|
||||
if (tab === "list" || tab === "message" || tab === "personal") {
|
||||
setCurrentTab(tab);
|
||||
}
|
||||
}, [params?.tab]);
|
||||
|
||||
// 初始化:自动微信授权并获取用户信息
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
@@ -153,6 +166,21 @@ const MainPage: React.FC = () => {
|
||||
[]
|
||||
);
|
||||
|
||||
// 分享:首页、个人页均支持转发
|
||||
// 分享图:配置 OSS 地址 + 路径(不含 ? 后参数),首页用 share_home.png,个人页用 share_self.png
|
||||
useShareAppMessage(() => {
|
||||
const isList = currentTab === "list";
|
||||
const isPersonal = currentTab === "personal";
|
||||
const title = isList ? "约球 - 发现身边的球局" : isPersonal ? "约球 - 我的约球" : "约球";
|
||||
const image_path = isPersonal ? "system/share_self.png" : "system/share_home.png";
|
||||
const imageUrl = OSS_BASE ? `${OSS_BASE.replace(/\/$/, "")}/${image_path}` : "";
|
||||
return {
|
||||
title,
|
||||
path: "/main_pages/index" + (isList ? "?tab=list" : isPersonal ? "?tab=personal" : ""),
|
||||
imageUrl: imageUrl,
|
||||
};
|
||||
});
|
||||
|
||||
// 滚动到顶部
|
||||
const scrollToTop = useCallback(() => {
|
||||
// 如果当前是列表页,触发列表页内部滚动
|
||||
|
||||
@@ -249,7 +249,7 @@ const CommentReply = () => {
|
||||
<View className="comment-left">
|
||||
<Image
|
||||
className="user-avatar"
|
||||
src={item.user_avatar || "https://img.yzcdn.cn/vant/cat.jpeg"}
|
||||
src={item.user_avatar }
|
||||
mode="aspectFill"
|
||||
onClick={(e) => handleUserClick(e, item.user_id)}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
.enable_notification_page {
|
||||
width: 100%;
|
||||
// min-height: 100vh;
|
||||
// background: radial-gradient(circle at 50% 0%, rgba(191, 255, 239, 1) 0%, rgba(255, 255, 255, 1) 37%);
|
||||
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;
|
||||
|
||||
@@ -34,6 +36,7 @@
|
||||
box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.08);
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
background: #ffffff;
|
||||
|
||||
// 第三个卡片(最上面)
|
||||
&--3 {
|
||||
|
||||
@@ -16,7 +16,7 @@ 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 { OSS_BASE } from "@/config/api";
|
||||
import CloseIcon from "@/static/ntrp/ntrp_close_icon.svg";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, Textarea, Image } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { ConfigProvider, Loading, Popup, Toast } from '@nutui/nutui-react-taro'
|
||||
import { ConfigProvider, Loading, Toast } from '@nutui/nutui-react-taro'
|
||||
import styles from './index.module.scss'
|
||||
import uploadFiles from '@/services/uploadFiles'
|
||||
import publishService from '@/services/publishService'
|
||||
@@ -109,7 +109,10 @@ const AiImportPopup: React.FC<AiImportPopupProps> = ({
|
||||
}
|
||||
|
||||
const handleTextChange = (e: any) => {
|
||||
setText(e.detail.value)
|
||||
const text = e.detail.value;
|
||||
const maxAllowedLength = 120;
|
||||
const truncatedVal = text.length > maxAllowedLength ? text.slice(0, maxAllowedLength) : text
|
||||
setText(truncatedVal)
|
||||
}
|
||||
|
||||
// 使用全局键盘状态监听
|
||||
@@ -191,14 +194,23 @@ const AiImportPopup: React.FC<AiImportPopupProps> = ({
|
||||
}
|
||||
|
||||
const showManualButton = uploadFailCount >= maxFailCount
|
||||
if (!visible) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 阻止弹窗内的触摸事件冒泡
|
||||
const handleTouchMoveInPopup = (e) => {
|
||||
if (!isKeyboardVisible) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Popup
|
||||
visible={visible}
|
||||
position="bottom"
|
||||
round={true}
|
||||
closeable={false}
|
||||
onClose={closePopupBefore}
|
||||
<View
|
||||
className={styles.aiImportPopupOverlay}
|
||||
>
|
||||
<View className={styles.aiImportPopupWrapper} onTouchMove={handleTouchMoveInPopup} catchMove></View>
|
||||
<View
|
||||
className={styles.aiImportPopup}
|
||||
style={{ paddingBottom: isKeyboardVisible ? `${keyboardHeight}px` : undefined }}
|
||||
>
|
||||
@@ -239,12 +251,21 @@ const AiImportPopup: React.FC<AiImportPopupProps> = ({
|
||||
|
||||
{/* 图片识别按钮 */}
|
||||
<View className={styles.imageRecognitionContainer}>
|
||||
<View className={`${styles.imageRecognitionButton} ${uploadLoading ? styles.uploadLoadingContainer : ''}`} onClick={handleImageRecognition}>
|
||||
{
|
||||
uploadLoading ? (<Image src={images.ICON_UPLOAD_SUCCESS} className={styles.cameraIcon} />) : (<Image src={images.ICON_UPLOAD_IMG} className={styles.cameraIcon} />)
|
||||
}
|
||||
<View
|
||||
className={`${styles.imageRecognitionButton} ${
|
||||
uploadLoading ? styles.uploadLoadingContainer : ''
|
||||
}`}
|
||||
onClick={handleImageRecognition}
|
||||
>
|
||||
{uploadLoading ? (
|
||||
<Image src={images.ICON_UPLOAD_SUCCESS} className={styles.cameraIcon} />
|
||||
) : (
|
||||
<Image src={images.ICON_UPLOAD_IMG} className={styles.cameraIcon} />
|
||||
)}
|
||||
<Text className={styles.imageRecognitionText}>图片识别</Text>
|
||||
<Text className={styles.imageRecognitionDesc}>{uploadLoading ? '已上传 1 张图片' : '支持订场截图/小红书笔记截图等图片'}</Text>
|
||||
<Text className={styles.imageRecognitionDesc}>
|
||||
{uploadLoading ? '已上传 1 张图片' : '支持订场截图/小红书笔记截图等图片'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -256,8 +277,7 @@ const AiImportPopup: React.FC<AiImportPopupProps> = ({
|
||||
</View>
|
||||
)}
|
||||
<View className={styles.pasteButton} onClick={handlePasteAndRecognize}>
|
||||
{
|
||||
loading ? (
|
||||
{loading ? (
|
||||
<View className={styles.loadingContainer}>
|
||||
<ConfigProvider theme={{ nutuiLoadingIconColor: '#fff', nutuiLoadingIconSize: '20px' }}>
|
||||
<Loading type="circular" />
|
||||
@@ -269,13 +289,13 @@ const AiImportPopup: React.FC<AiImportPopupProps> = ({
|
||||
<Image src={images.ICON_COPY} className={styles.clipboardIcon} />
|
||||
<Text className={styles.pasteButtonText}>粘贴并识别</Text>
|
||||
</>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<Toast id="toast" />
|
||||
</Popup>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,34 @@
|
||||
@use '~@/scss/themeColor.scss' as theme;
|
||||
|
||||
.aiImportPopup {
|
||||
background-color: #fff;
|
||||
&:global(.nut-popup-bottom.nut-popup-round) {
|
||||
border-radius: 20px 20px 0 0!important;
|
||||
.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 {
|
||||
width: 100%;
|
||||
background-color:#fafafa;
|
||||
border-radius: 16px 16px 0 0;
|
||||
position: relative;
|
||||
z-index: 9999;
|
||||
.popupContent {
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border-radius: 16px 16px 0 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
max-height: 80vh;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { View, Text, Input, ScrollView, Image } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { Loading } from '@nutui/nutui-react-taro'
|
||||
import StadiumDetail, { StadiumDetailRef } from './StadiumDetail'
|
||||
import { CommonPopup } from '../../../../components'
|
||||
import { CommonPopup, CustomPopup } from '../../../../components'
|
||||
import { getLocation } from '@/utils/locationUtils'
|
||||
import PublishService from '@/services/publishService'
|
||||
import images from '@/config/images'
|
||||
@@ -188,24 +188,20 @@ const SelectStadium: React.FC<SelectStadiumProps> = ({
|
||||
// 如果显示详情页面
|
||||
if (showDetail && selectedStadium) {
|
||||
return (
|
||||
<CommonPopup
|
||||
<CustomPopup
|
||||
visible={visible}
|
||||
onClose={handleCancel}
|
||||
cancelText="返回"
|
||||
confirmText="确认"
|
||||
className="select-stadium-popup"
|
||||
onCancel={handleDetailCancel}
|
||||
onConfirm={handleConfirm}
|
||||
position="bottom"
|
||||
//style={{ paddingBottom: keyboardVisible ? `20px` : undefined }}
|
||||
round
|
||||
>
|
||||
{/* 内容区域 */}
|
||||
<StadiumDetail
|
||||
ref={stadiumDetailRef}
|
||||
stadium={selectedStadium}
|
||||
//onAnyInput={handleAnyInput}
|
||||
/>
|
||||
</CommonPopup>
|
||||
</CustomPopup>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,40 @@ 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) => {
|
||||
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 +217,7 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
|
||||
refresherBackground="#FAFAFA"
|
||||
scrollY={!openPicker}
|
||||
scrollTop={scrollTop}
|
||||
style={{ maxHeight: scrollMaxHeight }}
|
||||
>
|
||||
{/* 已选球场 */}
|
||||
<View
|
||||
@@ -235,11 +262,13 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
|
||||
<TextareaTag
|
||||
value={formData[item.prop]}
|
||||
onChange={(value) => {
|
||||
changeTextarea(true)
|
||||
updateFormData(item.prop, value)
|
||||
}}
|
||||
// onBlur={() => changeTextarea(false)}
|
||||
onFocus={() => changeTextarea(true)}
|
||||
// onBlur={() => {
|
||||
// }}
|
||||
onFocus={() => {
|
||||
changeTextarea(true)
|
||||
}}
|
||||
placeholder='有其他场地信息可备注'
|
||||
options={(item.options || []).map((o) => ({ label: o, value: o }))}
|
||||
/>
|
||||
|
||||
@@ -783,6 +783,7 @@ const PublishBall: React.FC = () => {
|
||||
>
|
||||
<GeneralNavbar
|
||||
title={titleBar}
|
||||
backgroundColor={'#FAFAFA'}
|
||||
className={styles["publish-ball-navbar"]}
|
||||
/>
|
||||
<View
|
||||
|
||||
@@ -169,7 +169,7 @@ class GameDetailService {
|
||||
}
|
||||
|
||||
async getLinkUrl(req: { path: string, query: string }): Promise<ApiResponse<{ url_link: string, path: string, query: string }>> {
|
||||
return httpService.post('/user/generate_url_link', req, { showLoading: true })
|
||||
return httpService.post('/user/generate_url_link', req, { showLoading: false })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -151,6 +151,7 @@ interface BackendGameData {
|
||||
longitude: string;
|
||||
venue_type: string;
|
||||
surface_type: string;
|
||||
distance_km: string;
|
||||
};
|
||||
participants: {
|
||||
user: {
|
||||
@@ -206,7 +207,7 @@ export class UserService {
|
||||
latitude = parseFloat(game.venue_dtl.latitude) || latitude;
|
||||
longitude = parseFloat(game.venue_dtl.longitude) || longitude;
|
||||
}
|
||||
const distance = this.calculate_distance(latitude, longitude);
|
||||
|
||||
|
||||
// 处理地点信息 - 优先使用venue_dtl中的信息
|
||||
let location = game.location_name || game.location || "未知地点";
|
||||
@@ -227,7 +228,7 @@ export class UserService {
|
||||
original_start_time: game.start_time,
|
||||
end_time: game.end_time || "",
|
||||
location: location,
|
||||
distance_km: parseFloat(distance.replace("km", "")) || 0,
|
||||
distance_km: game.venue_dtl?.distance_km ,
|
||||
current_players: registered_count,
|
||||
max_players: max_count,
|
||||
skill_level_min: parseInt(game.skill_level_min) || 0,
|
||||
@@ -303,20 +304,7 @@ export class UserService {
|
||||
return `${date_str} ${time_str}`;
|
||||
}
|
||||
|
||||
// 计算距离(模拟实现,实际需要根据用户位置计算)
|
||||
private static calculate_distance(
|
||||
latitude: number,
|
||||
longitude: number
|
||||
): string {
|
||||
if (latitude === 0 && longitude === 0) {
|
||||
return "未知距离";
|
||||
}
|
||||
|
||||
// 这里应该根据用户当前位置计算实际距离
|
||||
// 暂时返回模拟距离
|
||||
const distances = ["1.2km", "2.5km", "3.8km", "5.1km", "7.3km"];
|
||||
return distances[Math.floor(Math.random() * distances.length)];
|
||||
}
|
||||
// 获取用户信息
|
||||
static async get_user_info(user_id?: string): Promise<UserInfo> {
|
||||
try {
|
||||
@@ -359,8 +347,6 @@ export class UserService {
|
||||
last_location_province: userData.last_location_province || "",
|
||||
last_location_city: userData.last_location_city || "",
|
||||
};
|
||||
|
||||
|
||||
} else {
|
||||
throw new Error(response.message || "获取用户信息失败");
|
||||
}
|
||||
@@ -747,7 +733,7 @@ export const updateUserLocation = async (
|
||||
const response = await httpService.post("/user/update_location", {
|
||||
latitude,
|
||||
longitude,
|
||||
force
|
||||
force,
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 44 KiB |
13
src/static/list/icon_game_type.svg
Normal file
13
src/static/list/icon_game_type.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_3006_15051)">
|
||||
<path d="M12.8375 3.17871C11.6157 2.03414 9.9731 1.33331 8.16683 1.33331C4.3929 1.33331 1.3335 4.39271 1.3335 8.16665C1.3335 11.9406 4.3929 15 8.16683 15C10.0384 15 11.7343 14.2475 12.9684 13.0287L8.00016 7.99998L12.8375 3.17871Z" stroke="black" stroke-width="1.33333" stroke-linejoin="round"/>
|
||||
<path d="M13.3333 9.33335C14.0697 9.33335 14.6667 8.73639 14.6667 8.00002C14.6667 7.26365 14.0697 6.66669 13.3333 6.66669C12.597 6.66669 12 7.26365 12 8.00002C12 8.73639 12.597 9.33335 13.3333 9.33335Z" stroke="black" stroke-width="1.33333" stroke-linejoin="round"/>
|
||||
<path d="M5.6665 4.33331V6.99998" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4.3335 5.66669H7.00016" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3006_15051">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -150,12 +150,13 @@ export const useKeyboardStore = create<KeyboardStore>((set, get) => ({
|
||||
|
||||
// 导出便捷的 hooks
|
||||
export const useKeyboardHeight = () => {
|
||||
const { keyboardHeight, isKeyboardVisible, addListener, initializeKeyboardListener } = useKeyboardStore()
|
||||
const { keyboardHeight, isKeyboardVisible, addListener, initializeKeyboardListener, setKeyboardVisible } = useKeyboardStore()
|
||||
|
||||
return {
|
||||
keyboardHeight,
|
||||
isKeyboardVisible,
|
||||
addListener,
|
||||
initializeKeyboardListener
|
||||
initializeKeyboardListener,
|
||||
setKeyboardVisible
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { UserService } from "@/services/userService";
|
||||
export interface PickerOptionState {
|
||||
cities: any[];
|
||||
professions: any[];
|
||||
ntrpLevels: string[];
|
||||
ntrpLevels: any[];
|
||||
getCities: () => Promise<any>;
|
||||
getProfessions: () => Promise<any>;
|
||||
}
|
||||
@@ -13,7 +13,40 @@ export interface PickerOptionState {
|
||||
export const usePickerOption = create<PickerOptionState>((set) => ({
|
||||
cities: [],
|
||||
professions: [],
|
||||
ntrpLevels: ["1.5", "2.0", "2.5", "3.0", "3.5", "4.0", "4.5", "4.5+"],
|
||||
ntrpLevels: [
|
||||
{
|
||||
text: "1.5",
|
||||
value: "1.5",
|
||||
},
|
||||
{
|
||||
text: "2.0",
|
||||
value: "2.0",
|
||||
},
|
||||
{
|
||||
text: "2.5",
|
||||
value: "2.5",
|
||||
},
|
||||
{
|
||||
text: "3.0",
|
||||
value: "3.0",
|
||||
},
|
||||
{
|
||||
text: "3.5",
|
||||
value: "3.5",
|
||||
},
|
||||
{
|
||||
text: "4.0",
|
||||
value: "4.0",
|
||||
},
|
||||
{
|
||||
text: "4.5",
|
||||
value: "4.5",
|
||||
},
|
||||
{
|
||||
text: "4.5+",
|
||||
value: "4.5+",
|
||||
},
|
||||
],
|
||||
getCities: async () => {
|
||||
try {
|
||||
const res = await UserService.getCities();
|
||||
|
||||
@@ -8,7 +8,9 @@ import {
|
||||
NicknameChangeStatus,
|
||||
updateNickname as updateNicknameApi,
|
||||
} from "@/services/userService";
|
||||
import evaluateService, { LastTimeTestResult } from "@/services/evaluateService";
|
||||
import evaluateService, {
|
||||
LastTimeTestResult,
|
||||
} from "@/services/evaluateService";
|
||||
import { useListStore } from "./listStore";
|
||||
|
||||
export interface UserState {
|
||||
@@ -23,7 +25,6 @@ export interface UserState {
|
||||
fetchLastTestResult: () => Promise<LastTimeTestResult | null>;
|
||||
}
|
||||
|
||||
|
||||
const getTimeNextDate = (time: string) => {
|
||||
const date = new Date(time);
|
||||
date.setDate(date.getDate() + 1);
|
||||
@@ -51,8 +52,6 @@ export const useUser = create<UserState>()((set) => ({
|
||||
|
||||
const cachedCity = (Taro as any).getStorageSync?.(CITY_CACHE_KEY);
|
||||
|
||||
|
||||
|
||||
if (cachedCity && Array.isArray(cachedCity) && cachedCity.length === 2) {
|
||||
// 如果有缓存的城市,使用缓存,不更新 area
|
||||
console.log("[userStore] 检测到缓存的城市,使用缓存,不更新 area");
|
||||
@@ -66,7 +65,10 @@ export const useUser = create<UserState>()((set) => ({
|
||||
|
||||
// 只有当 area 不存在时才使用用户信息中的位置
|
||||
if (!currentArea) {
|
||||
const newArea: [string, string] = [userData.last_location_province||"", userData.last_location_city||""];
|
||||
const newArea: [string, string] = [
|
||||
userData.last_location_province || "",
|
||||
userData.last_location_city || "",
|
||||
];
|
||||
listStore.updateArea(newArea);
|
||||
// 保存到缓存
|
||||
useUser.getState().updateCache(newArea);
|
||||
@@ -102,8 +104,14 @@ export const useUser = create<UserState>()((set) => ({
|
||||
const listStore = useListStore.getState();
|
||||
const currentArea = listStore.area;
|
||||
// 只有当 area 不存在或与 userLastLocationProvince 不一致时才更新
|
||||
if (!currentArea || currentArea[1] !== userInfo.last_location_province) {
|
||||
const newArea: [string, string] = [userInfo.last_location_province || "", userInfo.last_location_city || ""];
|
||||
if (
|
||||
!currentArea ||
|
||||
currentArea[1] !== userInfo.last_location_province
|
||||
) {
|
||||
const newArea: [string, string] = [
|
||||
userInfo.last_location_province || "",
|
||||
userInfo.last_location_city || "",
|
||||
];
|
||||
listStore.updateArea(newArea);
|
||||
}
|
||||
}
|
||||
@@ -127,7 +135,10 @@ export const useUser = create<UserState>()((set) => ({
|
||||
// 如果已经有状态数据且不是强制更新,跳过,避免重复调用
|
||||
if (!force) {
|
||||
const currentState = useUser.getState();
|
||||
if (currentState.nicknameChangeStatus && Object.keys(currentState.nicknameChangeStatus).length > 0) {
|
||||
if (
|
||||
currentState.nicknameChangeStatus &&
|
||||
Object.keys(currentState.nicknameChangeStatus).length > 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,34 @@ const DownloadBillRecords: React.FC = () => {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handlePreviewFile = (fileUrl: string) => {
|
||||
wx.downloadFile({
|
||||
url: fileUrl,
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
// 确保文件路径正确并添加扩展名
|
||||
const filePath = res.tempFilePath;
|
||||
wx.openDocument({
|
||||
filePath: filePath,
|
||||
fileType: 'xlsx', // 指定文件类型为xlsx
|
||||
showMenu: true, // 显示右上角菜单按钮
|
||||
success: (openRes) => {
|
||||
console.log('打开文档成功');
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('打开文档失败', err);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.error('下载失败,状态码:', res.statusCode);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('下载失败', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
return (
|
||||
<View className="download-bill-records-page">
|
||||
{/* 导航栏 */}
|
||||
@@ -111,7 +139,7 @@ const DownloadBillRecords: React.FC = () => {
|
||||
</View>
|
||||
<View className="info-item">
|
||||
<Text></Text>
|
||||
<Text className="btn">查看材料</Text>
|
||||
<Text className="btn" onClick={() => handlePreviewFile(record.file_url)}>查看材料</Text>
|
||||
</View>
|
||||
</View>
|
||||
)) : <EmptyState text="暂无数据" />}
|
||||
|
||||
@@ -44,6 +44,7 @@ const EditProfilePage: React.FC = () => {
|
||||
country: info?.country ?? "",
|
||||
province: info?.province ?? "",
|
||||
city: info?.city ?? "",
|
||||
district: info?.district ?? "",
|
||||
};
|
||||
};
|
||||
const [form_data, setFormData] = useState(getInitialFormData());
|
||||
@@ -85,6 +86,7 @@ const EditProfilePage: React.FC = () => {
|
||||
country: info?.country ?? "",
|
||||
province: info?.province ?? "",
|
||||
city: info?.city ?? "",
|
||||
district: info?.district ?? "",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -358,11 +360,11 @@ const EditProfilePage: React.FC = () => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
const [country, province, city] = e;
|
||||
const [province, city, district] = e;
|
||||
handle_field_edit({
|
||||
country: String(country ?? ""),
|
||||
province: String(province ?? ""),
|
||||
city: String(city ?? ""),
|
||||
district: String(district ?? ""),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -660,15 +662,17 @@ const EditProfilePage: React.FC = () => {
|
||||
<View className="item_right">
|
||||
<Text
|
||||
className={`item_value ${
|
||||
form_data.country ||
|
||||
form_data.province ||
|
||||
form_data.city
|
||||
form_data.city ||
|
||||
form_data.district
|
||||
? ""
|
||||
: "placehoder"
|
||||
}`}
|
||||
>
|
||||
{form_data.country || form_data.province || form_data.city
|
||||
? `${form_data.country} ${form_data.province} ${form_data.city}`
|
||||
{form_data.province ||
|
||||
form_data.city ||
|
||||
form_data.district
|
||||
? `${form_data.province} ${form_data.city} ${form_data.district}`
|
||||
: "选择所在地区"}
|
||||
</Text>
|
||||
<Image
|
||||
@@ -885,8 +889,8 @@ const EditProfilePage: React.FC = () => {
|
||||
visible={location_picker_visible}
|
||||
setvisible={setLocationPickerVisible}
|
||||
value={
|
||||
form_data.country
|
||||
? [form_data.country, form_data.province, form_data.city]
|
||||
form_data.province
|
||||
? [form_data.province, form_data.city, form_data.district]
|
||||
: getDefaultOption(cities)
|
||||
}
|
||||
onChange={handle_location_change}
|
||||
@@ -899,15 +903,12 @@ const EditProfilePage: React.FC = () => {
|
||||
title="选择 NTRP 自评水平"
|
||||
confirmText="保存"
|
||||
ntrpTested={ntrpTested}
|
||||
options={ntrpLevels.map((level) => ({
|
||||
text: level,
|
||||
value: level,
|
||||
}))}
|
||||
options={ntrpLevels}
|
||||
type="ntrp"
|
||||
// img={(user_info as UserInfoType)?.avatar_url}
|
||||
visible={ntrp_picker_visible}
|
||||
setvisible={setNtrpPickerVisible}
|
||||
value={[form_data.ntrp_level || "2.5"]}
|
||||
value={!form_data.ntrp_level ? ["2.5"] : [form_data.ntrp_level]}
|
||||
onChange={handle_ntrp_level_change}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
|
||||
.qrcode {
|
||||
width: 240px;
|
||||
height: 240px;
|
||||
margin: 32px 0 -20px;
|
||||
}
|
||||
|
||||
|
||||
@@ -329,9 +329,8 @@ const OtherUserPage: React.FC = () => {
|
||||
overflow: "hidden",
|
||||
}}
|
||||
listLoadErrorWrapperHeight="fit-content"
|
||||
listLoadErrorWidth="320px"
|
||||
listLoadErrorHeight="152px"
|
||||
listLoadErrorScale="1.2"
|
||||
listLoadErrorWidth="410px"
|
||||
listLoadErrorHeight="185px"
|
||||
defaultShowNum={3}
|
||||
/>
|
||||
</ScrollView>
|
||||
@@ -375,9 +374,8 @@ const OtherUserPage: React.FC = () => {
|
||||
collapse={true}
|
||||
style={{ paddingBottom: "90px", overflow: "hidden" }}
|
||||
listLoadErrorWrapperHeight="fit-content"
|
||||
listLoadErrorWidth="320px"
|
||||
listLoadErrorHeight="152px"
|
||||
listLoadErrorScale="1.2"
|
||||
listLoadErrorWidth="410px"
|
||||
listLoadErrorHeight="185px"
|
||||
defaultShowNum={3}
|
||||
/>
|
||||
</ScrollView>
|
||||
|
||||
@@ -35,21 +35,48 @@ export function base64ToTempFilePath(base64Data: string): Promise<string> {
|
||||
}
|
||||
|
||||
|
||||
interface TaroGetImageInfo {
|
||||
getImageInfo(option: {
|
||||
src: string;
|
||||
success?: (res: { width: number; height: number }) => void;
|
||||
fail?: (err: unknown) => void;
|
||||
}): void;
|
||||
}
|
||||
|
||||
/** 获取图片宽高 */
|
||||
function getImageWh(src: string): Promise<{ width: number; height: number }> {
|
||||
return new Promise((resolve) => {
|
||||
Taro.getImageInfo({
|
||||
return new Promise((resolve, reject) => {
|
||||
(Taro as TaroGetImageInfo).getImageInfo({
|
||||
src,
|
||||
success: ({ width, height }) => resolve({ width, height }),
|
||||
fail: (e) => reject(e),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** 加载图片 */
|
||||
function loadImage(canvas: any, src: string): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
let timer: any;
|
||||
const img = canvas.createImage();
|
||||
img.onload = () => resolve(img);
|
||||
|
||||
img.crossOrigin = "anonymous"
|
||||
|
||||
img.onload = () => {
|
||||
clearTimeout(timer);
|
||||
resolve(img);
|
||||
};
|
||||
img.onerror = () => {
|
||||
clearTimeout(timer);
|
||||
console.log('img error', src)
|
||||
}
|
||||
|
||||
|
||||
timer = setTimeout(() => {
|
||||
reject(new Error(`Image load timeout: ${src}`));
|
||||
}, 8000);
|
||||
|
||||
img.src = src;
|
||||
});
|
||||
}
|
||||
@@ -137,6 +164,7 @@ async function drawRotateCoverImage(
|
||||
rotate = 0 // 旋转角度(弧度)
|
||||
) {
|
||||
const { width, height } = await getImageWh(src);
|
||||
console.log('width', width, 'height', height)
|
||||
const scale = Math.max(w / width, h / height);
|
||||
const newW = width * scale;
|
||||
const newH = height * scale;
|
||||
@@ -170,6 +198,7 @@ async function drawRotateCoverImage(
|
||||
|
||||
// 绘制 cover
|
||||
ctx.drawImage(img, offsetX, offsetY, newW, newH);
|
||||
console.log('drawImage', offsetX, offsetY, newW, newH)
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
@@ -281,24 +310,39 @@ 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;
|
||||
|
||||
console.log('width', width, 'height', height)
|
||||
const canvas = Taro.createOffscreenCanvas({ type: "2d", width: width * dpr, height: height * dpr });
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.scale(dpr, dpr);
|
||||
|
||||
|
||||
console.log('ctx', ctx)
|
||||
|
||||
|
||||
// 背景渐变
|
||||
roundRectGradient(ctx, 0, 0, width, height, 24, "#BFFFEF", "#F2FFFC");
|
||||
|
||||
console.log('bgUrl', bgUrl)
|
||||
const bgImg = await loadImage(canvas, bgUrl);
|
||||
ctx.drawImage(bgImg, 0, 0, width, height);
|
||||
console.log('bgUrlend', )
|
||||
|
||||
|
||||
roundRotateRect(ctx, 70, 100, width - 140, width - 140, 20, '#fff', deg2rad(-6));
|
||||
|
||||
|
||||
// 顶部图片
|
||||
const mainImg = await loadImage(canvas, data.mainCoursal);
|
||||
console.log('mainCoursal', data.mainCoursal)
|
||||
await drawRotateCoverImage(
|
||||
ctx,
|
||||
canvas,
|
||||
@@ -367,6 +411,8 @@ export async function generatePosterImage(data: any): Promise<string> {
|
||||
left = 20;
|
||||
|
||||
const dateImg = await loadImage(canvas, dateIcon);
|
||||
|
||||
console.log('dateIcon', dateIcon)
|
||||
await drawCoverImage(
|
||||
ctx,
|
||||
canvas,
|
||||
@@ -393,6 +439,7 @@ export async function generatePosterImage(data: any): Promise<string> {
|
||||
left = 20;
|
||||
top += 24;
|
||||
|
||||
|
||||
const mapImg = await loadImage(canvas, mapIcon);
|
||||
await drawCoverImage(ctx, canvas, mapIcon, mapImg, left, top, 40, 40, 12);
|
||||
|
||||
@@ -429,11 +476,13 @@ export async function generatePosterImage(data: any): Promise<string> {
|
||||
ctx.font = "400 20px sans-serif";
|
||||
ctx.fillText("长按识别二维码,快来加入,有你就有场!", left, height - 30/* top */);
|
||||
|
||||
console.log('canvas', canvas)
|
||||
// 导出图片
|
||||
const { tempFilePath } = await Taro.canvasToTempFilePath({
|
||||
canvas,
|
||||
fileType: 'png',
|
||||
quality: 1,
|
||||
quality: 0.7,
|
||||
});
|
||||
console.log('tempFilePath', tempFilePath)
|
||||
return tempFilePath;
|
||||
}
|
||||
|
||||
@@ -533,7 +533,6 @@ const drawShareCard = async (ctx: any, data: ShareCardData, offscreen: any): Pro
|
||||
const locationPath = await loadImage(`${OSS_BASE}/front/ball/images/adc9a167-2ea9-4e3b-b963-6a894a1fd91b.jpg`)
|
||||
ctx.drawImage(locationPath, iconX, locationInfoY, iconSize, iconSize)
|
||||
drawBoldText(ctx, data.venueName, danDaX, locationInfoY + 10, locationFontSize, '#000000')
|
||||
|
||||
try {
|
||||
const wxAny: any = (typeof (globalThis as any) !== 'undefined' && (globalThis as any).wx) ? (globalThis as any).wx : null
|
||||
if (wxAny && typeof wxAny.canvasToTempFilePath === 'function') {
|
||||
|
||||
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