20 Commits

Author SHA1 Message Date
李瑞
49f53d60ed 处理列表请求 2026-02-06 22:37:35 +08:00
李瑞
4c75368fe8 优化banner逻辑 2026-02-01 23:37:31 +08:00
张成
8abf6e6f2b 1 2026-02-01 18:46:30 +08:00
李瑞
0c83aab053 Merge branch 'feature/juguohong/20260101' 2026-02-01 16:12:48 +08:00
李瑞
1cbec87f77 增加banneer占位图 2026-02-01 16:12:04 +08:00
张成
6d57654005 1 2026-01-29 22:56:33 +08:00
筱野
dade2e2491 修改发布页问题 2026-01-27 22:15:28 +08:00
筱野
4a6ac73ad7 修改文本域长度 2026-01-26 22:51:01 +08:00
筱野
de8677c64c 修改样式 2026-01-26 22:13:52 +08:00
筱野
3ab647f7c6 增加发布仅上海地区可发布、发布防抖 2026-01-05 21:26:02 +08:00
张成
fa328f893d 1 2026-01-05 18:34:05 +08:00
李瑞
d7c24ca8b3 显示空状态 2026-01-01 15:37:27 +08:00
0a3cdbedd2 Merge branch 'feat/liujie' 2025-12-31 11:20:53 +08:00
af131f228a style: 修改球局详情页场馆预定截图展示问题、修改截图地址取值问题 2025-12-31 11:20:41 +08:00
b5f9d23615 Merge branch 'master' into feat/liujie 2025-12-31 10:55:58 +08:00
76b105866c 下载账单页面样式优化 2025-12-29 16:13:33 +08:00
86581f3a11 Merge branch 'feat/liujie' 2025-12-29 15:05:41 +08:00
29094c7e6a feat: 发布分享弹窗样式修改,增加预览消息卡片 2025-12-29 15:05:29 +08:00
4578ca0cb1 Merge branch 'feat/liujie' 2025-12-29 11:41:00 +08:00
d5662e5810 feat: 样式调整 2025-12-29 11:40:52 +08:00
34 changed files with 531 additions and 129 deletions

View File

@@ -2,7 +2,7 @@
"miniprogramRoot": "dist/", "miniprogramRoot": "dist/",
"projectname": "playBallTogether", "projectname": "playBallTogether",
"description": "playBallTogether", "description": "playBallTogether",
"appid": "wx815b533167eb7b53", "appid": "wx915ecf6c01bea4ec",
"setting": { "setting": {
"urlCheck": true, "urlCheck": true,
"es6": true, "es6": true,

View File

@@ -57,6 +57,7 @@ export default defineAppConfig({
"ntrp-evaluate/index", // NTRP评估页 "ntrp-evaluate/index", // NTRP评估页
"enable_notification/index", // 开启消息通知 "enable_notification/index", // 开启消息通知
"emptyState/index", // 空状态页面 "emptyState/index", // 空状态页面
"bannerDetail/index", // Banner 图片详情页
], ],
}, },
], ],

View File

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

View File

@@ -7,7 +7,8 @@
border-radius: 20px 20px 0 0 !important; border-radius: 20px 20px 0 0 !important;
} }
.common-popup__drag-handle-container { .common-popup__drag-handle-container {
position: position; position: relative;
height: 0;
} }
.common-popup__drag-handle { .common-popup__drag-handle {

View File

@@ -7,6 +7,8 @@ import {
EvaluateCallback, EvaluateCallback,
EvaluateScene, EvaluateScene,
} from "@/store/evaluateStore"; } from "@/store/evaluateStore";
import { useListState } from "@/store/listStore";
import { navigateTo, redirectTo, navigateBack } from "@/utils/navigation"; import { navigateTo, redirectTo, navigateBack } from "@/utils/navigation";
import { requireLoginWithPhone } from "@/utils/helper"; import { requireLoginWithPhone } from "@/utils/helper";
import styles from "./index.module.scss"; import styles from "./index.module.scss";
@@ -24,6 +26,11 @@ const PublishMenu: React.FC<PublishMenuProps> = (props) => {
const { onVisibleChange } = props; const { onVisibleChange } = props;
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
const {
area
} = useListState();
// 使用 useEffect 监听 isVisible 变化,确保所有情况都能触发回调 // 使用 useEffect 监听 isVisible 变化,确保所有情况都能触发回调
useEffect(() => { useEffect(() => {
onVisibleChange?.(isVisible); onVisibleChange?.(isVisible);
@@ -59,6 +66,16 @@ const PublishMenu: React.FC<PublishMenuProps> = (props) => {
}); });
}; };
const handleMenuItemClick = (type: "individual" | "group" | "ai") => { const handleMenuItemClick = (type: "individual" | "group" | "ai") => {
const [_, address] = area;
if (address !== '上海') {
(Taro as any).showModal({
title: '提示',
content: '仅上海地区开放,您可加入社群或切换城市',
showCancel: false,
confirmText: '知道了'
})
return;
}
if (!userInfo.ntrp_level) { if (!userInfo.ntrp_level) {
ntrpRef.current.show({ ntrpRef.current.show({
type: EvaluateScene.publish, type: EvaluateScene.publish,

View File

@@ -29,8 +29,10 @@ const TextareaTag: React.FC<TextareaTagProps> = ({
// 处理文本输入变化 // 处理文本输入变化
const handleTextChange = useCallback((val: string) => { const handleTextChange = useCallback((val: string) => {
console.log(val,'e.detail.value') console.log(val,'e.detail.value')
onChange({...value, description: val}) const maxAllowedLength = Math.floor(maxLength * 1.2)
}, [onChange]) const truncatedVal = val.length > maxAllowedLength ? val.slice(0, maxAllowedLength) : val
onChange({...value, description: truncatedVal})
}, [onChange, maxLength, value])
// 处理标签选择变化 // 处理标签选择变化
const handleTagChange = useCallback((selectedTags: string[]) => { const handleTagChange = useCallback((selectedTags: string[]) => {

View File

@@ -33,7 +33,7 @@ async function convert_to_jpg_and_compress(
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: "png",
quality: 0.7, quality: 0.7,
success: (res) => resolve(res.tempFilePath), success: (res) => resolve(res.tempFilePath),
fail: reject, fail: reject,

View File

@@ -1,7 +1,7 @@
import envConfig from './env'// API配置 import envConfig from './env'// API配置
// OSS 基础路径配置 // OSS 基础路径配置
export const OSS_BASE_URL = 'https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball' export const OSS_BASE_URL = 'https://youchang2026.oss-cn-shanghai.aliyuncs.com/front/ball'
export const API_CONFIG = { export const API_CONFIG = {
// 基础URL // 基础URL

View File

@@ -3,6 +3,7 @@ import ListCard from "@/components/ListCard";
import ListLoadError from "@/components/ListLoadError"; import ListLoadError from "@/components/ListLoadError";
import ListCardSkeleton from "@/components/ListCardSkeleton"; import ListCardSkeleton from "@/components/ListCardSkeleton";
import { useReachBottom } from "@tarojs/taro"; 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 { NTRPTestEntryCard } from "@/components";
import { EvaluateScene } from "@/store/evaluateStore"; import { EvaluateScene } from "@/store/evaluateStore";
@@ -158,6 +159,35 @@ const ListContainer = (props) => {
[evaluateFlag, data, hasTestInLastMonth, showNumber] [evaluateFlag, data, hasTestInLastMonth, showNumber]
); );
// 渲染 banner 卡片
const renderBanner = (item, index) => {
if (!item?.banner_image_url) return null;
return (
<View
key={item.id || `banner-${index}`}
style={{
maxHeight: "122px",
overflow: "hidden",
borderRadius: "12px",
}}
>
<Image
src={item.banner_image_url}
mode="widthFix"
style={{ width: "100%", display: "block", maxHeight: "122px" }}
onClick={() => {
const target = item.banner_detail_url;
if (target) {
(Taro as any).navigateTo({
url: `/other_pages/bannerDetail/index?img=${encodeURIComponent(target)}`,
});
}
}}
/>
</View>
);
};
// 渲染列表 // 渲染列表
const renderList = () => { const renderList = () => {
// 请求数据为空 // 请求数据为空
@@ -181,6 +211,9 @@ const ListContainer = (props) => {
return ( return (
<> <>
{memoizedList.map((match, index) => { {memoizedList.map((match, index) => {
if (match.type === "banner") {
return renderBanner(match, index);
}
if (match.type === "evaluateCard") { if (match.type === "evaluateCard") {
return ( return (
<NTRPTestEntryCard key="evaluate" type={EvaluateScene.list} /> <NTRPTestEntryCard key="evaluate" type={EvaluateScene.list} />

View File

@@ -85,7 +85,7 @@ export default function Participants(props) {
// id: 18, // id: 18,
// nickname: "小猫开刀削面店往猫毛里面下面条", // nickname: "小猫开刀削面店往猫毛里面下面条",
// avatar_url: // avatar_url:
// "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/d284060f-248b-4d58-a153-4d37c0ca77c8.jpg", // "https://youchang2026.oss-cn-shanghai.aliyuncs.com/front/ball/images/d284060f-248b-4d58-a153-4d37c0ca77c8.jpg",
// phone: "18513125687", // phone: "18513125687",
// ntrp_level: "1.5", // ntrp_level: "1.5",
// }, // },

View File

@@ -1,4 +1,32 @@
.shareContainer { .shareContainer {
.opacityContainer {
height: 140px;
width: 100%;
}
.shareImageContainer {
position: absolute;
top: 10px;
left: 50%;
width: 220px;
height: 180px;
padding: 6px;
background-color: #fafafa;
border-radius: 16px;
transform: rotateZ(-5deg) translateX(-50%);
box-shadow: 0 3px 32px 0 rgba(0, 0, 0, 0.16);
.shareImage {
width: 100%;
height: 100%;
border-radius: 12px;
}
}
.contentContainer {
background-color: #fafafa;
border-top-left-radius: 16px;
border-top-right-radius: 16px;
}
.title { .title {
padding: 20px 20px 16px; padding: 20px 20px 16px;
color: #000; color: #000;
@@ -12,6 +40,18 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
&.publishTitle {
height: 100px;
.publishText {
align-self: flex-end;
}
.closeIconWrap {
align-self: flex-start;
}
}
.publishText { .publishText {
color: #2a4d44; color: #2a4d44;
font-family: "Noto Sans SC"; font-family: "Noto Sans SC";
@@ -62,6 +102,21 @@
padding-top: 12px; padding-top: 12px;
padding-bottom: 60px; padding-bottom: 60px;
.customBtnWrapper {
position: relative;
}
.button {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
opacity: 0;
z-index: 1;
}
.customButton,
.button { .button {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -76,7 +131,7 @@
font-weight: 600; font-weight: 600;
line-height: normal; line-height: normal;
background-color: #fff; // background-color: #fff;
border: none; border: none;
padding: 0; padding: 0;
margin: 0; margin: 0;

View File

@@ -26,6 +26,7 @@ dayjs.locale("zh-cn");
export default forwardRef(({ id, from, detail, userInfo }, ref) => { export default forwardRef(({ id, from, detail, userInfo }, ref) => {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [publishFlag, setPublishFlag] = useState(false); const [publishFlag, setPublishFlag] = useState(false);
const [shareImageUrl, setShareImageUrl] = useState("");
const { fetchUserInfo } = useUserActions(); const { fetchUserInfo } = useUserActions();
// const posterRef = useRef(); // const posterRef = useRef();
const { max_participants, participant_count } = detail || {}; const { max_participants, participant_count } = detail || {};
@@ -56,13 +57,17 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
} }
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
show: (publish_flag = false) => { show: async (publish_flag = false) => {
setPublishFlag(publish_flag); setPublishFlag(publish_flag);
if (publish_flag) {
const url = await generateShareImageUrl();
setShareImageUrl(url);
}
setVisible(true); setVisible(true);
}, },
})); }));
useShareAppMessage(async (res) => { async function generateShareImageUrl() {
const { const {
play_type, play_type,
skill_level_max, skill_level_max,
@@ -76,7 +81,6 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
const endTime = dayjs(end_time); const endTime = dayjs(end_time);
const dayofWeek = DayOfWeekMap.get(startTime.day()); const dayofWeek = DayOfWeekMap.get(startTime.day());
const gameLength = `${endTime.diff(startTime, "hour")}小时`; const gameLength = `${endTime.diff(startTime, "hour")}小时`;
await changeMessageType();
const url = await generateShareImage({ const url = await generateShareImage({
userAvatar: userInfo.avatar_url, userAvatar: userInfo.avatar_url,
userNickname: userInfo.nickname, userNickname: userInfo.nickname,
@@ -90,6 +94,12 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
venueName: location_name, venueName: location_name,
venueImages: image_list ? image_list : [], venueImages: image_list ? image_list : [],
}); });
return url;
}
useShareAppMessage(async (res) => {
await changeMessageType();
const url = await generateShareImageUrl();
// console.log(res, "res"); // console.log(res, "res");
return { return {
title: detail.title, title: detail.title,
@@ -168,11 +178,31 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
showHeader={false} showHeader={false}
hideFooter hideFooter
enableDragToClose={false} enableDragToClose={false}
style={{ minHeight: "100px" }} style={{ minHeight: "100px", background: "unset" }}
zIndex={1000} zIndex={1000}
> >
<View className={styles.shareContainer}> <View className={styles.shareContainer}>
<View catchMove className={styles.title}> {publishFlag && (
<>
<View className={styles.opacityContainer} />
<View className={styles.shareImageContainer}>
<Image className={styles.shareImage} src={shareImageUrl} />
</View>
</>
)}
<View
className={styles.contentContainer}
style={{
backgroundImage: `url(${OSS_BASE_URL}/images/215f1ce1-be52-4a92-8250-5a4a69e7f2b3.png)`,
}}
>
<View
catchMove
className={classnames(
styles.title,
publishFlag ? styles.publishTitle : ""
)}
>
{publishFlag ? ( {publishFlag ? (
<Text className={styles.publishText}> 🎉</Text> <Text className={styles.publishText}> 🎉</Text>
) : ( ) : (
@@ -195,24 +225,49 @@ export default forwardRef(({ id, from, detail, userInfo }, ref) => {
</View> </View>
)} )}
<View className={styles.shareItems}> <View className={styles.shareItems}>
<View className={styles.customBtnWrapper}>
<Button className={styles.button} openType="share"> <Button className={styles.button} openType="share">
<View className={classnames(styles.icon, styles.wechatIcon)}> <View className={classnames(styles.icon, styles.wechatIcon)}>
<Image className={styles.wechat} src={WechatLogo} /> <Image className={styles.wechat} src={WechatLogo} />
</View> </View>
<Text></Text> <Text></Text>
</Button> </Button>
<View className={styles.customButton}>
<View className={classnames(styles.icon, styles.wechatIcon)}>
<Image className={styles.wechat} src={WechatLogo} />
</View>
<Text></Text>
</View>
</View>
<View className={styles.customBtnWrapper}>
<Button className={styles.button} onClick={handlePost}> <Button className={styles.button} onClick={handlePost}>
<View className={styles.icon}> <View className={styles.icon}>
<Image className={styles.download} src={DownloadIcon} /> <Image className={styles.download} src={DownloadIcon} />
</View> </View>
<Text></Text> <Text></Text>
</Button> </Button>
<View className={styles.customButton}>
<View className={styles.icon}>
<Image className={styles.download} src={DownloadIcon} />
</View>
<Text></Text>
</View>
</View>
<View className={styles.customBtnWrapper}>
<Button className={styles.button}> <Button className={styles.button}>
<View className={styles.icon}> <View className={styles.icon}>
<Image className={styles.linkIcon} src={LinkIcon} /> <Image className={styles.linkIcon} src={LinkIcon} />
</View> </View>
<Text></Text> <Text></Text>
</Button> </Button>
<View className={styles.customButton}>
<View className={styles.icon}>
<Image className={styles.linkIcon} src={LinkIcon} />
</View>
<Text></Text>
</View>
</View>
</View>
</View> </View>
</View> </View>
</CommonPopup> </CommonPopup>

View File

@@ -81,9 +81,10 @@
} }
.venue-screenshot-scroll-view { .venue-screenshot-scroll-view {
max-height: calc(100vh - 260px); max-height: calc(100dvh - 260px);
overflow-y: auto; overflow-y: auto;
padding-bottom: 40px; padding-bottom: 40px;
box-sizing: border-box;
.venue-screenshot-image-list { .venue-screenshot-image-list {
width: 100%; width: 100%;
@@ -92,9 +93,11 @@
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
gap: 10px 10px; gap: 10px 10px;
overflow: hidden;
.venue-screenshot-image-item { .venue-screenshot-image-item {
aspect-ratio: 1/1; aspect-ratio: 1/1;
min-height: 100%;
border-radius: 9px; border-radius: 9px;
border: 1px solid rgba(0, 0, 0, 0.12); border: 1px solid rgba(0, 0, 0, 0.12);
box-sizing: border-box; box-sizing: border-box;

View File

@@ -26,8 +26,7 @@ export default function VenueInfo(props) {
function previewImage(current_url) { function previewImage(current_url) {
Taro.previewImage({ Taro.previewImage({
current: current_url, current: current_url,
urls: urls: venue_image_list || [],
venue_image_list?.length > 0 ? venue_image_list.map((c) => c.url) : [],
}); });
} }
return ( return (
@@ -83,16 +82,17 @@ export default function VenueInfo(props) {
<ScrollView scrollY className={styles["venue-screenshot-scroll-view"]}> <ScrollView scrollY className={styles["venue-screenshot-scroll-view"]}>
<View className={styles["venue-screenshot-image-list"]}> <View className={styles["venue-screenshot-image-list"]}>
{venue_image_list?.length > 0 && {venue_image_list?.length > 0 &&
venue_image_list.map((item) => { venue_image_list.map((url, index) => {
return ( return (
<View <View
className={styles["venue-screenshot-image-item"]} className={styles["venue-screenshot-image-item"]}
onClick={previewImage.bind(null, item.url)} onClick={previewImage.bind(null, url)}
key={index}
> >
<Image <Image
className={styles["venue-screenshot-image-item-image"]} className={styles["venue-screenshot-image-item-image"]}
mode="aspectFill" mode="aspectFill"
src={item.url} src={url}
/> />
</View> </View>
); );

View File

@@ -63,6 +63,7 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
area, area,
cityQrCode, cityQrCode,
districts, districts,
fetchMatches,
gamesNum, // 新增:获取球局数量 gamesNum, // 新增:获取球局数量
} = store; } = store;
@@ -77,6 +78,7 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
pageOption, pageOption,
isShowNoData, isShowNoData,
} = listPageState || {}; } = listPageState || {};
console.log('===matches', matches)
const scrollContextRef = useRef(null); const scrollContextRef = useRef(null);
const scrollViewRef = useRef(null); const scrollViewRef = useRef(null);
@@ -92,6 +94,8 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
// 记录上一次加载数据时的城市,用于检测城市变化 // 记录上一次加载数据时的城市,用于检测城市变化
const lastLoadedAreaRef = useRef<[string, string] | null>(null); const lastLoadedAreaRef = useRef<[string, string] | null>(null);
const prevIsActiveRef = useRef(isActive); const prevIsActiveRef = useRef(isActive);
// 首次加载标记:避免切回 tab 时使用 isRefresh 导致智能排序顺序抖动
const hasLoadedOnceRef = useRef(false);
// 处理距离筛选显示/隐藏 // 处理距离筛选显示/隐藏
const handleDistanceFilterVisibleChange = useCallback( const handleDistanceFilterVisibleChange = useCallback(
@@ -230,7 +234,14 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
// 只有当页面激活时才加载位置和列表数据 // 只有当页面激活时才加载位置和列表数据
if (isActive) { if (isActive) {
getLocation().catch((error) => { const firstLoad = !hasLoadedOnceRef.current;
getLocation(firstLoad)
.then(() => {
if (firstLoad) {
hasLoadedOnceRef.current = true;
}
})
.catch((error) => {
console.error('获取位置信息失败:', error); console.error('获取位置信息失败:', error);
}); });
} }
@@ -359,7 +370,7 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
}; };
}, []); }, []);
const getLocation = async () => { const getLocation = async (useRefresh = true) => {
const location = await getCurrentLocationInfo(); const location = await getCurrentLocationInfo();
updateState({ location }); updateState({ location });
if (location && location.latitude && location.longitude) { if (location && location.latitude && location.longitude) {
@@ -370,7 +381,7 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
} }
} }
// 先调用列表接口 // 先调用列表接口
await getMatchesData(); await fetchMatches({}, useRefresh);
// 列表接口完成后,再调用数量接口 // 列表接口完成后,再调用数量接口
await fetchGetGamesCount(); await fetchGetGamesCount();
// 初始数据加载完成后,记录当前城市 // 初始数据加载完成后,记录当前城市
@@ -385,7 +396,9 @@ const ListPageContent: React.FC<ListPageContentProps> = ({
}; };
const handleRefresh = async () => { const handleRefresh = async () => {
if (refreshing) {
return;
}
setRefreshing(true); setRefreshing(true);
try { try {

View File

@@ -64,11 +64,17 @@ const MessagePageContent: React.FC<MessagePageContentProps> = ({ isActive = true
} }
}; };
// 只有当页面激活且未加载过数据时才加载接口 // 当切换到消息 tab 时,调用红点信息接口
useEffect(() => {
if (isActive) {
fetchReddotInfo();
}
}, [isActive]);
// 只有当页面激活且未加载过数据时才加载通知列表
useEffect(() => { useEffect(() => {
if (isActive && !hasLoaded) { if (isActive && !hasLoaded) {
getNoticeList(); getNoticeList();
fetchReddotInfo();
setHasLoaded(true); setHasLoaded(true);
} }
}, [isActive, hasLoaded]); }, [isActive, hasLoaded]);

View File

@@ -13,6 +13,7 @@ import MessagePageContent from "./components/MessagePageContent";
import MyselfPageContent from "./components/MyselfPageContent"; import MyselfPageContent from "./components/MyselfPageContent";
import "./index.scss"; import "./index.scss";
import FamilyContext from "@/context"; import FamilyContext from "@/context";
import { useDictionaryStore } from "@/store/dictionaryStore";
type TabType = "list" | "message" | "personal"; type TabType = "list" | "message" | "personal";
@@ -66,6 +67,12 @@ const MainPage: React.FC = () => {
try { try {
await fetchUserInfo(); await fetchUserInfo();
await checkNicknameChangeStatus(); await checkNicknameChangeStatus();
// 启动时预取 Banner 字典(与业务无强依赖,失败不影响主流程)
try {
await useDictionaryStore.getState().fetchBannerDictionary();
} catch (e) {
console.error("预取 Banner 字典失败:", e);
}
} catch (error) { } catch (error) {
console.error("获取用户信息失败:", error); console.error("获取用户信息失败:", error);
} }

View File

@@ -0,0 +1,8 @@
export default definePageConfig({
navigationBarTitleText: '',
navigationStyle: 'custom',
navigationBarBackgroundColor: '#FFFFFF',
backgroundColor: '#FFFFFF',
});

View File

@@ -0,0 +1,16 @@
.banner_detail_page {
min-height: 100vh;
background: #ffffff;
}
.banner_detail_content {
padding: 12px;
}
.banner_detail_image {
width: 100%;
border-radius: 12px;
display: block;
}

View File

@@ -0,0 +1,36 @@
import { useEffect, useState } from 'react';
import { View, Image } from '@tarojs/components';
import Taro from '@tarojs/taro';
import { GeneralNavbar } from '@/components';
import './index.scss';
function Index() {
const [imgUrl, setImgUrl] = useState<string>('');
useEffect(() => {
const instance = (Taro as any).getCurrentInstance?.();
const params = instance?.router?.params || {};
const url = params?.img ? decodeURIComponent(params.img) : '';
setImgUrl(url);
}, []);
return (
<View className="banner_detail_page">
<GeneralNavbar title="" showBack={true} />
<View className="banner_detail_content">
{imgUrl ? (
<Image
className="banner_detail_image"
src={imgUrl}
mode="widthFix"
showMenuByLongpress
/>
) : null}
</View>
</View>
);
}
export default Index;

View File

@@ -71,14 +71,12 @@ const CommentReply = () => {
setCommentList(mappedList); setCommentList(mappedList);
// 获取未读评论ID并标记已读 // 获取所有评论ID列表并标记已读传入所有ID包括已读和未读
const unreadIds = res.data.rows const allCommentIds = res.data.rows.map((item: any) => item.id);
.filter((item: any) => item.is_read === 0)
.map((item: any) => item.id);
if (unreadIds.length > 0) { if (allCommentIds.length > 0) {
// 使用统一接口标记已读 // 使用统一接口标记已读传入所有评论ID
messageService.markAsRead('comment', unreadIds).catch(e => { messageService.markAsRead('comment', allCommentIds).catch(e => {
console.error("标记评论已读失败:", e); console.error("标记评论已读失败:", e);
}); });
} }
@@ -217,6 +215,15 @@ const CommentReply = () => {
})); }));
setCommentList(mappedList); setCommentList(mappedList);
// 获取所有评论ID列表并标记已读传入所有ID
const allCommentIds = res.data.rows.map((item: any) => item.id);
if (allCommentIds.length > 0) {
messageService.markAsRead('comment', allCommentIds).catch(e => {
console.error("标记评论已读失败:", e);
});
}
} }
} catch (e) { } catch (e) {
Taro.showToast({ Taro.showToast({

View File

@@ -56,14 +56,12 @@ const NewFollow = () => {
setFollowList(mappedList); setFollowList(mappedList);
// 获取未读关注ID并标记已读 // 获取所有关注ID列表并标记已读传入所有ID包括已读和未读
const unreadFanIds = res.list const allFanIds = res.list.map((item: any) => item.id);
.filter((item: any) => item.is_read === 0)
.map((item: any) => item.id);
if (unreadFanIds.length > 0) { if (allFanIds.length > 0) {
// 使用统一接口标记已读 // 使用统一接口标记已读传入所有关注者ID
messageService.markAsRead('follow', unreadFanIds).catch(e => { messageService.markAsRead('follow', allFanIds).catch(e => {
console.error("标记关注已读失败:", e); console.error("标记关注已读失败:", e);
}); });
} }
@@ -164,6 +162,15 @@ const NewFollow = () => {
})); }));
setFollowList(mappedList); setFollowList(mappedList);
// 获取所有关注者ID列表并标记已读传入所有ID
const allFanIds = res.list.map((item: any) => item.id);
if (allFanIds.length > 0) {
messageService.markAsRead('follow', allFanIds).catch(e => {
console.error("标记关注已读失败:", e);
});
}
} else { } else {
// 如果没有数据,设置为空数组以显示空状态 // 如果没有数据,设置为空数组以显示空状态
setFollowList([]); setFollowList([]);

View File

@@ -146,8 +146,22 @@
.introContainer { .introContainer {
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
background-size: cover;
background-repeat: no-repeat;
position: relative;
&::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(227.15% 100% at 50% 0%, #bfffef 0%, #fff 36.58%), background: radial-gradient(227.15% 100% at 50% 0%, #bfffef 0%, #fff 36.58%),
#fafafa; #fafafa;
z-index: -1;
pointer-events: none;
}
.result { .result {
.avatarWrap { .avatarWrap {
@@ -235,6 +249,8 @@
height: 52px; height: 52px;
border-radius: 16px; border-radius: 16px;
border: 1px solid rgba(0, 0, 0, 0.06); border: 1px solid rgba(0, 0, 0, 0.06);
background: #fff;
box-shadow: 0 8px 64px 0 rgba(0, 0, 0, 0.1);
overflow: hidden; overflow: hidden;
position: relative; position: relative;
@@ -258,6 +274,16 @@
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
line-height: normal; line-height: normal;
&.primary {
color: #fff;
background: #000;
.arrowImage {
width: 20px;
height: 20px;
}
}
} }
} }
@@ -363,8 +389,23 @@
.testContainer { .testContainer {
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
background-repeat: no-repeat;
background-size: cover;
background-position: center;
position: relative;
&::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(227.15% 100% at 50% 0%, #bfffef 0%, #fff 36.58%), background: radial-gradient(227.15% 100% at 50% 0%, #bfffef 0%, #fff 36.58%),
#fafafa; #fafafa;
z-index: -1;
pointer-events: none;
}
.bar { .bar {
margin: 12px 20px 36px; margin: 12px 20px 36px;

View File

@@ -223,7 +223,12 @@ function Intro() {
} }
return ( return (
<View className={styles.introContainer}> <View
className={styles.introContainer}
style={{
backgroundImage: `url(${OSS_BASE_URL}/images/215f1ce1-be52-4a92-8250-5a4a69e7f2b3.png)`,
}}
>
<CommonGuideBar /> <CommonGuideBar />
{ntrpData?.has_test_record ? ( {ntrpData?.has_test_record ? (
<View className={styles.result}> <View className={styles.result}>
@@ -271,7 +276,7 @@ function Intro() {
</View> </View>
</View> </View>
<View className={styles.actions}> <View className={styles.actions}>
<View className={styles.buttonWrap}> <View className={classnames(styles.buttonWrap, styles.customBtn)}>
<Button <Button
className={classnames(styles.button, styles.primary)} className={classnames(styles.button, styles.primary)}
type="primary" type="primary"
@@ -280,6 +285,12 @@ function Intro() {
<Text></Text> <Text></Text>
<Image className={styles.arrowImage} src={ArrowRight} /> <Image className={styles.arrowImage} src={ArrowRight} />
</Button> </Button>
<View
className={classnames(styles.customBtnCover, styles.primary)}
>
<Text></Text>
<Image className={styles.arrowImage} src={ArrowRight} />
</View>
</View> </View>
<View className={classnames(styles.buttonWrap, styles.customBtn)}> <View className={classnames(styles.buttonWrap, styles.customBtn)}>
<Button <Button
@@ -414,7 +425,12 @@ function Test() {
return ""; return "";
} }
return ( return (
<View className={styles.testContainer}> <View
className={styles.testContainer}
style={{
backgroundImage: `url(${OSS_BASE_URL}/images/215f1ce1-be52-4a92-8250-5a4a69e7f2b3.png)`,
}}
>
<CommonGuideBar confirm title={`${index + 1} / ${questions.length}`} /> <CommonGuideBar confirm title={`${index + 1} / ${questions.length}`} />
<View className={styles.bar}> <View className={styles.bar}>
<View <View

View File

@@ -9,7 +9,7 @@
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
padding-bottom: env(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom);
box-sizing: border-box;
// 搜索区域 // 搜索区域
.search-section { .search-section {
background: #f5f5f5; background: #f5f5f5;

View File

@@ -106,8 +106,15 @@ const SelectStadium: React.FC<SelectStadiumProps> = ({
}) })
setShowDetail(true) setShowDetail(true)
}, },
fail: (err) => { fail: (err: { errMsg: string }) => {
console.error('选择位置失败:', err) console.error('选择位置失败:', err)
const { errMsg } = err || {};
if (!errMsg.includes('fail cancel')) {
Taro.showToast({
title: errMsg,
icon: "none",
});
}
} }
}) })
} }

View File

@@ -145,12 +145,15 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
istance: null istance: null
}) })
}, },
fail: (err) => { fail: (err: { errMsg: string }) => {
console.error('选择位置失败:', err) console.error('选择位置失败:', err)
const { errMsg } = err || {};
if (!errMsg.includes('fail cancel')) {
Taro.showToast({ Taro.showToast({
title: '位置选择失败', title: errMsg,
icon: 'error' icon: "none",
}) });
}
} }
}) })
} }

View File

@@ -1,3 +1,5 @@
export default definePageConfig({ export default definePageConfig({
navigationStyle: 'custom' navigationStyle: 'custom',
// 禁止原生页面滚动,改用内部自定义滚动容器,避免顶部/底部回弹后中间无法继续滚动的问题
disableScroll: true,
}) })

View File

@@ -85,6 +85,7 @@ const PublishBall: React.FC = () => {
defaultFormData, defaultFormData,
]); ]);
const [checked, setChecked] = useState(true); const [checked, setChecked] = useState(true);
const [publishLoading, setPublishLoading] = useState(false);
const [titleBar, setTitleBar] = useState("发布球局"); const [titleBar, setTitleBar] = useState("发布球局");
// 控制是否响应全局键盘(由具体输入框 focus/blur 控制) // 控制是否响应全局键盘(由具体输入框 focus/blur 控制)
const [shouldReactToKeyboard, setShouldReactToKeyboard] = useState(false); const [shouldReactToKeyboard, setShouldReactToKeyboard] = useState(false);
@@ -372,9 +373,10 @@ const PublishBall: React.FC = () => {
const { republish } = params || {}; const { republish } = params || {};
if (activityType === "individual") { if (activityType === "individual") {
const isValid = validateFormData(formData[0]); const isValid = validateFormData(formData[0]);
if (!isValid) { if (!isValid || publishLoading) {
return; return;
} }
setPublishLoading(true);
const { const {
activityInfo, activityInfo,
descriptionInfo, descriptionInfo,
@@ -435,13 +437,15 @@ const PublishBall: React.FC = () => {
title: res.message, title: res.message,
icon: "none", icon: "none",
}); });
setPublishLoading(false);
} }
} }
if (activityType === "group") { if (activityType === "group") {
const isValid = formData.every((item) => validateFormData(item)); const isValid = formData.every((item) => validateFormData(item));
if (!isValid) { if (!isValid || publishLoading) {
return; return;
} }
setPublishLoading(true);
if (checkAdjacentDataSame(formData)) { if (checkAdjacentDataSame(formData)) {
Taro.showToast({ Taro.showToast({
title: "信息不可与前序场完全一致", title: "信息不可与前序场完全一致",
@@ -505,6 +509,7 @@ const PublishBall: React.FC = () => {
title: res.message, title: res.message,
icon: "none", icon: "none",
}); });
setPublishLoading(false);
} }
} }
}; };

View File

@@ -14,6 +14,13 @@ interface DictionaryState {
fetchDictionary: () => Promise<void> fetchDictionary: () => Promise<void>
getDictionaryValue: (key: string, defaultValue?: any) => any getDictionaryValue: (key: string, defaultValue?: any) => any
clearDictionary: () => void clearDictionary: () => void
// banner 字典(单独管理,保持原始值)
bannerDict: {
bannerListImage: string
bannerDetailImage: string
bannerListIndex: string
} | null
fetchBannerDictionary: () => Promise<void>
} }
// 创建字典Store // 创建字典Store
@@ -22,6 +29,7 @@ export const useDictionaryStore = create<DictionaryState>()((set, get) => ({
dictionaryData: {}, dictionaryData: {},
isLoading: false, isLoading: false,
error: null, error: null,
bannerDict: null,
// 获取字典数据 // 获取字典数据
fetchDictionary: async () => { fetchDictionary: async () => {
@@ -56,6 +64,27 @@ export const useDictionaryStore = create<DictionaryState>()((set, get) => ({
} }
}, },
// 获取 Banner 字典(启动时或手动调用)
fetchBannerDictionary: async () => {
try {
const keys = 'bannerListImage,bannerDetailImage,bannerListIndex';
const response = await commonApi.getDictionaryManyKey(keys)
if (response.code === 0 && response.data) {
const data = response.data || {};
set({
bannerDict: {
bannerListImage: data.bannerListImage || '',
bannerDetailImage: data.bannerDetailImage || '',
bannerListIndex: (data.bannerListIndex ?? '').toString(),
}
})
}
} catch (error) {
// 保持静默,避免影响启动流程
console.error('获取 Banner 字典失败:', error)
}
},
// 获取字典值 // 获取字典值
getDictionaryValue: (key: string, defaultValue?: any) => { getDictionaryValue: (key: string, defaultValue?: any) => {
const { dictionaryData } = get() const { dictionaryData } = get()

View File

@@ -11,6 +11,8 @@ import {
getCityQrCode, getCityQrCode,
getDistricts, getDistricts,
} from "../services/listApi"; } from "../services/listApi";
// 不再在这里请求 banner 字典,统一由 dictionaryStore 启动时获取
import { useDictionaryStore } from "./dictionaryStore";
import { import {
ListActions, ListActions,
IFilterOptions, IFilterOptions,
@@ -18,6 +20,26 @@ import {
IPayload, IPayload,
} from "../../types/list/types"; } from "../../types/list/types";
// 将 banner 按索引插入到列表的工具方法0基长度不足则插末尾先移除已存在的 banner
function insertBannersToRows(rows: any[], dictData: any) {
if (!Array.isArray(rows) || !dictData) return rows;
const img = (dictData?.bannerListImage || "").trim();
const indexRaw = (dictData?.bannerListIndex || "").toString().trim();
if (!img) return rows;
const parsed = parseInt(indexRaw, 10);
const normalized = Number.isFinite(parsed) ? parsed : 0;
// 先移除已有的 banner确保列表中仅一条 banner
const resultRows = rows?.filter((item) => item?.type !== "banner") || [];
const target = Math.max(0, Math.min(normalized, resultRows.length));
resultRows.splice(target, 0, {
type: "banner",
id: `banner-${target}`,
banner_image_url: img,
banner_detail_url: (dictData?.bannerDetailImage || "").trim(),
} as any);
return resultRows;
}
function translateCityData(dataTree) { function translateCityData(dataTree) {
return dataTree.map((item) => { return dataTree.map((item) => {
const { children, ...rest } = item; const { children, ...rest } = item;
@@ -250,10 +272,14 @@ export const useListStore = create<TennisStore>()((set, get) => ({
const currentPageState = state.isSearchResult ? state.searchPageState : state.listPageState; const currentPageState = state.isSearchResult ? state.searchPageState : state.listPageState;
const currentData = currentPageState?.data || []; const currentData = currentPageState?.data || [];
const newData = isAppend ? [...currentData, ...(data || [])] : (data || []); const newData = isAppend ? [...currentData, ...(data || [])] : (data || []);
// 从字典缓存获取 banner并将其插入到最终列表指定位置全局索引
const dictData = useDictionaryStore.getState().bannerDict;
const processedData = dictData ? insertBannersToRows(newData, dictData) : newData;
state.updateCurrentPageState({ state.updateCurrentPageState({
data: newData, data: processedData,
isHasMoreData, isHasMoreData,
isShowNoData: newData?.length === 0, // 使用插入后的最终数据判断是否显示空状态,避免有 banner 时仍显示空
isShowNoData: processedData?.length === 0,
}); });
set({ set({
@@ -296,7 +322,9 @@ export const useListStore = create<TennisStore>()((set, get) => ({
} }
} }
const resData = (await fetchFn(reqParams)) || {}; let resData: any = {};
resData = (await fetchFn(reqParams)) || {};
const { data = {}, code } = resData; const { data = {}, code } = resData;
if (code !== 0) { if (code !== 0) {
setListData({ setListData({
@@ -308,7 +336,9 @@ export const useListStore = create<TennisStore>()((set, get) => ({
}); });
return Promise.reject(new Error('获取数据失败')); return Promise.reject(new Error('获取数据失败'));
} }
const { count, rows } = data; const { count } = data;
let { rows } = data as any;
setListData({ setListData({
error: '', error: '',
data: rows || [], data: rows || [],
@@ -319,7 +349,7 @@ export const useListStore = create<TennisStore>()((set, get) => ({
return Promise.resolve(); return Promise.resolve();
} catch (error) { } catch (error) {
setListData({ setListData({
error: "-1", error: "",
data: [], data: [],
loading: false, loading: false,
count: 0, count: 0,
@@ -345,23 +375,24 @@ export const useListStore = create<TennisStore>()((set, get) => ({
try { try {
const searchParams = getSearchParams() || {}; const searchParams = getSearchParams() || {};
// 调用常规列表接口 // 并发请求:常规列表、智能排序列表
const listParams = { const [listResSettled, integrateResSettled] = await Promise.allSettled([
getGamesList({
...searchParams, ...searchParams,
order: searchParams.order || "distance", order: searchParams.order || "distance",
}; }),
const listRes = await getGamesList(listParams); getGamesIntegrateList({
// 调用智能排序列表接口
const integrateParams = {
...searchParams, ...searchParams,
order: "", order: "",
seachOption: { seachOption: {
...searchParams.seachOption, ...searchParams.seachOption,
isRefresh: true, isRefresh: true,
}, },
}; }),
const integrateRes = await getGamesIntegrateList(integrateParams); ]);
const listRes = listResSettled.status === "fulfilled" ? listResSettled.value : null;
const integrateRes = integrateResSettled.status === "fulfilled" ? integrateResSettled.value : null;
// 根据当前排序方式更新对应的数据 // 根据当前排序方式更新对应的数据
const currentPageState = state.isSearchResult ? state.searchPageState : state.listPageState; const currentPageState = state.isSearchResult ? state.searchPageState : state.listPageState;
@@ -369,7 +400,8 @@ export const useListStore = create<TennisStore>()((set, get) => ({
const isIntegrate = distanceQuickFilter?.order === "0"; const isIntegrate = distanceQuickFilter?.order === "0";
if (listRes?.code === 0 && listRes?.data) { if (listRes?.code === 0 && listRes?.data) {
const { count, rows } = listRes.data; const { count } = listRes.data;
let { rows } = listRes.data as any;
if (!isIntegrate) { if (!isIntegrate) {
// 如果当前是常规排序,更新常规列表数据 // 如果当前是常规排序,更新常规列表数据
setListData({ setListData({
@@ -383,7 +415,8 @@ export const useListStore = create<TennisStore>()((set, get) => ({
} }
if (integrateRes?.code === 0 && integrateRes?.data) { if (integrateRes?.code === 0 && integrateRes?.data) {
const { count, rows, recommendList } = integrateRes.data; const { count } = integrateRes.data;
let { rows, recommendList } = integrateRes.data as any;
if (isIntegrate) { if (isIntegrate) {
// 如果当前是智能排序,更新智能排序列表数据 // 如果当前是智能排序,更新智能排序列表数据
setListData({ setListData({

View File

@@ -275,10 +275,7 @@ const DownloadBill: React.FC = () => {
Taro.navigateBack(); Taro.navigateBack();
}} }}
/> />
<View <View className="hint_content" style={{ marginTop: `147px` }}>
className="hint_content"
style={{ marginTop: `${totalHeight}px` }}
>
<Text> </Text> <Text> </Text>
<Text className="button_text" onClick={downloadExample}> <Text className="button_text" onClick={downloadExample}>
@@ -322,7 +319,8 @@ const DownloadBill: React.FC = () => {
</View> </View>
<View <View
className={`option_button ${dateType === "month" ? "active" : "" className={`option_button ${
dateType === "month" ? "active" : ""
}`} }`}
onClick={() => { onClick={() => {
selectDateRange("month"); selectDateRange("month");
@@ -331,7 +329,8 @@ const DownloadBill: React.FC = () => {
</View> </View>
<View <View
className={`option_button ${dateType === "custom" ? "active" : "" className={`option_button ${
dateType === "custom" ? "active" : ""
}`} }`}
onClick={() => { onClick={() => {
selectDateRange("custom"); selectDateRange("custom");

View File

@@ -76,7 +76,7 @@
} }
.btn { .btn {
width: 78px; width: 90px;
height: 24px; height: 24px;
border: 1px solid rgba(0, 0, 0, 0.06); border: 1px solid rgba(0, 0, 0, 0.06);
display: flex; display: flex;

View File

@@ -64,7 +64,7 @@
} }
.btn { .btn {
width: 78px; width: 90px;
height: 24px; height: 24px;
border: 1px solid rgba(0, 0, 0, 0.06); border: 1px solid rgba(0, 0, 0, 0.06);
display: flex; display: flex;