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', 'nutui-react',
], ],
['transform-remove-console', { exclude: ['error', 'warn'] }], // ['transform-remove-console', { exclude: ['error', 'warn'] }],
['@babel/plugin-transform-runtime', { corejs: false }] ['@babel/plugin-transform-runtime', { corejs: false }]
], ],
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -192,6 +192,19 @@ const PublishBall: React.FC = () => {
} }
return false 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 (!title) {
if (!isOnSubmit) { if (!isOnSubmit) {
Taro.showToast({ Taro.showToast({

View File

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