diff --git a/babel.config.js b/babel.config.js index ee2edf5..f128a65 100644 --- a/babel.config.js +++ b/babel.config.js @@ -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 }] ], } diff --git a/src/components/GameManagePopup/index.tsx b/src/components/GameManagePopup/index.tsx index 2737115..e7c1daa 100644 --- a/src/components/GameManagePopup/index.tsx +++ b/src/components/GameManagePopup/index.tsx @@ -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); diff --git a/src/components/UploadCover/index.tsx b/src/components/UploadCover/index.tsx index 75e9510..d8e6b31 100644 --- a/src/components/UploadCover/index.tsx +++ b/src/components/UploadCover/index.tsx @@ -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} > - - { - source.map((item) => { - return ( - - { - item === 'album' ? ( - - ) : ( - uploadSourcePopupRef.current?.show(item, maxCount - value.length)}> - {sourceMap.get(item)} - - ) - } - - ) - }) - } + + {source.map((item) => { + return ( + + {item === "album" ? ( + + ) : ( + + uploadSourcePopupRef.current?.show( + item, + maxCount - value.length + ) + } + > + {sourceMap.get(item)} + + )} + + ); + })} -
+
{value.length < maxCount && (
openPicker(true)}> - +
添加活动封面
)} -
+
- { - value.map((item) => { - return ( - - - onDelete(item)} /> - - ) - }) - } + {value.map((item) => { + return ( + + + onDelete(item)} + /> + + ); + })}
); -}; - +} diff --git a/src/components/UploadCover/upload-from-wx.tsx b/src/components/UploadCover/upload-from-wx.tsx index 35aabda..9258045 100644 --- a/src/components/UploadCover/upload-from-wx.tsx +++ b/src/components/UploadCover/upload-from-wx.tsx @@ -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 { - 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 { + 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 ( 从相册添加 - ) -} \ No newline at end of file + ); +} diff --git a/src/game_pages/detail/components/GameInfo/index.tsx b/src/game_pages/detail/components/GameInfo/index.tsx index a7a2719..cf2b7c4 100644 --- a/src/game_pages/detail/components/GameInfo/index.tsx +++ b/src/game_pages/detail/components/GameInfo/index.tsx @@ -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}小时`; } diff --git a/src/game_pages/detail/components/StickyBottom/index.tsx b/src/game_pages/detail/components/StickyBottom/index.tsx index 53e986c..3271df6 100644 --- a/src/game_pages/detail/components/StickyBottom/index.tsx +++ b/src/game_pages/detail/components/StickyBottom/index.tsx @@ -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: "活动已结束", diff --git a/src/publish_pages/publishBall/index.tsx b/src/publish_pages/publishBall/index.tsx index 1c85b0b..1e775eb 100644 --- a/src/publish_pages/publishBall/index.tsx +++ b/src/publish_pages/publishBall/index.tsx @@ -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) diff --git a/src/services/uploadFiles.ts b/src/services/uploadFiles.ts index 7627454..7988dcc 100644 --- a/src/services/uploadFiles.ts +++ b/src/services/uploadFiles.ts @@ -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);