Merge branch master into feature/juguohong/20250816
This commit is contained in:
@@ -1,26 +1,9 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { View } from "@tarojs/components";
|
||||
import Taro from "@tarojs/taro";
|
||||
import { getCurrentFullPath } from '@/utils';
|
||||
import { check_login_status } from "@/services/loginService";
|
||||
|
||||
export function getCurrentFullPath(): string {
|
||||
const pages = Taro.getCurrentPages();
|
||||
const currentPage = pages.at(-1);
|
||||
|
||||
if (currentPage) {
|
||||
console.log(currentPage, "currentPage get");
|
||||
const route = currentPage.route;
|
||||
const options = currentPage.options || {};
|
||||
|
||||
const query = Object.keys(options)
|
||||
.map((key) => `${key}=${options[key]}`)
|
||||
.join("&");
|
||||
|
||||
return query ? `/${route}?${query}` : `/${route}`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export default function withAuth<P extends object>(
|
||||
WrappedComponent: React.ComponentType<P>,
|
||||
) {
|
||||
|
||||
214
src/components/Comments/index.module.scss
Normal file
214
src/components/Comments/index.module.scss
Normal file
@@ -0,0 +1,214 @@
|
||||
.container {
|
||||
.header {
|
||||
padding: 20px 20px 0;
|
||||
|
||||
.commentCount {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.10);
|
||||
padding-bottom: 8px;
|
||||
color: #FFF;
|
||||
font-feature-settings: 'liga' off, 'clig' off;
|
||||
text-overflow: ellipsis;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.addComment {
|
||||
height: 40px;
|
||||
padding: 0 12px;
|
||||
margin: 12px 0;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.20);
|
||||
|
||||
.addCommentImage {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.addCommentText {
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
font-feature-settings: 'liga' off, 'clig' off;
|
||||
text-overflow: ellipsis;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.list {
|
||||
padding: 12px 20px;
|
||||
|
||||
& > .commentItem {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.commentItem {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
|
||||
.avatar {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.contents {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
gap: 12px;
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
gap: 4px;
|
||||
|
||||
.publisherInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 4px;
|
||||
|
||||
.nickname {
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
font-feature-settings: 'liga' off, 'clig' off;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.role {
|
||||
padding: 0 4px;
|
||||
height: 18px;
|
||||
border-radius: 3px;
|
||||
background: rgba(255, 255, 255, 0.10);
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
font-feature-settings: 'liga' off, 'clig' off;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 10px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
color: #FFF;
|
||||
font-feature-settings: 'liga' off, 'clig' off;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
|
||||
.atPeople {
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
}
|
||||
|
||||
.addons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 6px;
|
||||
|
||||
.time, .location, .reply, .delete {
|
||||
font-feature-settings: 'liga' off, 'clig' off;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
line-height: 18px;
|
||||
|
||||
&.time, &.location {
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&.reply, &.delete {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.viewMore {
|
||||
color: #FFF;
|
||||
font-feature-settings: 'liga' off, 'clig' off;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inputContainer {
|
||||
height: 36px;
|
||||
padding: 12px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
|
||||
.inputWrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
& > .input {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.sendIcon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background-color: #000;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.06);
|
||||
|
||||
.sendImage {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty {
|
||||
height: 40vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
|
||||
.emptyTip {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-feature-settings: 'liga' off, 'clig' off;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
423
src/components/Comments/index.tsx
Normal file
423
src/components/Comments/index.tsx
Normal file
@@ -0,0 +1,423 @@
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
forwardRef,
|
||||
useRef,
|
||||
useImperativeHandle,
|
||||
} from "react";
|
||||
import { View, Text, Image, Input } from "@tarojs/components";
|
||||
import Taro from "@tarojs/taro";
|
||||
import dayjs from "dayjs";
|
||||
import CommentServices from "@/services/commentServices";
|
||||
import type {
|
||||
BaseComment,
|
||||
Comment,
|
||||
ReplyComment,
|
||||
} from "@/services/commentServices";
|
||||
import { useUserInfo } from "@/store/userStore";
|
||||
import sendImg from "@/static/detail/icon-sendup.svg";
|
||||
import addComment from "@/static/detail/icon-write.svg";
|
||||
import emptyComment from "@/static/emptyStatus/comment-empty.png";
|
||||
import CommonPopup from "../CommonPopup";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
// const PAGESIZE = 4;
|
||||
const PAGESIZE = 1000;
|
||||
|
||||
function toast(msg) {
|
||||
Taro.showToast({ title: msg, icon: "none" });
|
||||
}
|
||||
|
||||
interface CommentInputProps {
|
||||
onConfirm?: (
|
||||
value: { content: string } & Partial<CommentInputReplyParamsType>
|
||||
) => void;
|
||||
}
|
||||
|
||||
// 2️⃣ 定义通过 ref 暴露出去的方法类型
|
||||
interface CommentInputRef {
|
||||
show: (params?: CommentInputReplyParamsType) => void;
|
||||
}
|
||||
|
||||
interface CommentInputReplyParamsType {
|
||||
parent_id: number;
|
||||
reply_to_user_id: number;
|
||||
nickname: string;
|
||||
}
|
||||
|
||||
const CommentInput = forwardRef<CommentInputRef, CommentInputProps>(function (
|
||||
props,
|
||||
ref
|
||||
) {
|
||||
const { onConfirm } = props;
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [value, setValue] = useState("");
|
||||
const [params, setParams] = useState<
|
||||
CommentInputReplyParamsType | undefined
|
||||
>();
|
||||
|
||||
const inputDomRef = useRef(null);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
show: (_params: CommentInputReplyParamsType | undefined) => {
|
||||
setVisible(true);
|
||||
setTimeout(() => {
|
||||
inputDomRef.current && inputDomRef.current?.focus();
|
||||
}, 100);
|
||||
setParams(_params);
|
||||
},
|
||||
}));
|
||||
|
||||
function handleSend() {
|
||||
if (!value) {
|
||||
toast("评论内容不得为空");
|
||||
return;
|
||||
}
|
||||
onConfirm?.({ content: value, ...params });
|
||||
onClose();
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
setVisible(false);
|
||||
setValue("");
|
||||
inputDomRef.current && inputDomRef.current?.blur();
|
||||
}
|
||||
return (
|
||||
<CommonPopup
|
||||
visible={visible}
|
||||
showHeader={false}
|
||||
hideFooter
|
||||
zIndex={1002}
|
||||
onClose={onClose}
|
||||
style={{ height: "60px!important", minHeight: "unset" }}
|
||||
enableDragToClose={false}
|
||||
>
|
||||
<View className={styles.inputContainer}>
|
||||
<View className={styles.inputWrapper}>
|
||||
<Input
|
||||
ref={inputDomRef}
|
||||
className={styles.input}
|
||||
value={value}
|
||||
onInput={(e) => setValue(e.detail.value)}
|
||||
placeholder={
|
||||
params?.reply_to_user_id ? `回复 @${params.nickname}` : "写评论"
|
||||
}
|
||||
focus
|
||||
maxlength={100}
|
||||
/>
|
||||
</View>
|
||||
<View className={styles.sendIcon} onClick={handleSend}>
|
||||
<Image className={styles.sendImage} src={sendImg} />
|
||||
</View>
|
||||
</View>
|
||||
</CommonPopup>
|
||||
);
|
||||
});
|
||||
|
||||
function isReplyComment(item: BaseComment<any>): item is ReplyComment {
|
||||
return "reply_to_user" in item;
|
||||
}
|
||||
|
||||
function getRelativeDay(time) {
|
||||
const theTime = dayjs(time);
|
||||
const isThisYear = dayjs().isSame(theTime, "year");
|
||||
const diffDay = dayjs().startOf("day").diff(theTime, "day");
|
||||
return diffDay <= 3
|
||||
? diffDay >= 1
|
||||
? `${diffDay}天前`
|
||||
: theTime.format("HH:mm:ss")
|
||||
: theTime.format(isThisYear ? "MM-DD HH:mm:ss" : "YYYY-MM-DD HH:mm:ss");
|
||||
}
|
||||
|
||||
function CommentItem(props: {
|
||||
level: number;
|
||||
publisher_id: number;
|
||||
comment: Comment | ReplyComment;
|
||||
loadMore: (c: Comment) => void;
|
||||
handleReply: (options: CommentInputReplyParamsType) => void;
|
||||
handleDelete: (options: { parent_id: number | null; id: number }) => void;
|
||||
}) {
|
||||
const {
|
||||
level,
|
||||
publisher_id,
|
||||
comment,
|
||||
loadMore: handleLoadMore,
|
||||
handleReply,
|
||||
handleDelete,
|
||||
} = props;
|
||||
const currentUserInfo = useUserInfo();
|
||||
const isGamePublisher = publisher_id === comment.user.id;
|
||||
const isCommentPublisher = currentUserInfo.id === comment.user.id;
|
||||
return (
|
||||
<View className={styles.commentItem} key={comment.id}>
|
||||
<View style={{ width: level === 1 ? "36px" : "28px" }}>
|
||||
<Image
|
||||
className={styles.avatar}
|
||||
src={comment.user.avatar_url}
|
||||
mode="aspectFill"
|
||||
style={
|
||||
level === 1
|
||||
? { width: "36px", height: "36px" }
|
||||
: { width: "28px", height: "28px" }
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<View className={styles.contents}>
|
||||
<View className={styles.main}>
|
||||
<View className={styles.publisherInfo}>
|
||||
<View className={styles.nickname}>
|
||||
<Text>{comment.user.nickname}</Text>
|
||||
</View>
|
||||
{isGamePublisher && (
|
||||
<View className={styles.role}>
|
||||
<Text>组织者</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<View className={styles.content}>
|
||||
<Text className={styles.atPeople}>
|
||||
{isReplyComment(comment) && comment.reply_to_user
|
||||
? `@${comment.reply_to_user.nickname} `
|
||||
: ""}
|
||||
</Text>
|
||||
<Text>{comment.content}</Text>
|
||||
</View>
|
||||
<View className={styles.addons}>
|
||||
<View className={styles.time}>
|
||||
<Text>{getRelativeDay(comment.create_time)}</Text>
|
||||
</View>
|
||||
<View className={styles.location}>
|
||||
<Text>上海</Text>
|
||||
</View>
|
||||
<View
|
||||
className={styles.reply}
|
||||
onClick={() =>
|
||||
handleReply({
|
||||
parent_id: comment.parent_id || comment.id,
|
||||
reply_to_user_id: comment.user.id,
|
||||
nickname: comment.user.nickname,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Text>回复</Text>
|
||||
</View>
|
||||
{isGamePublisher || isCommentPublisher}
|
||||
<View
|
||||
className={styles.delete}
|
||||
onClick={() =>
|
||||
handleDelete({
|
||||
parent_id: comment.parent_id,
|
||||
id: comment.id,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Text>删除</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
{!isReplyComment(comment) &&
|
||||
comment.replies.map((item: ReplyComment) => (
|
||||
<CommentItem
|
||||
key={comment.id}
|
||||
publisher_id={publisher_id}
|
||||
comment={item}
|
||||
level={2}
|
||||
loadMore={handleLoadMore}
|
||||
handleReply={handleReply}
|
||||
handleDelete={handleDelete}
|
||||
/>
|
||||
))}
|
||||
{!isReplyComment(comment) &&
|
||||
comment.replies.length !== comment.reply_count && (
|
||||
<View
|
||||
className={styles.viewMore}
|
||||
onClick={() => handleLoadMore(comment)}
|
||||
>
|
||||
展开更多评论
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default forwardRef(function Comments(
|
||||
props: { game_id: number; publisher_id: number },
|
||||
ref
|
||||
) {
|
||||
const { game_id, publisher_id } = props;
|
||||
const [comments, setComments] = useState<Comment[]>([]);
|
||||
const inputRef = useRef<CommentInputRef>(null);
|
||||
|
||||
const commentCountUpdateRef = useRef()
|
||||
|
||||
useEffect(() => {
|
||||
getComments(1);
|
||||
}, [game_id]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
addComment: handleReply,
|
||||
getCommentCount: (onUpdate) => {
|
||||
commentCountUpdateRef.current = onUpdate
|
||||
onUpdate(comments.length)
|
||||
},
|
||||
}));
|
||||
|
||||
async function getComments(page) {
|
||||
if (!game_id) return;
|
||||
const res = await CommentServices.getComments({
|
||||
page,
|
||||
pageSize: PAGESIZE,
|
||||
game_id,
|
||||
});
|
||||
if (res.code === 0) {
|
||||
const newComments: Comment[] = res.data.rows;
|
||||
setComments((prev) => {
|
||||
const res = [...prev];
|
||||
res.splice(page * PAGESIZE - 1, newComments.length, ...newComments);
|
||||
commentCountUpdateRef.current?.(res.length)
|
||||
return res;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function getReplies(c: Comment) {
|
||||
const { replies, id: comment_id } = c;
|
||||
const page = replies.length < PAGESIZE ? 1 : replies.length / PAGESIZE + 1;
|
||||
const res = await CommentServices.getReplies({
|
||||
comment_id,
|
||||
page,
|
||||
pageSize: PAGESIZE,
|
||||
});
|
||||
if (res.code === 0) {
|
||||
const newReplies = res.data.rows;
|
||||
setComments((prev) => {
|
||||
const newComments = [...prev];
|
||||
newComments.forEach((item) => {
|
||||
if (item.id === comment_id) {
|
||||
item.replies.splice(
|
||||
page === 1 ? 0 : page * PAGESIZE - 1,
|
||||
newReplies.length,
|
||||
...newReplies
|
||||
);
|
||||
item.reply_count = res.data.count;
|
||||
}
|
||||
});
|
||||
commentCountUpdateRef.current?.(newComments.length)
|
||||
return newComments;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleReply(options?: CommentInputReplyParamsType) {
|
||||
inputRef.current?.show(options);
|
||||
}
|
||||
|
||||
function onSend({ content, parent_id, reply_to_user_id }) {
|
||||
if (!parent_id) {
|
||||
createComment(content);
|
||||
return;
|
||||
}
|
||||
replyComment({ content, parent_id, reply_to_user_id });
|
||||
}
|
||||
|
||||
async function createComment(val: string) {
|
||||
const res = await CommentServices.createComment({ game_id, content: val });
|
||||
if (res.code === 0) {
|
||||
setComments((prev) => {
|
||||
commentCountUpdateRef.current?.(prev.length + 1)
|
||||
return [{ ...res.data, replies: [] }, ...prev];
|
||||
});
|
||||
toast("发布成功");
|
||||
}
|
||||
}
|
||||
|
||||
async function replyComment({ parent_id, reply_to_user_id, content }) {
|
||||
const res = await CommentServices.replyComment({
|
||||
parent_id,
|
||||
reply_to_user_id,
|
||||
content,
|
||||
});
|
||||
if (res.code === 0) {
|
||||
setComments((prev) => {
|
||||
return prev.map((item) => {
|
||||
if (item.id === parent_id) {
|
||||
return {
|
||||
...item,
|
||||
replies: [res.data, ...item.replies],
|
||||
reply_count: item.reply_count + 1,
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
});
|
||||
toast("回复成功");
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteComment({ parent_id, id }) {
|
||||
const res = await CommentServices.deleteComment({ comment_id: id });
|
||||
if (res.code === 0) {
|
||||
if (parent_id) {
|
||||
setComments((prev) => {
|
||||
return prev.map((item) => {
|
||||
if (item.id === parent_id) {
|
||||
return {
|
||||
...item,
|
||||
replies: item.replies.filter(
|
||||
(replyItem) => replyItem.id !== id
|
||||
),
|
||||
reply_count: item.reply_count - 1,
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
});
|
||||
} else {
|
||||
setComments((prev) => {
|
||||
commentCountUpdateRef.current?.(prev.length - 1)
|
||||
return prev.filter((item) => item.id !== id);
|
||||
});
|
||||
}
|
||||
toast("评论已删除");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View className={styles.container}>
|
||||
<View className={styles.header}>
|
||||
<View className={styles.commentCount}>
|
||||
{comments.length > 0 ? `${comments.length} 条` : ""}评论
|
||||
</View>
|
||||
<View className={styles.addComment} onClick={() => handleReply()}>
|
||||
<Image className={styles.addCommentImage} src={addComment} />
|
||||
<Text className={styles.addCommentText}>写评论</Text>
|
||||
</View>
|
||||
</View>
|
||||
{comments.length > 0 ? (
|
||||
<View className={styles.list}>
|
||||
{comments.map((comment) => {
|
||||
return (
|
||||
<CommentItem
|
||||
key={comment.id}
|
||||
publisher_id={publisher_id}
|
||||
level={1}
|
||||
comment={comment}
|
||||
loadMore={getReplies}
|
||||
handleReply={handleReply}
|
||||
handleDelete={deleteComment}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
) : (
|
||||
<View className={styles.empty}>
|
||||
<Image className={styles.emptyImage} src={emptyComment} />
|
||||
<Text className={styles.emptyTip}>快来发表第一条评论</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<CommentInput ref={inputRef} onConfirm={onSend} />
|
||||
</View>
|
||||
);
|
||||
});
|
||||
@@ -63,7 +63,7 @@
|
||||
border: none;
|
||||
width: 154px;
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
border-radius: 12px!important;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.06);
|
||||
background: #fff;
|
||||
padding: 4px 10px;
|
||||
|
||||
125
src/components/FollowUserCard/index.scss
Normal file
125
src/components/FollowUserCard/index.scss
Normal file
@@ -0,0 +1,125 @@
|
||||
// 球友卡片样式
|
||||
.follow_user_card {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 20px;
|
||||
background: #ffffff;
|
||||
height: 56px;
|
||||
margin-top: 12px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.user_info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
gap: 12px;
|
||||
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.user_details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
max-width: 200px;
|
||||
|
||||
.nickname {
|
||||
font-family: PingFang SC;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 16px;
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.signature {
|
||||
font-family: PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: rgba(60, 60, 67, 0.6);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action_button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 6px 16px;
|
||||
border-radius: 20px;
|
||||
white-space: nowrap;
|
||||
|
||||
.button_text {
|
||||
font-family: PingFang SC;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
&.follow_button {
|
||||
border: 0.5px solid #000000 !important;
|
||||
background: transparent !important;
|
||||
|
||||
.button_text {
|
||||
color: #000000 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.following_button {
|
||||
border: 0.5px solid rgba(120, 120, 128, 0.12) !important;
|
||||
background: transparent !important;
|
||||
|
||||
.button_text {
|
||||
color: rgba(0, 0, 0, 0.8) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.mutual_button {
|
||||
border: 0.5px solid rgba(120, 120, 128, 0.12) !important;
|
||||
background: transparent !important;
|
||||
|
||||
.button_text {
|
||||
color: rgba(0, 0, 0, 0.8) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.processing {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.delete_button {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-left: 4px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
&::before, &::after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 13px;
|
||||
height: 2px;
|
||||
border-radius: 2px;
|
||||
background: #8c8c8c;
|
||||
position: absolute;
|
||||
}
|
||||
&::before {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
&::after {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
144
src/components/FollowUserCard/index.tsx
Normal file
144
src/components/FollowUserCard/index.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, Image } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { FollowUser } from '@/services/followService';
|
||||
import './index.scss';
|
||||
|
||||
|
||||
// 标签页类型
|
||||
type TabType = 'mutual_follow' | 'following' | 'follower' | 'recommend';
|
||||
|
||||
interface FollowUserCardProps {
|
||||
user: FollowUser;
|
||||
tabKey: TabType;
|
||||
onFollowChange?: (userId: number, isFollowing: boolean) => void;
|
||||
}
|
||||
|
||||
const FollowUserCard: React.FC<FollowUserCardProps> = ({ user, tabKey, onFollowChange }) => {
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
|
||||
// 防御性检查
|
||||
if (!user || !user.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 处理关注操作
|
||||
const handle_follow_action = async () => {
|
||||
if (isProcessing) return;
|
||||
|
||||
try {
|
||||
setIsProcessing(true);
|
||||
|
||||
// 根据当前状态决定操作
|
||||
let new_status = false;
|
||||
if (user.follow_status === 'follower' || user.follow_status === 'recommend') {
|
||||
// 粉丝或推荐用户,执行关注操作
|
||||
new_status = true;
|
||||
} else if (user.follow_status === 'following' || user.follow_status === 'mutual_follow') {
|
||||
// 已关注或互相关注,执行取消关注操作
|
||||
new_status = false;
|
||||
}
|
||||
|
||||
onFollowChange?.(user.id, new_status);
|
||||
} catch (error) {
|
||||
console.error('关注操作失败:', error);
|
||||
Taro.showToast({
|
||||
title: '操作失败',
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 加入黑名单
|
||||
const add_to_blacklist = () => {
|
||||
if (isProcessing) return;
|
||||
|
||||
try {
|
||||
setIsProcessing(true);
|
||||
// TODO: 加入黑名单逻辑
|
||||
Taro.showToast({
|
||||
title: '不会再为您推荐该用户',
|
||||
icon: 'none'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('删除推荐人员失败:', error);
|
||||
Taro.showToast({
|
||||
title: '操作失败',
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理用户点击
|
||||
const handle_user_click = () => {
|
||||
Taro.navigateTo({
|
||||
url: `/user_pages/other/index?userid=${user.id}`
|
||||
});
|
||||
};
|
||||
|
||||
// 获取按钮文本和样式
|
||||
const get_button_config = () => {
|
||||
switch (user.follow_status) {
|
||||
case 'follower':
|
||||
case 'recommend':
|
||||
return {
|
||||
text: '关注',
|
||||
className: 'follow_button'
|
||||
};
|
||||
case 'following':
|
||||
return {
|
||||
text: '已关注',
|
||||
className: 'following_button'
|
||||
};
|
||||
case 'mutual_follow':
|
||||
return {
|
||||
text: '互相关注',
|
||||
className: 'mutual_button'
|
||||
};
|
||||
default:
|
||||
return {
|
||||
text: '关注',
|
||||
className: 'follow_button'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const button_config = get_button_config();
|
||||
|
||||
return (
|
||||
<View className="follow_user_card">
|
||||
<View className="user_info" onClick={handle_user_click}>
|
||||
<Image
|
||||
className="avatar"
|
||||
src={user.avatar_url || require('@/static/userInfo/default_avatar.svg')}
|
||||
/>
|
||||
<View className="user_details">
|
||||
<Text className="nickname">{user.nickname}</Text>
|
||||
<Text className="signature">
|
||||
{user.personal_profile?.replace(/\n/g, ' ') || '签名写在这里'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
className={`action_button ${button_config.className} ${isProcessing ? 'processing' : ''}`}
|
||||
onClick={handle_follow_action}
|
||||
>
|
||||
<Text className="button_text">
|
||||
{isProcessing ? '处理中...' : button_config.text}
|
||||
</Text>
|
||||
</View>
|
||||
{
|
||||
tabKey === 'recommend' && (
|
||||
<View className='delete_button' onClick={add_to_blacklist}></View>
|
||||
)
|
||||
}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default FollowUserCard;
|
||||
112
src/components/GameManagePopup/index.module.scss
Normal file
112
src/components/GameManagePopup/index.module.scss
Normal file
@@ -0,0 +1,112 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 40px;
|
||||
|
||||
.button {
|
||||
width: 100%;
|
||||
padding: 20px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #000;
|
||||
text-align: center;
|
||||
font-feature-settings: 'liga' off, 'clig' off;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
|
||||
&:last-child {
|
||||
border-top: 8px solid #f5f5f5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.centerContainer {
|
||||
overflow: hidden;
|
||||
.title {
|
||||
padding-top: 24px;
|
||||
color: #000;
|
||||
text-align: center;
|
||||
font-feature-settings: 'liga' off, 'clig' off;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.tips {
|
||||
color: rgba(60, 60, 67, 0.60);
|
||||
font-feature-settings: 'liga' off, 'clig' off;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.cancelReason {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
background: #F0F0F0;
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
&:placeholder-shown {
|
||||
color: rgba(60, 60, 67, 0.30);
|
||||
font-feature-settings: 'liga' off, 'clig' off;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 44px;
|
||||
border-top: 0.5px solid #CECECE;
|
||||
background: #FFF;
|
||||
margin-top: 2px;
|
||||
|
||||
.confirm, .cancel {
|
||||
width: 50%;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #000;
|
||||
text-align: center;
|
||||
font-feature-settings: 'liga' off, 'clig' off;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
|
||||
&.cancel {
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
225
src/components/GameManagePopup/index.tsx
Normal file
225
src/components/GameManagePopup/index.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
import React, {
|
||||
useState,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
} from "react";
|
||||
import Taro from "@tarojs/taro";
|
||||
import { View, Text, Input } from "@tarojs/components";
|
||||
import CommonPopup from "../CommonPopup";
|
||||
import styles from "./index.module.scss";
|
||||
import detailService, { MATCH_STATUS } from "@/services/detailService";
|
||||
import { useUserInfo } from "@/store/userStore";
|
||||
|
||||
const CancelPopup = forwardRef((props, ref) => {
|
||||
const { detail } = props;
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [cancelReason, setCancelReason] = useState("");
|
||||
const onFinish = useRef(null);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
const { current_players, participants = [], publisher_id } = detail;
|
||||
const realParticipants = participants
|
||||
.filter((item) => item.status === "joined")
|
||||
.map((item) => item.user.id);
|
||||
const hasOtherJoin =
|
||||
current_players > 1 ||
|
||||
realParticipants.some((id) => id !== Number(publisher_id));
|
||||
// const hasOtherJoin = true;
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
show: (onAct) => {
|
||||
onFinish.current = onAct;
|
||||
setVisible(true);
|
||||
setTimeout(() => {
|
||||
inputRef.current && inputRef.current.focus();
|
||||
}, 0);
|
||||
},
|
||||
}));
|
||||
|
||||
function onClose() {
|
||||
setVisible(false);
|
||||
setCancelReason("");
|
||||
}
|
||||
|
||||
async function handleConfirm() {
|
||||
if (!cancelReason) {
|
||||
Taro.showToast({ title: "请输入取消原因", icon: "none" });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await onFinish.current(cancelReason);
|
||||
onClose();
|
||||
} catch (e) {
|
||||
console.log(e, 1221);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<CommonPopup
|
||||
visible={visible}
|
||||
showHeader={false}
|
||||
hideFooter
|
||||
zIndex={1002}
|
||||
enableDragToClose={false}
|
||||
onClose={onClose}
|
||||
position="center"
|
||||
style={{
|
||||
width: hasOtherJoin ? "360px" : "300px",
|
||||
borderRadius: "16px",
|
||||
}}
|
||||
>
|
||||
<View className={styles.centerContainer}>
|
||||
<View className={styles.title}>确定要取消活动吗?</View>
|
||||
<View className={styles.content}>
|
||||
<Text className={styles.tips}>
|
||||
{hasOtherJoin
|
||||
? "已有球友报名,取消后将为他们自动退款"
|
||||
: "有100+球友正在浏览您的球局哦~"}
|
||||
</Text>
|
||||
{hasOtherJoin && (
|
||||
<View className={styles.cancelReason}>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
className={styles.input}
|
||||
placeholder="请输入取消理由"
|
||||
focus
|
||||
value={cancelReason}
|
||||
onInput={(e) => setCancelReason(e.detail.value)}
|
||||
maxlength={100}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<View className={styles.actions}>
|
||||
<View className={styles.confirm} onClick={handleConfirm}>
|
||||
确认取消
|
||||
</View>
|
||||
<View className={styles.cancel} onClick={onClose}>
|
||||
再想想
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</CommonPopup>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default forwardRef(function GameManagePopup(props, ref) {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [detail, setDetail] = useState({});
|
||||
const onStatusChange = useRef(null);
|
||||
const cancelRef = useRef(null);
|
||||
const userInfo = useUserInfo();
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
show: (gameDetail, onChange) => {
|
||||
onStatusChange.current = onChange;
|
||||
setDetail(gameDetail);
|
||||
setVisible(true);
|
||||
},
|
||||
}));
|
||||
|
||||
function handleEditGame() {
|
||||
Taro.navigateTo({
|
||||
url: `/publish_pages/publishBall/index?gameId=${detail.id}&republish=0`,
|
||||
});
|
||||
onClose();
|
||||
}
|
||||
|
||||
function handleRepubGame() {
|
||||
Taro.navigateTo({
|
||||
url: `/publish_pages/publishBall/index?gameId=${detail.id}&republish=1`,
|
||||
});
|
||||
onClose();
|
||||
}
|
||||
|
||||
async function handleCancelGame() {
|
||||
cancelRef.current.show(async (result) => {
|
||||
if (result) {
|
||||
try {
|
||||
const res = await detailService.disbandGame({
|
||||
game_id: detail.id,
|
||||
settle_reason: result,
|
||||
});
|
||||
if (res.code === 0) {
|
||||
Taro.showToast({ title: "活动取消成功" });
|
||||
onStatusChange.current?.(true);
|
||||
}
|
||||
} catch (e) {
|
||||
Taro.showToast({ title: e.message, icon: "error" });
|
||||
return e;
|
||||
}
|
||||
}
|
||||
});
|
||||
onClose();
|
||||
}
|
||||
|
||||
async function handleQuitGame() {
|
||||
try {
|
||||
const res = await detailService.organizerQuit({
|
||||
game_id: detail.id,
|
||||
quit_reason: "组织者主动退出",
|
||||
});
|
||||
if (res.code === 0) {
|
||||
Taro.showToast({ title: "活动退出成功" });
|
||||
onStatusChange.current?.(true);
|
||||
}
|
||||
} catch (e) {
|
||||
Taro.showToast({ title: e.message, icon: "error" });
|
||||
} finally {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
setVisible(false);
|
||||
}
|
||||
|
||||
const hasJoin = (detail.participants || [])
|
||||
.filter((item) => item.status === "joined")
|
||||
.some((item) => item.user.id === userInfo.id);
|
||||
|
||||
const finished = [MATCH_STATUS.FINISHED, MATCH_STATUS.CANCELED].includes(
|
||||
detail.match_status
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CommonPopup
|
||||
visible={visible}
|
||||
showHeader={false}
|
||||
hideFooter
|
||||
zIndex={1001}
|
||||
enableDragToClose={false}
|
||||
onClose={onClose}
|
||||
>
|
||||
<View className={styles.container}>
|
||||
<View className={styles.button} onClick={handleEditGame}>
|
||||
编辑活动
|
||||
</View>
|
||||
{finished && (
|
||||
<View className={styles.button} onClick={handleRepubGame}>
|
||||
重新发布
|
||||
</View>
|
||||
)}
|
||||
{!finished && (
|
||||
<View className={styles.button} onClick={handleCancelGame}>
|
||||
取消活动
|
||||
</View>
|
||||
)}
|
||||
{hasJoin && (
|
||||
<View className={styles.button} onClick={handleQuitGame}>
|
||||
退出活动
|
||||
</View>
|
||||
)}
|
||||
<View className={styles.button} onClick={onClose}>
|
||||
取消
|
||||
</View>
|
||||
</View>
|
||||
</CommonPopup>
|
||||
<CancelPopup ref={cancelRef} detail={detail} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
49
src/components/GeneralNavbar/index.module.scss
Normal file
49
src/components/GeneralNavbar/index.module.scss
Normal file
@@ -0,0 +1,49 @@
|
||||
.customNavbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 9;
|
||||
width: 100%;
|
||||
background-color: #FAFAFA;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.navbarContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.leftSection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.centerSection {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.rightSection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.backIcon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
105
src/components/GeneralNavbar/index.tsx
Normal file
105
src/components/GeneralNavbar/index.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import React from 'react'
|
||||
import { View, Text, Image } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useGlobalState } from '@/store/global'
|
||||
import styles from './index.module.scss'
|
||||
import img from '@/config/images'
|
||||
|
||||
interface GeneralNavbarProps {
|
||||
title?: string
|
||||
titleStyle?: React.CSSProperties
|
||||
titleClassName?: string
|
||||
leftContent?: React.ReactNode
|
||||
backgroundColor?: string
|
||||
showBack?: boolean
|
||||
showLeft?: boolean
|
||||
onBack?: () => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
const GeneralNavbar: React.FC<GeneralNavbarProps> = ({
|
||||
title = '',
|
||||
titleStyle,
|
||||
titleClassName = '',
|
||||
leftContent,
|
||||
backgroundColor = '#FAFAFA',
|
||||
showBack = true,
|
||||
showLeft = true,
|
||||
onBack,
|
||||
className = ''
|
||||
}) => {
|
||||
const { statusNavbarHeightInfo } = useGlobalState()
|
||||
const { statusBarHeight, navBarHeight } = statusNavbarHeightInfo
|
||||
|
||||
const handleBack = () => {
|
||||
if (onBack) {
|
||||
onBack()
|
||||
} else {
|
||||
Taro.navigateBack()
|
||||
}
|
||||
}
|
||||
|
||||
const renderLeftContent = () => {
|
||||
if (!showLeft) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (leftContent) {
|
||||
return leftContent
|
||||
}
|
||||
|
||||
if (showBack) {
|
||||
return (
|
||||
<Image
|
||||
src={img.ICON_LIST_SEARCH_BACK}
|
||||
className={styles.backIcon}
|
||||
onClick={handleBack}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const renderTitle = () => {
|
||||
if (!title) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Text
|
||||
className={`${styles.title} ${titleClassName}`}
|
||||
style={titleStyle}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
className={`${styles.customNavbar} ${className}`}
|
||||
style={{
|
||||
height: `${navBarHeight}px`,
|
||||
paddingTop: `${statusBarHeight}px`,
|
||||
backgroundColor
|
||||
}}
|
||||
>
|
||||
<View className={styles.navbarContent}>
|
||||
<View className={styles.leftSection}>
|
||||
{renderLeftContent()}
|
||||
</View>
|
||||
|
||||
<View className={styles.centerSection}>
|
||||
{renderTitle()}
|
||||
</View>
|
||||
|
||||
<View className={styles.rightSection}>
|
||||
{/* 右侧占位,保持标题居中 */}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default GeneralNavbar
|
||||
@@ -1,12 +1,55 @@
|
||||
@use "~@/scss/images.scss" as img;
|
||||
|
||||
.container {
|
||||
width: calc(100vw - 40px);
|
||||
height: 400px;
|
||||
width: 100%;
|
||||
// height: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px;
|
||||
// padding: 20px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.entryCard {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 0 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
height: 72px;
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: #000;
|
||||
text-align: center;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 22px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 28px;
|
||||
|
||||
.closeBtn {
|
||||
display: flex;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
background: #fff;
|
||||
box-shadow: 0 4px 36px 0 rgba(0, 0, 0, 0.06);
|
||||
|
||||
.closeIcon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,17 @@ import React, {
|
||||
useImperativeHandle,
|
||||
useEffect,
|
||||
forwardRef,
|
||||
memo,
|
||||
} from "react";
|
||||
import { Button, Input, View, Text } from "@tarojs/components";
|
||||
import { Button, Input, View, Text, Image } from "@tarojs/components";
|
||||
import Taro from "@tarojs/taro";
|
||||
import CommonPopup from "../CommonPopup";
|
||||
import { getCurrentFullPath } from "@/components/Auth";
|
||||
import { useUserInfo, useUserActions } from "@/store/userStore";
|
||||
import style from "./index.module.scss";
|
||||
import { getCurrentFullPath } from "@/utils";
|
||||
import evaluateService from "@/services/evaluateService";
|
||||
import NTRPTestEntryCard from "../NTRPTestEntryCard";
|
||||
import NtrpPopupGuide from "../NTRPPopupGuide";
|
||||
import CloseIcon from "@/static/ntrp/ntrp_popup_close.svg";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
export enum EvaluateType {
|
||||
EDIT = "edit",
|
||||
@@ -32,6 +36,7 @@ interface NTRPEvaluatePopupProps {
|
||||
types: EvaluateType[];
|
||||
displayCondition: DisplayConditionType;
|
||||
scene: SceneType;
|
||||
showGuide: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
@@ -40,7 +45,7 @@ function showCondition(scene, ntrp) {
|
||||
// TODO: 显示频率
|
||||
return Math.random() < 0.1 && [0, undefined].includes(ntrp);
|
||||
}
|
||||
return [0, undefined].includes(ntrp);
|
||||
return ntrp === "0";
|
||||
}
|
||||
|
||||
const NTRPEvaluatePopup = (props: NTRPEvaluatePopupProps, ref) => {
|
||||
@@ -48,10 +53,11 @@ const NTRPEvaluatePopup = (props: NTRPEvaluatePopupProps, ref) => {
|
||||
types = ["edit", "evaluate"],
|
||||
displayCondition = "auto",
|
||||
scene = "list",
|
||||
showGuide = false,
|
||||
} = props;
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { ntrp } = useUserInfo();
|
||||
const { fetchUserInfo } = useUserActions();
|
||||
const [visible, setVisible] = useState(true);
|
||||
const [ntrp, setNtrp] = useState<undefined | string>();
|
||||
const [guideShow, setGuideShow] = useState(() => props.showGuide);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
show: () => setVisible(true),
|
||||
@@ -61,39 +67,69 @@ const NTRPEvaluatePopup = (props: NTRPEvaluatePopupProps, ref) => {
|
||||
setVisible(false);
|
||||
// TODO: 实现NTRP评估逻辑
|
||||
Taro.navigateTo({
|
||||
url: `/other_pages/ntrp-evaluate/index?redirect=${encodeURIComponent(getCurrentFullPath())}`,
|
||||
url: `/other_pages/ntrp-evaluate/index?redirect=${encodeURIComponent(
|
||||
getCurrentFullPath()
|
||||
)}`,
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// fetchUserInfo();
|
||||
getNtrp();
|
||||
}, []);
|
||||
|
||||
async function getNtrp() {
|
||||
const res = await evaluateService.getLastResult();
|
||||
if (res.code === 0 && res.data.has_ntrp_level) {
|
||||
// setNtrp(res.data.user_ntrp_level)
|
||||
setNtrp("0");
|
||||
} else {
|
||||
setNtrp("0");
|
||||
}
|
||||
}
|
||||
|
||||
const showEntry =
|
||||
displayCondition === "auto"
|
||||
? showCondition(scene, ntrp)
|
||||
: displayCondition === "always";
|
||||
|
||||
function handleClose() {
|
||||
setVisible(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<CommonPopup
|
||||
title="NTRP评估"
|
||||
visible={visible}
|
||||
onClose={() => setVisible(false)}
|
||||
position="center"
|
||||
onClose={handleClose}
|
||||
showHeader={false}
|
||||
hideFooter
|
||||
enableDragToClose={false}
|
||||
>
|
||||
<View className={style.container}>
|
||||
{/* TODO: 直接修改NTRP水平 */}
|
||||
<Text>您还未测评。。。</Text>
|
||||
<Text>请先进行NTRP评估</Text>
|
||||
<Button onClick={handleEvaluate}>开始评估</Button>
|
||||
</View>
|
||||
{guideShow ? (
|
||||
<NtrpPopupGuide
|
||||
close={handleClose}
|
||||
skipGuide={() => {
|
||||
setGuideShow(false);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<View className={styles.container}>
|
||||
<View className={styles.header}>
|
||||
<Text>选择 NTRP 自评水平</Text>
|
||||
<View className={styles.closeBtn} onClick={handleClose}>
|
||||
<Image className={styles.closeIcon} src={CloseIcon} />
|
||||
</View>
|
||||
</View>
|
||||
<View className={styles.entryCard}>
|
||||
<NTRPTestEntryCard />
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</CommonPopup>
|
||||
{showEntry && props.children}
|
||||
{showEntry ? props.children : ""}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(NTRPEvaluatePopup);
|
||||
export default memo(forwardRef(NTRPEvaluatePopup));
|
||||
|
||||
202
src/components/NTRPPopupGuide/index.module.scss
Normal file
202
src/components/NTRPPopupGuide/index.module.scss
Normal file
@@ -0,0 +1,202 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 540px;
|
||||
border-radius: 20px 20px 0 0;
|
||||
background: linear-gradient(180deg, #bfffef 0%, #f2fffc 100%), #fafafa;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.top {
|
||||
width: 100%;
|
||||
padding: 0 24px;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
width: 100%;
|
||||
padding: 0 10px 40px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
|
||||
.jump,
|
||||
.direct {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
overflow: hidden;
|
||||
|
||||
.button {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
background: #fff;
|
||||
color: #000;
|
||||
font-feature-settings: "liga" off, "clig" off;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: normal;
|
||||
|
||||
&.primary {
|
||||
color: #fff;
|
||||
background: #000;
|
||||
|
||||
.jumpIcon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
height: 72px;
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
.closeBtn {
|
||||
display: flex;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
background: #fff;
|
||||
box-shadow: 0 4px 36px 0 rgba(0, 0, 0, 0.06);
|
||||
|
||||
.closeIcon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.guideMsg {
|
||||
padding-bottom: 20px;
|
||||
|
||||
.title {
|
||||
color: #2a4d44;
|
||||
font-family: "Noto Sans SC";
|
||||
font-size: 32px;
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
line-height: 48px;
|
||||
|
||||
.colorTip {
|
||||
color: #00e5ad;
|
||||
font-family: "Noto Sans SC";
|
||||
font-size: 32px;
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
line-height: 48px;
|
||||
}
|
||||
|
||||
.strongTip {
|
||||
color: #00e5ad;
|
||||
font-family: "Noto Sans SC";
|
||||
font-size: 32px;
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
line-height: 48px;
|
||||
text-decoration-line: underline;
|
||||
text-decoration-style: solid;
|
||||
text-decoration-skip-ink: auto;
|
||||
text-decoration-thickness: auto;
|
||||
text-underline-offset: auto;
|
||||
text-underline-position: from-font;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.desc {
|
||||
color: #2a4d44;
|
||||
font-family: "Noto Sans SC";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
@mixin commonAvatarStyle($multiple: 1) {
|
||||
.avatar {
|
||||
flex: 0 0 auto;
|
||||
width: calc(100px * $multiple);
|
||||
height: calc(100px * $multiple);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #fff;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #efefef;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.2), 0 8px 20px 0 rgba(0, 0, 0, 0.12);
|
||||
|
||||
.avatarUrl {
|
||||
width: calc(90px * $multiple);
|
||||
height: calc(90px * $multiple);
|
||||
border-radius: 50%;
|
||||
border: 1px solid #efefef;
|
||||
}
|
||||
}
|
||||
|
||||
.addonImage {
|
||||
flex: 0 0 auto;
|
||||
width: calc(88px * $multiple);
|
||||
height: calc(88px * $multiple);
|
||||
transform: rotate(8deg);
|
||||
flex-shrink: 0;
|
||||
aspect-ratio: 1/1;
|
||||
border-radius: calc(20px * $multiple);
|
||||
border: 4px solid #fff;
|
||||
background: linear-gradient(
|
||||
0deg,
|
||||
rgba(89, 255, 214, 0.2) 0%,
|
||||
rgba(89, 255, 214, 0.2) 100%
|
||||
),
|
||||
#fff;
|
||||
box-shadow: 0 4px 36px 0 rgba(0, 0, 0, 0.12);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
margin-left: calc(-1 * 20px * $multiple);
|
||||
|
||||
.docImage {
|
||||
width: calc(48px * $multiple);
|
||||
height: calc(48px * $multiple);
|
||||
transform: rotate(-7deg);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatarWrap {
|
||||
width: 100%;
|
||||
padding-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
@include commonAvatarStyle(0.5);
|
||||
}
|
||||
81
src/components/NTRPPopupGuide/index.tsx
Normal file
81
src/components/NTRPPopupGuide/index.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import React from "react";
|
||||
import { View, Text, Button, Image } from "@tarojs/components";
|
||||
import Taro from "@tarojs/taro";
|
||||
import classnames from "classnames";
|
||||
import { useUserInfo } from "@/store/userStore";
|
||||
import ArrwoRight from "@/static/ntrp/ntrp_arrow_right.svg";
|
||||
import CloseIcon from "@/static/ntrp/ntrp_popup_close.svg";
|
||||
import DocCopy from "@/static/ntrp/ntrp_doc_copy.svg";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
function NtrpPopupGuide(props: { close: () => void; skipGuide: () => void }) {
|
||||
const { close, skipGuide } = props;
|
||||
const userInfo = useUserInfo();
|
||||
|
||||
function handleTest() {
|
||||
Taro.redirectTo({
|
||||
url: `/other_pages/ntrp-evaluate/index`,
|
||||
});
|
||||
}
|
||||
return (
|
||||
<View className={styles.container}>
|
||||
<View className={styles.header}>
|
||||
<View className={styles.closeBtn} onClick={close}>
|
||||
<Image className={styles.closeIcon} src={CloseIcon} />
|
||||
</View>
|
||||
</View>
|
||||
<View className={styles.top}>
|
||||
<View className={styles.avatarWrap}>
|
||||
<View className={styles.avatar}>
|
||||
<Image
|
||||
className={styles.avatarUrl}
|
||||
src={userInfo.avatar_url}
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</View>
|
||||
{/* avatar side */}
|
||||
<View className={styles.addonImage}>
|
||||
<Image
|
||||
className={styles.docImage}
|
||||
src={DocCopy}
|
||||
mode="aspectFill"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View className={styles.guideMsg}>
|
||||
<View className={styles.title}>
|
||||
<Text>快速测一测✏️</Text>
|
||||
</View>
|
||||
<View className={styles.title}>
|
||||
<Text>你的</Text>
|
||||
<Text className={styles.colorTip}> (</Text>
|
||||
<Text className={styles.strongTip}>NTRP</Text>
|
||||
<Text className={styles.colorTip}>) </Text>
|
||||
<Text>水平?</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className={styles.desc}>
|
||||
<Text>首次发布球局前,需完善 NTRP 水平信息</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className={styles.bottom}>
|
||||
<View className={styles.jump}>
|
||||
<Button
|
||||
className={classnames(styles.button, styles.primary)}
|
||||
onClick={handleTest}
|
||||
>
|
||||
<Text>快速测试</Text>
|
||||
<Image className={styles.jumpIcon} src={ArrwoRight} />
|
||||
</Button>
|
||||
</View>
|
||||
<View className={styles.direct}>
|
||||
<Button className={classnames(styles.button)} onClick={skipGuide}>
|
||||
<Text>我了解我的水平,无需测试</Text>
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default NtrpPopupGuide;
|
||||
138
src/components/NTRPTestEntryCard/index.module.scss
Normal file
138
src/components/NTRPTestEntryCard/index.module.scss
Normal file
@@ -0,0 +1,138 @@
|
||||
@mixin commonCardStyle {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 20px;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.08);
|
||||
background: linear-gradient(180deg, #BFFFEF 0%, #F2FFFC 100%), var(--Backgrounds-Primary, #FFF);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.higher {
|
||||
height: 100px;
|
||||
@include commonCardStyle();
|
||||
}
|
||||
|
||||
.lower {
|
||||
height: 80px;
|
||||
@include commonCardStyle();
|
||||
|
||||
|
||||
}
|
||||
|
||||
.desc {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
gap: 7px;
|
||||
|
||||
.title {
|
||||
color: #2A4D44;
|
||||
font-family: "Noto Sans SC";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
line-height: 24px;
|
||||
|
||||
.colorTip {
|
||||
color: #00E5AD;
|
||||
font-family: "Noto Sans SC";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.strongTip {
|
||||
color: #00E5AD;
|
||||
font-family: "Noto Sans SC";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
line-height: 24px;
|
||||
text-decoration-line: underline;
|
||||
text-decoration-style: solid;
|
||||
text-decoration-skip-ink: auto;
|
||||
text-decoration-thickness: auto;
|
||||
text-underline-offset: auto;
|
||||
text-underline-position: from-font;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 4px;
|
||||
color: #5CA693;
|
||||
font-feature-settings: 'liga' off, 'clig' off;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: normal;
|
||||
|
||||
.entryIcon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin commonAvatarStyle($multiple: 1) {
|
||||
.avatar {
|
||||
flex: 0 0 auto;
|
||||
width: calc(100px * $multiple);
|
||||
height: calc(100px * $multiple);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #fff;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #efefef;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.20), 0 8px 20px 0 rgba(0, 0, 0, 0.12);
|
||||
|
||||
.avatarUrl {
|
||||
width: calc(90px * $multiple);
|
||||
height: calc(90px * $multiple);
|
||||
border-radius: 50%;
|
||||
border: 1px solid #efefef;
|
||||
}
|
||||
}
|
||||
|
||||
.addonImage {
|
||||
flex: 0 0 auto;
|
||||
width: calc(88px * $multiple);
|
||||
height: calc(88px * $multiple);
|
||||
transform: rotate(8deg);
|
||||
flex-shrink: 0;
|
||||
aspect-ratio: 1/1;
|
||||
border-radius: calc(20px * $multiple);
|
||||
border: 4px solid #FFF;
|
||||
background: linear-gradient(0deg, rgba(89, 255, 214, 0.20) 0%, rgba(89, 255, 214, 0.20) 100%), #FFF;
|
||||
box-shadow: 0 4px 36px 0 rgba(0, 0, 0, 0.12);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
margin-left: calc(-1 * 20px * $multiple);
|
||||
|
||||
.docImage {
|
||||
width: calc(48px * $multiple);
|
||||
height: calc(48px * $multiple);
|
||||
transform: rotate(-7deg);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatarWrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
@include commonAvatarStyle(0.5);
|
||||
}
|
||||
82
src/components/NTRPTestEntryCard/index.tsx
Normal file
82
src/components/NTRPTestEntryCard/index.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { View, Image, Text } from "@tarojs/components";
|
||||
import { useUserInfo, useUserActions } from "@/store/userStore";
|
||||
import DocCopy from "@/static/ntrp/ntrp_doc_copy.svg";
|
||||
import ArrowRight from "@/static/ntrp/ntrp_arrow_right_color.svg";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
function NTRPTestEntryCard(props) {
|
||||
const userInfo = useUserInfo();
|
||||
// const { fetchUserInfo } = useUserActions()
|
||||
|
||||
// useEffect(() => {
|
||||
// fetchUserInfo()
|
||||
// }, [])
|
||||
const { type } = props;
|
||||
return type === "list" ? (
|
||||
<View className={styles.higher}>
|
||||
<View className={styles.desc}>
|
||||
<View>
|
||||
<View className={styles.title}>
|
||||
<Text>快速测一测✏️</Text>
|
||||
</View>
|
||||
<View className={styles.title}>
|
||||
<Text>你的</Text>
|
||||
<Text className={styles.colorTip}> (</Text>
|
||||
<Text className={styles.strongTip}>NTRP</Text>
|
||||
<Text className={styles.colorTip}>) </Text>
|
||||
<Text>水平?</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className={styles.entry}>
|
||||
<Text>快速测试</Text>
|
||||
<Image className={styles.entryIcon} src={ArrowRight} />
|
||||
</View>
|
||||
</View>
|
||||
<View className={styles.avatarWrap}>
|
||||
<View className={styles.avatar}>
|
||||
<Image
|
||||
className={styles.avatarUrl}
|
||||
src={userInfo.avatar_url}
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</View>
|
||||
{/* avatar side */}
|
||||
<View className={styles.addonImage}>
|
||||
<Image className={styles.docImage} src={DocCopy} mode="aspectFill" />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<View className={styles.lower}>
|
||||
<View className={styles.desc}>
|
||||
<View className={styles.title}>
|
||||
<Text>不知道自己的</Text>
|
||||
<Text className={styles.colorTip}> (</Text>
|
||||
<Text className={styles.strongTip}>NTRP</Text>
|
||||
<Text className={styles.colorTip}>) </Text>
|
||||
<Text>水平?</Text>
|
||||
</View>
|
||||
<View className={styles.entry}>
|
||||
<Text>快速测试</Text>
|
||||
<Image className={styles.entryIcon} src={ArrowRight} />
|
||||
</View>
|
||||
</View>
|
||||
<View className={styles.avatarWrap}>
|
||||
<View className={styles.avatar}>
|
||||
<Image
|
||||
className={styles.avatarUrl}
|
||||
src={userInfo.avatar_url}
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</View>
|
||||
{/* avatar side */}
|
||||
<View className={styles.addonImage}>
|
||||
<Image className={styles.docImage} src={DocCopy} mode="aspectFill" />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default NTRPTestEntryCard;
|
||||
@@ -6,26 +6,16 @@
|
||||
width: 100%;
|
||||
padding: 9px 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 48px;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
.participant-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
&:first-child{
|
||||
width: 50%;
|
||||
&::after{
|
||||
content: '';
|
||||
display: block;
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
background: #E5E5E5;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
justify-content: space-between;
|
||||
padding-bottom: 12px;
|
||||
&:last-child{
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.control-label {
|
||||
font-size: 13px;
|
||||
@@ -33,6 +23,17 @@
|
||||
white-space: nowrap;
|
||||
padding-right: 10px;
|
||||
}
|
||||
.participant-control-checkbox-wrapper{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
.participant-control-checkbox {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.control-buttons {
|
||||
display: flex;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React from 'react'
|
||||
import { View, Text, Button } from '@tarojs/components'
|
||||
import './NumberInterval.scss'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { InputNumber } from '@nutui/nutui-react-taro'
|
||||
import { Checkbox } from '@nutui/nutui-react-taro'
|
||||
import './NumberInterval.scss'
|
||||
|
||||
interface NumberIntervalProps {
|
||||
value: [number, number]
|
||||
onChange: (value: [number, number]) => void
|
||||
value: { min: number, max: number, organizer_joined: boolean }
|
||||
onChange: (value: { min: number, max: number, organizer_joined: boolean }) => void
|
||||
min: number
|
||||
max: number
|
||||
}
|
||||
@@ -16,48 +17,64 @@ const NumberInterval: React.FC<NumberIntervalProps> = ({
|
||||
min,
|
||||
max
|
||||
}) => {
|
||||
const [minParticipants, maxParticipants] = value || [1, 1]
|
||||
|
||||
const handleChange = (value: [number | string, number | string]) => {
|
||||
const newMin = Number(value[0])
|
||||
const newMax = Number(value[1])
|
||||
|
||||
// 确保最少人数不能大于最多人数
|
||||
if (newMin > newMax) {
|
||||
return
|
||||
const [organizer_joined, setOrganizerJoined] = useState(true);
|
||||
const [minParticipants, setMinParticipants] = useState(1);
|
||||
const [maxParticipants, setMaxParticipants] = useState(1);
|
||||
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
setOrganizerJoined(value.organizer_joined);
|
||||
setMinParticipants(value.min);
|
||||
setMaxParticipants(value.max);
|
||||
}
|
||||
|
||||
onChange([newMin, newMax])
|
||||
console.log(value, 'valuevaluevaluevaluevaluevalue');
|
||||
}, [value]);
|
||||
|
||||
const handleChange = (value: { min: number | string, max: number | string, organizer_joined: boolean }) => {
|
||||
const toNumber = (v: number | string): number => typeof v === 'string' ? Number(v) : v
|
||||
const { min, max, organizer_joined } = value;
|
||||
onChange({ min: toNumber(min), max: toNumber(max), organizer_joined })
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<View className='participants-control-section'>
|
||||
<View className='participant-control'>
|
||||
<Text className='control-label'>最少</Text>
|
||||
<Text className='control-label'>最小成局数</Text>
|
||||
<View className='control-buttons'>
|
||||
<InputNumber
|
||||
className="format-width"
|
||||
defaultValue={minParticipants}
|
||||
value={minParticipants}
|
||||
min={min}
|
||||
max={maxParticipants}
|
||||
onChange={(value) => handleChange([value, maxParticipants])}
|
||||
onChange={(value) => handleChange({ min: value, max: maxParticipants, organizer_joined: organizer_joined })}
|
||||
formatter={(value) => `${value}人`}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View className='participant-control'>
|
||||
<Text className='control-label'>最多</Text>
|
||||
<Text className='control-label'>球局总人数</Text>
|
||||
<View className='control-buttons'>
|
||||
<InputNumber
|
||||
className="format-width"
|
||||
defaultValue={maxParticipants}
|
||||
onChange={(value) => handleChange([minParticipants, value])}
|
||||
value={maxParticipants}
|
||||
onChange={(value) => handleChange({ min: minParticipants, max: value, organizer_joined: organizer_joined })}
|
||||
min={minParticipants}
|
||||
max={max}
|
||||
formatter={(value) => `${value}人`}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View className='participant-control'>
|
||||
<View className='participant-control-checkbox-wrapper'>
|
||||
<Checkbox
|
||||
className='participant-control-checkbox'
|
||||
checked={organizer_joined}
|
||||
onChange={(checked) => handleChange({ min: minParticipants, max: maxParticipants, organizer_joined: checked })}
|
||||
/>
|
||||
我也参与此球局
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,109 +1,177 @@
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import CommonPopup from '@/components/CommonPopup'
|
||||
import { View } from '@tarojs/components'
|
||||
import CalendarUI, { CalendarUIRef } from '@/components/Picker/CalendarUI/CalendarUI'
|
||||
import { PickerCommon, PickerCommonRef } from '@/components/Picker'
|
||||
import dayjs from 'dayjs'
|
||||
import styles from './index.module.scss'
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import CommonPopup from "@/components/CommonPopup";
|
||||
import { View } from "@tarojs/components";
|
||||
import CalendarUI, {
|
||||
CalendarUIRef,
|
||||
} from "@/components/Picker/CalendarUI/CalendarUI";
|
||||
import { PickerCommon, PickerCommonRef } from "@/components/Picker";
|
||||
import dayjs from "dayjs";
|
||||
import styles from "./index.module.scss";
|
||||
export interface DialogCalendarCardProps {
|
||||
value?: Date
|
||||
onChange?: (date: Date) => void
|
||||
visible: boolean
|
||||
onClose: () => void
|
||||
title?: React.ReactNode
|
||||
value?: Date | Date[];
|
||||
searchType?: "single" | "range" | "multiple";
|
||||
onChange?: (date: Date | Date[]) => void;
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
title?: React.ReactNode;
|
||||
}
|
||||
|
||||
const DialogCalendarCard: React.FC<DialogCalendarCardProps> = ({
|
||||
visible,
|
||||
searchType,
|
||||
onClose,
|
||||
title,
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const [selected, setSelected] = useState<Date>(value || new Date())
|
||||
const [selected, setSelected] = useState<Date | Date[]>(value || new Date());
|
||||
const [selectedBackup, setSelectedBackup] = useState<Date[]>(
|
||||
Array.isArray(value) ? [...(value as Date[])] : [value as Date]
|
||||
);
|
||||
const [current, setCurrent] = useState<Date>(new Date());
|
||||
const [delta, setDelta] = useState(0);
|
||||
const calendarRef = useRef<CalendarUIRef>(null);
|
||||
const [type, setType] = useState<'year' | 'month' | 'time'>('year');
|
||||
const [selectedHour, setSelectedHour] = useState(8)
|
||||
const [selectedMinute, setSelectedMinute] = useState(0)
|
||||
const [type, setType] = useState<"year" | "month" | "time">("year");
|
||||
const [selectedHour, setSelectedHour] = useState(8);
|
||||
const [selectedMinute, setSelectedMinute] = useState(0);
|
||||
const pickerRef = useRef<PickerCommonRef>(null);
|
||||
const hourMinutePickerRef = useRef<PickerCommonRef>(null);
|
||||
const [pendingJump, setPendingJump] = useState<{ year: number; month: number } | null>(null)
|
||||
const [pendingJump, setPendingJump] = useState<{
|
||||
year: number;
|
||||
month: number;
|
||||
} | null>(null);
|
||||
const handleConfirm = () => {
|
||||
if (type === 'year') {
|
||||
if (type === "year") {
|
||||
if (searchType === "range") {
|
||||
if (onChange) onChange(selected);
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
// 年份选择完成后,进入月份选择
|
||||
setType('time')
|
||||
} else if (type === 'month') {
|
||||
setType("time");
|
||||
} else if (type === "month") {
|
||||
// 月份选择完成后,进入时间选择
|
||||
const value = pickerRef.current?.getValue()
|
||||
const value = pickerRef.current?.getValue();
|
||||
if (value) {
|
||||
const year = value[0] as number
|
||||
const month = value[1] as number
|
||||
setSelected(new Date(year, month - 1, 1))
|
||||
setPendingJump({ year, month })
|
||||
const year = value[0] as number;
|
||||
const month = value[1] as number;
|
||||
setPendingJump({ year, month });
|
||||
setType("year");
|
||||
if (searchType === "range") {
|
||||
calculateMonthDifference(
|
||||
current,
|
||||
new Date(year, month - 1, 1)
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
setSelected(new Date(year, month - 1, 1));
|
||||
}
|
||||
setType('year')
|
||||
} else if (type === 'time') {
|
||||
} else if (type === "time") {
|
||||
// 时间选择完成后,调用onNext回调
|
||||
const value = hourMinutePickerRef.current?.getValue()
|
||||
const value = hourMinutePickerRef.current?.getValue();
|
||||
if (value) {
|
||||
const hour = value[0] as number
|
||||
const minute = value[1] as number
|
||||
setSelectedHour(hour)
|
||||
setSelectedMinute(minute)
|
||||
const hours = hour.toString().padStart(2, '0')
|
||||
const minutes = minute.toString().padStart(2, '0')
|
||||
const finalDate = new Date(dayjs(selected).format('YYYY-MM-DD') + ' ' + hours + ':' + minutes)
|
||||
if (onChange) onChange(finalDate)
|
||||
const hour = value[0] as number;
|
||||
const minute = value[1] as number;
|
||||
setSelectedHour(hour);
|
||||
setSelectedMinute(minute);
|
||||
const hours = hour.toString().padStart(2, "0");
|
||||
const minutes = minute.toString().padStart(2, "0");
|
||||
const finalDate = new Date(
|
||||
dayjs(selected as Date).format("YYYY-MM-DD") +
|
||||
" " +
|
||||
hours +
|
||||
":" +
|
||||
minutes
|
||||
);
|
||||
if (onChange) onChange(finalDate);
|
||||
}
|
||||
onClose()
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const calculateMonthDifference = (date1, date2) => {
|
||||
if (!(date1 instanceof Date) || !(date2 instanceof Date)) {
|
||||
throw new Error("Both arguments must be Date objects");
|
||||
}
|
||||
setCurrent(date1)
|
||||
let months = (date2.getFullYear() - date1.getFullYear()) * 12;
|
||||
months -= date1.getMonth();
|
||||
months += date2.getMonth();
|
||||
setDelta(months);
|
||||
};
|
||||
|
||||
const handleChange = (d: Date | Date[]) => {
|
||||
if (searchType === "range") {
|
||||
if (Array.isArray(d)) {
|
||||
if (d.length === 2) {
|
||||
return;
|
||||
} else if (d.length === 1) {
|
||||
if (selectedBackup.length === 0 || selectedBackup.length === 2) {
|
||||
setSelected([...d]);
|
||||
setSelectedBackup([...d]);
|
||||
} else {
|
||||
setSelected(
|
||||
[...selectedBackup, d[0]].sort(
|
||||
(a, b) => a.getTime() - b.getTime()
|
||||
)
|
||||
);
|
||||
setSelectedBackup([]);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (Array.isArray(d)) {
|
||||
setSelected(d[0])
|
||||
setSelected(d[0]);
|
||||
} else {
|
||||
setSelected(d)
|
||||
setSelected(d);
|
||||
}
|
||||
}
|
||||
};
|
||||
const onHeaderClick = (date: Date) => {
|
||||
setSelected(date)
|
||||
setType('month')
|
||||
}
|
||||
console.log("onHeaderClick", date);
|
||||
setSelected(date);
|
||||
setType("month");
|
||||
};
|
||||
const getConfirmText = () => {
|
||||
if (type === 'time' || type === 'month') return '完成'
|
||||
return '下一步'
|
||||
}
|
||||
if (type === "time" || type === "month" || searchType === "range")
|
||||
return "完成";
|
||||
return "下一步";
|
||||
};
|
||||
const handleDateTimePickerChange = (value: (string | number)[]) => {
|
||||
const year = value[0] as number
|
||||
const month = value[1] as number
|
||||
setSelected(new Date(year, month - 1, 1))
|
||||
}
|
||||
const year = value[0] as number;
|
||||
const month = value[1] as number;
|
||||
setSelected(new Date(year, month - 1, 1));
|
||||
};
|
||||
const dialogClose = () => {
|
||||
if (type === 'month') {
|
||||
setType('year')
|
||||
} else if (type === 'time') {
|
||||
setType('year')
|
||||
if (type === "month") {
|
||||
setType("year");
|
||||
} else if (type === "time") {
|
||||
setType("year");
|
||||
} else {
|
||||
onClose()
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
calendarRef.current?.gotoMonth(delta);
|
||||
}, [delta])
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && value) {
|
||||
setSelected(value || new Date())
|
||||
setSelectedHour(value ? dayjs(value).hour() : 8)
|
||||
setSelectedMinute(value ? dayjs(value).minute() : 0)
|
||||
setSelected(value || new Date());
|
||||
setSelectedHour(value ? dayjs(value as Date).hour() : 8);
|
||||
setSelectedMinute(value ? dayjs(value as Date).minute() : 0);
|
||||
}
|
||||
}, [value, visible])
|
||||
}, [value, visible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (type === 'year' && pendingJump && calendarRef.current) {
|
||||
calendarRef.current.jumpTo(pendingJump.year, pendingJump.month)
|
||||
setPendingJump(null)
|
||||
if (type === "year" && pendingJump && calendarRef.current) {
|
||||
calendarRef.current.jumpTo(pendingJump.year, pendingJump.month);
|
||||
setPendingJump(null);
|
||||
}
|
||||
}, [type, pendingJump])
|
||||
}, [type, pendingJump]);
|
||||
|
||||
console.log([selectedHour, selectedMinute], 'selectedHour, selectedMinute');
|
||||
console.log([selectedHour, selectedMinute], "selectedHour, selectedMinute");
|
||||
|
||||
return (
|
||||
<CommonPopup
|
||||
@@ -112,43 +180,45 @@ const DialogCalendarCard: React.FC<DialogCalendarCardProps> = ({
|
||||
showHeader={!!title}
|
||||
title={title}
|
||||
hideFooter={false}
|
||||
cancelText='取消'
|
||||
cancelText="取消"
|
||||
confirmText={getConfirmText()}
|
||||
onConfirm={handleConfirm}
|
||||
position='bottom'
|
||||
position="bottom"
|
||||
round
|
||||
zIndex={1000}
|
||||
>
|
||||
{
|
||||
type === 'year' &&
|
||||
<View className={styles['calendar-container']}>
|
||||
<CalendarUI
|
||||
ref={calendarRef}
|
||||
value={selected}
|
||||
onChange={handleChange}
|
||||
showQuickActions={false}
|
||||
onHeaderClick={onHeaderClick}
|
||||
/></View>
|
||||
}
|
||||
{
|
||||
type === 'month' && <PickerCommon
|
||||
{type === "year" && (
|
||||
<View className={styles["calendar-container"]}>
|
||||
<CalendarUI
|
||||
ref={calendarRef}
|
||||
type={searchType}
|
||||
value={selected}
|
||||
onChange={handleChange}
|
||||
showQuickActions={false}
|
||||
onHeaderClick={onHeaderClick}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
{type === "month" && (
|
||||
<PickerCommon
|
||||
ref={pickerRef}
|
||||
onChange={handleDateTimePickerChange}
|
||||
type="month"
|
||||
value={[selected.getFullYear(), selected.getMonth() + 1]}
|
||||
value={[
|
||||
(selected as Date).getFullYear(),
|
||||
(selected as Date).getMonth() + 1,
|
||||
]}
|
||||
/>
|
||||
|
||||
}
|
||||
{
|
||||
type === 'time' && <PickerCommon
|
||||
ref={hourMinutePickerRef}
|
||||
type="hour"
|
||||
value={[selectedHour, selectedMinute]}
|
||||
/>
|
||||
|
||||
}
|
||||
)}
|
||||
{type === "time" && (
|
||||
<PickerCommon
|
||||
ref={hourMinutePickerRef}
|
||||
type="hour"
|
||||
value={[selectedHour, selectedMinute]}
|
||||
/>
|
||||
)}
|
||||
</CommonPopup>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default DialogCalendarCard
|
||||
export default DialogCalendarCard;
|
||||
|
||||
@@ -1,211 +1,289 @@
|
||||
import React, { useState, useEffect, useRef, useImperativeHandle } from 'react'
|
||||
import { CalendarCard } from '@nutui/nutui-react-taro'
|
||||
import { View, Text, Image } from '@tarojs/components'
|
||||
import images from '@/config/images'
|
||||
import styles from './index.module.scss'
|
||||
import { getMonth, getWeekend, getWeekendOfCurrentWeek } from '@/utils/timeUtils'
|
||||
import { PopupPicker } from '@/components/Picker/index'
|
||||
import React, { useState, useEffect, useRef, useImperativeHandle } from "react";
|
||||
import { CalendarCard } from "@nutui/nutui-react-taro";
|
||||
import { View, Text, Image } from "@tarojs/components";
|
||||
import images from "@/config/images";
|
||||
import styles from "./index.module.scss";
|
||||
import {
|
||||
getMonth,
|
||||
getWeekend,
|
||||
getWeekendOfCurrentWeek,
|
||||
} from "@/utils/timeUtils";
|
||||
import { PopupPicker } from "@/components/Picker/index";
|
||||
import dayjs from "dayjs";
|
||||
interface NutUICalendarProps {
|
||||
type?: 'single' | 'range' | 'multiple'
|
||||
value?: string | Date | String[] | Date[]
|
||||
defaultValue?: string | string[]
|
||||
onChange?: (value: Date | Date[]) => void,
|
||||
isBorder?: boolean
|
||||
showQuickActions?: boolean,
|
||||
onHeaderClick?: (date: Date) => void
|
||||
type?: "single" | "range" | "multiple";
|
||||
value?: string | Date | String[] | Date[];
|
||||
defaultValue?: string | string[];
|
||||
onChange?: (value: Date | Date[]) => void;
|
||||
isBorder?: boolean;
|
||||
showQuickActions?: boolean;
|
||||
onHeaderClick?: (date: Date) => void;
|
||||
}
|
||||
|
||||
export interface CalendarUIRef {
|
||||
jumpTo: (year: number, month: number) => void
|
||||
jumpTo: (year: number, month: number) => void;
|
||||
gotoMonth: (delta: number) => void;
|
||||
}
|
||||
|
||||
const NutUICalendar = React.forwardRef<CalendarUIRef, NutUICalendarProps>(({
|
||||
type = 'single',
|
||||
value,
|
||||
onChange,
|
||||
isBorder = false,
|
||||
showQuickActions = true,
|
||||
onHeaderClick
|
||||
}, ref) => {
|
||||
// 根据类型初始化选中值
|
||||
// const getInitialValue = (): Date | Date[] => {
|
||||
// console.log(value,defaultValue,'today')
|
||||
const NutUICalendar = React.forwardRef<CalendarUIRef, NutUICalendarProps>(
|
||||
(
|
||||
{
|
||||
type = "single",
|
||||
value,
|
||||
onChange,
|
||||
isBorder = false,
|
||||
showQuickActions = true,
|
||||
onHeaderClick,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
// 根据类型初始化选中值
|
||||
// const getInitialValue = (): Date | Date[] => {
|
||||
// console.log(value,defaultValue,'today')
|
||||
|
||||
// if (typeof value === 'string' && value) {
|
||||
// return new Date(value)
|
||||
// }
|
||||
// if (Array.isArray(value) && value.length > 0) {
|
||||
// return value.map(item => new Date(item))
|
||||
// }
|
||||
// if (typeof defaultValue === 'string' && defaultValue) {
|
||||
// return new Date(defaultValue)
|
||||
// }
|
||||
// if (Array.isArray(defaultValue) && defaultValue.length > 0) {
|
||||
// return defaultValue.map(item => new Date(item))
|
||||
// }
|
||||
// const today = new Date();
|
||||
// if (type === 'multiple') {
|
||||
// return [today]
|
||||
// }
|
||||
// return today
|
||||
// }
|
||||
const startOfMonth = (date: Date) => new Date(date.getFullYear(), date.getMonth(), 1)
|
||||
// if (typeof value === 'string' && value) {
|
||||
// return new Date(value)
|
||||
// }
|
||||
// if (Array.isArray(value) && value.length > 0) {
|
||||
// return value.map(item => new Date(item))
|
||||
// }
|
||||
// if (typeof defaultValue === 'string' && defaultValue) {
|
||||
// return new Date(defaultValue)
|
||||
// }
|
||||
// if (Array.isArray(defaultValue) && defaultValue.length > 0) {
|
||||
// return defaultValue.map(item => new Date(item))
|
||||
// }
|
||||
// const today = new Date();
|
||||
// if (type === 'multiple') {
|
||||
// return [today]
|
||||
// }
|
||||
// return today
|
||||
// }
|
||||
const startOfMonth = (date: Date) =>
|
||||
new Date(date.getFullYear(), date.getMonth(), 1);
|
||||
|
||||
const [selectedValue, setSelectedValue] = useState<Date | Date[]>()
|
||||
const [current, setCurrent] = useState<Date>(startOfMonth(new Date()))
|
||||
const calendarRef = useRef<any>(null)
|
||||
const [visible, setvisible] = useState(false)
|
||||
console.log('current', current)
|
||||
// 当外部 value 变化时更新内部状态
|
||||
useEffect(() => {
|
||||
if (Array.isArray(value) && value.length > 0) {
|
||||
setSelectedValue(value.map(item => new Date(item)))
|
||||
setCurrent(new Date(value[0]))
|
||||
}
|
||||
if ((typeof value === 'string' || value instanceof Date) && value) {
|
||||
setSelectedValue(new Date(value))
|
||||
setCurrent(new Date(value))
|
||||
}
|
||||
}, [value])
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
jumpTo: (year: number, month: number) => {
|
||||
calendarRef.current?.jumpTo(year, month)
|
||||
}
|
||||
}))
|
||||
|
||||
const handleDateChange = (newValue: any) => {
|
||||
setSelectedValue(newValue)
|
||||
onChange?.(newValue as any)
|
||||
}
|
||||
const formatHeader = (date: Date) => `${getMonth(date)}`
|
||||
|
||||
const handlePageChange = (data: { year: number; month: number }) => {
|
||||
// 月份切换时的处理逻辑,如果需要的话
|
||||
console.log('月份切换:', data)
|
||||
}
|
||||
|
||||
const gotoMonth = (delta: number) => {
|
||||
const base = current instanceof Date ? new Date(current) : new Date()
|
||||
base.setMonth(base.getMonth() + delta)
|
||||
const next = startOfMonth(base)
|
||||
setCurrent(next)
|
||||
// 同步底部 CalendarCard 的月份
|
||||
try {
|
||||
calendarRef.current?.jump?.(delta)
|
||||
} catch (e) {
|
||||
console.warn('CalendarCardRef jump 调用失败', e)
|
||||
}
|
||||
handlePageChange({ year: next.getFullYear(), month: next.getMonth() + 1 })
|
||||
}
|
||||
|
||||
const handleHeaderClick = () => {
|
||||
onHeaderClick && onHeaderClick(current)
|
||||
setvisible(true)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const syncMonthTo = (anchor: Date) => {
|
||||
// 计算从 current 到目标 anchor 所在月份的偏移,调用 jump(delta)
|
||||
const monthsDelta = (anchor.getFullYear() - current.getFullYear()) * 12 + (anchor.getMonth() - current.getMonth())
|
||||
if (monthsDelta !== 0) {
|
||||
gotoMonth(monthsDelta)
|
||||
}
|
||||
}
|
||||
const renderDay = (day: any) => {
|
||||
const { date, month, year} = day;
|
||||
const today = new Date()
|
||||
if (date === today.getDate() && month === today.getMonth() + 1 && year === today.getFullYear()) {
|
||||
return (
|
||||
<View class="day-container">
|
||||
{date}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
return date
|
||||
}
|
||||
|
||||
const selectWeekend = () => {
|
||||
const [start, end] = getWeekend()
|
||||
setSelectedValue([start, end])
|
||||
syncMonthTo(start)
|
||||
onChange?.([start, end])
|
||||
}
|
||||
const selectWeek = () => {
|
||||
const dayList = getWeekendOfCurrentWeek(7)
|
||||
setSelectedValue(dayList)
|
||||
syncMonthTo(dayList[0])
|
||||
onChange?.(dayList)
|
||||
}
|
||||
const selectMonth = () => {
|
||||
const dayList = getWeekendOfCurrentWeek(30)
|
||||
setSelectedValue(dayList)
|
||||
syncMonthTo(dayList[0])
|
||||
onChange?.(dayList)
|
||||
}
|
||||
|
||||
const handleMonthChange = (value: any) => {
|
||||
const [year, month] = value;
|
||||
const newDate = new Date(year, month - 1, 1);
|
||||
setCurrent(newDate);
|
||||
calendarRef.current?.jumpTo(year, month)
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<View>
|
||||
{/* 快速操作行 */}
|
||||
{
|
||||
showQuickActions &&
|
||||
<View className={styles['quick-actions']}>
|
||||
<View className={styles['quick-action']} onClick={selectWeekend}>本周末</View>
|
||||
<View className={styles['quick-action']} onClick={selectWeek}>一周内</View>
|
||||
<View className={styles['quick-action']} onClick={selectMonth}>一个月</View>
|
||||
</View>
|
||||
const [selectedValue, setSelectedValue] = useState<Date | Date[]>();
|
||||
const [current, setCurrent] = useState<Date>(startOfMonth(new Date()));
|
||||
const calendarRef = useRef<any>(null);
|
||||
const [visible, setvisible] = useState(false);
|
||||
console.log("current", current);
|
||||
// 当外部 value 变化时更新内部状态
|
||||
useEffect(() => {
|
||||
if (Array.isArray(value) && value.length > 0) {
|
||||
setSelectedValue(value.map((item) => new Date(item)));
|
||||
setCurrent(new Date(value[0] as Date));
|
||||
}
|
||||
<View className={`${styles['calendar-card']} ${isBorder ? styles['border'] : ''}`}>
|
||||
{/* 自定义头部显示周一到周日 */}
|
||||
<View className={styles['header']}>
|
||||
<View className={styles['header-left']} onClick={handleHeaderClick}>
|
||||
<Text className={styles['header-text']}>{formatHeader(current as Date)}</Text>
|
||||
<Image src={images.ICON_RIGHT_MAX} className={`${styles['month-arrow']}`} onClick={() => gotoMonth(1)} />
|
||||
</View>
|
||||
<View className={styles['header-actions']}>
|
||||
<View className={styles['arrow-left-container']} onClick={() => gotoMonth(-1)}>
|
||||
<Image src={images.ICON_RIGHT_MAX} className={`${styles['arrow']} ${styles['left']}`} />
|
||||
</View>
|
||||
<View className={styles['arrow-right-container']} onClick={() => gotoMonth(1)}>
|
||||
<Image src={images.ICON_RIGHT_MAX} className={`${styles['arrow']}`} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View className={styles['week-header']}>
|
||||
{[ '周日', '周一', '周二', '周三', '周四', '周五', '周六'].map((day) => (
|
||||
<Text key={day} className={styles['week-day']}>
|
||||
{day}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* NutUI CalendarCard 组件 */}
|
||||
<CalendarCard
|
||||
ref={calendarRef}
|
||||
type={type}
|
||||
value={selectedValue}
|
||||
renderDay={renderDay}
|
||||
onChange={handleDateChange}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
</View>
|
||||
{ visible && <PopupPicker
|
||||
visible={visible}
|
||||
setvisible={setvisible}
|
||||
value={[current.getFullYear(), current.getMonth() + 1]}
|
||||
type="month"
|
||||
onChange={(value) => handleMonthChange(value)}/> }
|
||||
</View>
|
||||
)
|
||||
})
|
||||
if ((typeof value === "string" || value instanceof Date) && value) {
|
||||
setSelectedValue(new Date(value));
|
||||
setCurrent(new Date(value));
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
export default NutUICalendar
|
||||
useImperativeHandle(ref, () => ({
|
||||
jumpTo: (year: number, month: number) => {
|
||||
calendarRef.current?.jumpTo(year, month);
|
||||
},
|
||||
gotoMonth,
|
||||
}));
|
||||
|
||||
const handleDateChange = (newValue: any) => {
|
||||
if (type === "range") return;
|
||||
setSelectedValue(newValue);
|
||||
onChange?.(newValue as any);
|
||||
};
|
||||
const formatHeader = (date: Date) => `${getMonth(date)}`;
|
||||
|
||||
const handlePageChange = (data: { year: number; month: number }) => {
|
||||
// 月份切换时的处理逻辑,如果需要的话
|
||||
console.log("月份切换:", data);
|
||||
};
|
||||
|
||||
const handleDayClick = (day: any) => {
|
||||
const { type, year, month, date } = day;
|
||||
if (type === "next") return;
|
||||
onChange?.([new Date(year, month - 1, date)]);
|
||||
};
|
||||
|
||||
const gotoMonth = (delta: number) => {
|
||||
const base = current instanceof Date ? new Date(current) : new Date();
|
||||
base.setMonth(base.getMonth() + delta);
|
||||
const next = startOfMonth(base);
|
||||
setCurrent(next);
|
||||
// 同步底部 CalendarCard 的月份
|
||||
try {
|
||||
calendarRef.current?.jump?.(delta);
|
||||
} catch (e) {
|
||||
console.warn("CalendarCardRef jump 调用失败", e);
|
||||
}
|
||||
handlePageChange({
|
||||
year: next.getFullYear(),
|
||||
month: next.getMonth() + 1,
|
||||
});
|
||||
};
|
||||
|
||||
const handleHeaderClick = () => {
|
||||
onHeaderClick && onHeaderClick(current);
|
||||
setvisible(true);
|
||||
};
|
||||
|
||||
const syncMonthTo = (anchor: Date) => {
|
||||
// 计算从 current 到目标 anchor 所在月份的偏移,调用 jump(delta)
|
||||
const monthsDelta =
|
||||
(anchor.getFullYear() - current.getFullYear()) * 12 +
|
||||
(anchor.getMonth() - current.getMonth());
|
||||
if (monthsDelta !== 0) {
|
||||
gotoMonth(monthsDelta);
|
||||
}
|
||||
};
|
||||
const renderDay = (day: any) => {
|
||||
const { date, month, year } = day;
|
||||
const today = new Date();
|
||||
if (
|
||||
date === today.getDate() &&
|
||||
month === today.getMonth() + 1 &&
|
||||
year === today.getFullYear()
|
||||
) {
|
||||
return <View className="day-container">{date}</View>;
|
||||
}
|
||||
return date;
|
||||
};
|
||||
|
||||
const selectWeekend = () => {
|
||||
const [start, end] = getWeekend();
|
||||
setSelectedValue([start, end]);
|
||||
syncMonthTo(start);
|
||||
onChange?.([start, end]);
|
||||
};
|
||||
const selectWeek = () => {
|
||||
const dayList = getWeekendOfCurrentWeek(7);
|
||||
setSelectedValue(dayList);
|
||||
syncMonthTo(dayList[0]);
|
||||
onChange?.(dayList);
|
||||
};
|
||||
const selectMonth = () => {
|
||||
const dayList = getWeekendOfCurrentWeek(30);
|
||||
setSelectedValue(dayList);
|
||||
syncMonthTo(dayList[0]);
|
||||
onChange?.(dayList);
|
||||
};
|
||||
|
||||
const handleMonthChange = (value: any) => {
|
||||
const [year, month] = value;
|
||||
const newDate = new Date(year, month - 1, 1);
|
||||
setCurrent(newDate);
|
||||
calendarRef.current?.jumpTo(year, month);
|
||||
};
|
||||
|
||||
return (
|
||||
<View>
|
||||
{/* 快速操作行 */}
|
||||
{showQuickActions && (
|
||||
<View className={styles["quick-actions"]}>
|
||||
<View className={styles["quick-action"]} onClick={selectWeekend}>
|
||||
本周末
|
||||
</View>
|
||||
<View className={styles["quick-action"]} onClick={selectWeek}>
|
||||
一周内
|
||||
</View>
|
||||
<View className={styles["quick-action"]} onClick={selectMonth}>
|
||||
一个月
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
<View
|
||||
className={`${styles["calendar-card"]} ${
|
||||
isBorder ? styles["border"] : ""
|
||||
}`}
|
||||
>
|
||||
{type === "range" && (
|
||||
<View className={styles["date-range-container"]}>
|
||||
<Text
|
||||
className={`${styles["date-text-placeholder"]} ${
|
||||
(value as Date[]).length === 2 ? styles["date-text"] : ""
|
||||
}`}
|
||||
>
|
||||
{(value as Date[]).length === 2
|
||||
? dayjs(value?.[0] as Date).format("YYYY-MM-DD")
|
||||
: "起始时间"}
|
||||
</Text>
|
||||
<Text>至</Text>
|
||||
<Text
|
||||
className={`${styles["date-text-placeholder"]} ${
|
||||
(value as Date[]).length === 2 ? styles["date-text"] : ""
|
||||
}`}
|
||||
>
|
||||
{(value as Date[]).length === 2
|
||||
? dayjs(value?.[1] as Date).format("YYYY-MM-DD")
|
||||
: "结束时间"}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{/* 自定义头部显示周一到周日 */}
|
||||
<View className={styles["header"]}>
|
||||
<View className={styles["header-left"]} onClick={handleHeaderClick}>
|
||||
<Text className={styles["header-text"]}>
|
||||
{formatHeader(current as Date)}
|
||||
</Text>
|
||||
<Image
|
||||
src={images.ICON_RIGHT_MAX}
|
||||
className={`${styles["month-arrow"]}`}
|
||||
onClick={() => gotoMonth(1)}
|
||||
/>
|
||||
</View>
|
||||
<View className={styles["header-actions"]}>
|
||||
<View
|
||||
className={styles["arrow-left-container"]}
|
||||
onClick={() => gotoMonth(-1)}
|
||||
>
|
||||
<Image
|
||||
src={images.ICON_RIGHT_MAX}
|
||||
className={`${styles["arrow"]} ${styles["left"]}`}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
className={styles["arrow-right-container"]}
|
||||
onClick={() => gotoMonth(1)}
|
||||
>
|
||||
<Image
|
||||
src={images.ICON_RIGHT_MAX}
|
||||
className={`${styles["arrow"]}`}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View className={styles["week-header"]}>
|
||||
{["周日", "周一", "周二", "周三", "周四", "周五", "周六"].map(
|
||||
(day) => (
|
||||
<Text key={day} className={styles["week-day"]}>
|
||||
{day}
|
||||
</Text>
|
||||
)
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* NutUI CalendarCard 组件 */}
|
||||
<CalendarCard
|
||||
ref={calendarRef}
|
||||
type={type}
|
||||
value={selectedValue}
|
||||
renderDay={renderDay}
|
||||
onChange={handleDateChange}
|
||||
onPageChange={handlePageChange}
|
||||
onDayClick={handleDayClick}
|
||||
/>
|
||||
</View>
|
||||
{visible && (
|
||||
<PopupPicker
|
||||
visible={visible}
|
||||
setvisible={setvisible}
|
||||
value={[current.getFullYear(), current.getMonth() + 1]}
|
||||
type="month"
|
||||
onChange={(value) => handleMonthChange(value)}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default NutUICalendar;
|
||||
|
||||
@@ -1,177 +1,217 @@
|
||||
.calendar-card {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
&.border{
|
||||
border-radius: 12px;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.12);
|
||||
margin-bottom: 6px;
|
||||
padding: 12px 12px 8px;
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
|
||||
&.border {
|
||||
border-radius: 12px;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.12);
|
||||
margin-bottom: 6px;
|
||||
padding: 12px 12px 8px;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 9px 4px 11px 4px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.date-range-container {
|
||||
height: 55px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
color: #000;
|
||||
padding: 0 4px;
|
||||
font-size: 17.68px;
|
||||
}
|
||||
|
||||
.date-text-placeholder {
|
||||
font-family: PingFang SC;
|
||||
font-weight: 600;
|
||||
font-style: Semibold;
|
||||
font-size: 17.68px;
|
||||
color: #3C3C4399;
|
||||
}
|
||||
|
||||
.date-text {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.header-text {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
width: 60px;
|
||||
|
||||
.arrow-left-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 9px 4px 11px 4px;
|
||||
height: 24px;
|
||||
}
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.header-text {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
}
|
||||
.header-actions {
|
||||
display: flex;
|
||||
width: 60px;
|
||||
.arrow-left-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: 50%;
|
||||
flex: 1;
|
||||
}
|
||||
.arrow-right-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
width: 50%;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
.month-arrow{
|
||||
width: 8px;
|
||||
height: 24px;
|
||||
}
|
||||
.arrow {
|
||||
width: 10px;
|
||||
height: 24px;
|
||||
position: relative;
|
||||
}
|
||||
.arrow.left {
|
||||
left: 9px;
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
|
||||
.week-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
padding: 0 0 4px 0;
|
||||
}
|
||||
.week-item {
|
||||
text-align: center;
|
||||
color: rgba(60, 60, 67, 0.30);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
// 新增的周一到周日头部样式
|
||||
.week-header {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
padding: 8px 0;
|
||||
}
|
||||
.week-day {
|
||||
text-align: center;
|
||||
color: rgba(60, 60, 67, 0.30);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 8px 0;
|
||||
padding: 4px 0 16px;
|
||||
}
|
||||
.cell {
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
position: relative;
|
||||
}
|
||||
.cell.empty {
|
||||
opacity: 0;
|
||||
}
|
||||
.cell.disabled {
|
||||
color: rgba(0,0,0,0.2);
|
||||
}
|
||||
.cell-text.selected {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 22px;
|
||||
background: rgba(0,0,0,0.9);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
// 时间段选择样式
|
||||
.cell-text.range-start {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 22px;
|
||||
background: rgba(0,0,0,0.9);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cell-text.range-end {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 22px;
|
||||
background: rgba(0,0,0,0.9);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cell-text.in-range {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 22px;
|
||||
background: rgba(0,0,0,0.1);
|
||||
color: #000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
.btn {
|
||||
justify-content: flex-start;
|
||||
width: 50%;
|
||||
flex: 1;
|
||||
height: 44px;
|
||||
border-radius: 22px;
|
||||
background: rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
.arrow-right-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.btn.primary {
|
||||
background: #000;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.hm-placeholder {
|
||||
height: 240px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-content: flex-end;
|
||||
width: 50%;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.month-arrow {
|
||||
width: 8px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
width: 10px;
|
||||
height: 24px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.arrow.left {
|
||||
left: 9px;
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
|
||||
.week-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
padding: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.week-item {
|
||||
text-align: center;
|
||||
color: rgba(60, 60, 67, 0.30);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
// 新增的周一到周日头部样式
|
||||
.week-header {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.week-day {
|
||||
text-align: center;
|
||||
color: rgba(60, 60, 67, 0.30);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 8px 0;
|
||||
padding: 4px 0 16px;
|
||||
}
|
||||
|
||||
.cell {
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cell.empty {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.cell.disabled {
|
||||
color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.cell-text.selected {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 22px;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
// 时间段选择样式
|
||||
.cell-text.range-start {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 22px;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cell-text.range-end {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 22px;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cell-text.in-range {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 22px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
color: #000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
height: 44px;
|
||||
border-radius: 22px;
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn.primary {
|
||||
background: #000;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.hm-placeholder {
|
||||
height: 240px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
// CalendarRange 组件样式
|
||||
.calendar-range {
|
||||
@@ -234,50 +274,63 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 隐藏 CalendarCard 默认头部
|
||||
:global {
|
||||
.nut-calendarcard {
|
||||
.nut-calendarcard-header {
|
||||
display: none !important;
|
||||
}
|
||||
.nut-calendarcard-content{
|
||||
.nut-calendarcard-days{
|
||||
&:first-child{
|
||||
|
||||
.nut-calendarcard-content {
|
||||
.nut-calendarcard-days {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 4px;
|
||||
justify-items: center;
|
||||
|
||||
&:first-child {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
.nut-calendarcard-day{
|
||||
margin-bottom:0px!important;
|
||||
|
||||
.nut-calendarcard-day {
|
||||
margin-bottom: 0px !important;
|
||||
height: 44px;
|
||||
width: 44px!important;
|
||||
&.active{
|
||||
background-color: #000!important;
|
||||
color: #fff!important;
|
||||
width: 44px !important;
|
||||
|
||||
&.active {
|
||||
background-color: #000 !important;
|
||||
color: #fff !important;
|
||||
height: 44px;
|
||||
border-radius: 22px!important;
|
||||
border-radius: 22px !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px!important;
|
||||
font-size: 24px!important;
|
||||
.day-container{
|
||||
background-color: transparent!important;
|
||||
width: 44px !important;
|
||||
font-size: 24px !important;
|
||||
|
||||
.day-container {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
&.weekend{
|
||||
color: rgb(0,0,0)!important;
|
||||
&.active{
|
||||
color: #fff!important;
|
||||
|
||||
&.weekend {
|
||||
color: rgb(0, 0, 0) !important;
|
||||
|
||||
&.active {
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.nut-calendarcard-day-inner{
|
||||
|
||||
.nut-calendarcard-day-inner {
|
||||
font-size: 20px;
|
||||
.day-container{
|
||||
|
||||
.day-container {
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 22px;
|
||||
width: 44px;
|
||||
@@ -288,5 +341,22 @@
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nut-calendarcard-day.start,
|
||||
.nut-calendarcard-day.end {
|
||||
background-color: #000;
|
||||
border-radius: 50%;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.nut-calendarcard-day-inner .day-container {
|
||||
background-color: unset;
|
||||
color: unset;
|
||||
}
|
||||
|
||||
.nut-calendarcard-day.mid {
|
||||
background-color: rgba(0, 0, 0, 0.12);
|
||||
color: #000;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,26 @@ export const renderYearMonth = (minYear = 2020, maxYear = 2099) => {
|
||||
]
|
||||
}
|
||||
|
||||
export const renderYearMonthDay = (minYear = 2020, maxYear = 2099) => {
|
||||
return [
|
||||
// 年份列
|
||||
Array.from({ length: maxYear - minYear + 1 }, (_, index) => ({
|
||||
text: `${minYear + index}年`,
|
||||
value: minYear + index
|
||||
})),
|
||||
// 月份列
|
||||
Array.from({ length: 12 }, (_, index) => ({
|
||||
text: `${index + 1}月`,
|
||||
value: index + 1
|
||||
})),
|
||||
// 日期列 (默认31天,具体天数需在onChange时动态调整)
|
||||
Array.from({ length: 31 }, (_, index) => ({
|
||||
text: `${index + 1}日`,
|
||||
value: index + 1
|
||||
}))
|
||||
]
|
||||
}
|
||||
|
||||
export const renderHourMinute = (minHour = 0, maxHour = 23) => {
|
||||
// 生成小时和分钟的选项数据
|
||||
return [
|
||||
|
||||
@@ -1,66 +1,95 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import CommonPopup from '@/components/CommonPopup'
|
||||
import Picker from './Picker'
|
||||
import { renderYearMonth, renderHourMinute } from './PickerData'
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import CommonPopup from "@/components/CommonPopup";
|
||||
import { View, Text, Image } from "@tarojs/components";
|
||||
import Picker from "./Picker";
|
||||
import {
|
||||
renderYearMonth,
|
||||
renderYearMonthDay,
|
||||
renderHourMinute,
|
||||
} from "./PickerData";
|
||||
import imgs from "@/config/images";
|
||||
import styles from "./index.module.scss";
|
||||
interface PickerOption {
|
||||
text: string | number
|
||||
value: string | number
|
||||
text: string | number;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
interface PickerProps {
|
||||
visible: boolean
|
||||
setvisible: (visible: boolean) => void
|
||||
options?: PickerOption[][]
|
||||
value?: (string | number)[]
|
||||
type?: 'month' | 'hour' | null
|
||||
onConfirm?: (options: PickerOption[], values: (string | number)[]) => void
|
||||
onChange?: ( value: (string | number)[] ) => void
|
||||
visible: boolean;
|
||||
setvisible: (visible: boolean) => void;
|
||||
options?: PickerOption[][] | PickerOption[];
|
||||
value?: (string | number)[];
|
||||
type?: "month" | "day" | "hour" | "ntrp" | null;
|
||||
img?: string;
|
||||
onConfirm?: (options: PickerOption[], values: (string | number)[]) => void;
|
||||
onChange?: (value: (string | number)[]) => void;
|
||||
}
|
||||
|
||||
const PopupPicker = ({
|
||||
visible,
|
||||
setvisible,
|
||||
value = [],
|
||||
onConfirm,
|
||||
const PopupPicker = ({
|
||||
visible,
|
||||
setvisible,
|
||||
value = [],
|
||||
img,
|
||||
onConfirm,
|
||||
onChange,
|
||||
options = [],
|
||||
type = null
|
||||
type = null,
|
||||
}: PickerProps) => {
|
||||
|
||||
const [defaultValue, setDefaultValue] = useState<(string | number)[]>([])
|
||||
const [defaultOptions, setDefaultOptions] = useState<PickerOption[][]>([])
|
||||
const [defaultValue, setDefaultValue] = useState<(string | number)[]>([]);
|
||||
const [defaultOptions, setDefaultOptions] = useState<PickerOption[][]>([]);
|
||||
const changePicker = (options: any[], values: any, columnIndex: number) => {
|
||||
if (onChange) {
|
||||
console.log('picker onChange', columnIndex, values, options)
|
||||
console.log("picker onChange", columnIndex, values, options);
|
||||
|
||||
setDefaultValue(values)
|
||||
if (
|
||||
type === "day" &&
|
||||
JSON.stringify(defaultValue) !== JSON.stringify(values)
|
||||
) {
|
||||
const [year, month] = values;
|
||||
const daysInMonth = new Date(Number(year), Number(month), 0).getDate();
|
||||
const dayOptions = Array.from({ length: daysInMonth }, (_, i) => ({
|
||||
text: i + 1 + "日",
|
||||
value: i + 1,
|
||||
}));
|
||||
const newOptions = [...defaultOptions];
|
||||
if (JSON.stringify(newOptions[2]) !== JSON.stringify(dayOptions)) {
|
||||
newOptions[2] = dayOptions;
|
||||
setDefaultOptions(newOptions);
|
||||
}
|
||||
}
|
||||
|
||||
if (JSON.stringify(defaultValue) !== JSON.stringify(values)) {
|
||||
setDefaultValue(values);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
console.log(defaultValue,'defaultValue');
|
||||
onChange(defaultValue)
|
||||
setvisible(false)
|
||||
}
|
||||
|
||||
console.log(defaultValue, "defaultValue");
|
||||
onChange(defaultValue);
|
||||
setvisible(false);
|
||||
};
|
||||
|
||||
const dialogClose = () => {
|
||||
setvisible(false)
|
||||
}
|
||||
setvisible(false);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (type === 'month') {
|
||||
setDefaultOptions(renderYearMonth())
|
||||
} else if (type === 'hour') {
|
||||
setDefaultOptions(renderHourMinute())
|
||||
if (type === "month") {
|
||||
setDefaultOptions(renderYearMonth());
|
||||
} else if (type === "day") {
|
||||
setDefaultOptions(renderYearMonthDay());
|
||||
} else if (type === "hour") {
|
||||
setDefaultOptions(renderHourMinute());
|
||||
} else {
|
||||
setDefaultOptions(options)
|
||||
setDefaultOptions(options);
|
||||
}
|
||||
}, [type])
|
||||
|
||||
// useEffect(() => {
|
||||
// if (value.length > 0 && defaultOptions.length > 0) {
|
||||
// setDefaultValue([...value])
|
||||
// }
|
||||
// }, [value, defaultOptions])
|
||||
}, [type]);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (value.length > 0 && defaultOptions.length > 0) {
|
||||
// setDefaultValue([...value])
|
||||
// }
|
||||
// }, [value, defaultOptions])
|
||||
return (
|
||||
<>
|
||||
<CommonPopup
|
||||
@@ -69,13 +98,34 @@ const PopupPicker = ({
|
||||
showHeader={false}
|
||||
title={null}
|
||||
hideFooter={false}
|
||||
cancelText='取消'
|
||||
confirmText='完成'
|
||||
cancelText="取消"
|
||||
confirmText="完成"
|
||||
onConfirm={handleConfirm}
|
||||
position='bottom'
|
||||
position="bottom"
|
||||
round
|
||||
zIndex={1000}
|
||||
>
|
||||
{type === "ntrp" && (
|
||||
<View className={`${styles["examination-btn"]}}`}>
|
||||
<View className={`${styles["text-container"]}}`}>
|
||||
<View className={`${styles["text-title"]}}`}>
|
||||
不知道自己的<Text>(NTRP)</Text>水平
|
||||
</View>
|
||||
<View className={`${styles["text-btn"]}}`}>
|
||||
<Text>快速测试</Text>
|
||||
<Image src={imgs.ICON_ARROW_GREEN} className={`${styles["icon-arrow"]}`}></Image>
|
||||
</View>
|
||||
</View>
|
||||
<View className={`${styles["img-container"]}}`}>
|
||||
<View className={`${styles["img-box"]}`}>
|
||||
<Image src={img!}></Image>
|
||||
</View>
|
||||
<View className={`${styles["img-box"]}`}>
|
||||
<Image src={imgs.ICON_EXAMINATION}></Image>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
<Picker
|
||||
visible={visible}
|
||||
options={defaultOptions}
|
||||
@@ -84,7 +134,7 @@ const PopupPicker = ({
|
||||
/>
|
||||
</CommonPopup>
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default PopupPicker
|
||||
export default PopupPicker;
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
.picker-container {
|
||||
:global{
|
||||
.nut-popup-round{
|
||||
position: relative!important;
|
||||
:global {
|
||||
.nut-popup-round {
|
||||
position: relative !important;
|
||||
|
||||
.nut-picker-control {
|
||||
display: none!important;
|
||||
display: none !important;
|
||||
}
|
||||
.nut-picker{
|
||||
&::after{
|
||||
content: '';
|
||||
|
||||
.nut-picker {
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 16px;
|
||||
right: 16px!important;
|
||||
right: 16px !important;
|
||||
width: calc(100% - 32px);
|
||||
height: 48px;
|
||||
background: rgba(22, 24, 35, 0.05);
|
||||
@@ -23,3 +25,91 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.examination-btn {
|
||||
padding: 8px 16px;
|
||||
margin: 16px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(to bottom,
|
||||
#CCFFF2,
|
||||
/* 开始颜色 */
|
||||
#F7FFFD
|
||||
/* 结束颜色 */
|
||||
),
|
||||
repeating-linear-gradient(90deg,
|
||||
/* 垂直方向 */
|
||||
rgba(0, 0, 0, 1),
|
||||
/* 条纹的开始颜色 */
|
||||
rgba(0, 0, 0, 0.01) 1px,
|
||||
/* 条纹的结束颜色及宽度 */
|
||||
#CCFFF2 8px,
|
||||
/* 条纹之间的开始颜色 */
|
||||
#F7FFFD 10px
|
||||
/* 条纹之间的结束颜色及宽度 */
|
||||
);
|
||||
background-blend-mode: luminosity;
|
||||
/* 将两个渐变层叠在一起 */
|
||||
|
||||
.text-container {
|
||||
.text-title {
|
||||
font-family: Noto Sans SC;
|
||||
font-weight: 900;
|
||||
color: #2a4d44;
|
||||
font-size: 16px;
|
||||
margin-bottom: 4px;
|
||||
|
||||
Text {
|
||||
color: #00e5ad;
|
||||
}
|
||||
}
|
||||
|
||||
.text-btn {
|
||||
font-size: 12px;
|
||||
color: #5ca693;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.icon-arrow {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.img-container {
|
||||
display: flex;
|
||||
|
||||
.img-box {
|
||||
width: 47px;
|
||||
height: 47px;
|
||||
border: 3px solid #fff;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
Image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
border-radius: 8px;
|
||||
background-color: #ccfff2;
|
||||
transform: scale(0.88) rotate(15deg) translateX(-10px);
|
||||
|
||||
Image {
|
||||
width: 66%;
|
||||
height: 66%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,67 +1,122 @@
|
||||
import React, { useState } from 'react'
|
||||
import { View, Text, Image } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import styles from './index.module.scss'
|
||||
import images from '@/config/images'
|
||||
import React, { useState } from "react";
|
||||
import { View, Text, Image } from "@tarojs/components";
|
||||
import Taro from "@tarojs/taro";
|
||||
import styles from "./index.module.scss";
|
||||
import images from "@/config/images";
|
||||
import AiImportPopup from "@/publish_pages/publishBall/components/AiImportPopup";
|
||||
|
||||
export interface PublishMenuProps {
|
||||
onPersonalPublish?: () => void
|
||||
onActivityPublish?: () => void
|
||||
onPersonalPublish?: () => void;
|
||||
onActivityPublish?: () => void;
|
||||
}
|
||||
|
||||
const PublishMenu: React.FC<PublishMenuProps> = () => {
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [aiImportVisible, setAiImportVisible] = useState(false);
|
||||
|
||||
const handleIconClick = () => {
|
||||
setIsVisible(!isVisible)
|
||||
}
|
||||
|
||||
const handleMenuItemClick = (type: 'individual' | 'group') => {
|
||||
setIsVisible(!isVisible);
|
||||
};
|
||||
const handleOverlayClick = () => {
|
||||
setIsVisible(false);
|
||||
};
|
||||
const handleMenuItemClick = (type: "individual" | "group" | "ai") => {
|
||||
// 跳转到publishBall页面并传递type参数
|
||||
console.log(type, 'type');
|
||||
console.log(type, "type");
|
||||
if (type === "ai") {
|
||||
setAiImportVisible(true);
|
||||
setIsVisible(false);
|
||||
return;
|
||||
}
|
||||
Taro.navigateTo({
|
||||
url: `/publish_pages/publishBall/index?type=${type}`
|
||||
})
|
||||
setIsVisible(false)
|
||||
}
|
||||
url: `/publish_pages/publishBall/index?type=${type}`,
|
||||
});
|
||||
setIsVisible(false);
|
||||
};
|
||||
|
||||
const handleAiImportClose = () => {
|
||||
setAiImportVisible(false);
|
||||
};
|
||||
|
||||
const handleManualPublish = () => {
|
||||
Taro.navigateTo({
|
||||
url: "/publish_pages/publishBall/index?type=individual",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<View className={styles.publishMenu}>
|
||||
|
||||
{/* 蒙层 */}
|
||||
{isVisible && (
|
||||
<View className={styles.overlay} onClick={handleOverlayClick} />
|
||||
)}
|
||||
{/* 菜单选项 */}
|
||||
{isVisible && (
|
||||
<View className={styles.menuCard}>
|
||||
<View
|
||||
className={styles.menuItem}
|
||||
onClick={() => handleMenuItemClick('individual')}
|
||||
onClick={() => handleMenuItemClick("individual")}
|
||||
>
|
||||
<View className={styles.menuContent}>
|
||||
<View className={styles.menuTitle}>
|
||||
发布个人约球
|
||||
<View className={styles.menuArrow}>
|
||||
<Image
|
||||
src={images.ICON_ARROW_RIGHT_BLACK}
|
||||
className={styles.img}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<Text className={styles.menuDesc}>
|
||||
已订场,找球友;未订场,找搭子
|
||||
</Text>
|
||||
</View>
|
||||
<View className={styles.menuIcon}>
|
||||
<Image src={images.ICON_PERSON} />
|
||||
</View>
|
||||
<View className={styles.menuContent}>
|
||||
<Text className={styles.menuTitle}>发布个人约球</Text>
|
||||
<Text className={styles.menuDesc}>已订场,找球友;未订场,找搭子</Text>
|
||||
</View>
|
||||
<View className={styles.menuArrow}>
|
||||
<Image src={images.ICON_ARROW_RIGHT} className={styles.img} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
className={styles.menuItem}
|
||||
onClick={() => handleMenuItemClick('group')}
|
||||
onClick={() => handleMenuItemClick("group")}
|
||||
>
|
||||
<View className={styles.menuContent}>
|
||||
<View className={styles.menuTitle}>
|
||||
发布畅打活动
|
||||
<View className={styles.menuArrow}>
|
||||
<Image
|
||||
src={images.ICON_ARROW_RIGHT_BLACK}
|
||||
className={styles.img}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<Text className={styles.menuDesc}>认证球场官方组织</Text>
|
||||
</View>
|
||||
<View className={styles.menuIcon}>
|
||||
<Image src={images.ICON_GROUP} />
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
className={`${styles.menuItem} ${styles.aiItem}`}
|
||||
onClick={() => handleMenuItemClick("ai")}
|
||||
>
|
||||
<View className={styles.menuContent}>
|
||||
<Text className={styles.menuTitle}>发布畅打活动</Text>
|
||||
<Text className={styles.menuDesc}>认证球场官方组织</Text>
|
||||
<View className={styles.menuTitle}>
|
||||
智能发布球局
|
||||
<View className={styles.menuArrow}>
|
||||
<Image
|
||||
src={images.ICON_ARROW_RIGHT_WHITE}
|
||||
className={styles.img}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<Text className={styles.menuDesc}>
|
||||
识别文本/图片,快速导入球局信息
|
||||
</Text>
|
||||
</View>
|
||||
<View className={styles.menuArrow}>
|
||||
<Image src={images.ICON_ARROW_RIGHT} className={styles.img} />
|
||||
|
||||
<View className={styles.menuIcon}>
|
||||
<Image src={images.ICON_IMPORTANT_BTN} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -69,13 +124,20 @@ const PublishMenu: React.FC<PublishMenuProps> = () => {
|
||||
|
||||
{/* 绿色圆形按钮 */}
|
||||
<View
|
||||
className={`${styles.greenButton} ${isVisible ? styles.rotated : ''}`}
|
||||
className={`${styles.greenButton} ${isVisible ? styles.rotated : ""}`}
|
||||
onClick={handleIconClick}
|
||||
>
|
||||
<Image src={images.ICON_PUBLISH} className={styles.closeIcon} />
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default PublishMenu
|
||||
{/* AI导入弹窗 */}
|
||||
<AiImportPopup
|
||||
visible={aiImportVisible}
|
||||
onClose={handleAiImportClose}
|
||||
onManualPublish={handleManualPublish}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default PublishMenu;
|
||||
|
||||
@@ -3,48 +3,53 @@
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
|
||||
.overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
}
|
||||
.menuCard {
|
||||
position: absolute;
|
||||
bottom: 80px;
|
||||
right: 0;
|
||||
width: 302px;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
padding: 12px;
|
||||
width: 278px;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
/* 小三角指示器 */
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -8px;
|
||||
right: 20px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 8px solid transparent;
|
||||
border-right: 8px solid transparent;
|
||||
border-top: 8px solid white;
|
||||
/* 移除阴影,避免连接处的黑色 */
|
||||
}
|
||||
z-index: 1001;
|
||||
// /* 小三角指示器 */
|
||||
// &::after {
|
||||
// content: '';
|
||||
// position: absolute;
|
||||
// bottom: -8px;
|
||||
// right: 20px;
|
||||
// width: 0;
|
||||
// height: 0;
|
||||
// border-left: 8px solid transparent;
|
||||
// border-right: 8px solid transparent;
|
||||
// border-top: 8px solid white;
|
||||
// /* 移除阴影,避免连接处的黑色 */
|
||||
// }
|
||||
|
||||
/* 为小三角添加单独的阴影效果 */
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -9px;
|
||||
right: 20px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 8px solid transparent;
|
||||
border-right: 8px solid transparent;
|
||||
border-top: 8px solid rgba(0, 0, 0, 0.1);
|
||||
z-index: -1;
|
||||
}
|
||||
// /* 为小三角添加单独的阴影效果 */
|
||||
// &::before {
|
||||
// content: '';
|
||||
// position: absolute;
|
||||
// bottom: -9px;
|
||||
// right: 20px;
|
||||
// width: 0;
|
||||
// height: 0;
|
||||
// border-left: 8px solid transparent;
|
||||
// border-right: 8px solid transparent;
|
||||
// border-top: 8px solid rgba(0, 0, 0, 0.1);
|
||||
// z-index: -1;
|
||||
// }
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
@@ -71,12 +76,20 @@
|
||||
}
|
||||
|
||||
.menuIcon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
padding: 10px;
|
||||
justify-content: center;
|
||||
margin-right: 12px;
|
||||
align-items: center;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
box-sizing: border-box;
|
||||
image{
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.ballIcon {
|
||||
@@ -143,6 +156,7 @@
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.menuTitle {
|
||||
@@ -151,6 +165,8 @@
|
||||
color: #000;
|
||||
margin-bottom: 2px;
|
||||
line-height: 24px; /* 150% */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.menuDesc {
|
||||
@@ -162,7 +178,7 @@
|
||||
.menuArrow {
|
||||
font-size: 16px;
|
||||
color: #ccc;
|
||||
margin-left: 8px;
|
||||
margin-left: 4px;
|
||||
.img{
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
@@ -180,6 +196,8 @@
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
z-index: 1001;
|
||||
position: relative;
|
||||
&.rotated {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
@@ -193,3 +211,20 @@
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.aiItem{
|
||||
border-radius: 20px;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.08);
|
||||
background: #000;
|
||||
.menuTitle{
|
||||
color: #FFF;
|
||||
}
|
||||
.menuDesc{
|
||||
color: rgba(255, 255, 255, 0.60);
|
||||
}
|
||||
.menuIcon{
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
background: rgba(255, 255, 255, 0.20);
|
||||
}
|
||||
}
|
||||
164
src/components/Radar/index.tsx
Normal file
164
src/components/Radar/index.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
import Taro, { useReady } from "@tarojs/taro";
|
||||
import { View, Canvas, Button } from "@tarojs/components";
|
||||
import { useEffect, useRef, forwardRef, useImperativeHandle } from "react";
|
||||
|
||||
const RadarChart: React.FC = forwardRef((props, ref) => {
|
||||
const { data } = props
|
||||
|
||||
const renderFnRef = useRef()
|
||||
// const labels = [
|
||||
// "正手球质",
|
||||
// "正手控制",
|
||||
// "反手球质",
|
||||
// "反手控制",
|
||||
// "底线相持",
|
||||
// "场地覆盖",
|
||||
// "发球接发",
|
||||
// "接随机球",
|
||||
// "战术设计",
|
||||
// ];
|
||||
// const values = [50, 75, 60, 20, 40, 70, 65, 35, 75];
|
||||
const maxValue = 100;
|
||||
const levels = 4;
|
||||
const radius = 100;
|
||||
const center = { x: 160, y: 160 };
|
||||
|
||||
useEffect(() => {
|
||||
if (data.length > 0) {
|
||||
const {texts, vals} = data.reduce((res, item) => {
|
||||
const [text, val] = item
|
||||
return {
|
||||
texts: [...res.texts, text],
|
||||
vals: [...res.vals, val]
|
||||
}
|
||||
}, { texts: [], vals: [] })
|
||||
renderFnRef.current && renderFnRef.current(texts, vals)
|
||||
}
|
||||
}, [data])
|
||||
|
||||
useReady(() => {
|
||||
renderFnRef.current = renderCanvas
|
||||
});
|
||||
|
||||
function renderCanvas (labels, values) {
|
||||
const query = Taro.createSelectorQuery();
|
||||
query
|
||||
.select("#radarCanvas")
|
||||
.fields({ node: true, size: true })
|
||||
.exec((res) => {
|
||||
const canvas = res[0].node as HTMLCanvasElement;
|
||||
const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
|
||||
const dpr = Taro.getSystemInfoSync().pixelRatio;
|
||||
canvas.width = res[0].width * dpr;
|
||||
canvas.height = res[0].height * dpr;
|
||||
ctx.scale(dpr, dpr);
|
||||
|
||||
// === 绘制圆形网格 ===
|
||||
for (let i = 1; i <= levels; i++) {
|
||||
const r = (radius / levels) * i;
|
||||
ctx.beginPath();
|
||||
ctx.arc(center.x, center.y, r, 0, Math.PI * 2);
|
||||
if (i % 2 === 0) {
|
||||
ctx.fillStyle = "rgba(0, 150, 200, 0.1)";
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.strokeStyle = "#bbb";
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// === 坐标轴 & 标签 ===
|
||||
labels.forEach((label, i) => {
|
||||
const angle = ((Math.PI * 2) / labels.length) * i - Math.PI / 2;
|
||||
const x = center.x + radius * Math.cos(angle);
|
||||
const y = center.y + radius * Math.sin(angle);
|
||||
|
||||
// 坐标轴
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(center.x, center.y);
|
||||
ctx.lineTo(x, y);
|
||||
ctx.strokeStyle = "#bbb";
|
||||
ctx.stroke();
|
||||
|
||||
// 标签
|
||||
const offset = 10;
|
||||
const textX = center.x + (radius + offset) * Math.cos(angle);
|
||||
const textY = center.y + (radius + offset) * Math.sin(angle);
|
||||
|
||||
ctx.font = "12px sans-serif";
|
||||
ctx.fillStyle = "#333";
|
||||
ctx.textBaseline = "middle";
|
||||
|
||||
if (Math.abs(angle) < 0.01 || Math.abs(Math.abs(angle) - Math.PI) < 0.01) {
|
||||
ctx.textAlign = "center";
|
||||
} else if (angle > -Math.PI / 2 && angle < Math.PI / 2) {
|
||||
ctx.textAlign = "left";
|
||||
} else {
|
||||
ctx.textAlign = "right";
|
||||
}
|
||||
|
||||
ctx.fillText(label, textX, textY);
|
||||
});
|
||||
|
||||
// === 数据区域 ===
|
||||
ctx.beginPath();
|
||||
values.forEach((val, i) => {
|
||||
const angle = ((Math.PI * 2) / labels.length) * i - Math.PI / 2;
|
||||
const r = (val / maxValue) * radius;
|
||||
const x = center.x + r * Math.cos(angle);
|
||||
const y = center.y + r * Math.sin(angle);
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
});
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = "rgba(0,200,180,0.3)";
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = "#00c8b4";
|
||||
ctx.lineWidth = 3;
|
||||
ctx.stroke();
|
||||
});
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
generateImage: () => new Promise((resolve, reject) => {
|
||||
const query = Taro.createSelectorQuery()
|
||||
query.select("#radarCanvas")
|
||||
.fields({ node: true, size: true })
|
||||
.exec((res) => {
|
||||
const canvas = res[0].node
|
||||
// ⚠️ 关键:传 canvas,而不是 canvasId
|
||||
Taro.canvasToTempFilePath({
|
||||
canvas,
|
||||
success: (res) => resolve(res.tempFilePath),
|
||||
fail: (err) => reject(err),
|
||||
})
|
||||
})
|
||||
})
|
||||
}))
|
||||
|
||||
|
||||
// 保存为图片
|
||||
const saveImage = () => {
|
||||
Taro.canvasToTempFilePath({
|
||||
canvasId: "radarCanvas",
|
||||
success: (res) => {
|
||||
Taro.saveImageToPhotosAlbum({
|
||||
filePath: res.tempFilePath,
|
||||
success: () => Taro.showToast({ title: "保存成功" }),
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Canvas
|
||||
type="2d"
|
||||
id="radarCanvas"
|
||||
style={{ width: "320px", height: "320px", background: "transparent" }}
|
||||
/>
|
||||
{/* <Button onClick={saveImage}>保存为图片</Button> */}
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
export default RadarChart;
|
||||
@@ -66,8 +66,11 @@ const TimeSelector: React.FC<TimeSelectorProps> = ({
|
||||
<View className='time-content' onClick={() => openPicker('start')}>
|
||||
<Text className='time-label'>开始时间</Text>
|
||||
<view className='time-text-wrapper'>
|
||||
<Text className='time-text'>{getDate(value.start_time)}</Text>
|
||||
{value.start_time && (<>
|
||||
<Text className='time-text'>{getDate(value.start_time)}</Text>
|
||||
<Text className='time-text time-am'>{getTime(value.start_time)}</Text>
|
||||
</>)}
|
||||
{!value.start_time && (<Text className='time-text'>请选择开始时间</Text>)}
|
||||
</view>
|
||||
</View>
|
||||
</View>
|
||||
@@ -80,8 +83,9 @@ const TimeSelector: React.FC<TimeSelectorProps> = ({
|
||||
<View className='time-content' onClick={() => openPicker('end')}>
|
||||
<Text className='time-label'>结束时间</Text>
|
||||
<view className='time-text-wrapper'>
|
||||
{showEndTime && (<Text className='time-text'>{getDate(value.end_time)}</Text>)}
|
||||
<Text className='time-text time-am'>{getTime(value.end_time)}</Text>
|
||||
{value.end_time && (<>{showEndTime && (<Text className='time-text'>{getDate(value.end_time)}</Text>)}
|
||||
<Text className='time-text time-am'>{getTime(value.end_time)}</Text></>)}
|
||||
{!value.end_time && (<Text className='time-text'>请选择结束时间</Text>)}
|
||||
</view>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -126,7 +126,7 @@ export default function UploadCover(props: UploadCoverProps) {
|
||||
value.map((item) => {
|
||||
return (
|
||||
<View className="cover-image-item" key={item.id}>
|
||||
<Image className="cover-image-item-image" src={item.url} />
|
||||
<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>
|
||||
)
|
||||
|
||||
@@ -31,7 +31,8 @@
|
||||
}
|
||||
|
||||
.upload-popup-scroll-view {
|
||||
max-height: calc(100vh - 260px);
|
||||
// max-height: calc(100vh - 260px);
|
||||
height: 440px;
|
||||
overflow-y: auto;
|
||||
|
||||
.upload-popup-image-list {
|
||||
@@ -124,7 +125,7 @@
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 62px;
|
||||
padding: 8px 10px 10px 10px;
|
||||
padding: 8px 10px 50px 10px;
|
||||
box-sizing: border-box;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
|
||||
@@ -121,6 +121,7 @@ export default forwardRef(function UploadImage(props: UploadImageProps, ref) {
|
||||
<ScrollView
|
||||
scrollY
|
||||
className="upload-popup-scroll-view"
|
||||
// style={{ height: images.length / 3 * }}
|
||||
>
|
||||
{images.length > 0 ? (
|
||||
<View className="upload-popup-image-list">
|
||||
@@ -128,7 +129,7 @@ export default forwardRef(function UploadImage(props: UploadImageProps, ref) {
|
||||
const isSelected = checkImageSelected(selectedImages, item)
|
||||
return (
|
||||
<View className={`upload-popup-image-item ${outOfMax ? 'disabled' : ''} ${isSelected ? 'selected' : ''}`} onClick={() => handleImageClick(item)}>
|
||||
<Image className="upload-popup-image-item-image" src={item.url} />
|
||||
<Image className="upload-popup-image-item-image" src={item.url} mode="aspectFill" />
|
||||
<View className={`upload-popup-image-item-select ${isSelected ? 'selected' : ''}`}>
|
||||
{isSelected ? (
|
||||
<Image className="select-image-icon" src={img.ICON_CIRCLE_SELECT_ARROW} />
|
||||
|
||||
@@ -91,6 +91,31 @@
|
||||
letter-spacing: 3.2%;
|
||||
color: rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
// 可点击的统计项样式
|
||||
&.clickable {
|
||||
// cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
// padding: 4px 8px;
|
||||
// border-radius: 8px;
|
||||
|
||||
// &:hover {
|
||||
// background-color: rgba(0, 0, 0, 0.05);
|
||||
// }
|
||||
|
||||
// &:active {
|
||||
// background-color: rgba(0, 0, 0, 0.1);
|
||||
// transform: scale(0.98);
|
||||
// }
|
||||
|
||||
.stat_number {
|
||||
color: rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
|
||||
.stat_label {
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +123,8 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
border-radius: 20px;
|
||||
|
||||
.follow_button {
|
||||
display: flex;
|
||||
@@ -106,14 +133,22 @@
|
||||
padding: 12px 16px 12px 12px;
|
||||
height: 40px;
|
||||
background: #000000;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.06);
|
||||
border-radius: 999px;
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.following {
|
||||
background: #FFFFFF;
|
||||
color: #000000;
|
||||
|
||||
.button_text {
|
||||
color: #000000 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.button_icon {
|
||||
@@ -127,19 +162,16 @@
|
||||
font-size: 14px;
|
||||
line-height: 1.4em;
|
||||
color: #FFFFFF;
|
||||
|
||||
.following & {
|
||||
color: #000000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message_button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: unset;
|
||||
background: #FFFFFF;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: 999px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -147,8 +179,8 @@
|
||||
transition: all 0.3s ease;
|
||||
|
||||
.button_icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,7 +242,8 @@
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.tag_item {
|
||||
.tag_item,
|
||||
.button_edit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
@@ -224,11 +257,7 @@
|
||||
.tag_icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
|
||||
/* Frame 1912054928 */
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
.tag_text {
|
||||
@@ -240,15 +269,61 @@
|
||||
color: #000000;
|
||||
}
|
||||
}
|
||||
|
||||
.button_edit {
|
||||
font-family: 'PingFang SC';
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
line-height: 1.8em;
|
||||
letter-spacing: -2.1%;
|
||||
color: rgba(60, 60, 67, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding-right: 20px;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
width: 6px;
|
||||
height: 1px;
|
||||
display: inline-block;
|
||||
background-color: rgba(60, 60, 67, 0.6);
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
transform: rotate(45deg);
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
transform: rotate(-45deg);
|
||||
translate: 4.2px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bio_text {
|
||||
font-family: 'PingFang SC';
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 1.571em;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
white-space: pre-line;
|
||||
.personal_profile {
|
||||
.personal_profile_edit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
.edit_icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.bio_text {
|
||||
font-family: 'PingFang SC';
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 1.571em;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import React from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { View, Text, Image, Button } from '@tarojs/components';
|
||||
import './index.scss';
|
||||
import React, { useState } from "react";
|
||||
import Taro from "@tarojs/taro";
|
||||
import { View, Text, Image, Button } from "@tarojs/components";
|
||||
import "./index.scss";
|
||||
|
||||
import { EditModal } from "@/components";
|
||||
import { UserService } from "@/services/userService";
|
||||
import { PopupPicker } from "@/components/Picker/index";
|
||||
|
||||
// 用户信息接口
|
||||
export interface UserInfo {
|
||||
id: string;
|
||||
id: string | number;
|
||||
nickname: string;
|
||||
avatar: string;
|
||||
join_date: string;
|
||||
@@ -16,17 +20,22 @@ export interface UserInfo {
|
||||
participated: number;
|
||||
};
|
||||
personal_profile: string;
|
||||
location: string;
|
||||
occupation: string;
|
||||
ntrp_level: string;
|
||||
phone?: string;
|
||||
gender?: string;
|
||||
|
||||
latitude?: string,
|
||||
longitude?: string,
|
||||
gender: string;
|
||||
bio?: string;
|
||||
latitude?: string;
|
||||
longitude?: string;
|
||||
birthday?: string;
|
||||
is_following?: boolean;
|
||||
tags?: string[];
|
||||
ongoing_games?: string[];
|
||||
country: string;
|
||||
province: string;
|
||||
city: string;
|
||||
}
|
||||
|
||||
|
||||
// 用户信息卡片组件属性
|
||||
interface UserInfoCardProps {
|
||||
user_info: UserInfo;
|
||||
@@ -35,13 +44,13 @@ interface UserInfoCardProps {
|
||||
on_follow?: () => void;
|
||||
on_message?: () => void;
|
||||
on_share?: () => void;
|
||||
set_user_info?: (info: UserInfo) => void;
|
||||
}
|
||||
|
||||
|
||||
// 处理编辑用户信息
|
||||
const on_edit = () => {
|
||||
Taro.navigateTo({
|
||||
url: '/user_pages/edit/index'
|
||||
url: "/user_pages/edit/index",
|
||||
});
|
||||
};
|
||||
// 用户信息卡片组件
|
||||
@@ -51,8 +60,172 @@ export const UserInfoCard: React.FC<UserInfoCardProps> = ({
|
||||
is_following = false,
|
||||
on_follow,
|
||||
on_message,
|
||||
on_share
|
||||
on_share,
|
||||
set_user_info,
|
||||
}) => {
|
||||
console.log("UserInfoCard 用户信息:", user_info);
|
||||
// 编辑个人简介弹窗状态
|
||||
const [edit_modal_visible, setEditModalVisible] = useState(false);
|
||||
const [editing_field, setEditingField] = useState<string>("");
|
||||
const [gender_picker_visible, setGenderPickerVisible] = useState(false);
|
||||
const [location_picker_visible, setLocationPickerVisible] = useState(false);
|
||||
const [ntrp_picker_visible, setNtrpPickerVisible] = useState(false);
|
||||
const [occupation_picker_visible, setOccupationPickerVisible] =
|
||||
useState(false);
|
||||
|
||||
// 表单状态
|
||||
const [form_data, setFormData] = useState<UserInfo>({ ...user_info });
|
||||
|
||||
// 处理编辑弹窗
|
||||
const handle_open_edit_modal = (field: string) => {
|
||||
if (field === "gender") {
|
||||
setGenderPickerVisible(true);
|
||||
return;
|
||||
}
|
||||
if (field === "location") {
|
||||
setLocationPickerVisible(true);
|
||||
return;
|
||||
}
|
||||
if (field === "ntrp_level") {
|
||||
setNtrpPickerVisible(true);
|
||||
return;
|
||||
}
|
||||
if (field === "occupation") {
|
||||
setOccupationPickerVisible(true);
|
||||
return;
|
||||
}
|
||||
if (field === "nickname") {
|
||||
// 手动输入
|
||||
setEditingField(field);
|
||||
setEditModalVisible(true);
|
||||
} else {
|
||||
setEditingField(field);
|
||||
setEditModalVisible(true);
|
||||
}
|
||||
};
|
||||
const handle_edit_modal_save = async (value: string) => {
|
||||
try {
|
||||
// 调用更新用户信息接口,只传递修改的字段
|
||||
const update_data = { [editing_field]: value };
|
||||
await UserService.update_user_info(update_data);
|
||||
|
||||
// 更新本地状态
|
||||
setFormData((prev) => {
|
||||
const updated = { ...prev, [editing_field]: value };
|
||||
typeof set_user_info === "function" && set_user_info(updated);
|
||||
return updated;
|
||||
});
|
||||
|
||||
// 关闭弹窗
|
||||
setEditModalVisible(false);
|
||||
setEditingField("");
|
||||
|
||||
// 显示成功提示
|
||||
Taro.showToast({
|
||||
title: "保存成功",
|
||||
icon: "success",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("保存失败:", error);
|
||||
Taro.showToast({
|
||||
title: "保存失败",
|
||||
icon: "error",
|
||||
});
|
||||
}
|
||||
};
|
||||
// 处理字段编辑
|
||||
const handle_field_edit = async (
|
||||
field: string | { [key: string]: string },
|
||||
value?: string
|
||||
) => {
|
||||
try {
|
||||
if (
|
||||
typeof field === "object" &&
|
||||
field !== null &&
|
||||
!Array.isArray(field)
|
||||
) {
|
||||
await UserService.update_user_info({ ...field });
|
||||
// 更新本地状态
|
||||
setFormData((prev) => ({ ...prev, ...field }));
|
||||
// setUserInfo((prev) => ({ ...prev, ...field }));
|
||||
} else {
|
||||
// 调用更新用户信息接口,只传递修改的字段
|
||||
const update_data = { [field as string]: value };
|
||||
await UserService.update_user_info(update_data);
|
||||
|
||||
// 更新本地状态
|
||||
setFormData((prev) => ({ ...prev, [field as string]: value }));
|
||||
// setUserInfo((prev) => ({ ...prev, [field as string]: value }));
|
||||
}
|
||||
|
||||
// 显示成功提示
|
||||
Taro.showToast({
|
||||
title: "保存成功",
|
||||
icon: "success",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("保存失败:", error);
|
||||
Taro.showToast({
|
||||
title: "保存失败",
|
||||
icon: "error",
|
||||
});
|
||||
}
|
||||
};
|
||||
// 处理性别选择
|
||||
const handle_gender_change = (e: any) => {
|
||||
const gender_value = e[0];
|
||||
handle_field_edit("gender", gender_value);
|
||||
};
|
||||
|
||||
// 处理地区选择
|
||||
const handle_location_change = (e: any) => {
|
||||
const [country, province, city] = e;
|
||||
handle_field_edit({ country, province, city });
|
||||
};
|
||||
|
||||
// 处理NTRP水平选择
|
||||
const handle_ntrp_level_change = (e: any) => {
|
||||
const ntrp_level_value = e[0];
|
||||
handle_field_edit("ntrp_level", ntrp_level_value);
|
||||
};
|
||||
|
||||
// 处理职业选择
|
||||
const handle_occupation_change = (e: any) => {
|
||||
const [country, province] = e;
|
||||
handle_field_edit("occupation", `${country} ${province}`);
|
||||
};
|
||||
const handle_edit_modal_cancel = () => {
|
||||
setEditModalVisible(false);
|
||||
setEditingField("");
|
||||
};
|
||||
|
||||
// 处理统计项点击
|
||||
const handle_stats_click = (
|
||||
type: "following" | "friends" | "hosted" | "participated"
|
||||
) => {
|
||||
// 只有当前用户才能查看关注相关页面
|
||||
if (!is_current_user) {
|
||||
Taro.showToast({
|
||||
title: "暂不支持查看他人关注信息",
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "following") {
|
||||
// 跳转到关注列表页面
|
||||
Taro.navigateTo({
|
||||
url: "/user_pages/follow/index?tab=following",
|
||||
});
|
||||
} else if (type === "friends") {
|
||||
// 跳转到球友(粉丝)页面,显示粉丝标签
|
||||
Taro.navigateTo({
|
||||
url: "/user_pages/follow/index?tab=follower",
|
||||
});
|
||||
}
|
||||
// 主办和参加暂时不处理,可以后续扩展
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="user_info_card">
|
||||
{/* 头像和基本信息 */}
|
||||
@@ -64,21 +237,30 @@ export const UserInfoCard: React.FC<UserInfoCardProps> = ({
|
||||
<Text className="nickname">{user_info.nickname}</Text>
|
||||
<Text className="join_date">{user_info.join_date}</Text>
|
||||
</View>
|
||||
<View className='tag_item' onClick={on_edit}>
|
||||
<Image
|
||||
className="tag_icon"
|
||||
src={require('../../static/userInfo/edit.svg')}
|
||||
/> </View>
|
||||
{is_current_user && (
|
||||
<View className="tag_item" onClick={on_edit}>
|
||||
<Image
|
||||
className="tag_icon"
|
||||
src={require("../../static/userInfo/edit.svg")}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 统计数据 */}
|
||||
<View className="stats_section">
|
||||
<View className="stats_container">
|
||||
<View className="stat_item">
|
||||
<View
|
||||
className="stat_item clickable"
|
||||
onClick={() => handle_stats_click("following")}
|
||||
>
|
||||
<Text className="stat_number">{user_info.stats.following}</Text>
|
||||
<Text className="stat_label">关注</Text>
|
||||
</View>
|
||||
<View className="stat_item">
|
||||
<View
|
||||
className="stat_item clickable"
|
||||
onClick={() => handle_stats_click("friends")}
|
||||
>
|
||||
<Text className="stat_number">{user_info.stats.friends}</Text>
|
||||
<Text className="stat_label">球友</Text>
|
||||
</View>
|
||||
@@ -95,27 +277,31 @@ export const UserInfoCard: React.FC<UserInfoCardProps> = ({
|
||||
{/* 只有非当前用户才显示关注按钮 */}
|
||||
{!is_current_user && on_follow && (
|
||||
<Button
|
||||
className={`follow_button ${is_following ? 'following' : ''}`}
|
||||
className={`follow_button ${is_following ? "following" : ""}`}
|
||||
onClick={on_follow}
|
||||
>
|
||||
<Image
|
||||
className="button_icon"
|
||||
src={require('../../static/userInfo/plus.svg')}
|
||||
src={require(is_following
|
||||
? "@/static/userInfo/following.svg"
|
||||
: "@/static/userInfo/unfollow.svg")}
|
||||
/>
|
||||
<Text className="button_text">
|
||||
{is_following ? '已关注' : '关注'}
|
||||
<Text
|
||||
className={`button_text ${is_following ? "following" : ""}`}
|
||||
>
|
||||
{is_following ? "已关注" : "关注"}
|
||||
</Text>
|
||||
</Button>
|
||||
)}
|
||||
{/* 只有非当前用户才显示消息按钮 */}
|
||||
{!is_current_user && on_message && (
|
||||
{/* {!is_current_user && on_message && (
|
||||
<Button className="message_button" onClick={on_message}>
|
||||
<Image
|
||||
className="button_icon"
|
||||
src={require('../../static/userInfo/message.svg')}
|
||||
src={require("@/static/userInfo/chat.svg")}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
)} */}
|
||||
|
||||
{/* 只有当前用户才显示分享按钮 */}
|
||||
{is_current_user && on_share && (
|
||||
@@ -129,38 +315,155 @@ export const UserInfoCard: React.FC<UserInfoCardProps> = ({
|
||||
{/* 标签和简介 */}
|
||||
<View className="tags_bio_section">
|
||||
<View className="tags_container">
|
||||
<View className="tag_item">
|
||||
|
||||
{user_info.gender === "0" && (
|
||||
<Image
|
||||
className="tag_icon"
|
||||
src={require('../../static/userInfo/male.svg')}
|
||||
/>
|
||||
)}
|
||||
{user_info.gender === "1" && (
|
||||
<Image
|
||||
className="tag_icon"
|
||||
src={require('../../static/userInfo/female.svg')}
|
||||
/>
|
||||
)}
|
||||
|
||||
</View>
|
||||
<View className="tag_item">
|
||||
<Text className="tag_text">{user_info.ntrp_level || '未设置'}</Text>
|
||||
</View>
|
||||
<View className="tag_item">
|
||||
<Text className="tag_text">{user_info.occupation || '未设置'}</Text>
|
||||
</View>
|
||||
<View className="tag_item">
|
||||
<Image
|
||||
className="tag_icon"
|
||||
src={require('../../static/userInfo/location.svg')}
|
||||
/>
|
||||
<Text className="tag_text">{user_info.location || '未设置'}</Text>
|
||||
</View>
|
||||
{user_info.gender ? (
|
||||
<View className="tag_item">
|
||||
{user_info.gender === "0" && (
|
||||
<Image
|
||||
className="tag_icon"
|
||||
src={require("../../static/userInfo/male.svg")}
|
||||
/>
|
||||
)}
|
||||
{user_info.gender === "1" && (
|
||||
<Image
|
||||
className="tag_icon"
|
||||
src={require("../../static/userInfo/female.svg")}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
) : is_current_user ? (
|
||||
<View className="button_edit" onClick={() => { handle_open_edit_modal('gender') }}>
|
||||
<Text>选择性别</Text>
|
||||
</View>
|
||||
) : null}
|
||||
{user_info.ntrp_level ? (
|
||||
<View className="tag_item">
|
||||
<Text className="tag_text">{`NTRP ${user_info.ntrp_level}`}</Text>
|
||||
</View>
|
||||
) : is_current_user ? (
|
||||
<View className="button_edit" onClick={() => { handle_open_edit_modal('ntrp_level') }}>
|
||||
<Text>测测你的NTRP水平</Text>
|
||||
</View>
|
||||
) : null}
|
||||
{user_info.occupation ? (
|
||||
<View className="tag_item">
|
||||
<Text className="tag_text">{user_info.occupation.split(" ")[1]}</Text>
|
||||
</View>
|
||||
) : is_current_user ? (
|
||||
<View className="button_edit" onClick={() => { handle_open_edit_modal('occupation') }}>
|
||||
<Text>选择职业</Text>
|
||||
</View>
|
||||
) : null}
|
||||
{user_info.country || user_info.province || user_info.city ? (
|
||||
<View className="tag_item">
|
||||
<Text className="tag_text">{`${user_info.province}${user_info.city}`}</Text>
|
||||
</View>
|
||||
) : is_current_user ? (
|
||||
<View className="button_edit" onClick={() => handle_open_edit_modal('location')}>
|
||||
<Text>选择地区</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
<View className="personal_profile">
|
||||
{user_info.personal_profile ? (
|
||||
<Text className="bio_text">{user_info.personal_profile}</Text>
|
||||
) : is_current_user ? (
|
||||
<View
|
||||
className="personal_profile_edit"
|
||||
onClick={() => handle_open_edit_modal("personal_profile")}
|
||||
>
|
||||
<Image
|
||||
className="edit_icon"
|
||||
src={require("../../static/userInfo/info_edit.svg")}
|
||||
/>
|
||||
<Text className="bio_text">点击添加简介,让更多人了解你</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
<Text className="bio_text">{user_info.personal_profile}</Text>
|
||||
</View>
|
||||
|
||||
{/* 编辑个人简介弹窗 */}
|
||||
<EditModal
|
||||
visible={edit_modal_visible}
|
||||
type={editing_field}
|
||||
title="编辑简介"
|
||||
placeholder="介绍一下你的喜好,或者训练习惯"
|
||||
initialValue={form_data["personal_profile"] || ""}
|
||||
maxLength={100}
|
||||
onSave={handle_edit_modal_save}
|
||||
onCancel={handle_edit_modal_cancel}
|
||||
validationMessage="请填写 2-100 个字符"
|
||||
/>
|
||||
{/* 性别选择弹窗 */}
|
||||
{gender_picker_visible && (
|
||||
<PopupPicker
|
||||
options={[
|
||||
[
|
||||
{ text: "男", value: "0" },
|
||||
{ text: "女", value: "1" },
|
||||
{ text: "保密", value: "2" },
|
||||
],
|
||||
]}
|
||||
visible={gender_picker_visible}
|
||||
setvisible={setGenderPickerVisible}
|
||||
value={[form_data.gender]}
|
||||
onChange={handle_gender_change}
|
||||
/>
|
||||
)}
|
||||
{/* 地区选择弹窗 */}
|
||||
{location_picker_visible && (
|
||||
<PopupPicker
|
||||
options={[
|
||||
[{ text: "中国", value: "中国" }],
|
||||
[{ text: "上海", value: "上海" }],
|
||||
[
|
||||
{ text: "浦东新区", value: "浦东新区" },
|
||||
{ text: "静安区", value: "静安区" },
|
||||
],
|
||||
]}
|
||||
visible={location_picker_visible}
|
||||
setvisible={setLocationPickerVisible}
|
||||
value={[form_data.country, form_data.province, form_data.city]}
|
||||
onChange={handle_location_change}
|
||||
/>
|
||||
)}
|
||||
{/* NTRP水平选择弹窗 */}
|
||||
{ntrp_picker_visible && (
|
||||
<PopupPicker
|
||||
options={[
|
||||
[
|
||||
{ text: "1.5", value: "1.5" },
|
||||
{ text: "2.0", value: "2.0" },
|
||||
{ text: "2.5", value: "2.5" },
|
||||
{ text: "3.0", value: "3.0" },
|
||||
{ text: "3.5", value: "3.5" },
|
||||
{ text: "4.0", value: "4.0" },
|
||||
{ text: "4.5", value: "4.5" },
|
||||
],
|
||||
]}
|
||||
type="ntrp"
|
||||
img={user_info.avatar}
|
||||
visible={ntrp_picker_visible}
|
||||
setvisible={setNtrpPickerVisible}
|
||||
value={[form_data.ntrp_level]}
|
||||
onChange={handle_ntrp_level_change}
|
||||
/>
|
||||
)}
|
||||
{/* 职业选择弹窗 */}
|
||||
{occupation_picker_visible && (
|
||||
<PopupPicker
|
||||
options={[
|
||||
[{ text: "时尚", value: "时尚" }],
|
||||
[
|
||||
{ text: "美妆博主", value: "美妆博主" },
|
||||
{ text: "设计师", value: "设计师" },
|
||||
],
|
||||
]}
|
||||
visible={occupation_picker_visible}
|
||||
setvisible={setOccupationPickerVisible}
|
||||
value={[...form_data.occupation.split(" ")]}
|
||||
onChange={handle_occupation_change}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -183,7 +486,9 @@ export interface GameRecord {
|
||||
current_participants: number;
|
||||
level_range: string;
|
||||
game_type: string;
|
||||
images: string[];
|
||||
image_list: string[];
|
||||
deadline_hours: number;
|
||||
end_time: string;
|
||||
}
|
||||
|
||||
// 球局卡片组件属性
|
||||
@@ -197,20 +502,17 @@ interface GameCardProps {
|
||||
export const GameCard: React.FC<GameCardProps> = ({
|
||||
game,
|
||||
on_click,
|
||||
on_participant_click
|
||||
on_participant_click,
|
||||
}) => {
|
||||
return (
|
||||
<View
|
||||
className="game_card"
|
||||
onClick={() => on_click(game.id)}
|
||||
>
|
||||
<View className="game_card" onClick={() => on_click(game.id)}>
|
||||
{/* 球局标题和类型 */}
|
||||
<View className="game_header">
|
||||
<Text className="game_title">{game.title}</Text>
|
||||
<View className="game_type_icon">
|
||||
<Image
|
||||
className="type_icon"
|
||||
src={require('../../static/userInfo/tennis.svg')}
|
||||
src={require("../../static/userInfo/tennis.svg")}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
@@ -233,12 +535,8 @@ export const GameCard: React.FC<GameCardProps> = ({
|
||||
|
||||
{/* 球局图片 */}
|
||||
<View className="game_images">
|
||||
{game.images.map((image, index) => (
|
||||
<Image
|
||||
key={index}
|
||||
className="game_image"
|
||||
src={image}
|
||||
/>
|
||||
{game.image_list.map((image, index) => (
|
||||
<Image key={index} className="game_image" src={image} />
|
||||
))}
|
||||
</View>
|
||||
|
||||
@@ -246,7 +544,7 @@ export const GameCard: React.FC<GameCardProps> = ({
|
||||
<View className="game_tags">
|
||||
<View className="participants_info">
|
||||
<View className="avatars">
|
||||
{game.participants.map((participant, index) => (
|
||||
{game.participants?.map((participant, index) => (
|
||||
<Image
|
||||
key={index}
|
||||
className="participant_avatar"
|
||||
@@ -279,8 +577,8 @@ export const GameCard: React.FC<GameCardProps> = ({
|
||||
|
||||
// 球局标签页组件属性
|
||||
interface GameTabsProps {
|
||||
active_tab: 'hosted' | 'participated';
|
||||
on_tab_change: (tab: 'hosted' | 'participated') => void;
|
||||
active_tab: "hosted" | "participated";
|
||||
on_tab_change: (tab: "hosted" | "participated") => void;
|
||||
is_current_user: boolean;
|
||||
}
|
||||
|
||||
@@ -288,21 +586,28 @@ interface GameTabsProps {
|
||||
export const GameTabs: React.FC<GameTabsProps> = ({
|
||||
active_tab,
|
||||
on_tab_change,
|
||||
is_current_user
|
||||
is_current_user,
|
||||
}) => {
|
||||
const hosted_text = is_current_user ? '我主办的' : '他主办的';
|
||||
const participated_text = is_current_user ? '我参与的' : '他参与的';
|
||||
const hosted_text = is_current_user ? "我主办的" : "主办球局";
|
||||
const participated_text = is_current_user ? "我参与的" : "参与球局";
|
||||
|
||||
return (
|
||||
<View className="game_tabs_section">
|
||||
<View className="tab_container">
|
||||
<View className={`tab_item ${active_tab === 'hosted' ? 'active' : ''}`} onClick={() => on_tab_change('hosted')}>
|
||||
<View
|
||||
className={`tab_item ${active_tab === "hosted" ? "active" : ""}`}
|
||||
onClick={() => on_tab_change("hosted")}
|
||||
>
|
||||
<Text className="tab_text">{hosted_text}</Text>
|
||||
</View>
|
||||
<View className={`tab_item ${active_tab === 'participated' ? 'active' : ''}`} onClick={() => on_tab_change('participated')}>
|
||||
<View
|
||||
className={`tab_item ${active_tab === "participated" ? "active" : ""
|
||||
}`}
|
||||
onClick={() => on_tab_change("participated")}
|
||||
>
|
||||
<Text className="tab_text">{participated_text}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -17,6 +17,12 @@ import withAuth from "./Auth";
|
||||
import { CustomPicker, PopupPicker } from "./Picker";
|
||||
import NTRPEvaluatePopup from "./NTRPEvaluatePopup";
|
||||
import ShareCardCanvas from "./ShareCardCanvas";
|
||||
import RefundPopup from "./refundPopup";
|
||||
import GameManagePopup from './GameManagePopup';
|
||||
import FollowUserCard from './FollowUserCard/index';
|
||||
import Comments from "./Comments";
|
||||
import GeneralNavbar from "./GeneralNavbar";
|
||||
import RadarChart from './Radar'
|
||||
|
||||
export {
|
||||
ActivityTypeSwitch,
|
||||
@@ -39,4 +45,10 @@ export {
|
||||
PopupPicker,
|
||||
NTRPEvaluatePopup,
|
||||
ShareCardCanvas,
|
||||
RefundPopup,
|
||||
GameManagePopup,
|
||||
FollowUserCard,
|
||||
Comments,
|
||||
GeneralNavbar,
|
||||
RadarChart,
|
||||
};
|
||||
|
||||
132
src/components/refundPopup/index.module.scss
Normal file
132
src/components/refundPopup/index.module.scss
Normal file
@@ -0,0 +1,132 @@
|
||||
.refundPolicy {
|
||||
padding-top: 20px;
|
||||
// .moduleTitle {
|
||||
// display: flex;
|
||||
// padding: 15px 0 8px;
|
||||
// justify-content: space-between;
|
||||
// align-items: center;
|
||||
// align-self: stretch;
|
||||
// color: #000;
|
||||
// font-feature-settings:
|
||||
// "liga" off,
|
||||
// "clig" off;
|
||||
// font-family: "PingFang SC";
|
||||
// font-size: 14px;
|
||||
// font-style: normal;
|
||||
// font-weight: 600;
|
||||
// line-height: 20px;
|
||||
// letter-spacing: -0.23px;
|
||||
// }
|
||||
|
||||
.specTips {
|
||||
padding-bottom: 20px;
|
||||
color: rgba(60, 60, 67, 0.60);
|
||||
text-align: center;
|
||||
font-feature-settings: 'liga' off, 'clig' off;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.policyList {
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
background: #fff;
|
||||
box-shadow: 0 4px 36px 0 rgba(0, 0, 0, 0.06);
|
||||
|
||||
.policyItem {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
color: #000;
|
||||
text-align: center;
|
||||
font-feature-settings:
|
||||
"liga" off,
|
||||
"clig" off;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
||||
|
||||
&:nth-child(1) {
|
||||
color: #000;
|
||||
text-align: center;
|
||||
font-feature-settings:
|
||||
"liga" off,
|
||||
"clig" off;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.time,
|
||||
.rule {
|
||||
width: 50%;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.rule {
|
||||
border-left: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0 15px 40px;
|
||||
|
||||
.header {
|
||||
padding: 24px 15px 0;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.title {
|
||||
color: #000;
|
||||
text-align: center;
|
||||
font-feature-settings: 'liga' off, 'clig' off;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.closeIcon {
|
||||
margin-left: auto;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
padding: 2px 6px;
|
||||
height: 52px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
box-shadow: 0 8px 64px 0 rgba(0, 0, 0, 0.10);
|
||||
backdrop-filter: blur(16px);
|
||||
color: #fff;
|
||||
background-color: #000;
|
||||
font-feature-settings: 'liga' off, 'clig' off;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.23px;
|
||||
}
|
||||
}
|
||||
139
src/components/refundPopup/index.tsx
Normal file
139
src/components/refundPopup/index.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import React, { useState, useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
import { View, Text, Button, Image } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro';
|
||||
import dayjs from 'dayjs'
|
||||
import { CommonPopup } from '@/components';
|
||||
import orderService from '@/services/orderService';
|
||||
import styles from './index.module.scss'
|
||||
import closeIcon from '@/static/order/orderListClose.svg'
|
||||
|
||||
function genRefundNotice (refund_policy) {
|
||||
if (refund_policy.length === 0) {
|
||||
return {}
|
||||
}
|
||||
const now = dayjs()
|
||||
const deadlines = refund_policy.map(item => dayjs(item.deadline_formatted))
|
||||
let matchPolicyIndex = deadlines.findIndex(d => d.isAfter(now))
|
||||
if (matchPolicyIndex === -1) {
|
||||
matchPolicyIndex = refund_policy.length - 1
|
||||
}
|
||||
const { deadline_formatted, price, refund_rate } = refund_policy[matchPolicyIndex]
|
||||
if (refund_rate === 1) {
|
||||
return { refundPrice: price, notice: `本次可全额退款, ¥${price} 将原路退回,请查收` }
|
||||
} else if (refund_rate === 0) {
|
||||
return { refundPrice: 0, notice: `当前退出不可退款,后续流程未明确,@麻真瑜` }
|
||||
}
|
||||
const refundPrice = price * refund_rate
|
||||
const leftHours = dayjs(deadline_formatted).diff(dayjs(), 'hour')
|
||||
return { refundPrice, notice: `距活动开始已不足${leftHours}h,当前退出您需扣除${price - refundPrice}元` }
|
||||
}
|
||||
|
||||
function renderCancelContent(checkOrderInfo) {
|
||||
const { refund_policy = [] } = checkOrderInfo;
|
||||
const policyList = [
|
||||
{
|
||||
time: "申请退款时间",
|
||||
rule: "退款规则",
|
||||
},
|
||||
...refund_policy.map((item) => {
|
||||
return {
|
||||
time: item.application_time,
|
||||
rule: item.refund_rule,
|
||||
};
|
||||
}),
|
||||
];
|
||||
const { notice } = genRefundNotice(refund_policy)
|
||||
return (
|
||||
<View className={styles.refundPolicy}>
|
||||
{/* <View className={styles.moduleTitle}>
|
||||
<Text>退款政策</Text>
|
||||
</View> */}
|
||||
{<View className={styles.specTips}>{notice}</View>}
|
||||
{/* 订单信息摘要 */}
|
||||
<View className={styles.policyList}>
|
||||
{policyList.map((item, index) => (
|
||||
<View key={index} className={styles.policyItem}>
|
||||
<View className={styles.time}>{item.time}</View>
|
||||
<View className={styles.rule}>{item.rule}</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export type RefundRef = {
|
||||
show: (item: any, callback: (result: boolean) => void) => void
|
||||
}
|
||||
|
||||
export default forwardRef<RefundRef>(function RefundPopup(_props, ref) {
|
||||
const [visible, setVisible] = useState(false)
|
||||
const [checkOrderInfo, setCheckOrderInfo] = useState({})
|
||||
const [orderData, setOrderData] = useState({})
|
||||
const onDown = useRef<((result: boolean) => void) | null>(null)
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
show: onShow,
|
||||
}))
|
||||
|
||||
async function onShow (orderItem, onFinish: (result: boolean) => void) {
|
||||
const {
|
||||
game_info,
|
||||
} = orderItem
|
||||
onDown.current = onFinish
|
||||
setOrderData(orderItem)
|
||||
const res = await orderService.getCheckOrderInfo(game_info.id);
|
||||
setCheckOrderInfo(res.data);
|
||||
setVisible(true)
|
||||
}
|
||||
|
||||
function onClose () {
|
||||
setVisible(false)
|
||||
onDown.current?.(false)
|
||||
}
|
||||
|
||||
async function handleConfirmQuit () {
|
||||
const { order_no, amount } = orderData
|
||||
try {
|
||||
const refundRes = await orderService.applicateRefund({
|
||||
order_no,
|
||||
refund_amount: amount,
|
||||
refund_reason: "用户主动退款",
|
||||
});
|
||||
if (refundRes.code !== 0) {
|
||||
throw new Error(refundRes.message);
|
||||
}
|
||||
Taro.showToast({
|
||||
title: "退出成功",
|
||||
icon: "none",
|
||||
})
|
||||
onDown.current?.(true)
|
||||
} catch (e) {
|
||||
Taro.showToast({
|
||||
title: e.message,
|
||||
icon: "error",
|
||||
});
|
||||
} finally {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
return (
|
||||
<CommonPopup
|
||||
visible={visible}
|
||||
onClose={onClose}
|
||||
// title="退出活动"
|
||||
enableDragToClose={false}
|
||||
zIndex={1001}
|
||||
hideFooter
|
||||
>
|
||||
<View className={styles.container}>
|
||||
<View className={styles.header}>
|
||||
<Text className={styles.title}>退出活动</Text>
|
||||
<Image className={styles.closeIcon} src={closeIcon} onClick={onClose} />
|
||||
</View>
|
||||
{renderCancelContent(checkOrderInfo)}
|
||||
<Button className={styles.action} onClick={handleConfirmQuit}>确认并退出</Button>
|
||||
</View>
|
||||
</CommonPopup>
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user