101 Commits

Author SHA1 Message Date
李瑞
4dc8e84f5c 修改日期筛选交互 2026-03-27 22:30:46 +08:00
b84c3bb409 fix: 时间展示修复 2026-03-25 06:03:07 +08:00
fa41842e75 fix: 逻辑补全 2026-03-20 23:24:22 +08:00
3bbb64d58c style: 修复地址展示的问题 2026-03-20 23:10:23 +08:00
58cf46e93d fix: 修复订单详情页球局时间展示问题、修复候补列表NTRP文案缺失的问题 2026-03-20 22:56:18 +08:00
张成
8004b26bd1 换logo 2026-03-19 15:00:08 +08:00
张成
98baa371ee 1 2026-03-16 11:25:50 +08:00
张成
f87859da0e 1 2026-03-11 15:33:26 +08:00
d3390d5e81 fix: debug 分享卡片生成问题 2026-03-11 11:48:40 +08:00
筱野
883ce3c2c4 修改标题换行替换 2026-03-10 21:34:45 +08:00
张成
63bcf6fe86 Merge branch 'fix/jgh/0310' 2026-03-10 11:35:19 +08:00
a68da08c85 feat: 订单详情页退款政策和报名须知修改文案和页面结构 2026-03-10 11:09:32 +08:00
李瑞
b854ef7505 处理场馆方字段 2026-03-10 00:03:42 +08:00
张成
2e4fd16383 修改文字 2026-03-09 17:23:32 +08:00
张成
636e218a63 1 2026-03-09 16:15:06 +08:00
张成
47c19f0fa5 1 2026-03-09 16:02:26 +08:00
243bb59c1d fix: 修改以支持非整数小时数 2026-03-09 15:48:34 +08:00
aed3c4cc54 feat: 排查生成海报失败的原因 2026-03-06 11:10:07 +08:00
05b89a4aeb fix: 修复取消活动管理弹窗输入框聚焦问题 2026-03-06 10:30:37 +08:00
张成
1aa12a86c2 钱包页返回也要拉数据 2026-03-04 16:03:23 +08:00
69248d33c8 优化样式 2026-03-02 16:15:59 +08:00
1973ec3faa input placeholder样式修改 2026-02-27 15:35:03 +08:00
abc2dfeecf 设置交易密码页面样式优化 2026-02-27 12:08:47 +08:00
bafb44ff06 设置交易密码页面样式优化 2026-02-27 12:06:40 +08:00
0e27d801a4 fix: 文案修改 2026-02-27 10:06:49 +08:00
0a0203e36d fix: 修复退出活动弹窗文案展示 2026-02-26 20:34:13 +08:00
2656c59475 feat: 退款政策接口获取 2026-02-26 19:43:40 +08:00
23eb9dc467 fix: 退款展示退款金额、球局详情页样式修复 2026-02-26 17:56:42 +08:00
44f971b1c2 往期球局排序处理 2026-02-25 16:42:14 +08:00
4a553c63fc 往期球局排序处理 2026-02-25 16:37:47 +08:00
张成
baa60bbfcb 添加内容过滤功能 2026-02-14 13:56:54 +08:00
张成
64f0267457 修改上传图片安全验证问题 2026-02-14 12:59:21 +08:00
张成
8688b6b82d 1 2026-02-13 11:30:08 +08:00
张成
1678f787a3 1 2026-02-13 11:30:03 +08:00
张成
3571740280 添加审核不展示二维码的功能 2026-02-13 10:29:18 +08:00
张成
b6801cdde2 Merge branch 'master' of https://git.bimwe.com/bimwe/mini-programs 2026-02-12 10:06:50 +08:00
张成
e1ebcd949b 1 2026-02-12 10:06:48 +08:00
张成
044e84a5b4 Merge branch 'master' of https://git.bimwe.com/bimwe/mini-programs 2026-02-11 23:23:37 +08:00
张成
7833c2f552 1 2026-02-11 23:23:36 +08:00
李瑞
9e4282545f Merge branch 'feat/juguohong/20260206' 2026-02-11 23:14:22 +08:00
李瑞
99c8026f61 数据为空时允许展示banner 2026-02-11 23:13:49 +08:00
2a9e8668a0 style: 修复订单详情页订单号超长导致的样式问题 2026-02-11 10:11:55 +08:00
08092a89ab 添加发布球局拦截 2026-02-11 09:09:53 +08:00
筱野
4f0cdad920 修改提示文案 2026-02-10 23:04:29 +08:00
05966b2acb 优化ntrp和性别picker偶尔选不上值 2026-02-10 18:00:26 +08:00
张成
4cf2b959b5 1 2026-02-10 12:42:42 +08:00
张成
43610dcf99 修复首页数据少的问题 2026-02-10 11:46:39 +08:00
张成
05aa820466 Merge branch 'master' of https://git.bimwe.com/bimwe/mini-programs 2026-02-09 22:03:59 +08:00
筱野
b154e31f8f Merge branch 'master' of https://git.bimwe.com/bimwe/mini-programs 2026-02-09 22:03:09 +08:00
筱野
669ee2fe4e 解决按钮问题与键盘弹出问题 2026-02-09 22:03:01 +08:00
张成
281ee2b746 Merge branch 'master' of https://git.bimwe.com/bimwe/mini-programs 2026-02-09 22:02:46 +08:00
张成
132c74d27c 1 2026-02-09 22:02:43 +08:00
李瑞
6b6a4c9480 替换玩法icon 2026-02-09 22:01:36 +08:00
筱野
0f8dd44f5a 解决按钮问题与键盘弹出问题 2026-02-09 21:16:48 +08:00
82ba753b8b Merge remote-tracking branch 'refs/remotes/origin/master' 2026-02-09 20:07:47 +08:00
159d81ed12 fix: 详情管理功能按钮逻辑修改 2026-02-09 20:07:05 +08:00
22965eedf3 个人简介和昵称修改后显示底部导航 2026-02-09 16:36:15 +08:00
49935dd049 优化省市区和占位图片 2026-02-09 13:53:19 +08:00
张成
cab90aa1cb 1 2026-02-09 13:25:13 +08:00
632da5112d feat: 删除小猫 2026-02-09 09:49:43 +08:00
28955e9da1 fix: 修改分享初始化逻辑、去除小猫图 2026-02-08 23:58:37 +08:00
70a66fabdc Merge branch 'feat/liujie' 2026-02-08 22:57:48 +08:00
c47ebce43c fix: 修复取消活动后还可以编辑和取消、详情页参与者卡片展示NTRP 等级、生成海报的图片质量降低到1M以下, 详情页海报与测试结果页海报 2026-02-08 22:57:35 +08:00
b0f4b5713d Merge branch 'master' into feat/liujie 2026-02-08 21:25:19 +08:00
f7f10f5d15 查询下载账单 2026-02-08 16:03:08 +08:00
李瑞
2bcdd93479 Merge branch 'feat/juguohong/20260206' 2026-02-08 12:46:27 +08:00
李瑞
af2c472030 处理图片顺序 2026-02-08 12:46:01 +08:00
张成
8d0ed5b1b3 1 2026-02-08 12:36:21 +08:00
张成
e99986c52a 修改审核不通过的问题 2026-02-08 12:29:48 +08:00
张成
4b2f6707cc 1 2026-02-08 12:18:04 +08:00
张成
a019fe473b Merge branch 'master' of https://git.bimwe.com/bimwe/mini-programs 2026-02-08 12:14:11 +08:00
张成
1d0d2edaa2 修改项目 build 结构 2026-02-08 12:14:10 +08:00
5926e096b5 图片样式优化 2026-02-08 12:09:46 +08:00
筱野
e07f2ad2d1 解决按钮问题与键盘弹出问题 2026-02-07 23:37:28 +08:00
筱野
bfc6a149f0 修改日期问题弹出问题 2026-02-07 22:15:14 +08:00
李瑞
6f73bb6d99 Merge branch 'feat/juguohong/20260206' 2026-02-07 22:09:39 +08:00
李瑞
744169fe34 处理地点展示样式 2026-02-07 22:08:30 +08:00
54b7a27af5 Merge branch 'feat/liujie' 2026-02-07 20:25:51 +08:00
396ff4a347 fix: 修改取值 2026-02-07 20:25:37 +08:00
张成
b732bd361e Merge branch 'master' of https://git.bimwe.com/bimwe/mini-programs 2026-02-07 20:24:50 +08:00
张成
5146894d92 1 2026-02-07 20:24:49 +08:00
张成
07cf8e884e 1 2026-02-07 20:24:45 +08:00
5416ea127c Merge branch 'master' into feat/liujie 2026-02-07 20:19:23 +08:00
a7bc517fae Merge branch 'feat/liujie' 2026-02-07 20:19:15 +08:00
16b38539f6 fix: 修改取值 2026-02-07 20:19:04 +08:00
张成
0d46311bbc 1 2026-02-07 19:27:58 +08:00
e884b1f258 Merge branch 'master' into feat/liujie 2026-02-07 18:13:23 +08:00
84159a4835 Merge branch 'feat/liujie' 2026-02-07 18:13:15 +08:00
2acee85dd5 fix: 修复分享弹窗打开逻辑 2026-02-07 18:13:00 +08:00
ba72e0ec97 Merge branch 'master' into feat/liujie 2026-02-07 18:11:15 +08:00
张成
32f5339cc2 Merge branch 'master' of https://git.bimwe.com/bimwe/mini-programs 2026-02-07 18:11:14 +08:00
张成
2cbbc7f432 1 2026-02-07 18:11:12 +08:00
694b00e011 Merge branch 'feat/liujie' 2026-02-07 18:11:01 +08:00
87eaa31cef fix: 修复发布后分享弹窗打开问题 2026-02-07 18:10:44 +08:00
张成
f131c9896d 修改oss 路径 2026-02-07 18:07:33 +08:00
b08f3325e6 Merge branch 'feat/liujie' 2026-02-07 17:38:28 +08:00
ff864fe64d feat: 修改两处海报logo-text图片、修改创建球局后跳转详情页打开分享弹窗位置、修改分享二维码接口取值、修改ntrp修改弹窗的初始值、增加复制链接功能 2026-02-07 17:37:07 +08:00
张成
da0ae6046c 1 2026-02-07 16:45:10 +08:00
42025d49f8 Merge branch 'master' into feat/liujie 2026-02-07 16:03:34 +08:00
张成
536619ebfc 1 2026-02-07 13:08:28 +08:00
张成
5a10c73adf 修复一堆问题 2026-02-07 11:56:43 +08:00
133 changed files with 3870 additions and 2088 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

4
.gitignore vendored
View File

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

View File

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

View File

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

View File

@@ -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
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];
}

128
config/env.ts Normal file
View 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()

View File

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

View File

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

View File

@@ -2,8 +2,7 @@
"miniprogramRoot": "dist/",
"projectname": "playBallTogether",
"description": "playBallTogether",
"appid": "wx915ecf6c01bea4ec",
"appid": "wx815b533167eb7b53",
"setting": {
"urlCheck": true,
"es6": true,
@@ -47,4 +46,4 @@
"simulatorType": "wechat",
"simulatorPluginLibVersion": {},
"condition": {}
}
}

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.warn(`[sync-project-config] Unknown APP_ENV: ${appEnv}`);
process.exit(1);
}
const projectConfigPath = path.resolve(__dirname, '../project.config.json');
const projectConfigRaw = fs.readFileSync(projectConfigPath, 'utf-8');
const projectConfig = JSON.parse(projectConfigRaw);
projectConfig.appid = config.appid;
fs.writeFileSync(projectConfigPath, JSON.stringify(projectConfig, null, 2) + '\n', 'utf-8');
console.log(`[sync-project-config] project.config.json appid -> ${config.appid} (${appEnv})`);

View File

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

View File

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

View File

@@ -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;

View 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

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

@@ -134,7 +134,7 @@ const DistanceQuickFilterV2 = (props) => {
throw new Error('获取位置信息失败');
}
} catch (error: any) {
console.error('重新定位失败:', error);
console.warn('重新定位失败:', error);
(Taro as any).showToast({
title: error?.message || '定位失败,请检查定位权限',
icon: 'none',

View File

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

View File

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

View File

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

View File

@@ -16,8 +16,8 @@ const CancelPopup = forwardRef((props, ref) => {
const { detail } = props;
const [visible, setVisible] = useState(false);
const [cancelReason, setCancelReason] = useState("");
const [inputFocus, setInputFocus] = useState(false);
const onFinish = useRef(null);
const inputRef = useRef(null);
const { current_players, participants = [], publisher_id } = detail;
const realParticipants = participants
@@ -32,16 +32,15 @@ const CancelPopup = forwardRef((props, ref) => {
show: (onAct) => {
onFinish.current = onAct;
setVisible(true);
// 使用 requestAnimationFrame 替代 setTimeout(0),性能更好
requestAnimationFrame(() => {
requestAnimationFrame(() => {
inputRef.current && inputRef.current.focus();
});
// 使用 Taro.nextTick 确保在下一个渲染周期后聚焦
Taro.nextTick(() => {
setInputFocus(true);
});
},
}));
function onClose() {
setInputFocus(false);
setVisible(false);
setCancelReason("");
}
@@ -85,13 +84,13 @@ const CancelPopup = forwardRef((props, ref) => {
{hasOtherJoin && (
<View className={styles.cancelReason}>
<Input
ref={inputRef}
className={styles.input}
placeholder="请输入取消理由"
focus
focus={inputFocus}
value={cancelReason}
onInput={(e) => setCancelReason(e.detail.value)}
maxlength={100}
onBlur={() => setInputFocus(false)}
/>
</View>
)}
@@ -186,10 +185,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 +207,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 +217,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>

View File

@@ -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}

View File

@@ -146,7 +146,7 @@ const HomeNavbar = (props: IProps) => {
console.log(`[HomeNavbar] 距离上次选择"继续浏览"还不到2小时剩余时间: ${Math.ceil((TWO_HOURS_MS - time_diff) / 1000 / 60)}分钟`);
return false;
} catch (error) {
console.error('[HomeNavbar] 检查定位弹窗显示条件失败:', error);
console.warn('[HomeNavbar] 检查定位弹窗显示条件失败:', error);
return true; // 出错时默认显示
}
};
@@ -239,7 +239,7 @@ const HomeNavbar = (props: IProps) => {
// console.log(`距离上次选择"继续浏览"还不到2小时剩余时间: ${Math.ceil((TWO_HOURS_MS - time_diff) / 1000 / 60)}分钟`);
// return false;
// } catch (error) {
// console.error('检查定位弹窗显示条件失败:', error);
// console.warn('检查定位弹窗显示条件失败:', error);
// return true; // 出错时默认显示
// }
// };
@@ -276,7 +276,7 @@ const HomeNavbar = (props: IProps) => {
(Taro as any).setStorageSync(CITY_CHANGE_TIME_KEY, current_time);
console.log(`[LocationDialog] 已记录用户切换城市的时间2小时内不再提示`);
} catch (error) {
console.error('保存城市切换时间失败:', error);
console.warn('保存城市切换时间失败:', error);
}
console.log("切换到用户详情中的位置信息并更新缓存:", detectedProvince);
@@ -304,7 +304,7 @@ const HomeNavbar = (props: IProps) => {
(Taro as any).setStorageSync(LOCATION_DIALOG_DISMISS_TIME_KEY, current_time);
console.log(`[LocationDialog] 已记录用户选择"继续浏览"的时间2小时内不再提示`);
} catch (error) {
console.error('保存定位弹窗关闭时间失败:', error);
console.warn('保存定位弹窗关闭时间失败:', error);
}
// 关闭弹窗
@@ -409,7 +409,7 @@ const HomeNavbar = (props: IProps) => {
(Taro as any).setStorageSync(CITY_CHANGE_TIME_KEY, current_time);
console.log("已保存城市到缓存并记录切换时间:", _newArea, current_time);
} catch (error) {
console.error("保存城市缓存失败:", error);
console.warn("保存城市缓存失败:", error);
}
// 先调用列表接口(会使用更新后的 state.area

View File

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

View File

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

View File

@@ -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,
@@ -26,7 +27,7 @@ const ListCard: React.FC<ListCardProps> = ({
key,
participants, // 参与者图片
venue_image_list, // 场馆图片
venue_description,
location_name = '', // 场馆方
game_type, // 球局类型
}) => {
// 参与者要前三个数据
@@ -45,7 +46,7 @@ const ListCard: React.FC<ListCardProps> = ({
className="image"
mode="aspectFill"
lazyLoad
defaultSource="https://images.unsplash.com/photo-1554068865-24cecd4e34b8?w=200&h=200&fit=crop&crop=center"
defaultSource={`${OSS_BASE}/front/ball/images/publish-empty-card.png`}
/>
);
};
@@ -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,22 +257,16 @@ const ListCard: React.FC<ListCardProps> = ({
/>
{/* <Text className="smoothTitle">{game_type}</Text> */}
</View>
{
venue_description && (<View className="line" />)
}
{
venue_description &&
(
<View className="localAreaContainer">
<View className="localAreaTitle">:</View>
<View className="localAreaWrapper">
<Image className="localArea" src={venueImage} />
<Text className="localAreaText">{venue_description}</Text>
</View>
{location_name && <View className="line" />}
{location_name && (
<View className="localAreaContainer">
<View className="localAreaTitle">:</View>
<View className="localAreaWrapper">
{venueImage && <Image className="localArea" src={venueImage} />}
<Text className="localAreaText">{location_name}</Text>
</View>
)
}
</View>
)}
</View>
)}
</View>

View File

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

View File

@@ -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>}

View File

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

View File

@@ -8,7 +8,7 @@ import {
useLastTestResult,
} from "@/store/userStore";
// import { getCurrentFullPath } from "@/utils";
import { OSS_BASE_URL } from "@/config/api";
import { OSS_BASE } from "@/config/api";
import { StageType } from "@/services/evaluateService";
import { waitForAuthInit } from "@/utils/authInit";
import DocCopy from "@/static/ntrp/ntrp_doc_copy.svg";
@@ -148,7 +148,7 @@ function NTRPTestEntryCard(props: {
<View
className={styles.lines}
style={{
backgroundImage: `url(${OSS_BASE_URL}/images/215f1ce1-be52-4a92-8250-5a4a69e7f2b3.png)`,
backgroundImage: `url(${OSS_BASE}/front/ball/images/215f1ce1-be52-4a92-8250-5a4a69e7f2b3.png)`,
}}
/>
<View className={styles.desc}>
@@ -188,7 +188,7 @@ function NTRPTestEntryCard(props: {
<View
className={styles.lines}
style={{
backgroundImage: `url(${OSS_BASE_URL}/images/215f1ce1-be52-4a92-8250-5a4a69e7f2b3.png)`,
backgroundImage: `url(${OSS_BASE}/front/ball/images/215f1ce1-be52-4a92-8250-5a4a69e7f2b3.png)`,
}}
/>
<View className={styles.desc}>

View File

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

View File

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

View File

@@ -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);

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,14 +6,19 @@ 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,
useProfessions,
useNtrpLevels,
} from "@/store/pickerOptionsStore";
import { formatNtrpDisplay } from "@/utils/helper";
import { formatNtrpDisplay, getBackendErrorMsg } from "@/utils/helper";
import { useGlobalState } from "@/store/global";
// 用户信息接口
@@ -69,7 +74,7 @@ const on_edit = () => {
// 用户信息卡片组件
const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
editable = true,
user_info,
user_info: user_info_prop,
is_current_user,
is_following = false,
collapseProfile,
@@ -80,9 +85,13 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
set_user_info,
onTab,
}) => {
const global_user_info = useUserInfo();
// 查看别人页面时用传入的 user_info个人页用全局 store
const user_info = is_current_user ? global_user_info : (user_info_prop ?? global_user_info);
const nickname_change_status = useNicknameChangeStatus();
const { setShowGuideBar } = useGlobalState();
const { updateUserInfo, updateNickname, fetchLastTestResult } = useUserActions();
const { updateUserInfo, updateNickname, fetchLastTestResult } =
useUserActions();
const ntrpLevels = useNtrpLevels();
// 使用全局状态中的测试结果,避免重复调用接口
const lastTestResult = useLastTestResult();
@@ -91,18 +100,16 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
const prevUserInfoRef = useRef<Partial<UserInfoType>>();
useEffect(() => {
// 只在 user_info 真正变化时打印(通过 JSON 序列化比较)
const prevStr = JSON.stringify(prevUserInfoRef.current);
const currentStr = JSON.stringify(user_info);
if (prevStr !== currentStr) {
console.log("UserInfoCard 用户信息变化:", user_info);
prevUserInfoRef.current = user_info;
}
// 如果全局状态中没有测试结果,则调用接口(使用请求锁,多个组件同时调用时只会请求一次)
if (!lastTestResult && user_info?.id) {
// 仅当前用户才拉取 NTRP 测试结果
if (is_current_user && !lastTestResult && user_info?.id) {
fetchLastTestResult();
}
}, [user_info?.id, lastTestResult, fetchLastTestResult]);
}, [user_info?.id, lastTestResult, fetchLastTestResult, is_current_user]);
// 从全局状态中获取测试状态
const ntrpTested = lastTestResult?.has_test_in_last_month || false;
@@ -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,
]);
// 职业数据
@@ -237,10 +250,10 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
icon: "success",
});
} catch (error) {
console.error("保存失败:", error);
console.warn("保存失败:", error);
Taro.showToast({
title: "保存失败",
icon: "error",
title: getBackendErrorMsg(error, "保存失败"),
icon: "none",
});
}
};
@@ -280,10 +293,10 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
icon: "success",
});
} catch (error) {
console.error("保存失败:", error);
console.warn("保存失败:", error);
Taro.showToast({
title: "保存失败",
icon: "error",
title: getBackendErrorMsg(error, "保存失败"),
icon: "none",
});
}
};
@@ -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">
{/* 头像和基本信息 */}
@@ -406,11 +418,11 @@ const UserInfoCardComponent: React.FC<UserInfoCardProps> = ({
<View className="stats_section">
<View
className="stats_container"
// style={{
// marginBottom: `${
// collapseProfile && setMarginBottom ? "16px" : "unset"
// }`,
// }}
// style={{
// marginBottom: `${
// collapseProfile && setMarginBottom ? "16px" : "unset"
// }`,
// }}
>
<View
className="stat_item clickable"
@@ -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,9 +873,8 @@ 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")}
>
<Text className="tab_text">{participated_text}</Text>

View File

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

View File

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

View File

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

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

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

View File

@@ -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.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_GAME_PLAY: require("@/static/list/icon_game_type.svg"),
};

View File

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

View File

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

View File

@@ -48,8 +48,8 @@ function genRecommendGames(games, location, avatar) {
formatNtrpDisplay(skill_level_max) || "-"
}`
: skill_level_min === "1"
? "无要求"
: `${formatNtrpDisplay(skill_level_min)}以上`,
? "无要求"
: `${formatNtrpDisplay(skill_level_min)}以上`,
playType: play_type,
};
});
@@ -220,7 +220,9 @@ export default function OrganizerInfo(props) {
>
<Text>{game.venue}</Text>
<Text>·</Text>
<Text>{game.venueType}</Text>
<Text style={{ whiteSpace: "nowrap" }}>
{game.venueType}
</Text>
<Text>·</Text>
<Text>{game.distance}</Text>
</View>
@@ -247,7 +249,7 @@ export default function OrganizerInfo(props) {
styles[
"recommend-games-list-item-addon-message-applications"
],
styles.joinMsg
styles.joinMsg,
)}
>
<Text></Text>

View File

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

View File

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

View File

@@ -24,12 +24,12 @@ function isFull(counts) {
} = counts;
if (
max_players === current_players &&
current_players >= max_players &&
is_substitute_supported === IsSubstituteSupported.NOTSUPPORT
) {
return true;
} else if (
max_players === current_players &&
current_players >= max_players &&
is_substitute_supported === IsSubstituteSupported.SUPPORT
) {
return max_substitute_players === current_substitute_count;
@@ -45,7 +45,7 @@ function RmbIcon() {
function matchNtrpRequestment(
target?: string,
min?: string,
max?: string
max?: string,
): boolean {
// 目标值为空或 undefined
if (!target?.trim()) return true;
@@ -123,7 +123,7 @@ export default function StickyButton(props) {
Taro.navigateTo({
url: `/login_pages/index/index?redirect=${encodeURIComponent(
fullPath
fullPath,
)}`,
});
}
@@ -138,7 +138,7 @@ export default function StickyButton(props) {
const matchNtrpReq = matchNtrpRequestment(
ntrp_level,
skill_level_min,
skill_level_max
skill_level_max,
);
const gameManageRef = useRef();
@@ -173,7 +173,7 @@ export default function StickyButton(props) {
}, [getCommentCount]);
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 } {
@@ -271,7 +271,7 @@ export default function StickyButton(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}`,
);
}
}),
@@ -387,7 +387,7 @@ export default function StickyButton(props) {
<View
className={classnames(
styles["detail-main-action"],
available ? "" : styles.disabled
available ? "" : styles.disabled,
)}
>
<View

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ import Taro, { useRouter } from "@tarojs/taro";
import classnames from "classnames";
import dayjs from "dayjs";
import "dayjs/locale/zh-cn";
import { generatePosterImage, base64ToTempFilePath, delay } from "@/utils";
import { generatePosterImage, delay } from "@/utils";
import { withAuth } from "@/components";
import GeneralNavbar from "@/components/GeneralNavbar";
import DetailService from "@/services/detailService";
@@ -16,7 +16,7 @@ import { useUserActions } from "@/store/userStore";
import { DayOfWeekMap } from "../detail/config";
import { genNTRPRequirementText } from "@/utils/helper";
import { waitForAuthInit } from "@/utils/authInit";
import { OSS_BASE_URL } from "@/config/api";
import { OSS_BASE } from "@/config/api";
import styles from "./index.module.scss";
dayjs.locale("zh-cn");
@@ -59,10 +59,11 @@ function SharePoster(props) {
page: "game_pages/detail/index",
scene: `id=${id}`,
});
const qrCodeUrl = await base64ToTempFilePath(
qrCodeUrlRes.data.qr_code_base64
);
debugger
const qrCodeUrl = qrCodeUrlRes.data.ossPath;
// const qrCodeUrl = await base64ToTempFilePath(
// qrCodeUrlRes.data.qr_code_base64
// );
// debugger
await delay(100);
const url = await generatePosterImage({
playType: play_type,
@@ -70,7 +71,7 @@ function SharePoster(props) {
mainCoursal:
image_list[0] && image_list[0].startsWith("http")
? image_list[0]
: `${OSS_BASE_URL}/images/0621b8cf-f7d6-43ad-b852-7dc39f29a782.png`,
: `${OSS_BASE}/front/ball/images/0621b8cf-f7d6-43ad-b852-7dc39f29a782.png`,
nickname,
avatarUrl: avatar_url,
title,

View File

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

View File

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

View File

@@ -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}>

View File

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

View File

@@ -67,6 +67,8 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
} = store;
const supportedCitiesList = useDictionaryStore((s) => s.getDictionaryValue('supported_cities', ['上海市'])) || [];
// 首页是否展示二维码,由 getDictionaryManyKey 的 show_home_qrcode 控制,默认 true 保持原样
const showHomeQrcode = useDictionaryStore((s) => s.getDictionaryValue('show_home_qrcode', true));
const {
isShowFilterPopup,
@@ -227,18 +229,16 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
useEffect(() => {
// 分批异步执行初始化操作,避免阻塞首屏渲染
// 1. 立即执行:获取城市、二维码和行政区列表(轻量操作)
getCities();
getCityQrCode();
getDistricts(); // 新增:获取行政区列表
// 只有当页面激活时才加载位置和列表数据
if (showHomeQrcode) getCityQrCode();
getDistricts();
if (isActive) {
getLocation().catch((error) => {
console.error('获取位置信息失败:', error);
console.warn('获取位置信息失败:', error);
});
}
}, [isActive]);
}, [isActive, showHomeQrcode]);
// 记录上一次的城市,用于检测城市变化
const prevAreaRef = useRef<[string, string] | null>(null);
@@ -309,7 +309,7 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
lastLoadedAreaRef.current = [...currentArea] as [string, string];
}
} catch (error) {
console.error("重新加载数据失败:", error);
console.warn("重新加载数据失败:", error);
}
}, delayMs);
prevIsActiveRef.current = isActive;
@@ -375,7 +375,7 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
await updateUserLocation(location.latitude, location.longitude, isFirstCall);
hasUpdatedLocationRef.current = true;
} catch (error) {
console.error("更新用户位置失败:", error);
console.warn("更新用户位置失败:", error);
}
}
// 先调用列表接口
@@ -462,7 +462,7 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
await getMatchesData();
await fetchGetGamesCount();
} catch (error) {
console.error("刷新列表失败:", error);
console.warn("刷新列表失败:", error);
}
};
@@ -477,7 +477,7 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
const { fetchDictionary } = useDictionaryStore.getState();
await fetchDictionary();
} catch (error) {
console.error("初始化字典数据失败:", error);
console.warn("初始化字典数据失败:", error);
}
};
@@ -537,7 +537,13 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
return (
<>
{shouldShowNoGames ? (
renderCityQrcode()
showHomeQrcode ? (
renderCityQrcode()
) : (
<View className={styles.cqContainer}>
<Text></Text>
</View>
)
) : (
<View ref={scrollContextRef}>
<View className={styles.listPage} style={{ paddingTop: totalHeight }}>
@@ -610,7 +616,7 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
try {
await loadMoreMatches();
} catch (error) {
console.error("加载更多失败:", error);
console.warn("加载更多失败:", error);
} finally {
loadingMoreRef.current = false;
}
@@ -627,6 +633,7 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
reload={refreshMatches}
loadMoreMatches={loadMoreMatches}
evaluateFlag
enableHomeCards
/>
</ScrollView>
</View>

View File

@@ -11,12 +11,16 @@ import { EvaluateScene } from "@/store/evaluateStore";
import { useUserInfo, useUserActions } from "@/store/userStore";
import { usePickerOption } from "@/store/pickerOptionsStore";
import { useGlobalState } from "@/store/global";
import { useListState } from "@/store/listStore";
import { useDictionaryStore } from "@/store/dictionaryStore";
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 || {};
@@ -37,6 +41,11 @@ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({ isActive = true }
const [hasLoaded, setHasLoaded] = useState(false); // 记录是否已经加载过数据
const [collapseProfile, setCollapseProfile] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const { area } = useListState();
const supportedCitiesList =
useDictionaryStore((s) => s.getDictionaryValue("supported_cities")) || [];
useEffect(() => {
pickerOption.getCities();
@@ -64,20 +73,21 @@ 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.unshift(game);
}
console.log("notEndGames", notEndGames);
return { notEndGames, finishedGames };
},
[]
);
@@ -94,6 +104,7 @@ 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() -
@@ -101,10 +112,12 @@ 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) {
console.error("加载球局数据失败:", error);
console.warn("加载球局数据失败:", error);
}
}, [active_tab, user_info, classifyGameRecords]);
@@ -137,7 +150,7 @@ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({ isActive = true }
duration: 1500,
});
} catch (error) {
console.error("关注操作失败:", error);
console.warn("关注操作失败:", error);
(Taro as any).showToast({
title: "操作失败,请重试",
icon: "error",
@@ -147,6 +160,16 @@ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({ isActive = true }
};
const goPublish = () => {
const [_, address] = area;
if (!supportedCitiesList.includes(address)) {
(Taro as any).showModal({
title: "提示",
content: "该城市尚未开放,您可加入社群或切换城市",
showCancel: false,
confirmText: "知道了",
});
return;
}
(Taro as any).navigateTo({
url: "/publish_pages/publishBall/index",
});
@@ -169,6 +192,23 @@ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({ isActive = true }
setActiveTab(tab);
};
// 下拉刷新:刷新用户信息和球局数据
const handle_refresh = async () => {
setRefreshing(true);
try {
await Promise.all([fetchUserInfo(), load_game_data()]);
} catch (error) {
console.warn("刷新失败:", error);
(Taro as any).showToast({
title: "刷新失败,请重试",
icon: "none",
duration: 2000,
});
} finally {
setRefreshing(false);
}
};
// const handleScroll = (event: any) => {
// const scrollData = event.detail;
// setCollapseProfile(scrollData.scrollTop > 1);
@@ -178,6 +218,9 @@ const MyselfPageContent: React.FC<MyselfPageContentProps> = ({ isActive = true }
<ScrollView
scrollY
refresherBackground="#FAFAFA"
refresherEnabled
refresherTriggered={refreshing}
onRefresherRefresh={handle_refresh}
className={styles.myselfPage}
>
<View
@@ -267,9 +310,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>
@@ -291,9 +333,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>

View File

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

View File

@@ -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 () => {
@@ -55,7 +68,7 @@ const MainPage: React.FC = () => {
return;
}
} catch (error) {
console.error("微信授权异常:", error);
console.warn("微信授权异常:", error);
setAuthErrorMessage("微信授权失败,请重试");
setShowAuthError(true);
return;
@@ -67,14 +80,8 @@ const MainPage: React.FC = () => {
try {
await fetchUserInfo();
await checkNicknameChangeStatus();
// 启动时预取 Banner 字典(与业务无强依赖,失败不影响主流程)
try {
await useDictionaryStore.getState().fetchBannerDictionary();
} catch (e) {
console.error("预取 Banner 字典失败:", e);
}
} catch (error) {
console.error("获取用户信息失败:", error);
console.warn("获取用户信息失败:", error);
}
}
};
@@ -159,6 +166,43 @@ const MainPage: React.FC = () => {
[]
);
// 分享:按 tab 用 map 对应文案与分享图
const share_config: Record<
TabType,
{ title: string; image_path: string; query: string }
> = {
list: {
title: "有你就有场,发现身边好球友和好球局",
image_path: "system/share_home.png",
query: "?tab=list",
},
personal: {
title: "快来有场,约我一起打网球~",
image_path: "system/share_self.png",
query: "?tab=personal",
},
message: {
title: "查看球友动态",
image_path: "system/share_home.png",
query: "?tab=message",
},
};
useShareAppMessage(() => {
const config = share_config[currentTab] ?? {
title: "约球",
image_path: "system/share_home.png",
query: "",
};
// const imageUrl = OSS_BASE
// ? `${OSS_BASE.replace(/\/$/, "")}/${config.image_path}`
// : "";
return {
title: config.title,
path: "/main_pages/index" + config.query,
// imageUrl,
};
});
// 滚动到顶部
const scrollToTop = useCallback(() => {
// 如果当前是列表页,触发列表页内部滚动

View File

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

View File

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

View File

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

View File

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

View File

@@ -77,7 +77,7 @@ const CommentReply = () => {
if (allCommentIds.length > 0) {
// 使用统一接口标记已读传入所有评论ID
messageService.markAsRead('comment', allCommentIds).catch(e => {
console.error("标记评论已读失败:", e);
console.warn("标记评论已读失败:", e);
});
}
}
@@ -221,7 +221,7 @@ const CommentReply = () => {
if (allCommentIds.length > 0) {
messageService.markAsRead('comment', allCommentIds).catch(e => {
console.error("标记评论已读失败:", e);
console.warn("标记评论已读失败:", e);
});
}
}
@@ -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)}
/>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,13 @@
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 { normalize_address } from '@/utils/locationUtils'
import './StadiumDetail.scss'
@@ -69,12 +71,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: '场地类型',
@@ -140,14 +146,14 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
setFormData({
...formData,
name: res.name,
address: res.address,
address: normalize_address(res.address || ''),
latitude: res.latitude,
longitude: res.longitude,
istance: null
})
},
fail: (err: { errMsg: string }) => {
console.error('选择位置失败:', err)
console.warn('选择位置失败:', err)
const { errMsg } = err || {};
if (!errMsg.includes('fail cancel')) {
Taro.showToast({
@@ -171,19 +177,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 +218,7 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
refresherBackground="#FAFAFA"
scrollY={!openPicker}
scrollTop={scrollTop}
style={{ maxHeight: scrollMaxHeight }}
>
{/* 已选球场 */}
<View
@@ -203,7 +231,7 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
<View className='stadium-item-right'>
<View className='stadium-name'>{formData.name}</View>
<View className='stadium-address'>
<Text>{calculateDistance(formData.istance || null)} · </Text>
<Text>{calculateDistance(formData.istance || null) + ' · '}</Text>
<Text>{formData.address}</Text>
<Image src={images.ICON_ARRORW_SMALL} className='stadium-map-icon' />
</View>
@@ -235,11 +263,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 }))}
/>

View File

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

View File

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

View File

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

View File

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

View File

@@ -51,7 +51,7 @@ export class FollowService {
throw new Error(response.message || '获取互关列表失败');
}
} catch (error) {
console.error('获取互关列表失败:', error);
console.warn('获取互关列表失败:', error);
return { list: [], total: 0 };
}
}
@@ -79,7 +79,7 @@ export class FollowService {
throw new Error(response.message || '获取粉丝列表失败');
}
} catch (error) {
console.error('获取粉丝列表失败:', error);
console.warn('获取粉丝列表失败:', error);
return { list: [], total: 0 };
}
}
@@ -107,7 +107,7 @@ export class FollowService {
throw new Error(response.message || '获取新增粉丝列表失败');
}
} catch (error) {
console.error('获取新增粉丝列表失败:', error);
console.warn('获取新增粉丝列表失败:', error);
return { list: [], total: 0 };
}
}
@@ -135,7 +135,7 @@ export class FollowService {
throw new Error(response.message || '获取关注列表失败');
}
} catch (error) {
console.error('获取关注列表失败:', error);
console.warn('获取关注列表失败:', error);
return { list: [], total: 0 };
}
}
@@ -178,7 +178,7 @@ export class FollowService {
throw new Error(response.message || '关注失败');
}
} catch (error) {
console.error('关注失败:', error);
console.warn('关注失败:', error);
throw error;
}
}
@@ -201,7 +201,7 @@ export class FollowService {
throw new Error(response.message || '取消关注失败');
}
} catch (error) {
console.error('取消关注失败:', error);
console.warn('取消关注失败:', error);
throw error;
}
}
@@ -231,7 +231,7 @@ export class FollowService {
throw new Error(response.message || '获取推荐用户失败');
}
} catch (error) {
console.error('获取推荐用户失败:', error);
console.warn('获取推荐用户失败:', error);
return { list: [], total: 0 };
}
}
@@ -251,7 +251,7 @@ export class FollowService {
return 'none';
}
} catch (error) {
console.error('检查关注状态失败:', error);
console.warn('检查关注状态失败:', error);
return 'none';
}
}
@@ -269,7 +269,7 @@ export class FollowService {
throw new Error(response.message || '屏蔽推荐用户失败');
}
} catch (error) {
console.error('屏蔽推荐用户失败:', error);
console.warn('屏蔽推荐用户失败:', error);
throw error;
}
}

View File

@@ -129,23 +129,34 @@ class HttpService {
// 隐藏loading支持多个并发请求
private hideLoading(): void {
this.loadingCount = Math.max(0, this.loadingCount - 1)
try {
this.loadingCount = Math.max(0, this.loadingCount - 1)
// 只有所有请求都完成时才隐藏loading
if (this.loadingCount === 0) {
// 清除之前的延时器
if (this.hideLoadingTimer) {
clearTimeout(this.hideLoadingTimer)
this.hideLoadingTimer = null
// 只有所有请求都完成时才隐藏loading
if (this.loadingCount === 0) {
// 清除之前的延时器
if (this.hideLoadingTimer) {
clearTimeout(this.hideLoadingTimer)
this.hideLoadingTimer = null
}
// 延时 800ms 后隐藏 loading避免频繁切换捕获 hideLoading 失败(如 toast 已被关闭)避免报错打断流程
this.hideLoadingTimer = setTimeout(() => {
try {
Taro.hideLoading()
} catch (e) {
// 忽略 "toast can't be found" 等,避免发布后分享等流程报错
}
this.currentLoadingText = ''
this.hideLoadingTimer = null
}, 800)
}
// 延时300ms后隐藏loading避免频繁切换
this.hideLoadingTimer = setTimeout(() => {
Taro.hideLoading()
this.currentLoadingText = ''
this.hideLoadingTimer = null
}, 800)
}
catch (e) {
console.warn(e)
}
}
// 处理响应
@@ -175,7 +186,7 @@ class HttpService {
url: '/login_pages/index/index'
})
reject(new Error('用户不存在'))
return response.data
return response.data
}
@@ -187,7 +198,7 @@ class HttpService {
} else {
reject(response.data)
}
return response.data
return response.data
}
}

View File

@@ -27,7 +27,7 @@ export const getGamesList = async (params?: Record<string, any>) => {
// const fetchApi = isIntegrate ? '/games/integrate_list' : '/games/list'
return httpService.post('/games/list', params, { showLoading: false })
} catch (error) {
console.error("列表数据获取失败:", error);
console.warn("列表数据获取失败:", error);
throw error;
}
};
@@ -41,7 +41,7 @@ export const getGamesIntegrateList = async (params?: Record<string, any>) => {
try {
return httpService.post('/games/integrate_list', params, { showLoading: false })
} catch (error) {
console.error("列表数据获取失败:", error);
console.warn("列表数据获取失败:", error);
throw error;
}
};
@@ -55,7 +55,7 @@ export const getGamesCount = async (params?: Record<string, any>) => {
try {
return httpService.post('/games/count', params, { showLoading: false })
} catch (error) {
console.error("列表数量获取失败:", error);
console.warn("列表数量获取失败:", error);
throw error;
}
};
@@ -71,7 +71,7 @@ export const getSearchHistory = async (params) => {
return httpService.post('/games/search_history', params, { showLoading: false })
} catch (error) {
// 捕获并打印错误信息
console.error("历史记录获取失败:", error);
console.warn("历史记录获取失败:", error);
// 抛出错误以便上层处理
throw error;
}
@@ -87,7 +87,7 @@ export const clearHistory = async (params) => {
return httpService.post('/games/search_history/delete_all', params, { showLoading: false })
} catch (error) {
// 捕获并打印错误信息
console.error("清除历史记录失败:", error);
console.warn("清除历史记录失败:", error);
// 抛出错误以便上层处理
throw error;
}
@@ -104,7 +104,7 @@ export const searchSuggestion = async (params) => {
return httpService.post('/games/search_recommendations', params)
} catch (error) {
// 捕获并打印错误信息
console.error("搜索建议获取失败:", error);
console.warn("搜索建议获取失败:", error);
// 抛出错误以便上层处理
throw error;
}
@@ -116,7 +116,7 @@ export const getCities = async () => {
return httpService.post('/cities/tree', {})
} catch (error) {
// 捕获并打印错误信息
console.error("城市列表获取失败:", error);
console.warn("城市列表获取失败:", error);
// 抛出错误以便上层处理
throw error;
}
@@ -127,7 +127,7 @@ export const getCityQrCode = async () => {
return httpService.post('/hot_city_qr/list', {})
} catch (error) {
// 捕获并打印错误信息
console.error("城市二维码获取失败:", error);
console.warn("城市二维码获取失败:", error);
// 抛出错误以便上层处理
throw error;
}
@@ -140,7 +140,7 @@ export const getDistricts = async (params: { province: string; city: string }) =
return httpService.post('/cities/cities', params)
} catch (error) {
// 捕获并打印错误信息
console.error("行政区列表获取失败:", error);
console.warn("行政区列表获取失败:", error);
// 抛出错误以便上层处理
throw error;
}

View File

@@ -122,7 +122,7 @@ export const wechat_auth_login = async (
};
}
} catch (error) {
console.error("微信授权登录失败:", error);
console.warn("微信授权登录失败:", error);
return {
success: false,
message: "微信授权失败,请重试",
@@ -160,7 +160,7 @@ export const phone_auth_login = async (
await useUser.getState().fetchUserInfo();
await useUser.getState().checkNicknameChangeStatus();
} catch (error) {
console.error("更新用户信息到 store 失败:", error);
console.warn("更新用户信息到 store 失败:", error);
}
return {
@@ -178,7 +178,7 @@ export const phone_auth_login = async (
};
}
} catch (error) {
console.error("手机号登录失败:", error);
console.warn("手机号登录失败:", error);
return {
success: false,
message: error.message,
@@ -206,7 +206,7 @@ export const send_sms_code = async (phone: string): Promise<SmsResponse> => {
};
}
} catch (error) {
console.error("发送短信失败:", error);
console.warn("发送短信失败:", error);
return {
success: false,
message: error.message,
@@ -232,7 +232,7 @@ export const verify_sms_code = async (
user_info: response.data?.userInfo,
};
} catch (error) {
console.error("验证验证码失败:", error);
console.warn("验证验证码失败:", error);
return {
success: false,
message: error.message,
@@ -255,7 +255,7 @@ export const save_login_state = (token: string, user_info: WechatUserInfo) => {
Taro.setStorageSync("is_logged_in", true);
Taro.setStorageSync("login_time", Date.now());
} catch (error) {
console.error("保存登录状态失败:", error);
console.warn("保存登录状态失败:", error);
}
};
@@ -263,14 +263,14 @@ 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");
Taro.removeStorageSync("is_logged_in");
Taro.removeStorageSync("login_time");
} catch (error) {
console.error("清除登录状态失败:", error);
console.warn("清除登录状态失败:", error);
}
};
@@ -374,7 +374,7 @@ export const refresh_login_status = async (): Promise<boolean> => {
// 检查本地存储的登录状态
return check_login_status();
} catch (error) {
console.error("刷新登录状态失败:", error);
console.warn("刷新登录状态失败:", error);
return false;
}
};
@@ -385,7 +385,7 @@ export const updateUserPhone = async (payload: ChangePhoneParams) => {
const response = await httpService.post("/user/update_phone", payload);
return response;
} catch (error) {
console.error("更新用户手机号失败:", error);
console.warn("更新用户手机号失败:", error);
throw error;
}
};
@@ -402,7 +402,7 @@ export const getUserInfoById = async (id) => {
);
return response;
} catch (error) {
console.error("获取用户信息失败:", error);
console.warn("获取用户信息失败:", error);
throw error;
}
};
@@ -415,7 +415,7 @@ export const followUser = async (following_id) => {
});
return response;
} catch (error) {
console.error("关注失败:", error);
console.warn("关注失败:", error);
throw error;
}
};
@@ -428,7 +428,7 @@ export const unFollowUser = async (following_id) => {
});
return response;
} catch (error) {
console.error("取消关注失败:", error);
console.warn("取消关注失败:", error);
throw error;
}
};
@@ -464,7 +464,7 @@ export const silentLogin = async (): Promise<LoginResponse> => {
console.log("微信登录结果:", login_result);
if (!login_result.code) {
console.error("微信登录失败未获取到code");
console.warn("微信登录失败未获取到code");
return {
success: false,
message: "微信登录失败",
@@ -506,14 +506,14 @@ export const silentLogin = async (): Promise<LoginResponse> => {
user_info,
};
} else {
console.error("静默登录失败:", auth_response.message);
console.warn("静默登录失败:", auth_response.message);
return {
success: false,
message: auth_response.message || "静默登录失败",
};
}
} catch (error) {
console.error("静默登录异常:", error);
console.warn("静默登录异常:", error);
return {
success: false,
message: "静默登录失败,请重试",

View File

@@ -188,6 +188,21 @@ class OrderService {
},
);
}
// 获取退款政策
async getRefundPolicy({
order_id,
}: {
order_id: number;
}): Promise<ApiResponse<any>> {
return httpService.post(
"/payment/order_refund_policy",
{ order_id },
{
showLoading: true,
},
);
}
}
// 导出认证服务实例

View File

@@ -39,19 +39,28 @@ export interface uploadFileResponseData {
updated_at: string,
}
// 从上传错误中取出可展示的文案
function get_upload_error_msg(error: any): string {
if (!error) return "上传失败";
const msg =
error?.message ||
(typeof error?.error === "string" ? error.error : error?.error?.message) ||
(error?.data?.message ?? error?.data?.msg) ||
"";
return (msg && String(msg).trim()) || "上传失败";
}
// 发布球局类
class UploadApi {
async upload(req: UploadFilesData): Promise<{ id: string, data: uploadFileResponseData }> {
let fullUrl = `${envConfig.apiBaseURL}/api/gallery/upload`
const authHeader = tokenManager.getAuthHeader()
const { id, ...rest } = req
const fullUrl = `${envConfig.apiBaseURL}/api/gallery/upload`;
const authHeader = tokenManager.getAuthHeader();
const { id, ...rest } = req;
try {
const res = await Taro.uploadFile({
url: fullUrl,
filePath: rest.filePath,
name: 'file',
name: "file",
formData: {
description: rest.description,
tags: rest.tags,
@@ -59,12 +68,17 @@ class UploadApi {
},
header: authHeader,
});
return {
id,
data: JSON.parse(res.data).data,
const parsed = JSON.parse(res.data);
if (parsed.code !== 0) {
const msg = get_upload_error_msg(parsed);
Taro.showToast({ title: msg, icon: "none" });
throw new Error(msg);
}
return { id, data: parsed.data };
} catch (error) {
throw { id, error }
const msg = get_upload_error_msg(error);
Taro.showToast({ title: msg, icon: "none" });
throw { id, error };
}
}
@@ -103,7 +117,7 @@ class UploadApi {
throw new Error(result.message || '上传失败');
}
} catch (error) {
console.error('上传图片失败:', error);
console.warn('上传图片失败:', error);
throw error;
}
}

View File

@@ -2,7 +2,7 @@ import { UserInfo } from "@/components/UserInfo";
import { API_CONFIG } from "@/config/api";
import httpService, { ApiResponse } from "./httpService";
import uploadFiles from "./uploadFiles";
import * as Taro from "@tarojs/taro";
import * as Taro from "@tarojs/taro";
import getCurrentConfig from "@/config/env";
import { clear_login_state } from "@/services/loginService";
@@ -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 {
@@ -330,7 +318,7 @@ export class UserService {
if (response.code === 0) {
const userData = response.data;
return {
id: userData.id || "",
nickname: userData.nickname || "",
@@ -359,13 +347,11 @@ export class UserService {
last_location_province: userData.last_location_province || "",
last_location_city: userData.last_location_city || "",
};
} else {
throw new Error(response.message || "获取用户信息失败");
}
} catch (error) {
console.error("获取用户信息失败:", error);
console.warn("获取用户信息失败:", error);
// 返回默认用户信息
return {} as UserInfo;
}
@@ -406,7 +392,7 @@ export class UserService {
throw new Error(response.message || "更新用户信息失败");
}
} catch (error) {
console.error("更新用户信息失败:", error);
console.warn("更新用户信息失败:", error);
throw error;
}
}
@@ -431,7 +417,7 @@ export class UserService {
throw new Error(response.message || "获取主办球局失败");
}
} catch (error) {
console.error("获取主办球局失败:", error);
console.warn("获取主办球局失败:", error);
// 返回符合ListContainer data格式的模拟数据
return [];
}
@@ -457,7 +443,7 @@ export class UserService {
throw new Error(response.message || "获取参与球局失败");
}
} catch (error) {
console.error("获取参与球局失败:", error);
console.warn("获取参与球局失败:", error);
// 返回符合ListContainer data格式的模拟数据
return [];
}
@@ -499,7 +485,7 @@ export class UserService {
throw new Error(response.message || "操作失败");
}
} catch (error) {
console.error("关注操作失败:", error);
console.warn("关注操作失败:", error);
throw error;
}
}
@@ -557,7 +543,7 @@ export class UserService {
throw new Error(response.message || "更新用户信息失败");
}
} catch (error) {
console.error("保存用户信息失败:", error);
console.warn("保存用户信息失败:", error);
throw error;
}
}
@@ -587,26 +573,16 @@ export class UserService {
throw new Error(response.message || "获取用户动态失败");
}
} catch (error) {
console.error("获取用户动态失败:", error);
console.warn("获取用户动态失败:", error);
return [];
}
}
// 上传头像
// 上传头像:仅在上传成功且 save 成功时返回 ossPath失败则抛出由调用方处理避免误调 user/update
static async upload_avatar(file_path: string): Promise<string> {
try {
// 先上传文件到服务器
const result = await uploadFiles.upload_oss_img(file_path);
await this.save_user_info({ avatar: result.ossPath });
// 使用新的响应格式中的file_url字段
return result.ossPath;
} catch (error) {
console.error("头像上传失败:", error);
// 如果上传失败,返回默认头像
return require("../static/userInfo/default_avatar.svg");
}
const result = await uploadFiles.upload_oss_img(file_path);
await this.save_user_info({ avatar: result.ossPath });
return result.ossPath;
}
// 解析用户手机号
@@ -626,7 +602,7 @@ export class UserService {
throw new Error(response.message || "获取手机号失败");
}
} catch (error) {
console.error("获取手机号失败:", error);
console.warn("获取手机号失败:", error);
return "";
}
}
@@ -642,7 +618,7 @@ export class UserService {
throw new Error(message || "获取职业树失败");
}
} catch (error) {
console.error("获取职业树失败:", error);
console.warn("获取职业树失败:", error);
return [];
}
}
@@ -658,7 +634,7 @@ export class UserService {
throw new Error(message || "获取城市树失败");
}
} catch (error) {
console.error("获取职业树失败:", error);
console.warn("获取职业树失败:", error);
return [];
}
}
@@ -679,7 +655,7 @@ export class UserService {
throw new Error(message || "注销账户失败");
}
} catch (error) {
console.error("注销账户失败:", error);
console.warn("注销账户失败:", error);
}
}
}
@@ -694,7 +670,7 @@ export const fetchUserProfile = async (): Promise<
const response = await httpService.post("user/detail");
return response;
} catch (error) {
console.error("获取用户信息失败:", error);
console.warn("获取用户信息失败:", error);
throw error;
}
};
@@ -709,7 +685,7 @@ export const checkNicknameChangeStatus = async (): Promise<
);
return response;
} catch (error) {
console.error("获取昵称修改状态失败:", error);
console.warn("获取昵称修改状态失败:", error);
throw error;
}
};
@@ -721,7 +697,7 @@ export const updateNickname = async (nickname: string) => {
});
return response;
} catch (error) {
console.error("昵称修改失败:", error);
console.warn("昵称修改失败:", error);
throw error;
}
};
@@ -732,7 +708,7 @@ export const updateUserProfile = async (payload: Partial<UserInfoType>) => {
const response = await httpService.post("/user/update", payload);
return response;
} catch (error) {
console.error("更新用户信息失败:", error);
console.warn("更新用户信息失败:", error);
throw error;
}
};
@@ -747,11 +723,11 @@ export const updateUserLocation = async (
const response = await httpService.post("/user/update_location", {
latitude,
longitude,
force
force,
});
return response;
} catch (error) {
console.error("更新用户坐标位置失败:", error);
console.warn("更新用户坐标位置失败:", error);
throw error;
}
};
@@ -788,13 +764,13 @@ export const handleCustomerService = async (): Promise<void> => {
console.log("打开客服成功:", res);
},
fail: (error) => {
console.error("打开客服失败:", error);
console.warn("打开客服失败:", error);
// 如果官方客服不可用,显示备用联系方式
showCustomerServiceFallback(customerService);
},
});
} catch (error) {
console.error("客服功能异常:", error);
console.warn("客服功能异常:", error);
// 备用方案:显示联系信息
showCustomerServiceFallback();
}
@@ -824,7 +800,7 @@ const showCustomerServiceFallback = (customerInfo?: any) => {
phoneNumber: customerInfo.phoneNumber,
});
} catch (error) {
console.error("拨打电话失败:", error);
console.warn("拨打电话失败:", error);
Taro.showToast({
title: "拨打电话失败",
icon: "none",
@@ -841,7 +817,7 @@ const showCustomerServiceFallback = (customerInfo?: any) => {
icon: "success",
});
} catch (error) {
console.error("复制邮箱失败:", error);
console.warn("复制邮箱失败:", error);
Taro.showToast({
title: "复制失败",
icon: "none",

View File

@@ -39,7 +39,7 @@ export class WalletService {
throw new Error(response.message || "获取钱包信息失败");
}
} catch (error) {
console.error("获取钱包信息失败:", error);
console.warn("获取钱包信息失败:", error);
// 返回模拟数据
return {
balance: 1588.80,
@@ -62,7 +62,7 @@ export class WalletService {
throw new Error(response.message || "获取交易记录失败");
}
} catch (error) {
console.error("获取交易记录失败:", error);
console.warn("获取交易记录失败:", error);
// 返回模拟数据
return [
{
@@ -107,7 +107,7 @@ export class WalletService {
throw new Error(response.message || "提现申请提交失败");
}
} catch (error) {
console.error("提现申请提交失败:", error);
console.warn("提现申请提交失败:", error);
throw error;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

View 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

View File

@@ -1,11 +1,18 @@
<svg width="24" height="32" viewBox="0 0 24 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_7504_18610)">
<path d="M12.3076 0.0971837C12.7381 0.438102 12.2325 2.22445 12.328 2.76282C13.7134 2.51292 15.1145 2.01928 16.5062 1.81874C16.8928 1.76321 17.9933 1.57501 17.89 2.28307C17.8274 2.71346 16.6377 4.52912 16.73 4.62167C17.9667 4.78673 19.2128 4.97647 20.4214 5.28962C20.8346 5.39607 21.9226 5.57501 21.8209 6.15503C21.7223 6.71192 20.0394 8.18974 20.0848 8.37331C21.1211 9.25414 22.4017 9.9838 23.402 10.8909C23.712 11.1716 24.119 11.457 23.9671 11.926C23.8028 12.4366 22.0009 12.6541 21.5469 12.9965C21.525 13.1215 22.552 14.7351 22.7069 15.0204C22.8995 15.3752 23.8028 17.0613 23.7871 17.3236C23.726 18.2754 22.051 17.5827 21.4827 17.6383C21.6596 18.3648 21.8131 19.1408 21.8929 19.8843C21.9774 20.6679 22.2514 21.6753 21.0632 21.4932C21.237 29.4763 12.3624 34.5546 5.2928 30.6641C-0.183159 27.6514 -1.69069 20.6001 2.09614 15.6467C2.21199 15.494 2.76459 14.9402 2.78964 14.8801C2.81782 14.8122 2.73642 14.2831 2.75364 14.0902C2.80999 13.4624 3.15439 12.8824 3.58802 12.4366C2.80686 11.693 1.97874 10.8461 1.52945 9.85268C1.36977 9.50096 1.30559 9.0027 1.16 8.70189C1.04103 8.45661 0.72324 8.31623 0.593307 8.0216C0.190985 7.11762 0.835953 6.08561 1.87228 6.20902C2.38262 6.26919 2.4609 6.6317 3.13091 6.54069C4.43807 6.36637 5.06425 4.00154 5.53545 2.98804C5.73583 2.5592 6.59839 0.737369 6.94749 0.59082C7.976 0.161974 7.82102 2.05939 8.23899 2.54994C9.08747 1.94986 9.8702 1.2526 10.7281 0.664866C11.0991 0.410335 11.8724 -0.251447 12.3076 0.0925559V0.0971837ZM11.1993 6.0563C10.9629 6.20285 10.2944 5.55804 10.022 5.41149C8.99824 4.86232 7.73648 4.72348 6.62814 5.09988C5.22236 5.57809 5.13626 6.89394 3.68039 7.42923C3.52854 7.48477 3.02133 7.55418 2.9775 7.59738C2.94462 7.62977 2.87418 7.99846 2.75364 8.16814C2.6331 8.33475 2.4061 8.51832 2.19477 8.55071C2.40297 9.81874 3.62873 11.7146 5.15661 11.0189C5.63407 10.8014 5.73583 10.263 5.89707 10.1643C6.15694 10.0069 6.41367 10.1658 6.45437 10.4466C6.57178 11.258 5.30689 11.9676 4.57113 11.9213L5.26776 12.3702C6.99289 13.2865 8.93719 13.6429 10.8846 13.3575C10.9989 13.322 11.2165 12.5245 11.3605 12.3116C11.5515 12.0278 11.9178 11.8072 12.1182 11.5418C12.5033 11.0297 12.6598 9.76166 12.6066 9.13228C12.5847 8.87466 12.3452 8.2931 12.4297 8.13266C12.9792 7.84265 13.5897 7.80409 13.8793 7.14693C14.5384 5.64905 12.6035 4.40879 11.4904 5.55033C11.3777 5.66602 11.2259 6.03934 11.2008 6.05476L11.1993 6.0563ZM12.8665 12.2808C12.2513 12.418 11.6439 13.683 12.1511 14.1273C12.7882 14.6826 14.2582 13.5488 14.6981 13.049C14.4006 12.5183 13.4473 12.1512 12.8665 12.2808ZM6.45594 13.7802C6.16163 13.7324 5.8642 13.6645 5.58085 13.575C4.81847 13.3313 4.25647 12.7312 3.85572 13.7324C3.66786 14.1998 3.64751 14.823 4.27213 14.9387C4.7809 15.0328 6.34323 14.4142 6.45594 13.7817V13.7802ZM10.9973 13.8511C9.58215 14.1921 8.2343 14.1859 6.80817 13.919C6.76746 14.8091 5.6012 15.4477 4.79656 15.5526C4.52573 15.5881 4.07645 15.5156 3.87294 15.6066C3.74144 15.6653 3.02603 16.4165 2.89609 16.5708C0.767073 19.1192 0.635574 22.5885 2.06484 25.5118C2.36853 26.1319 2.43428 26.3756 3.20918 26.4512C5.43839 26.6672 6.16633 25.3606 7.9713 24.6016C11.4137 23.1562 15.7767 24.2237 17.6051 27.5526C20.7376 24.0617 20.4417 18.7906 17.2576 15.4323C16.9257 15.0837 15.7328 14.118 15.3117 13.9699C15.0675 13.8835 14.8123 14.297 14.6292 14.4296C13.3659 15.3521 11.3589 15.9105 10.9989 13.8527L10.9973 13.8511ZM3.59115 27.6915C3.55985 27.8103 3.64125 27.8288 3.69447 27.8982C4.0968 28.4103 4.98284 29.0305 5.55267 29.3729C8.98102 31.4354 13.5036 31.1145 16.5719 28.5553C15.415 25.5164 11.647 24.6232 8.74463 25.6429C6.98819 26.2599 5.62468 28.0972 3.59115 27.6915Z" fill="black"/>
<path d="M7.69734 6.77362C8.84169 6.70883 9.28002 8.38257 8.18264 8.8361C6.57804 9.49788 6.06457 6.86463 7.69734 6.77362Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_7504_18610">
<rect width="24" height="32" fill="white"/>
</clipPath>
</defs>
</svg>
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_图层_2" data-name="图层 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 74.87 89.85">
<defs>
<style>
.cls-1 {
fill: #e6ff54;
}
</style>
</defs>
<g id="_图层_1-2" data-name=" 图层 1">
<path class="cls-1" d="M39.79,82.22h0c-3.1,1.7-6.7,2.6-10.3,2.6-9.9,0-18.3-6.7-21-15.7,1.7,1.4,3.6,2.3,5.6,2.7,4.7,1,8.3,0,11.8-1,3.1-.8,5.9-1.6,9.7-1.2,3.9.5,6.6,2.6,7.3,5.7.6,2.7-.5,5.4-3,6.9h-.1Z"/>
<path class="cls-1" d="M51.29,62.92c0,.9,0,1.9-.2,2.8-.5,3.7-1.8,7.2-4,10.2,0-.5,0-1-.2-1.5-1.1-4.9-5.3-8.3-10.9-9-4.5-.6-8.1.4-11.3,1.3-3.3,1-6.2,1.7-9.9,1-3.9-.8-7.6-4.7-6.6-10.4h0c0-.6.3-1.1.5-1.7,0-.4.3-.8.5-1.2,1.1.2,2.2.2,3.3,0h.4c2.6-.9,4.4-2.8,4.8-5.2v-.2c4.8,1.4,10.4,1.3,15.4,0h0c.2-.2.4-.2.6-.3,0,1,.5,2,1.1,2.9,1.1,1.6,2.9,2.6,5,2.8,1.1,0,2.2,0,3.3-.4h0c1-.2,2-.7,3-1.3.6-.4,1.1-.8,1.6-1.2,2.3,3.5,3.5,7.6,3.5,11.9v-.3l.1-.2Z"/>
<g>
<path d="M74.59,37.52c-2.1-6.1-6.4-11.7-12.2-15.9h0c1.7-2,2.6-3,4.5-4.6.5-.4.7-1,.6-1.6,0-.6-.4-1.1-1-1.4-6.5-3.2-12.1-4.7-18.9-5,.9-2.4,1.8-4.1,3-6.4.3-.6.3-1.3,0-1.9-.4-.6-1.1-.8-1.8-.7-7.6,1.7-13.8,4-20.9,8.1-1.9-2.4-4.1-4.3-6.8-6-.5-.3-1.2-.3-1.8,0s-.9.9-.9,1.5c0,5.6-1,9.9-3.2,14.4-1.6,3.3-3.3,5.7-4.4,7.4-1.3,1.9-2.1,2.9-3,3.4-1.35-1.86-4.14-2.86-6.42-.58-1.03,1.03-1.51,2.46-1.33,3.91.21,1.67,1.05,2.27,2.25,2.87.7,2.3,1.7,4.5,2.9,6.3-1.4,1.1-2.3,2.7-2.7,4.5-.6,2.7,0,5.3,1.9,7.2-1.2,3.1-1.8,6.4-1.8,9.8,0,14.9,13.39,27.65,27,27,6.31-.3,9.55-.58,15.79-5.21,1.57-1.16,3.02-2.5,4.26-4.01,3.31-4.03,5.89-8.65,6.65-13.38,1.2.4,2.4.9,3.3,1.3.4.2.8.2,1.3,0h.2c.5-.2.8-.6,1-1.1.9-2.9,1.4-7.5,1.6-10.6,1.5,0,3.1.3,5.2.7.5,0,1.1,0,1.5-.4s.6-.9.6-1.4c-.2-5.6-1-10-2.5-14.3,1.8-.8,3.3-1.4,5.1-1.8.5,0,.9-.4,1.1-.9.2-.4.3-.9,0-1.4v.2h-.1ZM44.09,41.82c.7,0,1.2.3,1.7.8l.17.17c.26.26.52,1.15.72,1.96.29,1.21-.08,2.48-.97,3.36,0,0-.01.01-.02.02-.4.4-.8.7-1.3,1-1.4.9-2.8,1.3-4.1,1.2-1,0-1.8-.5-2.3-1.2-.5-.8-.7-1.6-.5-2.4,0-.4.2-.8.4-1.2.2-.3.4-.6.6-.9h0c.2-.3.5-.6.8-.8s.6-.4.9-.6c.8-.5,1.6-.9,2.3-1.1.48-.09.65-.19,1.21-.27l.39-.03ZM7.09,34.12h.4c3.7-.5,5.6-3.2,7.3-5.8,1.3-1.8,2.6-3.8,4.8-5.3,3.7-2.5,8.6-2.6,12.6,0,.3.2.6.2,1,.2h.4c.5,0,.9-.5,1.1-.9.6-1.3,1.7-2.2,3-2.4,1.1-.2,2.1,0,2.8.7.8.7,1.2,1.8,1,2.9-.2,1.2-1.2,2.1-2.6,2.7-.6.2-.9.6-1.1,1.2-.2.6,0,1.2.3,1.6,2.1,2.7,2.9,6.5,2.1,9.5,0,.3-.2.6-.3.9-.6.3-1.1.6-1.7,1-1.6,1-2.8,2.2-3.5,3.6,0,0-.2.3-.2.5l-1.1.3c-1.1.4-2.2.7-3.4.9-2.7.5-5.5.6-8.2.2-1.2-.2-2.3-.4-3.4-.8h0c-.4,0-.8-.2-1.2-.4-.3,0-.5-.2-.8-.3h-.3c-.2,0-.4-.2-.7-.3-.2,0-.4-.2-.6-.3.2,0,.4-.2.6-.4,0,0,.2,0,.3-.2,1.1-.9,2.1-2.1,2.7-3.7.2-.5.2-1,0-1.5s-.6-.9-1.1-1c-1-.4-2.2,0-2.6,1.1-.6,1.4-1.5,2.3-2.6,2.4-.5,0-1.1,0-1.7-.3h0c-1.3-1.6-2.3-3.5-3-5.6v-.2h-.2l-.1-.3ZM7.79,49.72c-.2-.2-.3-.5-.5-.9h0c0-.4-.2-.8,0-1.4h0v-.5c.3-1.4,1.2-2,1.8-2.3h.7c.2,0,.3.3.5.4.2.2.5.3.7.5.4.2.7.5,1.1.7.5.3,1,.6,1.5.8,0,0,.2,0,.4.2h0v1.1c-.2,1.1-1.1,2-2.5,2.3h-.5c-.8,0-1.6,0-2.3-.3-.3-.2-.6-.4-.8-.6s0,0-.2-.2h.2l-.1.2ZM39.79,82.22h0c-3.1,1.7-6.7,2.6-10.3,2.6-9.9,0-18.3-6.7-21-15.7,1.7,1.4,3.6,2.3,5.6,2.7,4.7,1,8.3,0,11.8-1,3.1-.8,5.9-1.6,9.7-1.2,3.9.5,6.6,2.6,7.3,5.7.6,2.7-.5,5.4-3,6.9h-.1ZM47.09,75.92c0-.5,0-1-.2-1.5-1.1-4.9-5.3-8.3-10.9-9-4.5-.6-8.1.4-11.3,1.3-3.3,1-6.2,1.7-9.9,1-3.9-.8-7.6-4.7-6.6-10.4h0c0-.6.3-1.1.5-1.7,0-.4.3-.8.5-1.2,1.1.2,2.2.2,3.3,0h.4c2.6-.9,4.4-2.8,4.8-5.2v-.2c4.8,1.4,10.2,1.4,15.1,0,0,0,.6-.3.8-.4,0,1,.5,2,1.1,2.9,1.1,1.6,2.9,2.6,5,2.8,1.1,0,3.4-.3,3.4-.3,1-.3,2-.8,3-1.4.6-.4,1.1-.8,1.6-1.2,2.3,3.5,3.52,7.6,3.5,11.9-.01,2.59,0,1.9-.2,2.8,0,0-1.7,6.8-3.9,9.8Z"/>
<path d="M24.69,34.92c-1.6.2-3.1-.9-3.3-2.5-.3-1.6.8-3.1,2.4-3.3h0c1.6-.2,3.1.9,3.3,2.5.3,1.6-.8,3.1-2.4,3.3Z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 18 KiB

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