28 Commits

Author SHA1 Message Date
c47ebce43c fix: 修复取消活动后还可以编辑和取消、详情页参与者卡片展示NTRP 等级、生成海报的图片质量降低到1M以下, 详情页海报与测试结果页海报 2026-02-08 22:57:35 +08:00
b0f4b5713d Merge branch 'master' into feat/liujie 2026-02-08 21:25:19 +08:00
f7f10f5d15 查询下载账单 2026-02-08 16:03:08 +08:00
李瑞
2bcdd93479 Merge branch 'feat/juguohong/20260206' 2026-02-08 12:46:27 +08:00
李瑞
af2c472030 处理图片顺序 2026-02-08 12:46:01 +08:00
张成
8d0ed5b1b3 1 2026-02-08 12:36:21 +08:00
张成
e99986c52a 修改审核不通过的问题 2026-02-08 12:29:48 +08:00
张成
4b2f6707cc 1 2026-02-08 12:18:04 +08:00
张成
a019fe473b Merge branch 'master' of https://git.bimwe.com/bimwe/mini-programs 2026-02-08 12:14:11 +08:00
张成
1d0d2edaa2 修改项目 build 结构 2026-02-08 12:14:10 +08:00
5926e096b5 图片样式优化 2026-02-08 12:09:46 +08:00
筱野
e07f2ad2d1 解决按钮问题与键盘弹出问题 2026-02-07 23:37:28 +08:00
筱野
bfc6a149f0 修改日期问题弹出问题 2026-02-07 22:15:14 +08:00
李瑞
6f73bb6d99 Merge branch 'feat/juguohong/20260206' 2026-02-07 22:09:39 +08:00
李瑞
744169fe34 处理地点展示样式 2026-02-07 22:08:30 +08:00
54b7a27af5 Merge branch 'feat/liujie' 2026-02-07 20:25:51 +08:00
396ff4a347 fix: 修改取值 2026-02-07 20:25:37 +08:00
张成
b732bd361e Merge branch 'master' of https://git.bimwe.com/bimwe/mini-programs 2026-02-07 20:24:50 +08:00
张成
5146894d92 1 2026-02-07 20:24:49 +08:00
张成
07cf8e884e 1 2026-02-07 20:24:45 +08:00
5416ea127c Merge branch 'master' into feat/liujie 2026-02-07 20:19:23 +08:00
a7bc517fae Merge branch 'feat/liujie' 2026-02-07 20:19:15 +08:00
16b38539f6 fix: 修改取值 2026-02-07 20:19:04 +08:00
张成
0d46311bbc 1 2026-02-07 19:27:58 +08:00
e884b1f258 Merge branch 'master' into feat/liujie 2026-02-07 18:13:23 +08:00
84159a4835 Merge branch 'feat/liujie' 2026-02-07 18:13:15 +08:00
2acee85dd5 fix: 修复分享弹窗打开逻辑 2026-02-07 18:13:00 +08:00
ba72e0ec97 Merge branch 'master' into feat/liujie 2026-02-07 18:11:15 +08:00
42 changed files with 1546 additions and 747 deletions

2
.env.dev Normal file
View File

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

2
.env.dev_local Normal file
View File

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

2
.env.pr Normal file
View File

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

2
.env.sit Normal file
View File

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

2
.gitignore vendored
View File

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

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

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

View File

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

View File

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

View File

@@ -3,7 +3,6 @@
"projectname": "playBallTogether", "projectname": "playBallTogether",
"description": "playBallTogether", "description": "playBallTogether",
"appid": "wx815b533167eb7b53", "appid": "wx815b533167eb7b53",
"setting": { "setting": {
"urlCheck": true, "urlCheck": true,
"es6": true, "es6": true,
@@ -47,4 +46,4 @@
"simulatorType": "wechat", "simulatorType": "wechat",
"simulatorPluginLibVersion": {}, "simulatorPluginLibVersion": {},
"condition": {} "condition": {}
} }

View File

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

View File

@@ -20,6 +20,6 @@ page {
@font-face { @font-face {
font-family: "Quicksand"; font-family: "Quicksand";
// 注意:此路径对应 @/config/api.ts 中的 OSS_BASE // 注意:此路径对应 @/config/api.ts 中的 OSS_BASE
src: url("https://bimwe-oss.oss-cn-shanghai.aliyuncs.com/front/ball/other/57dc951f-f10e-45b7-9157-0b1e468187fd.ttf") format("truetype"); src: url("https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/other/57dc951f-f10e-45b7-9157-0b1e468187fd.ttf") format("truetype");
font-display: swap; font-display: swap;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

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

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

View File

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

View File

@@ -57,7 +57,7 @@ function Index() {
await delay(1000); await delay(1000);
if (from !== "publish") { if (from === "publish") {
handleShare(true); handleShare(true);
} }
}; };
@@ -298,7 +298,7 @@ function Index() {
id={id as string} id={id as string}
from={from as string} from={from as string}
detail={detail} detail={detail}
userInfo={userInfo} userInfo={myInfo}
/> />
</View> </View>
</ScrollView> </ScrollView>

View File

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

View File

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

View File

@@ -1,7 +1,9 @@
.enable_notification_page { .enable_notification_page {
width: 100%; width: 100%;
// min-height: 100vh; height: 100%;
// background: radial-gradient(circle at 50% 0%, rgba(191, 255, 239, 1) 0%, rgba(255, 255, 255, 1) 37%); background: radial-gradient(circle at 50% 0%, rgba(191, 255, 239, 1) 0%, rgba(255, 255, 255, 1) 37%);
box-sizing: border-box;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -34,6 +36,7 @@
box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.08); box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.08);
box-sizing: border-box; box-sizing: border-box;
position: absolute; position: absolute;
background: #ffffff;
// 第三个卡片(最上面) // 第三个卡片(最上面)
&--3 { &--3 {

View File

@@ -16,7 +16,7 @@ import { useGlobalState } from "@/store/global";
import { delay, getCurrentFullPath } from "@/utils"; import { delay, getCurrentFullPath } from "@/utils";
import { formatNtrpDisplay } from "@/utils/helper"; import { formatNtrpDisplay } from "@/utils/helper";
import { waitForAuthInit } from "@/utils/authInit"; import { waitForAuthInit } from "@/utils/authInit";
import httpService from "@/services/httpService"; // import httpService from "@/services/httpService";
import DetailService from "@/services/detailService"; import DetailService from "@/services/detailService";
import { OSS_BASE } from "@/config/api"; import { OSS_BASE } from "@/config/api";
import CloseIcon from "@/static/ntrp/ntrp_close_icon.svg"; import CloseIcon from "@/static/ntrp/ntrp_close_icon.svg";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -783,6 +783,7 @@ const PublishBall: React.FC = () => {
> >
<GeneralNavbar <GeneralNavbar
title={titleBar} title={titleBar}
backgroundColor={'#FAFAFA'}
className={styles["publish-ball-navbar"]} className={styles["publish-ball-navbar"]}
/> />
<View <View

View File

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

View File

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

View File

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

View File

@@ -282,7 +282,9 @@ function drawTextWrap(
/** 核心纯函数:生成海报图片 */ /** 核心纯函数:生成海报图片 */
export async function generatePosterImage(data: any): Promise<string> { export async function generatePosterImage(data: any): Promise<string> {
console.log("start !!!!"); console.log("start !!!!");
const dpr = Taro.getWindowInfo().pixelRatio; // const dpr = Taro.getWindowInfo().pixelRatio;
const dpr = 1;
// console.log(dpr, 'dpr')
const width = 600; const width = 600;
const height = 1000; const height = 1000;
@@ -433,7 +435,7 @@ export async function generatePosterImage(data: any): Promise<string> {
const { tempFilePath } = await Taro.canvasToTempFilePath({ const { tempFilePath } = await Taro.canvasToTempFilePath({
canvas, canvas,
fileType: 'png', fileType: 'png',
quality: 1, quality: 0.7,
}); });
return tempFilePath; return tempFilePath;
} }

2
types/global.d.ts vendored
View File

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