feat: 修复发布时图片未上传完成导致详情图片丢失的问题,修复详情球局日期展示问题,修复球局管理取消活动无响应的问题

This commit is contained in:
2025-10-24 15:59:03 +08:00
parent 4126ad5679
commit f93b27c4a7
8 changed files with 308 additions and 193 deletions

View File

@@ -52,7 +52,7 @@ module.exports = {
'nutui-react',
],
['transform-remove-console', { exclude: ['error', 'warn'] }],
// ['transform-remove-console', { exclude: ['error', 'warn'] }],
['@babel/plugin-transform-runtime', { corejs: false }]
],
}

View File

@@ -43,12 +43,12 @@ const CancelPopup = forwardRef((props, ref) => {
}
async function handleConfirm() {
if (!cancelReason) {
if (!cancelReason && hasOtherJoin) {
Taro.showToast({ title: "请输入取消原因", icon: "none" });
return;
}
try {
await onFinish.current(cancelReason);
await onFinish.current(hasOtherJoin ? cancelReason : "无责取消");
onClose();
} catch (e) {
console.log(e, 1221);

View File

@@ -1,95 +1,123 @@
import React, { useCallback, useRef, useState } from 'react'
import { Image, View, Text } from '@tarojs/components'
import img from '../../config/images'
import UploadSourcePopup, { sourceMap } from './upload-source-popup'
import UploadFromWx from './upload-from-wx'
import { CommonPopup } from '../'
import React, { useCallback, useRef, useState } from "react";
import Taro from "@tarojs/taro";
import { Image, View, Text } from "@tarojs/components";
import img from "../../config/images";
import UploadSourcePopup, { sourceMap } from "./upload-source-popup";
import UploadFromWx from "./upload-from-wx";
import { CommonPopup } from "../";
import './index.scss'
import { uploadFileResponseData } from '@/services/uploadFiles'
import "./index.scss";
// import { uploadFileResponseData } from "@/services/uploadFiles";
export type sourceType = 'album' | 'history' | 'preset'
export type sourceType = "album" | "history" | "preset";
export type source = sourceType[]
export type source = sourceType[];
export type CoverImageValue = {
id: string
url: string
tempFilePath?: string
}
id: string;
url: string;
tempFilePath?: string;
};
export interface UploadCoverProps {
value: CoverImageValue[]
changePicker?: (value: boolean) => void
onChange: (value: CoverImageValue[] | ((prev: CoverImageValue[]) => CoverImageValue[])
) => void
source?: source
maxCount?: number
align?: 'center' | 'left'
tag?: 'cover' | 'screenshot'
value: CoverImageValue[];
changePicker?: (value: boolean) => void;
onChange: (
value: CoverImageValue[] | ((prev: CoverImageValue[]) => CoverImageValue[])
) => void;
source?: source;
maxCount?: number;
align?: "center" | "left";
tag?: "cover" | "screenshot";
}
// const values = [
// 'http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/1a35ebbf-2361-44da-b338-7608561d0b31.png',
// 'http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/cf5a82ba-90af-4138-a1b3-9119adcde9e0.png',
// 'http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/49d7cdf0-b03c-4a0f-91c6-e7778080cfcd.png'
// ]
const mergeCoverImages = (value: CoverImageValue[], images: CoverImageValue[]) => {
const mergeCoverImages = (
value: CoverImageValue[], // prev value
images: CoverImageValue[] // new value
) => {
// 根据id来更新url, 如果id不存在则添加到value中
const newImages = images
const updatedValue = value.map(item => {
const index = images.findIndex(image => image.id === item.id)
if (index !== -1) {
newImages.splice(index, 1)
return { ...item, url: images[index].url }
}
return item
})
return [...updatedValue, ...newImages]
}
const newImages = images;
const failedIdList = images
.filter((item) => !item.url)
.map((item) => item.id);
const failedIndexList: number[] = [];
const updatedValue = value
.map((item, i) => {
const index = newImages.findIndex((image) => image.id === item.id);
if (index !== -1) {
const newUrl = newImages[index].url;
newImages.splice(index, 1);
if (failedIdList.includes(item.id)) {
failedIndexList.push(i + 1);
return null;
}
return { ...item, url: newUrl };
}
return item;
})
.filter((item) => item);
if (failedIndexList.length > 0) {
Taro.showToast({
title: `${failedIndexList.join("、")}张图片上传失败,请检查重试`,
icon: "none",
});
}
return [...updatedValue, ...newImages];
};
export default function UploadCover(props: UploadCoverProps) {
const {
value = [],
onChange = () => void 0,
source = ['album', 'history', 'preset'] as source,
source = ["album", "history", "preset"] as source,
maxCount = 9,
align = 'center',
tag = 'cover',
changePicker
} = props
align = "center",
tag = "cover",
changePicker,
} = props;
const [visible, setVisible] = useState(false)
const [visible, setVisible] = useState(false);
const uploadSourcePopupRef = useRef<{
show: (sourceType: sourceType, maxCount: number) => void
}>(null)
show: (sourceType: sourceType, maxCount: number) => void;
}>(null);
const onAdd = useCallback((images: CoverImageValue[]) => {
// FIXME: prev is not latest value
onChange(prev => mergeCoverImages(prev, images))
setVisible(false)
}, [value])
const onAdd = useCallback(
(images: CoverImageValue[]) => {
// FIXME: prev is not latest value
onChange((prev) => mergeCoverImages(prev, images));
setVisible(false);
},
[value]
);
const onWxAdd = useCallback((images: CoverImageValue[], onFileUpdate: Promise<{ id: string, url: string }[]>) => {
onAdd(images)
onFileUpdate.then(res => {
onAdd(res.map(item => ({
id: item.id,
url: item.url,
})))
})
}, [onAdd])
const onWxAdd = useCallback(
async (
images: CoverImageValue[],
onFileUpdate: Promise<{ id: string; url: string }[]>
) => {
onAdd(images);
onFileUpdate.then((res) => {
console.log("onWxAdd update");
onAdd(
res.map((item) => ({
id: item.id,
url: item.url,
}))
);
});
},
[onAdd]
);
const onDelete = (image: CoverImageValue) => {
onChange(value.filter(item => item.id !== image.id))
}
onChange(value.filter((item) => item.id !== image.id));
};
const openPicker = (value: boolean) => {
setVisible(value)
setVisible(value);
if (changePicker) {
changePicker(value)
changePicker(value);
}
}
};
return (
<>
@@ -101,50 +129,75 @@ export default function UploadCover(props: UploadCoverProps) {
hideFooter
zIndex={1000}
>
<View className="upload-source-popup-container" style={{ height: source.length * 56 + 52 + 'px' }}>
{
source.map((item) => {
return (
<View className="upload-source-popup-item" key={item}>
{
item === 'album' ? (
<UploadFromWx onAdd={onWxAdd} maxCount={maxCount - value.length} />
) : (
<View className="upload-source-popup-item-text" onClick={() => uploadSourcePopupRef.current?.show(item, maxCount - value.length)}>
<Text>{sourceMap.get(item)}</Text>
</View>
)
}
</View>
)
})
}
<View
className="upload-source-popup-container"
style={{ height: source.length * 56 + 52 + "px" }}
>
{source.map((item) => {
return (
<View className="upload-source-popup-item" key={item}>
{item === "album" ? (
<UploadFromWx
onAdd={onWxAdd}
maxCount={maxCount - value.length}
/>
) : (
<View
className="upload-source-popup-item-text"
onClick={() =>
uploadSourcePopupRef.current?.show(
item,
maxCount - value.length
)
}
>
<Text>{sourceMap.get(item)}</Text>
</View>
)}
</View>
);
})}
</View>
</CommonPopup>
<UploadSourcePopup tag={tag} ref={uploadSourcePopupRef} onAdd={onAdd} />
<div className={`upload-cover-root ${value.length === 0 && align === 'center' ? 'upload-cover-act-center' : ''}`}>
<div
className={`upload-cover-root ${
value.length === 0 && align === "center"
? "upload-cover-act-center"
: ""
}`}
>
{value.length < maxCount && (
<div className="upload-cover-act" onClick={() => openPicker(true)}>
<Image className='upload-cover-act-icon' src={img.ICON_ADD} />
<Image className="upload-cover-act-icon" src={img.ICON_ADD} />
<div className="upload-cover-text"></div>
</div>
)}
<div className={`cover-image-list-container ${value.length === maxCount ? 'full' : ''}`}>
<div
className={`cover-image-list-container ${
value.length === maxCount ? "full" : ""
}`}
>
<div className="cover-image-list">
{
value.map((item) => {
return (
<View className="cover-image-item" key={item.id}>
<Image className="cover-image-item-image" src={item.url} mode="aspectFill" />
<Image className="cover-image-item-delete" src={img.ICON_REMOVE} onClick={() => onDelete(item)} />
</View>
)
})
}
{value.map((item) => {
return (
<View className="cover-image-item" key={item.id}>
<Image
className="cover-image-item-image"
src={item.url}
mode="aspectFill"
/>
<Image
className="cover-image-item-delete"
src={img.ICON_REMOVE}
onClick={() => onDelete(item)}
/>
</View>
);
})}
</div>
</div>
</div>
</>
);
};
}

View File

@@ -1,123 +1,153 @@
import React from 'react'
import { View, Text } from '@tarojs/components'
import Taro from '@tarojs/taro'
import uploadApi from '@/services/uploadFiles'
import './upload-from-wx.scss'
import { CoverImageValue } from '.'
import React from "react";
import { View, Text } from "@tarojs/components";
import Taro from "@tarojs/taro";
import uploadApi from "@/services/uploadFiles";
import "./upload-from-wx.scss";
import { CoverImageValue } from ".";
export interface UploadFromWxProps {
onAdd: (images: CoverImageValue[], onFileUploaded: Promise<{ id: string, url: string }[]>) => void
maxCount: number
onAdd: (
images: CoverImageValue[],
onFileUploaded: Promise<{ id: string; url: string }[]>
) => void;
maxCount: number;
}
async function convert_to_jpg_and_compress (src: string, { width, height }): Promise<string> {
const canvas = Taro.createOffscreenCanvas({ type: '2d', width, height })
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
async function convert_to_jpg_and_compress(
src: string,
{ width, height }
): Promise<string> {
const canvas = Taro.createOffscreenCanvas({ type: "2d", width, height });
const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
const image = canvas.createImage()
await new Promise(resolve => {
image.onload = resolve
image.src = src
})
ctx.clearRect(0, 0, width, height)
ctx.drawImage(image as unknown as CanvasImageSource, 0, 0, width, height)
const image = canvas.createImage();
await new Promise((resolve) => {
image.onload = resolve;
image.src = src;
});
ctx.clearRect(0, 0, width, height);
ctx.drawImage(image as unknown as CanvasImageSource, 0, 0, width, height);
// const imageData = ctx.getImageData(0, 0, width, height)
return new Promise((resolve, reject) => {
Taro.canvasToTempFilePath({
canvas: canvas as unknown as Taro.Canvas,
fileType: 'jpg',
fileType: "jpg",
quality: 0.7,
success: res => resolve(res.tempFilePath),
fail: reject
})
})
success: (res) => resolve(res.tempFilePath),
fail: reject,
});
});
}
async function compressImage(files) {
const res: string[] = []
const res: string[] = [];
for (const file of files) {
const compressed_image = await convert_to_jpg_and_compress(file.path, { width: file.width, height: file.height })
res.push(compressed_image)
const compressed_image = await convert_to_jpg_and_compress(file.path, {
width: file.width,
height: file.height,
});
res.push(compressed_image);
}
return res
return res;
}
// 图片标准容器为 360 * 240 3:2
// 压缩后图片最大宽高
const IMAGE_MAX_SIZE = {
width: 1080,
height: 720,
}
};
// 标准长宽比,判断标准
const STANDARD_ASPECT_RATIO = IMAGE_MAX_SIZE.width / IMAGE_MAX_SIZE.height
const STANDARD_ASPECT_RATIO = IMAGE_MAX_SIZE.width / IMAGE_MAX_SIZE.height;
type ChoosenImageRes = { path: string, size: number, width: number, height: number }
type ChoosenImageRes = {
path: string;
size: number;
width: number;
height: number;
};
// 根据图片标准重新设置图片尺寸
async function onChooseImageSuccess(tempFiles) {
const result: ChoosenImageRes[] = []
const result: ChoosenImageRes[] = [];
for (const tempFile of tempFiles) {
const { width, height } = await Taro.getImageInfo({ src: tempFile.path })
const image_aspect_ratio = width / height
let fileRes: ChoosenImageRes = { path: tempFile.path, size: tempFile.size, width: 0, height: 0 }
const { width, height } = await Taro.getImageInfo({ src: tempFile.path });
const image_aspect_ratio = width / height;
let fileRes: ChoosenImageRes = {
path: tempFile.path,
size: tempFile.size,
width: 0,
height: 0,
};
// 如果图片长宽比小于标准长宽比,则依照图片高度以及图片最大高度来重新设置图片尺寸
if (image_aspect_ratio < STANDARD_ASPECT_RATIO) {
fileRes = {
...fileRes,
...(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 }),
}
...(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 {
fileRes = {
...fileRes,
...(width > IMAGE_MAX_SIZE.width ? { width: IMAGE_MAX_SIZE.width, height: Math.floor(IMAGE_MAX_SIZE.width / image_aspect_ratio) } : { width, height: Math.floor(width / image_aspect_ratio) }),
}
...(width > IMAGE_MAX_SIZE.width
? {
width: IMAGE_MAX_SIZE.width,
height: Math.floor(IMAGE_MAX_SIZE.width / image_aspect_ratio),
}
: { width, height: Math.floor(width / image_aspect_ratio) }),
};
}
result.push(fileRes)
result.push(fileRes);
}
return result
return result;
}
export default function UploadFromWx(props: UploadFromWxProps) {
const {
onAdd = () => void 0,
maxCount = 9, // calc from parent
} = props
} = props;
const handleImportFromWx = () => {
Taro.chooseImage({
count: maxCount,
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
sizeType: ["original", "compressed"],
sourceType: ["album", "camera"],
}).then(async (res) => {
const analyzedFiles = await onChooseImageSuccess(res.tempFiles)
// compress image
const analyzedFiles = await onChooseImageSuccess(res.tempFiles);
// cropping image to standard size
const compressedTempFiles = await compressImage(analyzedFiles)
const compressedTempFiles = await compressImage(analyzedFiles);
let start = Date.now()
const files = compressedTempFiles.map(path => ({
let start = Date.now();
const files = compressedTempFiles.map((path) => ({
filePath: path,
description: '封面图',
tags: 'cover',
description: "封面图",
tags: "cover",
is_public: 1 as unknown as 0 | 1,
id: (start++).toString(),
}))
const onFileUpdate = uploadApi.batchUpload(files).then(res => {
return res.map(item => ({
}));
const onFileUpdate = uploadApi.batchUpload(files).then((res) => {
return res.map((item) => ({
id: item.id,
url: item.data.file_url
}))
})
onAdd(files.map(item => ({
id: item.id,
url: item.filePath,
})), onFileUpdate)
})
}
url: item ? item.data.file_url : "",
}));
});
onAdd(
files.map((item) => ({
id: item.id,
url: item.filePath,
})),
onFileUpdate
);
});
};
return (
<View onClick={handleImportFromWx}>
<Text className="upload-from-wx-text"></Text>
</View>
)
}
);
}

View File

@@ -13,8 +13,11 @@ function genGameLength(startTime: Dayjs, endTime: Dayjs) {
return "";
}
const hours = endTime.diff(startTime, "hour");
if (Math.floor(hours / 24) > 1) {
return `${Math.floor(hours / 24)}${hours % 24}小时`;
if (Math.floor(hours / 24) >= 1) {
const leftHours = Math.floor(hours % 24);
return `${Math.floor(hours / 24)}${
leftHours !== 0 ? `${leftHours}小时` : ""
}`;
}
return `${hours}小时`;
}

View File

@@ -105,6 +105,12 @@ export default function StickyButton(props) {
available: false,
// action: () => toast("活动已取消"),
};
} else if (MATCH_STATUS.FINISHED === match_status) {
return {
text: "活动已结束",
available: false,
// action: () => toast("活动已取消"),
};
} else if (dayjs(end_time).isBefore(dayjs())) {
return {
text: "活动已结束",

View File

@@ -57,7 +57,7 @@ const defaultFormData: PublishBallFormData = {
wechat_contact: '',
default_wechat_contact: ''
}
}
const PublishBall: React.FC = () => {
@@ -66,7 +66,7 @@ const PublishBall: React.FC = () => {
const userInfo = useUserInfo();
const publishAiData = usePublishBallData()
const { statusNavbarHeightInfo } = useGlobalState();
// 使用全局键盘状态
const { keyboardHeight, isKeyboardVisible, addListener, initializeKeyboardListener } = useKeyboardHeight()
// 获取页面参数并设置导航标题
@@ -181,7 +181,7 @@ const PublishBall: React.FC = () => {
const validateFormData = (formData: PublishBallFormData, isOnSubmit: boolean = false) => {
const { activityInfo, title, timeRange, image_list, players, current_players } = formData;
const { play_type, price, location_name } = activityInfo;
const { max } = players;
if (!image_list?.length && activityType === 'group') {
if (!isOnSubmit) {
@@ -192,6 +192,19 @@ const PublishBall: React.FC = () => {
}
return false
}
// 判断图片是否上传完成
if (image_list?.length > 0) {
const uploadInProgress = image_list.some((item) =>
item.url.startsWith("http://tmp/")
);
if (uploadInProgress) {
Taro.showToast({
title: `封面图片上传中...`,
icon: "none",
});
return;
}
}
if (!title) {
if (!isOnSubmit) {
Taro.showToast({
@@ -268,7 +281,7 @@ const PublishBall: React.FC = () => {
})
}
return false
}
}
}
if (current_players && (current_players > max)) {
if (!isOnSubmit) {
@@ -464,7 +477,7 @@ const PublishBall: React.FC = () => {
wechat: { ...defaultFormData.wechat, default_wechat_contact: userPhone, is_wechat_contact: is_wechat_contact === 0 ? false : true},
image_list: image_list?.map(item => ({ url: item, id: item })) || [],
...(current_players ? { current_players } : {}),
}
}
@@ -487,7 +500,7 @@ const PublishBall: React.FC = () => {
return acc
}, [] as FormFieldConfig[])
setOptionsConfig(newFormSchema)
}
}
const initFormData = () => {
const params = getParams()
const userPhone = (userInfo as any)?.phone || ''
@@ -532,7 +545,7 @@ const PublishBall: React.FC = () => {
}
}
const getGameDetail = async (gameId) => {
if (!gameId) return;
@@ -577,7 +590,7 @@ const PublishBall: React.FC = () => {
useEffect(() => {
// 初始化全局键盘监听器
initializeKeyboardListener()
// 添加本地监听器
const removeListener = addListener((height, visible) => {
console.log('PublishBall 收到键盘变化:', height, visible)

View File

@@ -48,26 +48,36 @@ class UploadApi {
const authHeader = tokenManager.getAuthHeader()
const { id, ...rest } = req
return Taro.uploadFile({
url: fullUrl,
filePath: rest.filePath,
name: 'file',
formData: {
description: rest.description,
tags: rest.tags,
is_public: rest.is_public,
},
header: authHeader,
}).then(res => {
try {
const res = await Taro.uploadFile({
url: fullUrl,
filePath: rest.filePath,
name: 'file',
formData: {
description: rest.description,
tags: rest.tags,
is_public: rest.is_public,
},
header: authHeader,
});
return {
id,
data: JSON.parse(res.data).data,
}
})
} catch (error) {
throw { id, error }
}
}
async batchUpload(req: UploadFilesData[]): Promise<{ id: string, data: uploadFileResponseData }[]> {
return Promise.all(req.map(item => this.upload(item)))
async batchUpload(req: UploadFilesData[]): Promise<{ id: string, data: uploadFileResponseData | null }[]> {
return Promise.all(req.map(async (item) => {
try {
const res = await this.upload(item);
return res;
} catch (error) {
return { id: item.id, data: null }
}
}))
}
// 上传单张图片到OSS
@@ -84,7 +94,7 @@ class UploadApi {
header: authHeader,
});
const result = JSON.parse(response.data);