Merge branch master into feature/juguohong/20250816
2
.gitignore
vendored
@@ -6,3 +6,5 @@ node_modules/
|
||||
.DS_Store
|
||||
.swc
|
||||
src/config/env.ts
|
||||
.vscode
|
||||
*.http
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default defineAppConfig({
|
||||
pages: [
|
||||
|
||||
"home_pages/index", //中转页
|
||||
"login_pages/index/index",
|
||||
"login_pages/verification/index",
|
||||
@@ -21,7 +22,17 @@ export default defineAppConfig({
|
||||
root: "user_pages",
|
||||
pages: [
|
||||
"myself/index", // 个人中心
|
||||
"edit/index", // 个人中心
|
||||
"edit/index", // 编辑个人中心
|
||||
"other/index", // 他人个人主页
|
||||
"follow/index", // 球友关注页
|
||||
"wallet/index", // 钱包页
|
||||
"queryTransactions/index", // 查询交易
|
||||
"downloadBill/index", // 下载账单
|
||||
"downloadBillRecords/index", // 下载账单记录
|
||||
"billDetail/index", // 账单详情
|
||||
"setTransactionPassword/index", // 设置交易密码
|
||||
"validPhone/index", // 验证手机号
|
||||
"withdrawal/index", // 提现
|
||||
],
|
||||
},
|
||||
// {
|
||||
|
||||
12
src/app.ts
@@ -2,7 +2,6 @@ import { Component, ReactNode } from "react";
|
||||
import "./nutui-theme.scss";
|
||||
import "./app.scss";
|
||||
import "qweather-icons/font/qweather-icons.css";
|
||||
import { useDictionaryStore } from "./store/dictionaryStore";
|
||||
import { useGlobalStore } from "./store/global";
|
||||
|
||||
interface AppProps {
|
||||
@@ -17,7 +16,6 @@ class App extends Component<AppProps> {
|
||||
|
||||
componentDidMount() {
|
||||
// 初始化字典数据
|
||||
this.initDictionaryData();
|
||||
this.getNavBarHeight();
|
||||
// this.getLocation()
|
||||
}
|
||||
@@ -26,15 +24,7 @@ class App extends Component<AppProps> {
|
||||
|
||||
componentDidHide() {}
|
||||
|
||||
// 初始化字典数据
|
||||
private async initDictionaryData() {
|
||||
try {
|
||||
const { fetchDictionary } = useDictionaryStore.getState();
|
||||
await fetchDictionary();
|
||||
} catch (error) {
|
||||
console.error("初始化字典数据失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 获取导航高度
|
||||
getNavBarHeight = () => {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
{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
@@ -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
@@ -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
@@ -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
@@ -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 [organizer_joined, setOrganizerJoined] = useState(true);
|
||||
const [minParticipants, setMinParticipants] = useState(1);
|
||||
const [maxParticipants, setMaxParticipants] = useState(1);
|
||||
|
||||
const handleChange = (value: [number | string, number | string]) => {
|
||||
const newMin = Number(value[0])
|
||||
const newMax = Number(value[1])
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
setOrganizerJoined(value.organizer_joined);
|
||||
setMinParticipants(value.min);
|
||||
setMaxParticipants(value.max);
|
||||
}
|
||||
console.log(value, 'valuevaluevaluevaluevaluevalue');
|
||||
}, [value]);
|
||||
|
||||
// 确保最少人数不能大于最多人数
|
||||
if (newMin > newMax) {
|
||||
return
|
||||
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 })
|
||||
}
|
||||
|
||||
onChange([newMin, newMax])
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
setType('year')
|
||||
} else if (type === 'time') {
|
||||
setSelected(new Date(year, month - 1, 1));
|
||||
}
|
||||
} 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)) {
|
||||
setSelected(d[0])
|
||||
if (d.length === 2) {
|
||||
return;
|
||||
} else if (d.length === 1) {
|
||||
if (selectedBackup.length === 0 || selectedBackup.length === 2) {
|
||||
setSelected([...d]);
|
||||
setSelectedBackup([...d]);
|
||||
} else {
|
||||
setSelected(d)
|
||||
setSelected(
|
||||
[...selectedBackup, d[0]].sort(
|
||||
(a, b) => a.getTime() - b.getTime()
|
||||
)
|
||||
);
|
||||
setSelectedBackup([]);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (Array.isArray(d)) {
|
||||
setSelected(d[0]);
|
||||
} else {
|
||||
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']}>
|
||||
{type === "year" && (
|
||||
<View className={styles["calendar-container"]}>
|
||||
<CalendarUI
|
||||
ref={calendarRef}
|
||||
type={searchType}
|
||||
value={selected}
|
||||
onChange={handleChange}
|
||||
showQuickActions={false}
|
||||
onHeaderClick={onHeaderClick}
|
||||
/></View>
|
||||
}
|
||||
{
|
||||
type === 'month' && <PickerCommon
|
||||
/>
|
||||
</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
|
||||
)}
|
||||
{type === "time" && (
|
||||
<PickerCommon
|
||||
ref={hourMinutePickerRef}
|
||||
type="hour"
|
||||
value={[selectedHour, selectedMinute]}
|
||||
/>
|
||||
|
||||
}
|
||||
)}
|
||||
</CommonPopup>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default DialogCalendarCard
|
||||
export default DialogCalendarCard;
|
||||
|
||||
@@ -1,32 +1,42 @@
|
||||
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',
|
||||
const NutUICalendar = React.forwardRef<CalendarUIRef, NutUICalendarProps>(
|
||||
(
|
||||
{
|
||||
type = "single",
|
||||
value,
|
||||
onChange,
|
||||
isBorder = false,
|
||||
showQuickActions = true,
|
||||
onHeaderClick
|
||||
}, ref) => {
|
||||
onHeaderClick,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
// 根据类型初始化选中值
|
||||
// const getInitialValue = (): Date | Date[] => {
|
||||
// console.log(value,defaultValue,'today')
|
||||
@@ -49,143 +59,206 @@ const NutUICalendar = React.forwardRef<CalendarUIRef, NutUICalendarProps>(({
|
||||
// }
|
||||
// return today
|
||||
// }
|
||||
const startOfMonth = (date: Date) => new Date(date.getFullYear(), date.getMonth(), 1)
|
||||
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)
|
||||
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]))
|
||||
setSelectedValue(value.map((item) => new Date(item)));
|
||||
setCurrent(new Date(value[0] as Date));
|
||||
}
|
||||
if ((typeof value === 'string' || value instanceof Date) && value) {
|
||||
setSelectedValue(new Date(value))
|
||||
setCurrent(new Date(value))
|
||||
if ((typeof value === "string" || value instanceof Date) && value) {
|
||||
setSelectedValue(new Date(value));
|
||||
setCurrent(new Date(value));
|
||||
}
|
||||
}, [value])
|
||||
}, [value]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
jumpTo: (year: number, month: number) => {
|
||||
calendarRef.current?.jumpTo(year, month)
|
||||
}
|
||||
}))
|
||||
calendarRef.current?.jumpTo(year, month);
|
||||
},
|
||||
gotoMonth,
|
||||
}));
|
||||
|
||||
const handleDateChange = (newValue: any) => {
|
||||
setSelectedValue(newValue)
|
||||
onChange?.(newValue as any)
|
||||
}
|
||||
const formatHeader = (date: Date) => `${getMonth(date)}`
|
||||
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)
|
||||
}
|
||||
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)
|
||||
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)
|
||||
calendarRef.current?.jump?.(delta);
|
||||
} catch (e) {
|
||||
console.warn('CalendarCardRef jump 调用失败', e)
|
||||
}
|
||||
handlePageChange({ year: next.getFullYear(), month: next.getMonth() + 1 })
|
||||
console.warn("CalendarCardRef jump 调用失败", e);
|
||||
}
|
||||
handlePageChange({
|
||||
year: next.getFullYear(),
|
||||
month: next.getMonth() + 1,
|
||||
});
|
||||
};
|
||||
|
||||
const handleHeaderClick = () => {
|
||||
onHeaderClick && onHeaderClick(current)
|
||||
setvisible(true)
|
||||
}
|
||||
|
||||
|
||||
onHeaderClick && onHeaderClick(current);
|
||||
setvisible(true);
|
||||
};
|
||||
|
||||
const syncMonthTo = (anchor: Date) => {
|
||||
// 计算从 current 到目标 anchor 所在月份的偏移,调用 jump(delta)
|
||||
const monthsDelta = (anchor.getFullYear() - current.getFullYear()) * 12 + (anchor.getMonth() - current.getMonth())
|
||||
const monthsDelta =
|
||||
(anchor.getFullYear() - current.getFullYear()) * 12 +
|
||||
(anchor.getMonth() - current.getMonth());
|
||||
if (monthsDelta !== 0) {
|
||||
gotoMonth(monthsDelta)
|
||||
}
|
||||
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 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 [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 dayList = getWeekendOfCurrentWeek(7);
|
||||
setSelectedValue(dayList);
|
||||
syncMonthTo(dayList[0]);
|
||||
onChange?.(dayList);
|
||||
};
|
||||
const selectMonth = () => {
|
||||
const dayList = getWeekendOfCurrentWeek(30)
|
||||
setSelectedValue(dayList)
|
||||
syncMonthTo(dayList[0])
|
||||
onChange?.(dayList)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
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>
|
||||
{showQuickActions && (
|
||||
<View className={styles["quick-actions"]}>
|
||||
<View className={styles["quick-action"]} onClick={selectWeekend}>
|
||||
本周末
|
||||
</View>
|
||||
}
|
||||
<View className={`${styles['calendar-card']} ${isBorder ? styles['border'] : ''}`}>
|
||||
<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 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 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
|
||||
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']}>
|
||||
<View className={styles["week-header"]}>
|
||||
{["周日", "周一", "周二", "周三", "周四", "周五", "周六"].map(
|
||||
(day) => (
|
||||
<Text key={day} className={styles["week-day"]}>
|
||||
{day}
|
||||
</Text>
|
||||
))}
|
||||
)
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* NutUI CalendarCard 组件 */}
|
||||
@@ -196,16 +269,21 @@ const NutUICalendar = React.forwardRef<CalendarUIRef, NutUICalendarProps>(({
|
||||
renderDay={renderDay}
|
||||
onChange={handleDateChange}
|
||||
onPageChange={handlePageChange}
|
||||
onDayClick={handleDayClick}
|
||||
/>
|
||||
</View>
|
||||
{ visible && <PopupPicker
|
||||
{visible && (
|
||||
<PopupPicker
|
||||
visible={visible}
|
||||
setvisible={setvisible}
|
||||
value={[current.getFullYear(), current.getMonth() + 1]}
|
||||
type="month"
|
||||
onChange={(value) => handleMonthChange(value)}/> }
|
||||
onChange={(value) => handleMonthChange(value)}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default NutUICalendar
|
||||
export default NutUICalendar;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.calendar-card {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
|
||||
&.border {
|
||||
border-radius: 12px;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.12);
|
||||
@@ -17,19 +18,46 @@
|
||||
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;
|
||||
@@ -37,6 +65,7 @@
|
||||
width: 50%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.arrow-right-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -45,15 +74,18 @@
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.month-arrow {
|
||||
width: 8px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
width: 10px;
|
||||
height: 24px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.arrow.left {
|
||||
left: 9px;
|
||||
transform: rotate(-180deg);
|
||||
@@ -64,6 +96,7 @@
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
padding: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.week-item {
|
||||
text-align: center;
|
||||
color: rgba(60, 60, 67, 0.30);
|
||||
@@ -76,6 +109,7 @@
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.week-day {
|
||||
text-align: center;
|
||||
color: rgba(60, 60, 67, 0.30);
|
||||
@@ -89,6 +123,7 @@
|
||||
gap: 8px 0;
|
||||
padding: 4px 0 16px;
|
||||
}
|
||||
|
||||
.cell {
|
||||
height: 44px;
|
||||
display: flex;
|
||||
@@ -97,12 +132,15 @@
|
||||
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;
|
||||
@@ -152,6 +190,7 @@
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
height: 44px;
|
||||
@@ -161,6 +200,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn.primary {
|
||||
background: #000;
|
||||
color: #fff;
|
||||
@@ -241,8 +281,14 @@
|
||||
.nut-calendarcard-header {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.nut-calendarcard-content {
|
||||
.nut-calendarcard-days {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 4px;
|
||||
justify-items: center;
|
||||
|
||||
&:first-child {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -250,10 +296,12 @@
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.nut-calendarcard-day {
|
||||
margin-bottom: 0px !important;
|
||||
height: 44px;
|
||||
width: 44px !important;
|
||||
|
||||
&.active {
|
||||
background-color: #000 !important;
|
||||
color: #fff !important;
|
||||
@@ -264,19 +312,24 @@
|
||||
justify-content: center;
|
||||
width: 44px !important;
|
||||
font-size: 24px !important;
|
||||
|
||||
.day-container {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.weekend {
|
||||
color: rgb(0, 0, 0) !important;
|
||||
|
||||
&.active {
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nut-calendarcard-day-inner {
|
||||
font-size: 20px;
|
||||
|
||||
.day-container {
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 22px;
|
||||
@@ -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,60 +1,89 @@
|
||||
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 = [],
|
||||
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])
|
||||
}, [type]);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (value.length > 0 && defaultOptions.length > 0) {
|
||||
@@ -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;
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
:global {
|
||||
.nut-popup-round {
|
||||
position: relative !important;
|
||||
|
||||
.nut-picker-control {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.nut-picker {
|
||||
&::after {
|
||||
content: '';
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 16px;
|
||||
@@ -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');
|
||||
Taro.navigateTo({
|
||||
url: `/publish_pages/publishBall/index?type=${type}`
|
||||
})
|
||||
setIsVisible(false)
|
||||
console.log(type, "type");
|
||||
if (type === "ai") {
|
||||
setAiImportVisible(true);
|
||||
setIsVisible(false);
|
||||
return;
|
||||
}
|
||||
Taro.navigateTo({
|
||||
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 className={styles.menuContent}>
|
||||
<Text className={styles.menuTitle}>发布畅打活动</Text>
|
||||
<Text className={styles.menuDesc}>认证球场官方组织</Text>
|
||||
</View>
|
||||
<View
|
||||
className={`${styles.menuItem} ${styles.aiItem}`}
|
||||
onClick={() => handleMenuItemClick("ai")}
|
||||
>
|
||||
<View className={styles.menuContent}>
|
||||
<View className={styles.menuTitle}>
|
||||
智能发布球局
|
||||
<View className={styles.menuArrow}>
|
||||
<Image src={images.ICON_ARROW_RIGHT} className={styles.img} />
|
||||
<Image
|
||||
src={images.ICON_ARROW_RIGHT_WHITE}
|
||||
className={styles.img}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<Text className={styles.menuDesc}>
|
||||
识别文本/图片,快速导入球局信息
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<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
@@ -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'>
|
||||
{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,6 +269,50 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.personal_profile {
|
||||
.personal_profile_edit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
.edit_icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.bio_text {
|
||||
@@ -250,6 +323,8 @@
|
||||
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}>
|
||||
{is_current_user && (
|
||||
<View className="tag_item" onClick={on_edit}>
|
||||
<Image
|
||||
className="tag_icon"
|
||||
src={require('../../static/userInfo/edit.svg')}
|
||||
/> </View>
|
||||
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">
|
||||
{user_info.gender ? (
|
||||
<View className="tag_item">
|
||||
|
||||
{user_info.gender === "0" && (
|
||||
<Image
|
||||
className="tag_icon"
|
||||
src={require('../../static/userInfo/male.svg')}
|
||||
src={require("../../static/userInfo/male.svg")}
|
||||
/>
|
||||
)}
|
||||
{user_info.gender === "1" && (
|
||||
<Image
|
||||
className="tag_icon"
|
||||
src={require('../../static/userInfo/female.svg')}
|
||||
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">{user_info.ntrp_level || '未设置'}</Text>
|
||||
<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 || '未设置'}</Text>
|
||||
<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">
|
||||
<Image
|
||||
className="tag_icon"
|
||||
src={require('../../static/userInfo/location.svg')}
|
||||
/>
|
||||
<Text className="tag_text">{user_info.location || '未设置'}</Text>
|
||||
<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>
|
||||
</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,18 +586,25 @@ 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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
)
|
||||
})
|
||||
@@ -8,10 +8,11 @@ export const API_CONFIG = {
|
||||
USER: {
|
||||
DETAIL: '/user/detail',
|
||||
UPDATE: '/user/update',
|
||||
FOLLOW: '/user/follow',
|
||||
UNFOLLOW: '/user/unfollow',
|
||||
FOLLOW: '/wch_users/follow',
|
||||
UNFOLLOW: '/wch_users/unfollow',
|
||||
HOSTED_GAMES: '/user/games',
|
||||
PARTICIPATED_GAMES: '/user/participated'
|
||||
PARTICIPATED_GAMES: '/user/participated',
|
||||
PARSE_PHONE: '/user/parse_phone',
|
||||
},
|
||||
|
||||
// 文件上传接口
|
||||
@@ -28,7 +29,9 @@ export const API_CONFIG = {
|
||||
CREATE: '/game/create',
|
||||
JOIN: '/game/join',
|
||||
LEAVE: '/game/leave'
|
||||
}
|
||||
},
|
||||
PROFESSIONS: '/professions/tree',
|
||||
CITIS: '/cities/tree'
|
||||
};
|
||||
|
||||
// 请求拦截器配置
|
||||
|
||||
@@ -10,6 +10,13 @@ export interface EnvConfig {
|
||||
timeout: number
|
||||
enableLog: boolean
|
||||
enableMock: boolean
|
||||
// 客服配置
|
||||
customerService: {
|
||||
corpId: string
|
||||
serviceUrl: string
|
||||
phoneNumber?: string
|
||||
email?: string
|
||||
}
|
||||
}
|
||||
|
||||
// 各环境配置
|
||||
@@ -21,7 +28,14 @@ const envConfigs: Record<EnvType, EnvConfig> = {
|
||||
// apiBaseURL: 'http://localhost:9098',
|
||||
timeout: 15000,
|
||||
enableLog: true,
|
||||
enableMock: true
|
||||
enableMock: true,
|
||||
// 客服配置
|
||||
customerService: {
|
||||
corpId: 'ww51fc969e8b76af82', // 企业ID
|
||||
serviceUrl: 'https://work.weixin.qq.com/kfid/kfc64085b93243c5c91',
|
||||
phoneNumber: '400-888-8888',
|
||||
email: 'service@light120.com'
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -31,7 +45,14 @@ const envConfigs: Record<EnvType, EnvConfig> = {
|
||||
apiBaseURL: 'https://sit.light120.com',
|
||||
timeout: 10000,
|
||||
enableLog: false,
|
||||
enableMock: false
|
||||
enableMock: false,
|
||||
// 客服配置
|
||||
customerService: {
|
||||
corpId: 'ww51fc969e8b76af82', // 企业ID
|
||||
serviceUrl: 'https://work.weixin.qq.com/kfid/kfc64085b93243c5c91',
|
||||
phoneNumber: '400-888-8888',
|
||||
email: 'service@light120.com'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +107,7 @@ export const getEnvInfo = () => {
|
||||
config,
|
||||
taroEnv: Taro.getEnv(),
|
||||
platform: Taro.getEnv() === Taro.ENV_TYPE.WEAPP ? '微信小程序' :
|
||||
Taro.getEnv() === Taro.ENV_TYPE.H5 ? 'H5' :
|
||||
Taro.getEnv() === Taro.ENV_TYPE.WEB ? 'Web' :
|
||||
Taro.getEnv() === Taro.ENV_TYPE.RN ? 'React Native' : '未知'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ export const publishBallFormSchema: FormFieldConfig[] = [
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'is_wechat_contact',
|
||||
prop: 'wechat',
|
||||
label: '',
|
||||
type: FieldType.WECHATCONTACT,
|
||||
required: true,
|
||||
|
||||
@@ -56,4 +56,14 @@ export default {
|
||||
ICON_LIST_SEARCH_CLEAR_HISTORY: require('@/static/search/icon-clear-history.svg'),
|
||||
ICON_LIST_SEARCH_SUGGESTION: require('@/static/search/icon-search-suggestion.svg'),
|
||||
ICON_LIST_INPUT_LOGO: require('@/static/list/icon-input-logo.svg'),
|
||||
ICON_IMPORTANT_BTN: require('@/static/publishBall/icon-important-btn.svg'),
|
||||
ICON_IMPORTANT_BLACK: require('@/static/publishBall/icon-important-black.svg'),
|
||||
ICON_ARROW_RIGHT_WHITE: require('@/static/publishBall/icon-arrow-right-white.svg'),
|
||||
ICON_ARROW_RIGHT_BLACK: require('@/static/publishBall/icon-arrow-right-black.svg'),
|
||||
ICON_EXAMINATION: require('@/static/userInfo/examination.svg'),
|
||||
ICON_ARROW_GREEN: require('@/static/userInfo/arrow-green.svg'),
|
||||
ICON_COPY: require('@/static/publishBall/icon-copy.svg'),
|
||||
ICON_UPLOAD_IMG: require('@/static/publishBall/icon-upload-img.svg'),
|
||||
ICON_UPLOAD_SUCCESS: require('@/static/publishBall/icon-upload-success.svg'),
|
||||
ICON_CLOSE: require('@/static/publishBall/icon-close.svg'),
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
padding-bottom: 34px;
|
||||
min-height: 100vh;
|
||||
// min-height: 100vh;
|
||||
|
||||
.recommendTextWrapper {
|
||||
display: flex;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '球局详情',
|
||||
navigationStyle: 'custom',
|
||||
enableShareAppMessage: true,
|
||||
})
|
||||
|
||||
@@ -162,7 +162,6 @@
|
||||
&-image {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -690,9 +689,11 @@
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
flex: 0 0 auto;
|
||||
|
||||
&-avatar {
|
||||
.participants-list-item-avatar {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&-name {
|
||||
@@ -806,7 +807,7 @@
|
||||
}
|
||||
|
||||
&-organizer-recommend-games {
|
||||
padding: 24px 15px 0;
|
||||
padding: 24px 15px 10px;
|
||||
|
||||
.organizer-title {
|
||||
overflow: hidden;
|
||||
@@ -836,6 +837,8 @@
|
||||
&-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
&-message {
|
||||
@@ -1012,6 +1015,8 @@
|
||||
&-avatar {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
&-message {
|
||||
@@ -1061,7 +1066,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 52px;
|
||||
width: 113px;
|
||||
width: 120px;
|
||||
box-sizing: border-box;
|
||||
padding: 2px 20px;
|
||||
justify-content: center;
|
||||
@@ -1117,19 +1122,34 @@
|
||||
}
|
||||
}
|
||||
|
||||
&-join-game {
|
||||
.detail-main-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 52px;
|
||||
width: auto;
|
||||
padding: 2px 6px;
|
||||
// padding: 2px 6px;
|
||||
box-sizing: border-box;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
// gap: 12px;
|
||||
flex: 1 0 0;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
// border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
|
||||
&.disabled {
|
||||
background-color: #B4B4B4;
|
||||
color: rgba(60, 60, 67, 0.60);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.sticky-bottom-bar-join-game {
|
||||
margin-left: auto;
|
||||
// width: 151px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
|
||||
&-price {
|
||||
font-family: "PoetsenOne";
|
||||
@@ -1140,37 +1160,20 @@
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.share-popup-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 20px 16px env(safe-area-inset-bottom);
|
||||
box-sizing: border-box;
|
||||
// padding-bottom: env(safe-area-inset-bottom);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
|
||||
& > view {
|
||||
.game_manage {
|
||||
width: 100px;
|
||||
height: 64px;
|
||||
border-radius: 12px;
|
||||
margin-left: auto;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
& > image {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
justify-content: center;
|
||||
background: #000;
|
||||
color: #fff;
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > text {
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,41 +5,53 @@ import React, {
|
||||
useImperativeHandle,
|
||||
forwardRef,
|
||||
} from "react";
|
||||
import { View, Text, Image, Map, ScrollView } from "@tarojs/components";
|
||||
import { Avatar } from "@nutui/nutui-react-taro";
|
||||
import { View, Text, Image, Map, ScrollView, Button } from "@tarojs/components";
|
||||
// import { Avatar } from "@nutui/nutui-react-taro";
|
||||
import Taro, {
|
||||
useRouter,
|
||||
useShareAppMessage,
|
||||
useShareTimeline,
|
||||
useDidShow,
|
||||
} from "@tarojs/taro";
|
||||
import classnames from "classnames";
|
||||
import dayjs from "dayjs";
|
||||
import "dayjs/locale/zh-cn";
|
||||
// 导入API服务
|
||||
import { CommonPopup, withAuth, NTRPEvaluatePopup } from "@/components";
|
||||
import {
|
||||
CommonPopup,
|
||||
withAuth,
|
||||
NTRPEvaluatePopup,
|
||||
GameManagePopup,
|
||||
Comments,
|
||||
} from "@/components";
|
||||
import {
|
||||
EvaluateType,
|
||||
SceneType,
|
||||
DisplayConditionType,
|
||||
} from "@/components/NTRPEvaluatePopup";
|
||||
import DetailService, { MATCH_STATUS } from "@/services/detailService";
|
||||
import DetailService, {
|
||||
MATCH_STATUS,
|
||||
IsSubstituteSupported,
|
||||
} from "@/services/detailService";
|
||||
import * as LoginService from "@/services/loginService";
|
||||
import OrderService from "@/services/orderService";
|
||||
import { getCurrentLocation, calculateDistance } from "@/utils/locationUtils";
|
||||
import { useUserInfo, useUserActions } from "@/store/userStore";
|
||||
import img from "@/config/images";
|
||||
import styles from "./style.module.scss";
|
||||
import "./index.scss";
|
||||
|
||||
dayjs.locale("zh-cn");
|
||||
|
||||
// 将·作为连接符插入到标签文本之间
|
||||
function insertDotInTags(tags: string[]) {
|
||||
if (!tags) return [];
|
||||
return tags.join("-·-").split("-");
|
||||
}
|
||||
|
||||
function GameTags(props) {
|
||||
const { userInfo } = props;
|
||||
const { avatar_url } = userInfo;
|
||||
const { userInfo, handleViewUserInfo } = props;
|
||||
const { avatar_url, id } = userInfo;
|
||||
const tags = [
|
||||
{
|
||||
name: "🕙 急招",
|
||||
@@ -64,7 +76,9 @@ function GameTags(props) {
|
||||
{/* network image mock */}
|
||||
<Image
|
||||
className="detail-page-content-avatar-tags-avatar-image"
|
||||
mode="aspectFill"
|
||||
src={avatar_url}
|
||||
onClick={handleViewUserInfo.bind(null, id)}
|
||||
/>
|
||||
</View>
|
||||
<View className="detail-page-content-avatar-tags-tags">
|
||||
@@ -96,7 +110,6 @@ function Coursel(props) {
|
||||
async function getImagesMsg(imageList) {
|
||||
const latest_list: CourselItemType[] = [];
|
||||
const sys_info = await Taro.getSystemInfo();
|
||||
console.log(sys_info, "info");
|
||||
const max_width = sys_info.screenWidth - 30;
|
||||
const max_height = 240;
|
||||
const current_aspect_ratio = max_width / max_height;
|
||||
@@ -163,14 +176,14 @@ const SharePopup = forwardRef(
|
||||
},
|
||||
}));
|
||||
|
||||
// function handleShareToWechat() {
|
||||
// useShareAppMessage(() => {
|
||||
// return {
|
||||
// title: '分享',
|
||||
// path: `/game_pages/detail/index?id=${id}&from=share`,
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
useShareAppMessage((res) => {
|
||||
console.log(res, "res");
|
||||
return {
|
||||
title: "分享",
|
||||
imageUrl: "https://img.yzcdn.cn/vant/cat.jpeg",
|
||||
path: `/game_pages/detail/index?id=${id}&from=share`,
|
||||
};
|
||||
});
|
||||
|
||||
// function handleShareToWechatMoments() {
|
||||
// useShareTimeline(() => {
|
||||
@@ -181,17 +194,19 @@ const SharePopup = forwardRef(
|
||||
// })
|
||||
// }
|
||||
|
||||
// function handleSaveToLocal() {
|
||||
// Taro.saveImageToPhotosAlbum({
|
||||
// filePath: images[0],
|
||||
// success: () => {
|
||||
// Taro.showToast({ title: '保存成功', icon: 'success' })
|
||||
// },
|
||||
// fail: () => {
|
||||
// Taro.showToast({ title: '保存失败', icon: 'none' })
|
||||
// },
|
||||
// })
|
||||
// }
|
||||
function handleSaveToLocal() {
|
||||
Taro.showToast({ title: "not yet", icon: "error" });
|
||||
return;
|
||||
Taro.saveImageToPhotosAlbum({
|
||||
filePath: "",
|
||||
success: () => {
|
||||
Taro.showToast({ title: "保存成功", icon: "success" });
|
||||
},
|
||||
fail: () => {
|
||||
Taro.showToast({ title: "保存失败", icon: "none" });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<CommonPopup
|
||||
@@ -203,12 +218,28 @@ const SharePopup = forwardRef(
|
||||
hideFooter
|
||||
style={{ minHeight: "100px" }}
|
||||
>
|
||||
<View catchMove className="share-popup-content">
|
||||
<View className={styles.shareContainer}>
|
||||
<View catchMove className={styles.title}>
|
||||
分享卡片
|
||||
</View>
|
||||
<View className={styles.shareItems}>
|
||||
<Button
|
||||
className={classnames(styles.button, styles.share)}
|
||||
openType="share"
|
||||
>
|
||||
分享到聊天
|
||||
</Button>
|
||||
<Button
|
||||
className={classnames(styles.button, styles.save)}
|
||||
onClick={handleSaveToLocal}
|
||||
>
|
||||
保存到本地
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</CommonPopup>
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
function navto(url) {
|
||||
@@ -217,34 +248,74 @@ function navto(url) {
|
||||
});
|
||||
}
|
||||
|
||||
function toast(message) {
|
||||
Taro.showToast({ title: message, icon: "none" });
|
||||
}
|
||||
|
||||
function isFull(counts) {
|
||||
const {
|
||||
max_players,
|
||||
current_players,
|
||||
max_substitute_players,
|
||||
current_substitute_count,
|
||||
is_substitute_supported,
|
||||
} = counts;
|
||||
if (max_players === current_players) {
|
||||
return true;
|
||||
} else if (is_substitute_supported === IsSubstituteSupported.SUPPORT) {
|
||||
return max_substitute_players === current_substitute_count;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// 底部操作栏
|
||||
function StickyButton(props) {
|
||||
const { handleShare, handleJoinGame, detail } = props;
|
||||
const {
|
||||
handleShare,
|
||||
handleJoinGame,
|
||||
detail,
|
||||
onStatusChange,
|
||||
handleAddComment,
|
||||
getCommentCount,
|
||||
} = props;
|
||||
const [commentCount, setCommentCount] = useState(0);
|
||||
const ntrpRef = useRef(null);
|
||||
// const userInfo = useUserInfo();
|
||||
// const { id } = userInfo;
|
||||
const {
|
||||
id,
|
||||
publisher_id,
|
||||
match_status,
|
||||
price,
|
||||
user_action_status,
|
||||
match_status,
|
||||
start_time,
|
||||
end_time,
|
||||
is_organizer,
|
||||
} = detail || {};
|
||||
|
||||
const gameManageRef = useRef();
|
||||
|
||||
function handleSelfEvaluate() {
|
||||
// TODO: 打开自评弹窗
|
||||
ntrpRef?.current?.show();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getCommentCount?.((count) => {
|
||||
setCommentCount(count);
|
||||
});
|
||||
}, [getCommentCount]);
|
||||
|
||||
function generateTextAndAction(
|
||||
user_action_status: null | { [key: string]: boolean },
|
||||
): undefined | { text: string | React.FC; action: () => void } {
|
||||
user_action_status: null | { [key: string]: boolean }
|
||||
):
|
||||
| undefined
|
||||
| { text: string | React.FC; action?: () => void; available?: boolean } {
|
||||
if (!user_action_status) {
|
||||
return;
|
||||
}
|
||||
const displayPrice = is_organizer ? 0 : price;
|
||||
// user_action_status.can_assess = true;
|
||||
user_action_status.can_join = true;
|
||||
// user_action_status.can_join = false;
|
||||
// console.log(user_action_status, "user_action");
|
||||
const {
|
||||
can_assess,
|
||||
can_join,
|
||||
@@ -253,54 +324,62 @@ function StickyButton(props) {
|
||||
is_substituting,
|
||||
waiting_start,
|
||||
} = user_action_status || {};
|
||||
if (
|
||||
Object.values(user_action_status).every((value) => !value) &&
|
||||
dayjs(end_time).isBefore(dayjs())
|
||||
) {
|
||||
if (MATCH_STATUS.CANCELED === match_status) {
|
||||
return {
|
||||
text: "球局已结束,查看其他球局",
|
||||
action: navto.bind(null, "/game_pages/list/index"),
|
||||
text: "活动已取消",
|
||||
available: false,
|
||||
// action: () => toast("活动已取消"),
|
||||
};
|
||||
} else if (dayjs(end_time).isBefore(dayjs())) {
|
||||
return {
|
||||
text: "活动已结束",
|
||||
available: false,
|
||||
// action: () => toast("活动已结束"),
|
||||
};
|
||||
} else if (dayjs(start_time).isBefore(dayjs())) {
|
||||
return {
|
||||
text: "活动已开始",
|
||||
available: false,
|
||||
// action: () => toast("活动已开始"),
|
||||
};
|
||||
} else if (isFull(detail)) {
|
||||
return {
|
||||
text: "活动已满员",
|
||||
available: false,
|
||||
// action: () => toast("活动已满员"),
|
||||
};
|
||||
}
|
||||
if (waiting_start) {
|
||||
return {
|
||||
text: "等待开始, 查看更多球局",
|
||||
action: navto.bind(null, "/game_pages/list/index"),
|
||||
text: () => <Text>¥{displayPrice} 已加入</Text>,
|
||||
action: () => toast("已加入"),
|
||||
};
|
||||
} else if (is_substituting) {
|
||||
return {
|
||||
text: "候补中,查看其他球局",
|
||||
action: navto.bind(null, "/game_pages/list/index"),
|
||||
text: () => <Text>¥{displayPrice} 已加入候补</Text>,
|
||||
action: () => toast("已加入候补"),
|
||||
};
|
||||
} else if (can_pay) {
|
||||
return {
|
||||
text: "继续支付",
|
||||
text: () => <Text>¥{price} 继续支付</Text>,
|
||||
action: async () => {
|
||||
const res = await OrderService.getUnpaidOrder(id);
|
||||
if (res.code === 0) {
|
||||
Taro.navigateTo({
|
||||
url: `/order_pages/orderDetail/index?id=${res.data.order_info.order_id}`,
|
||||
});
|
||||
navto(
|
||||
`/order_pages/orderDetail/index?id=${res.data.order_info.order_id}`
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
} else if (can_substitute) {
|
||||
return {
|
||||
text: "立即候补",
|
||||
text: () => <Text>¥{displayPrice} 我要候补</Text>,
|
||||
action: handleJoinGame,
|
||||
};
|
||||
} else if (can_join) {
|
||||
return {
|
||||
text: () => {
|
||||
return (
|
||||
<>
|
||||
<Text>🎾</Text>
|
||||
<Text>立即加入</Text>
|
||||
<View className="game-price">
|
||||
<Text>¥ {price}</Text>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
return <Text>¥{displayPrice} 立即加入</Text>;
|
||||
},
|
||||
action: handleJoinGame,
|
||||
};
|
||||
@@ -312,8 +391,9 @@ function StickyButton(props) {
|
||||
types={[EvaluateType.EDIT, EvaluateType.EVALUATE]}
|
||||
scene={SceneType.DETAIL}
|
||||
displayCondition={DisplayConditionType.AUTO}
|
||||
showGuide={false}
|
||||
>
|
||||
<Text>NTRP自评</Text>
|
||||
<Text>¥{displayPrice} 立即加入</Text>
|
||||
</NTRPEvaluatePopup>
|
||||
),
|
||||
action: handleSelfEvaluate,
|
||||
@@ -321,7 +401,7 @@ function StickyButton(props) {
|
||||
}
|
||||
return {
|
||||
text: "球局无法加入",
|
||||
action: () => {},
|
||||
available: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -329,7 +409,11 @@ function StickyButton(props) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const { text, action } = generateTextAndAction(user_action_status)!;
|
||||
const {
|
||||
text,
|
||||
available = true,
|
||||
action = () => {},
|
||||
} = generateTextAndAction(user_action_status)!;
|
||||
|
||||
let ActionText: React.FC | string = text;
|
||||
|
||||
@@ -339,9 +423,8 @@ function StickyButton(props) {
|
||||
};
|
||||
}
|
||||
|
||||
// const role = Number(publisher_id) === id ? "ownner" : "visitor";
|
||||
|
||||
return (
|
||||
<>
|
||||
<View className="sticky-bottom-bar">
|
||||
<View className="sticky-bottom-bar-share-and-comment">
|
||||
<View className="sticky-bottom-bar-share" onClick={handleShare}>
|
||||
@@ -355,20 +438,46 @@ function StickyButton(props) {
|
||||
<View
|
||||
className="sticky-bottom-bar-comment"
|
||||
onClick={() => {
|
||||
Taro.showToast({ title: "To be continued", icon: "none" });
|
||||
// Taro.showToast({ title: "To be continued", icon: "none" });
|
||||
handleAddComment();
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
className="sticky-bottom-bar-comment-icon"
|
||||
src={img.ICON_DETAIL_COMMENT_DARK}
|
||||
/>
|
||||
<Text className="sticky-bottom-bar-comment-text">32</Text>
|
||||
<Text className="sticky-bottom-bar-comment-text">
|
||||
{commentCount > 0 ? commentCount : "评论"}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className="sticky-bottom-bar-join-game" onClick={action}>
|
||||
<View
|
||||
className={classnames(
|
||||
"detail-main-action",
|
||||
available ? "" : "disabled"
|
||||
)}
|
||||
>
|
||||
<View
|
||||
style={is_organizer ? {} : { margin: "auto" }}
|
||||
className="sticky-bottom-bar-join-game"
|
||||
onClick={action}
|
||||
>
|
||||
<ActionText />
|
||||
</View>
|
||||
{is_organizer && (
|
||||
<View
|
||||
className="game_manage"
|
||||
onClick={() => {
|
||||
gameManageRef.current.show(detail, onStatusChange);
|
||||
}}
|
||||
>
|
||||
管理
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
<GameManagePopup ref={gameManageRef} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -382,10 +491,10 @@ function GameInfo(props) {
|
||||
location_name,
|
||||
start_time,
|
||||
end_time,
|
||||
weather = [{}],
|
||||
weather,
|
||||
} = detail || {};
|
||||
|
||||
const [{ iconDay, tempMax, tempMin }] = weather;
|
||||
const [{ iconDay, tempMax, tempMin }] = weather || [{}];
|
||||
|
||||
const openMap = () => {
|
||||
Taro.openLocation({
|
||||
@@ -539,7 +648,8 @@ function VenueInfo(props) {
|
||||
function previewImage(current_url) {
|
||||
Taro.previewImage({
|
||||
current: current_url,
|
||||
urls: venue_image_list.map((c) => c.url),
|
||||
urls:
|
||||
venue_image_list?.length > 0 ? venue_image_list.map((c) => c.url) : [],
|
||||
});
|
||||
}
|
||||
return (
|
||||
@@ -588,7 +698,8 @@ function VenueInfo(props) {
|
||||
<View className="venue-screenshot-title">预定截图</View>
|
||||
<ScrollView scrollY className="venue-screenshot-scroll-view">
|
||||
<View className="venue-screenshot-image-list">
|
||||
{venue_image_list.map((item) => {
|
||||
{venue_image_list?.length > 0 &&
|
||||
venue_image_list.map((item) => {
|
||||
return (
|
||||
<View
|
||||
className="venue-screenshot-image-item"
|
||||
@@ -596,6 +707,7 @@ function VenueInfo(props) {
|
||||
>
|
||||
<Image
|
||||
className="venue-screenshot-image-item-image"
|
||||
mode="aspectFill"
|
||||
src={item.url}
|
||||
/>
|
||||
</View>
|
||||
@@ -611,8 +723,8 @@ function VenueInfo(props) {
|
||||
function genNTRPRequirementText(min, max) {
|
||||
if (min && max && min !== max) {
|
||||
return `${min} - ${max} 之间`;
|
||||
} else if (max === 1) {
|
||||
return "没有要求";
|
||||
} else if (max === "1") {
|
||||
return "无要求";
|
||||
} else if (max) {
|
||||
return `${max} 以上`;
|
||||
}
|
||||
@@ -661,7 +773,7 @@ function GamePlayAndRequirement(props) {
|
||||
|
||||
// 参与者
|
||||
function Participants(props) {
|
||||
const { detail = {}, handleJoinGame } = props;
|
||||
const { detail = {}, handleJoinGame, handleViewUserInfo } = props;
|
||||
const participants = detail.participants || [];
|
||||
const {
|
||||
participant_count,
|
||||
@@ -672,10 +784,9 @@ function Participants(props) {
|
||||
user_action_status;
|
||||
const showApplicationEntry =
|
||||
[can_pay, can_substitute, is_substituting, waiting_start].every(
|
||||
(item) => !item,
|
||||
(item) => !item
|
||||
) && can_join;
|
||||
const leftCount = max_participants - participant_count;
|
||||
const organizer_id = Number(detail.publisher_id);
|
||||
return (
|
||||
<View className="detail-page-content-participants">
|
||||
<View className="participants-title">
|
||||
@@ -691,7 +802,6 @@ function Participants(props) {
|
||||
className="participants-list-application"
|
||||
onClick={() => {
|
||||
handleJoinGame();
|
||||
// Taro.showToast({ title: "To be continued", icon: "none" });
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
@@ -708,11 +818,14 @@ function Participants(props) {
|
||||
<View
|
||||
className="participants-list-scroll-content"
|
||||
style={{
|
||||
width: `${participants.length * 103 + (participants.length - 1) * 8}px`,
|
||||
width: `${
|
||||
participants.length * 103 + (participants.length - 1) * 8
|
||||
}px`,
|
||||
}}
|
||||
>
|
||||
{participants.map((participant) => {
|
||||
const {
|
||||
is_organizer,
|
||||
user: {
|
||||
avatar_url,
|
||||
nickname,
|
||||
@@ -720,13 +833,17 @@ function Participants(props) {
|
||||
id: participant_user_id,
|
||||
},
|
||||
} = participant;
|
||||
const role =
|
||||
participant_user_id === organizer_id ? "组织者" : "参与者";
|
||||
const role = is_organizer ? "组织者" : "参与者";
|
||||
return (
|
||||
<View key={participant.id} className="participants-list-item">
|
||||
<Avatar
|
||||
<Image
|
||||
className="participants-list-item-avatar"
|
||||
mode="aspectFill"
|
||||
src={avatar_url}
|
||||
onClick={handleViewUserInfo.bind(
|
||||
null,
|
||||
participant_user_id
|
||||
)}
|
||||
/>
|
||||
<Text className="participants-list-item-name">
|
||||
{nickname || "未知"}
|
||||
@@ -750,7 +867,7 @@ function Participants(props) {
|
||||
|
||||
function SupplementalNotes(props) {
|
||||
const {
|
||||
detail: { description, description_tag = [] },
|
||||
detail: { description, description_tag },
|
||||
} = props;
|
||||
return (
|
||||
<View className="detail-page-content-supplemental-notes">
|
||||
@@ -760,7 +877,7 @@ function SupplementalNotes(props) {
|
||||
<View className="supplemental-notes-content">
|
||||
{/* supplemental notes tags */}
|
||||
<View className="supplemental-notes-content-tags">
|
||||
{insertDotInTags(description_tag).map((tag, index) => (
|
||||
{insertDotInTags(description_tag || []).map((tag, index) => (
|
||||
<View key={index} className="supplemental-notes-content-tags-tag">
|
||||
<Text>{tag}</Text>
|
||||
</View>
|
||||
@@ -808,7 +925,12 @@ function genRecommendGames(games, location, avatar) {
|
||||
avatar,
|
||||
applications: max_players,
|
||||
checkedApplications: current_players,
|
||||
levelRequirements: `NTRP ${genNTRPRequirementText(skill_level_min, skill_level_max)}`,
|
||||
levelRequirements:
|
||||
skill_level_max !== skill_level_min
|
||||
? `${skill_level_min || "-"}至${skill_level_max || "-"}`
|
||||
: skill_level_min === "1"
|
||||
? "无要求"
|
||||
: `${skill_level_min}以上`,
|
||||
playType: play_type,
|
||||
};
|
||||
});
|
||||
@@ -819,6 +941,8 @@ function OrganizerInfo(props) {
|
||||
userInfo,
|
||||
currentLocation: location,
|
||||
onUpdateUserInfo = () => {},
|
||||
handleViewUserInfo,
|
||||
handleAddComment,
|
||||
} = props;
|
||||
const {
|
||||
id,
|
||||
@@ -855,6 +979,10 @@ function OrganizerInfo(props) {
|
||||
}
|
||||
};
|
||||
|
||||
function handleViewGame(gameId) {
|
||||
navto(`/game_pages/detail/index?id=${gameId}&from=current`);
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="detail-page-content-organizer-recommend-games">
|
||||
{/* orgnizer title */}
|
||||
@@ -863,7 +991,12 @@ function OrganizerInfo(props) {
|
||||
</View>
|
||||
{/* organizer avatar and name */}
|
||||
<View className="organizer-avatar-name">
|
||||
<Avatar className="organizer-avatar-name-avatar" src={avatar_url} />
|
||||
<Image
|
||||
className="organizer-avatar-name-avatar"
|
||||
src={avatar_url}
|
||||
mode="aspectFill"
|
||||
onClick={handleViewUserInfo.bind(null, id)}
|
||||
/>
|
||||
<View className="organizer-avatar-name-message">
|
||||
<Text className="organizer-avatar-name-message-name">{nickname}</Text>
|
||||
<View className="organizer-avatar-name-message-stats">
|
||||
@@ -893,7 +1026,10 @@ function OrganizerInfo(props) {
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
<View className="organizer-actions-comment">
|
||||
<View
|
||||
className="organizer-actions-comment"
|
||||
onClick={() => handleAddComment()}
|
||||
>
|
||||
<Image
|
||||
className="organizer-actions-comment-icon"
|
||||
src={img.ICON_DETAIL_COMMENT}
|
||||
@@ -902,8 +1038,12 @@ function OrganizerInfo(props) {
|
||||
</View>
|
||||
</View>
|
||||
{/* recommend games by organizer */}
|
||||
{recommendGames.length > 0 && (
|
||||
<View className="organizer-recommend-games">
|
||||
<View className="organizer-recommend-games-title" onClick={() => {}}>
|
||||
<View
|
||||
className="organizer-recommend-games-title"
|
||||
onClick={handleViewUserInfo.bind(null, id)}
|
||||
>
|
||||
<Text>TA的更多活动</Text>
|
||||
<Image
|
||||
className="organizer-recommend-games-title-arrow"
|
||||
@@ -913,7 +1053,11 @@ function OrganizerInfo(props) {
|
||||
<ScrollView className="recommend-games-list" scrollX>
|
||||
<View className="recommend-games-list-content">
|
||||
{recommendGames.map((game, index) => (
|
||||
<View key={index} className="recommend-games-list-item">
|
||||
<View
|
||||
key={index}
|
||||
className="recommend-games-list-item"
|
||||
onClick={handleViewGame.bind(null, game.id)}
|
||||
>
|
||||
{/* game title */}
|
||||
<View className="recommend-games-list-item-title">
|
||||
<Text>{game.title}</Text>
|
||||
@@ -937,14 +1081,20 @@ function OrganizerInfo(props) {
|
||||
</View>
|
||||
{/* organizer avatar、applications、level requirements、play type */}
|
||||
<View className="recommend-games-list-item-addon">
|
||||
<Avatar
|
||||
<Image
|
||||
className="recommend-games-list-item-addon-avatar"
|
||||
mode="aspectFill"
|
||||
src={game.avatar}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleViewUserInfo(id);
|
||||
}}
|
||||
/>
|
||||
<View className="recommend-games-list-item-addon-message">
|
||||
<View className="recommend-games-list-item-addon-message-applications">
|
||||
<Text>
|
||||
报名人数 {game.checkedApplications}/{game.applications}
|
||||
报名人数 {game.checkedApplications}/
|
||||
{game.applications}
|
||||
</Text>
|
||||
</View>
|
||||
<View className="recommend-games-list-item-addon-message-level-requirements">
|
||||
@@ -960,6 +1110,7 @@ function OrganizerInfo(props) {
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -973,8 +1124,12 @@ function Index() {
|
||||
const { id, from } = params;
|
||||
const [userInfo, setUserInfo] = useState({}); // 组织者的userInfo
|
||||
const { fetchUserInfo } = useUserActions(); // 获取登录用户的userInfo
|
||||
const myInfo = useUserInfo();
|
||||
|
||||
const isMyOwn = userInfo.id === myInfo.id;
|
||||
|
||||
const sharePopupRef = useRef<any>(null);
|
||||
const commentRef = useRef();
|
||||
|
||||
useDidShow(async () => {
|
||||
await updateLocation();
|
||||
@@ -1003,11 +1158,17 @@ function Index() {
|
||||
|
||||
const fetchDetail = async () => {
|
||||
if (!id) return;
|
||||
try {
|
||||
const res = await DetailService.getDetail(Number(id));
|
||||
if (res.code === 0) {
|
||||
setDetail(res.data);
|
||||
fetchUserInfoById(res.data.publisher_id);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.message === "球局不存在") {
|
||||
handleBack();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onUpdateUserInfo = () => {
|
||||
@@ -1017,7 +1178,6 @@ function Index() {
|
||||
async function fetchUserInfoById(user_id) {
|
||||
const userDetailInfo = await LoginService.getUserInfoById(user_id);
|
||||
if (userDetailInfo.code === 0) {
|
||||
// console.log(userDetailInfo.data);
|
||||
setUserInfo(userDetailInfo.data);
|
||||
}
|
||||
}
|
||||
@@ -1026,12 +1186,24 @@ function Index() {
|
||||
sharePopupRef.current.show();
|
||||
}
|
||||
|
||||
const handleJoinGame = () => {
|
||||
Taro.navigateTo({
|
||||
url: `/order_pages/orderDetail/index?gameId=${id}`,
|
||||
});
|
||||
const handleJoinGame = async () => {
|
||||
if (isMyOwn) {
|
||||
const res = await DetailService.organizerJoin(Number(id));
|
||||
if (res.code === 0) {
|
||||
toast("加入成功");
|
||||
fetchDetail();
|
||||
}
|
||||
return;
|
||||
}
|
||||
navto(`/order_pages/orderDetail/index?gameId=${id}`);
|
||||
};
|
||||
|
||||
function onStatusChange(result) {
|
||||
if (result) {
|
||||
fetchDetail();
|
||||
}
|
||||
}
|
||||
|
||||
function handleBack() {
|
||||
const pages = Taro.getCurrentPages();
|
||||
if (pages.length <= 1) {
|
||||
@@ -1043,7 +1215,10 @@ function Index() {
|
||||
}
|
||||
}
|
||||
|
||||
console.log("detail", detail);
|
||||
function handleViewUserInfo(userId) {
|
||||
navto(`/user_pages/other/index?userid=${userId}`);
|
||||
}
|
||||
|
||||
const backgroundImage = detail?.image_list?.[0]
|
||||
? { backgroundImage: `url(${detail?.image_list?.[0]})` }
|
||||
: {};
|
||||
@@ -1073,7 +1248,11 @@ function Index() {
|
||||
{/* content */}
|
||||
<View className="detail-page-content">
|
||||
{/* avatar and tags */}
|
||||
<GameTags detail={detail} userInfo={userInfo} />
|
||||
<GameTags
|
||||
detail={detail}
|
||||
userInfo={userInfo}
|
||||
handleViewUserInfo={handleViewUserInfo}
|
||||
/>
|
||||
{/* title */}
|
||||
<View className="detail-page-content-title">
|
||||
<Text className="detail-page-content-title-text">{detail.title}</Text>
|
||||
@@ -1085,7 +1264,11 @@ function Index() {
|
||||
{/* gameplay requirements */}
|
||||
<GamePlayAndRequirement detail={detail} />
|
||||
{/* participants */}
|
||||
<Participants detail={detail} handleJoinGame={handleJoinGame} />
|
||||
<Participants
|
||||
detail={detail}
|
||||
handleJoinGame={handleJoinGame}
|
||||
handleViewUserInfo={handleViewUserInfo}
|
||||
/>
|
||||
{/* supplemental notes */}
|
||||
<SupplementalNotes detail={detail} />
|
||||
{/* organizer and recommend games by organizer */}
|
||||
@@ -1094,12 +1277,28 @@ function Index() {
|
||||
userInfo={userInfo}
|
||||
currentLocation={currentLocation}
|
||||
onUpdateUserInfo={onUpdateUserInfo}
|
||||
handleViewUserInfo={handleViewUserInfo}
|
||||
handleAddComment={() => {
|
||||
commentRef.current && commentRef.current.addComment();
|
||||
}}
|
||||
/>
|
||||
<Comments
|
||||
ref={commentRef}
|
||||
game_id={Number(id)}
|
||||
publisher_id={Number(detail.publisher_id)}
|
||||
/>
|
||||
{/* sticky bottom action bar */}
|
||||
<StickyButton
|
||||
handleShare={handleShare}
|
||||
handleJoinGame={handleJoinGame}
|
||||
detail={detail}
|
||||
onStatusChange={onStatusChange}
|
||||
handleAddComment={() => {
|
||||
commentRef.current && commentRef.current.addComment();
|
||||
}}
|
||||
getCommentCount={
|
||||
commentRef.current && commentRef.current.getCommentCount
|
||||
}
|
||||
/>
|
||||
{/* share popup */}
|
||||
<SharePopup
|
||||
|
||||
24
src/game_pages/detail/style.module.scss
Normal file
@@ -0,0 +1,24 @@
|
||||
.shareContainer {
|
||||
.title {
|
||||
padding: 20px;
|
||||
color: #000;
|
||||
text-align: center;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 18px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.shareItems {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 60px;
|
||||
|
||||
.button {
|
||||
width: 140px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import DistanceQuickFilter from "@/components/DistanceQuickFilter";
|
||||
import { withAuth } from "@/components";
|
||||
import { updateUserLocation } from "@/services/userService";
|
||||
// import ShareCardCanvas from "@/components/ShareCardCanvas";
|
||||
import { useDictionaryStore } from "@/store/dictionaryStore";
|
||||
|
||||
const ListPage = () => {
|
||||
|
||||
@@ -163,11 +164,11 @@ const ListPage = () => {
|
||||
Taro.stopPullDownRefresh();
|
||||
|
||||
// 显示刷新成功提示
|
||||
Taro.showToast({
|
||||
title: "刷新成功",
|
||||
icon: "success",
|
||||
duration: 1000,
|
||||
});
|
||||
// Taro.showToast({
|
||||
// title: "刷新成功",
|
||||
// icon: "success",
|
||||
// duration: 1000,
|
||||
// });
|
||||
} catch (error) {
|
||||
// 刷新失败时也停止动画
|
||||
Taro.stopPullDownRefresh();
|
||||
@@ -269,6 +270,19 @@ const ListPage = () => {
|
||||
// imageUrl: shareImagePath || ''
|
||||
// }
|
||||
// })
|
||||
// 初始化字典数据
|
||||
const initDictionaryData = async () => {
|
||||
try {
|
||||
const { fetchDictionary } = useDictionaryStore.getState();
|
||||
await fetchDictionary();
|
||||
} catch (error) {
|
||||
console.error("初始化字典数据失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
initDictionaryData()
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -10,7 +10,6 @@ const HomePage: React.FC = () => {
|
||||
useEffect(() => {
|
||||
const handleLoginRedirect = async () => {
|
||||
const login_status = check_login_status();
|
||||
|
||||
if (login_status) {
|
||||
try {
|
||||
// 先获取用户信息
|
||||
|
||||
@@ -159,6 +159,34 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.weather {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.weatherIcon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.temperature {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
font-feature-settings:
|
||||
"liga" off,
|
||||
"clig" off;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&Place {
|
||||
@@ -229,7 +257,34 @@
|
||||
|
||||
.gameInfoActions {
|
||||
min-height: 12px;
|
||||
padding: 0 12px;
|
||||
border-top: 0.5px solid rgba(0, 0, 0, 0.06);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
|
||||
& > .button {
|
||||
margin: 12px 0;
|
||||
padding: 4px 10px;
|
||||
height: 28px;
|
||||
border-radius: 999px;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.06);
|
||||
color: #000;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.23px;
|
||||
|
||||
&:first-child {
|
||||
background: #000;
|
||||
color: #fff;
|
||||
&.payNow {
|
||||
background-color: #ff3b30;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,6 +354,17 @@
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.orderNo {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.copy {
|
||||
color: #007AFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -432,3 +498,59 @@
|
||||
font-weight: 600;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.dialogFooter {
|
||||
// width: 100%;
|
||||
width: calc(100% + 1px);
|
||||
height: 44px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
position: absolute;
|
||||
// margin: 0 -24px -24px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
||||
border-bottom-left-radius: 16px;
|
||||
border-bottom-right-radius: 16px;
|
||||
overflow: hidden;
|
||||
|
||||
& > .cancel, & > .confirm {
|
||||
padding: 12px 10px;
|
||||
height: 44px;
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
// border: 0.5px solid rgba(0, 0, 0, 0.06);
|
||||
color: #000;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
|
||||
&:last-child {
|
||||
background: #000;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
& > .cancel {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
& > .confirm {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.cancelTip {
|
||||
padding: 12px 15px;
|
||||
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: 24px;
|
||||
}
|
||||
|
||||
@@ -1,31 +1,77 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useRef } from "react";
|
||||
import { View, Text, Button, Image } from "@tarojs/components";
|
||||
import { Dialog } from "@nutui/nutui-react-taro";
|
||||
import Taro, { useDidShow, useRouter } from "@tarojs/taro";
|
||||
import dayjs from "dayjs";
|
||||
import "dayjs/locale/zh-cn";
|
||||
import classnames from "classnames";
|
||||
import orderService, {
|
||||
CancelType,
|
||||
GameOrderRes,
|
||||
OrderStatus,
|
||||
refundTextMap,
|
||||
} from "@/services/orderService";
|
||||
import {
|
||||
payOrder,
|
||||
delay,
|
||||
calculateDistance,
|
||||
getCurrentLocation,
|
||||
getOrderStatus,
|
||||
generateOrderActions,
|
||||
} from "@/utils";
|
||||
import detailService, { GameData } from "@/services/detailService";
|
||||
import { withAuth } from "@/components";
|
||||
import { withAuth, RefundPopup } from "@/components";
|
||||
import img from "@/config/images";
|
||||
import { DECLAIMER } from "./config";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
dayjs.locale("zh-cn");
|
||||
|
||||
const gameNoticeMap = new Map([
|
||||
[
|
||||
"pending",
|
||||
{ title: "球局暂未开始", content: "球局开始前2小时,我们将通过短信通知你" },
|
||||
],
|
||||
[
|
||||
"pendinging",
|
||||
{
|
||||
title: "球局即将开始,请按时抵达球局",
|
||||
content: "球局开始前2小时,我们将通过短信通知你",
|
||||
},
|
||||
],
|
||||
["progress", { title: "球局已开始", content: "友谊第一,比赛第二" }],
|
||||
["finish", { title: "球局已结束", content: "" }],
|
||||
]);
|
||||
|
||||
function genGameNotice(order_status, start_time) {
|
||||
const startTime = dayjs(start_time);
|
||||
let key = "";
|
||||
if (order_status === OrderStatus.FINISHED) {
|
||||
key = "finish";
|
||||
}
|
||||
const leftHour = startTime.diff(dayjs(), "hour");
|
||||
const start = startTime.isBefore(dayjs());
|
||||
if (start) {
|
||||
key = "progress";
|
||||
} else if (leftHour > 2) {
|
||||
key = "pending";
|
||||
} else if (leftHour < 2) {
|
||||
key = "pendinging";
|
||||
}
|
||||
|
||||
return gameNoticeMap.get(key) || {};
|
||||
}
|
||||
|
||||
function GameInfo(props) {
|
||||
const { detail, currentLocation, orderDetail } = props;
|
||||
const { order_status } = orderDetail;
|
||||
const { latitude, longitude, location, location_name, start_time, end_time } =
|
||||
const { detail, currentLocation, orderDetail, init } = props;
|
||||
const { order_status, refund_status, amount } = orderDetail;
|
||||
const { latitude, longitude, location, location_name, start_time, end_time, weather } =
|
||||
detail || {};
|
||||
|
||||
const [{ iconDay, tempMax, tempMin }] = weather || [{}];
|
||||
|
||||
const refundRef = useRef(null);
|
||||
|
||||
const openMap = () => {
|
||||
Taro.openLocation({
|
||||
latitude, // 纬度(必填)
|
||||
@@ -52,16 +98,138 @@ function GameInfo(props) {
|
||||
const startDate = `${startMonth}月${startDay}日 ${theDayOfWeek}`;
|
||||
const gameRange = `${startTime.format("HH:mm")} - ${endTime.format("HH:mm")}`;
|
||||
|
||||
const orderStatus = getOrderStatus(orderDetail);
|
||||
|
||||
const gameNotice = genGameNotice(order_status, start_time);
|
||||
|
||||
function handleViewGame(gameId) {
|
||||
Taro.navigateTo({
|
||||
url: `/game_pages/detail/index?id=${gameId}&from=orderList`,
|
||||
});
|
||||
}
|
||||
|
||||
async function handleDeleteOrder(item) {
|
||||
const { order_id } = item;
|
||||
const onCancel = () => {
|
||||
Dialog.close("detailCancelOrder");
|
||||
};
|
||||
const onConfirm = async () => {
|
||||
try {
|
||||
const deleteRes = await orderService.deleteOrder({
|
||||
order_id,
|
||||
});
|
||||
if (deleteRes.code !== 0) {
|
||||
throw new Error(deleteRes.message);
|
||||
}
|
||||
Taro.showToast({
|
||||
title: "删除成功",
|
||||
icon: "none",
|
||||
});
|
||||
delay(2000);
|
||||
Taro.redirectTo({ url: "/order_pages/orderList/index" });
|
||||
} catch (e) {
|
||||
Taro.showToast({
|
||||
title: e.message,
|
||||
icon: "error",
|
||||
});
|
||||
} finally {
|
||||
Dialog.close("detailCancelOrder");
|
||||
}
|
||||
};
|
||||
Dialog.open("detailCancelOrder", {
|
||||
title: "确定删除订单吗?",
|
||||
content: (
|
||||
<View className={styles.cancelTip}>
|
||||
<Text>删除订单后,您将无法恢复订单。请确认是否继续取消?</Text>
|
||||
</View>
|
||||
),
|
||||
footer: (
|
||||
<View className={styles.dialogFooter}>
|
||||
<Button className={styles.cancel} onClick={onCancel}>
|
||||
取消
|
||||
</Button>
|
||||
<Button className={styles.confirm} type="primary" onClick={onConfirm}>
|
||||
确认
|
||||
</Button>
|
||||
</View>
|
||||
),
|
||||
onConfirm,
|
||||
onCancel,
|
||||
});
|
||||
}
|
||||
|
||||
async function handleCancelOrder(item) {
|
||||
const { order_no } = item;
|
||||
const onCancel = () => {
|
||||
Dialog.close("detailCancelOrder");
|
||||
};
|
||||
const onConfirm = async () => {
|
||||
try {
|
||||
const cancelRes = await orderService.cancelUnpaidOrder({
|
||||
order_no,
|
||||
cancel_reason: "用户主动取消",
|
||||
});
|
||||
if (cancelRes.code !== 0) {
|
||||
throw new Error(cancelRes.message);
|
||||
}
|
||||
init();
|
||||
Taro.showToast({
|
||||
title: "取消成功",
|
||||
icon: "none",
|
||||
});
|
||||
} catch (e) {
|
||||
Taro.showToast({
|
||||
title: e.message,
|
||||
icon: "error",
|
||||
});
|
||||
} finally {
|
||||
Dialog.close("detailCancelOrder");
|
||||
}
|
||||
};
|
||||
Dialog.open("detailCancelOrder", {
|
||||
title: "确定取消订单吗?",
|
||||
content: (
|
||||
<View className={styles.cancelTip}>
|
||||
<Text>取消订单后,您将无法恢复订单。请确认是否继续取消?</Text>
|
||||
</View>
|
||||
),
|
||||
footer: (
|
||||
<View className={styles.dialogFooter}>
|
||||
<Button className={styles.cancel} onClick={onCancel}>
|
||||
取消
|
||||
</Button>
|
||||
<Button className={styles.confirm} type="primary" onClick={onConfirm}>
|
||||
确认
|
||||
</Button>
|
||||
</View>
|
||||
),
|
||||
onConfirm,
|
||||
onCancel,
|
||||
});
|
||||
}
|
||||
|
||||
function handleQuit(item) {
|
||||
if (refundRef.current) {
|
||||
refundRef.current.show(item, (result) => {
|
||||
if (result) {
|
||||
init();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View className={styles.gameInfoContainer}>
|
||||
{Boolean(order_status) && order_status !== OrderStatus.PENDING && (
|
||||
<>
|
||||
<View className={styles.paidInfo}>已支付 ¥ 90</View>
|
||||
<View className={styles.gameStatus}>
|
||||
<Text className={styles.statusText}>球局暂未开始</Text>
|
||||
<Text>球局开始前2小时,我们将通过短信通知你</Text>
|
||||
{["refund", "progress", "expired"].includes(orderStatus) && (
|
||||
<View className={styles.paidInfo}>
|
||||
{refundTextMap.get(refund_status)} ¥ {amount}
|
||||
</View>
|
||||
)}
|
||||
{["progress", "expired"].includes(orderStatus) && (
|
||||
<View className={styles.gameStatus}>
|
||||
<Text className={styles.statusText}>{gameNotice.title}</Text>
|
||||
{gameNotice.content && <Text>{gameNotice.content}</Text>}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
<View className={styles.gameInfo}>
|
||||
{/* Date and Weather */}
|
||||
@@ -81,6 +249,21 @@ function GameInfo(props) {
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View className={styles.weather}>
|
||||
{/* Weather icon */}
|
||||
<View className={styles.weatherIcon}>
|
||||
{/*<Image className="weather-icon" src={img.ICON_WEATHER_SUN} />*/}
|
||||
<i className={`qi-${iconDay}`}></i>
|
||||
</View>
|
||||
{/* Weather text and temperature */}
|
||||
<View className={styles.temperature}>
|
||||
{tempMin && tempMax && (
|
||||
<Text>
|
||||
{tempMin}℃ - {tempMax}℃
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
{/* Place */}
|
||||
<View className={styles.gameInfoPlace}>
|
||||
@@ -122,13 +305,42 @@ function GameInfo(props) {
|
||||
</View>
|
||||
</View>
|
||||
{/* Action bar */}
|
||||
<View className={styles.gameInfoActions}></View>
|
||||
<View className={styles.gameInfoActions}>
|
||||
{orderDetail.order_id
|
||||
? generateOrderActions(
|
||||
orderDetail,
|
||||
{
|
||||
handleDeleteOrder,
|
||||
handleCancelOrder,
|
||||
handleQuit,
|
||||
handlePayNow: () => {},
|
||||
handleViewGame,
|
||||
},
|
||||
"detail"
|
||||
)?.map((obj) => (
|
||||
<Button
|
||||
className={classnames(styles.button, styles[obj.className])}
|
||||
onClick={obj.action}
|
||||
>
|
||||
{obj.text}
|
||||
</Button>
|
||||
))
|
||||
: ""}
|
||||
</View>
|
||||
<Dialog id="detailCancelOrder" />
|
||||
<RefundPopup ref={refundRef} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function handleCopy(msg) {
|
||||
Taro.setClipboardData({
|
||||
data: msg,
|
||||
});
|
||||
}
|
||||
|
||||
function OrderMsg(props) {
|
||||
const { detail, checkOrderInfo } = props;
|
||||
const { detail, orderDetail, checkOrderInfo } = props;
|
||||
const {
|
||||
start_time,
|
||||
end_time,
|
||||
@@ -137,7 +349,8 @@ function OrderMsg(props) {
|
||||
wechat_contact,
|
||||
price,
|
||||
} = detail;
|
||||
const { order_info: { registrant_nickname } = {} } = checkOrderInfo;
|
||||
const { order_no } = orderDetail;
|
||||
const { order_info: { registrant_phone } = {} } = checkOrderInfo;
|
||||
const startTime = dayjs(start_time);
|
||||
const endTime = dayjs(end_time);
|
||||
const startYear = startTime.format("YYYY");
|
||||
@@ -160,17 +373,39 @@ function OrderMsg(props) {
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "组织者昵称",
|
||||
content: registrant_nickname,
|
||||
title: "报名人电话",
|
||||
content: registrant_phone,
|
||||
},
|
||||
{
|
||||
title: "组织者电话",
|
||||
title: "组织人微信号",
|
||||
content: wechat_contact,
|
||||
},
|
||||
{
|
||||
title: "组织人电话",
|
||||
content: wechat_contact,
|
||||
},
|
||||
{
|
||||
title: "费用",
|
||||
content: `${price} 元 / 人`,
|
||||
},
|
||||
...(order_no
|
||||
? [
|
||||
{
|
||||
title: "订单号",
|
||||
content: (
|
||||
<View className={styles.orderNo}>
|
||||
<Text>{order_no}</Text>
|
||||
<Text
|
||||
className={styles.copy}
|
||||
onClick={handleCopy.bind(null, order_no)}
|
||||
>
|
||||
复制
|
||||
</Text>
|
||||
</View>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
return (
|
||||
<View className={styles.orderSummary}>
|
||||
@@ -199,14 +434,18 @@ function RefundPolicy(props) {
|
||||
rule: "退款规则",
|
||||
},
|
||||
...refund_policy.map((item, index) => {
|
||||
const [, theTime] = item.application_time.split("undefined ");
|
||||
const theTimeObj = dayjs(theTime);
|
||||
const isLast = index === refund_policy.length - 1;
|
||||
const theTimeObj = dayjs(
|
||||
isLast
|
||||
? refund_policy.at(-2).deadline_formatted
|
||||
: item.deadline_formatted
|
||||
);
|
||||
const year = theTimeObj.format("YYYY");
|
||||
const month = theTimeObj.format("M");
|
||||
const day = theTimeObj.format("D");
|
||||
const time = theTimeObj.format("HH:MM");
|
||||
const time = theTimeObj.format("HH:mm");
|
||||
return {
|
||||
time: `${year}年${month}月${day}日${time}${index === 0 ? "前" : "后"}`,
|
||||
time: `${year}年${month}月${day}日${time} ${isLast ? "后" : "前"}`,
|
||||
rule: item.refund_rule,
|
||||
};
|
||||
}),
|
||||
@@ -247,7 +486,11 @@ const OrderCheck = () => {
|
||||
const [checkOrderInfo, setCheckOrderInfo] = useState<GameOrderRes | {}>({});
|
||||
const [orderDetail, setOrderDetail] = useState({});
|
||||
|
||||
useDidShow(async () => {
|
||||
useDidShow(() => {
|
||||
init()
|
||||
});
|
||||
|
||||
async function init() {
|
||||
let gameDetail = {};
|
||||
if (id) {
|
||||
const res = await orderService.getOrderDetail(id);
|
||||
@@ -265,7 +508,7 @@ const OrderCheck = () => {
|
||||
setDetail(gameDetail);
|
||||
onInit(gameDetail.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function checkOrder(gid) {
|
||||
const orderRes = await orderService.getCheckOrderInfo(gid);
|
||||
@@ -297,24 +540,33 @@ const OrderCheck = () => {
|
||||
mask: true,
|
||||
});
|
||||
|
||||
let payment_params = {}
|
||||
try {
|
||||
const payment_params = await getPaymentParams();
|
||||
payment_params = await getPaymentParams();
|
||||
await payOrder(payment_params);
|
||||
Taro.hideLoading();
|
||||
Taro.showToast({
|
||||
title: "支付成功",
|
||||
icon: "success",
|
||||
});
|
||||
await delay(1000);
|
||||
Taro.navigateBack({
|
||||
delta: 1,
|
||||
});
|
||||
// Taro.navigateBack({
|
||||
// delta: 1,
|
||||
// });
|
||||
} catch (error) {
|
||||
Taro.hideLoading();
|
||||
Taro.showToast({
|
||||
title: error.message,
|
||||
icon: "none",
|
||||
});
|
||||
} finally {
|
||||
await delay(1000);
|
||||
if (!id) {
|
||||
Taro.redirectTo({
|
||||
url: `/order_pages/orderDetail/index?id=${payment_params.order_id}`,
|
||||
});
|
||||
} else {
|
||||
init()
|
||||
}
|
||||
}
|
||||
};
|
||||
if (!id && !gameId) {
|
||||
@@ -332,6 +584,9 @@ const OrderCheck = () => {
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const { order_status, cancel_type } = orderDetail;
|
||||
|
||||
return (
|
||||
<View className={styles.container}>
|
||||
{/* Game Date and Address */}
|
||||
@@ -339,16 +594,23 @@ const OrderCheck = () => {
|
||||
detail={detail}
|
||||
orderDetail={orderDetail}
|
||||
currentLocation={location}
|
||||
init={init}
|
||||
/>
|
||||
{/* Order message */}
|
||||
<OrderMsg detail={detail} checkOrderInfo={checkOrderInfo} />
|
||||
<OrderMsg
|
||||
detail={detail}
|
||||
orderDetail={orderDetail}
|
||||
checkOrderInfo={checkOrderInfo}
|
||||
/>
|
||||
{/* Refund policy */}
|
||||
<RefundPolicy checkOrderInfo={checkOrderInfo} />
|
||||
{/* Disclaimer */}
|
||||
<Disclaimer />
|
||||
{(!id || orderDetail.order_status === OrderStatus.PENDING) && (
|
||||
{(!id ||
|
||||
(order_status === OrderStatus.PENDING &&
|
||||
cancel_type === CancelType.NONE)) && (
|
||||
<Button className={styles.payButton} onClick={handlePay}>
|
||||
{orderDetail.order_status === OrderStatus.PENDING ? "继续" : "确认"}
|
||||
{order_status === OrderStatus.PENDING ? "继续" : "确认"}
|
||||
支付
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -1,42 +1,34 @@
|
||||
@use "~@/scss/images.scss" as img;
|
||||
|
||||
.container {
|
||||
padding: 12px;
|
||||
padding: 12px 12px 40px;
|
||||
background-color: #fafafa;
|
||||
min-height: 100vh;
|
||||
|
||||
.orderItem {
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
height: 222px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.list {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
background-color: #fff;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
box-shadow: 0 4px 36px 0 rgba(0, 0, 0, 0.06);
|
||||
margin-bottom: 12px;
|
||||
|
||||
.orderTitle {
|
||||
height: 18px;
|
||||
padding: 15px 15px 12px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
// .bg {
|
||||
// position: absolute;
|
||||
// left: 0;
|
||||
// top: 0;
|
||||
// width: 100%;
|
||||
// height: 100%;
|
||||
// background-color: #fafafa;
|
||||
// z-index: -1;
|
||||
// }
|
||||
|
||||
.endTips {
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.userInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 6px;
|
||||
|
||||
.avatar {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.nickName {
|
||||
display: contents;
|
||||
.nickNameText {
|
||||
color: #000;
|
||||
justify-content: center;
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
font-feature-settings:
|
||||
"liga" off,
|
||||
"clig" off;
|
||||
@@ -45,37 +37,122 @@
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
}
|
||||
.arrowRight {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
|
||||
.paidInfo {
|
||||
.orderItem {
|
||||
width: 100%;
|
||||
// height: 222px;
|
||||
background-color: #fff;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
box-shadow: 0 4px 36px 0 rgba(0, 0, 0, 0.06);
|
||||
margin-bottom: 12px;
|
||||
|
||||
// .orderTitle {
|
||||
// height: 18px;
|
||||
// padding: 15px 15px 12px;
|
||||
// border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
// justify-content: space-between;
|
||||
|
||||
// .userInfo {
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
// justify-content: flex-start;
|
||||
// gap: 6px;
|
||||
|
||||
// .avatar {
|
||||
// width: 16px;
|
||||
// height: 16px;
|
||||
// }
|
||||
|
||||
// .nickName {
|
||||
// display: contents;
|
||||
// .nickNameText {
|
||||
// color: #000;
|
||||
// font-feature-settings:
|
||||
// "liga" off,
|
||||
// "clig" off;
|
||||
// font-family: "PingFang SC";
|
||||
// font-size: 12px;
|
||||
// font-style: normal;
|
||||
// font-weight: 500;
|
||||
// line-height: 18px;
|
||||
// }
|
||||
// .arrowRight {
|
||||
// width: 8px;
|
||||
// height: 8px;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// .paidInfo {
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
// justify-content: flex-end;
|
||||
// gap: 8px;
|
||||
|
||||
// .payTime {
|
||||
// font-feature-settings:
|
||||
// "liga" off,
|
||||
// "clig" off;
|
||||
// font-family: "PingFang SC";
|
||||
// font-size: 12px;
|
||||
// font-style: normal;
|
||||
// font-weight: 400;
|
||||
// line-height: 18px;
|
||||
|
||||
// &.paid {
|
||||
// color: rgba(60, 60, 67, 0.6);
|
||||
// }
|
||||
|
||||
// &.pending {
|
||||
// color: #000;
|
||||
// }
|
||||
// }
|
||||
|
||||
// .payNum {
|
||||
// font-feature-settings:
|
||||
// "liga" off,
|
||||
// "clig" off;
|
||||
// font-family: "PingFang SC";
|
||||
// font-size: 12px;
|
||||
// font-style: normal;
|
||||
// font-weight: 600;
|
||||
// line-height: 18px;
|
||||
// &.paid {
|
||||
// color: #000;
|
||||
// }
|
||||
|
||||
// &.pending {
|
||||
// color: #ff3b30;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
.gameInfo {
|
||||
height: 122px;
|
||||
|
||||
.gameTitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
padding: 12px 15px 0;
|
||||
|
||||
.payTime {
|
||||
font-feature-settings:
|
||||
"liga" off,
|
||||
"clig" off;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
|
||||
&.paid {
|
||||
color: rgba(60, 60, 67, 0.6);
|
||||
}
|
||||
|
||||
&.pending {
|
||||
.title {
|
||||
overflow: hidden;
|
||||
color: #000;
|
||||
}
|
||||
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; /* 150% */
|
||||
}
|
||||
|
||||
.payNum {
|
||||
@@ -96,14 +173,96 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gameTime {
|
||||
padding: 6px 0 0 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
color: rgba(60, 60, 67, 0.60);
|
||||
font-feature-settings: 'liga' off, 'clig' off;
|
||||
text-overflow: ellipsis;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
}
|
||||
|
||||
.gameInfo {
|
||||
height: 122px;
|
||||
.address {
|
||||
padding: 6px 0 0 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 4px;
|
||||
|
||||
color: rgba(60, 60, 67, 0.60);
|
||||
font-feature-settings: 'liga' off, 'clig' off;
|
||||
text-overflow: ellipsis;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
}
|
||||
|
||||
.gameOtherInfo {
|
||||
padding: 8px 0 12px 15px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 4px;
|
||||
|
||||
.avatarCards {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
height: 20px;
|
||||
|
||||
.avatar {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
&+.avatar {
|
||||
margin-left: -10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.participantProgress, .levelReq, .playType {
|
||||
display: flex;
|
||||
height: 20px;
|
||||
padding: 0px 8px;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border-radius: 999px;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.16);
|
||||
background: #FFF;
|
||||
color: #000;
|
||||
font-feature-settings: 'liga' off, 'clig' off;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px; /* 181.818% */
|
||||
letter-spacing: -0.23px;
|
||||
}
|
||||
|
||||
.participantProgress {
|
||||
color: #c4c4c7;
|
||||
|
||||
.current {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.orderActions {
|
||||
height: 28px;
|
||||
min-height: 28px;
|
||||
padding: 12px 12px 15px;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
||||
|
||||
@@ -135,6 +294,9 @@
|
||||
&:last-child {
|
||||
background: #000;
|
||||
color: #fff;
|
||||
&.payNow {
|
||||
background-color: #ff3b30;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,76 +304,84 @@
|
||||
}
|
||||
|
||||
.payNow {
|
||||
background-color: #ff3b30;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.refundPolicy {
|
||||
.moduleTitle {
|
||||
.dialogFooter {
|
||||
// width: 100%;
|
||||
width: calc(100% + 1px);
|
||||
height: 44px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
padding: 15px 0 8px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
justify-content: flex-end;
|
||||
position: absolute;
|
||||
// margin: 0 -24px -24px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
||||
border-bottom-left-radius: 16px;
|
||||
border-bottom-right-radius: 16px;
|
||||
overflow: hidden;
|
||||
|
||||
& > .cancel, & > .confirm {
|
||||
padding: 12px 10px;
|
||||
height: 44px;
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
// border: 0.5px solid rgba(0, 0, 0, 0.06);
|
||||
color: #000;
|
||||
font-feature-settings:
|
||||
"liga" off,
|
||||
"clig" off;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 14px;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.23px;
|
||||
|
||||
&:last-child {
|
||||
background: #000;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.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);
|
||||
& > .cancel {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.policyItem {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
color: #000;
|
||||
& > .confirm {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.cancelTip {
|
||||
padding: 12px 15px;
|
||||
color: rgba(60, 60, 67, 0.60);
|
||||
text-align: center;
|
||||
font-feature-settings:
|
||||
"liga" off,
|
||||
"clig" off;
|
||||
font-feature-settings: 'liga' off, 'clig' off;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 12px;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
&:nth-child(1) {
|
||||
color: #000;
|
||||
text-align: center;
|
||||
font-feature-settings:
|
||||
"liga" off,
|
||||
"clig" off;
|
||||
.emptyNotice {
|
||||
height: 40vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
|
||||
.emptyTip {
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
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);
|
||||
}
|
||||
}
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,109 @@
|
||||
import React, { useState } from "react";
|
||||
import { View, Text, Button, Image } from "@tarojs/components";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { View, Text, Button, Image, ScrollView } from "@tarojs/components";
|
||||
import Taro, { useDidShow } from "@tarojs/taro";
|
||||
import { Avatar, Dialog } from "@nutui/nutui-react-taro";
|
||||
import dayjs from "dayjs";
|
||||
import "dayjs/locale/zh-cn";
|
||||
import classnames from "classnames";
|
||||
import orderService, { OrderStatus, CancelType } from "@/services/orderService";
|
||||
import { withAuth } from "@/components";
|
||||
import { payOrder } from "@/utils";
|
||||
import orderService, { OrderStatus, CancelType, refundTextMap } from "@/services/orderService";
|
||||
import { withAuth, RefundPopup } from "@/components";
|
||||
import { payOrder, generateOrderActions } from "@/utils";
|
||||
import emptyContent from "@/static/emptyStatus/publish-empty.png";
|
||||
import styles from "./index.module.scss";
|
||||
import orderListArrowRight from "@/static/order/orderListArrowRight.svg";
|
||||
|
||||
dayjs.locale("zh-cn");
|
||||
|
||||
const PAGESIZE = 100;
|
||||
|
||||
// 将·作为连接符插入到标签文本之间
|
||||
function insertDotInTags(tags: string[]) {
|
||||
return tags.join("-·-").split("-");
|
||||
}
|
||||
|
||||
const diffDayMap = new Map([
|
||||
[0, "今天"],
|
||||
[1, "明天"],
|
||||
[2, "后天"],
|
||||
]);
|
||||
|
||||
const DayOfWeekMap = new Map([
|
||||
[0, "周日"],
|
||||
[1, "周一"],
|
||||
[2, "周二"],
|
||||
[3, "周三"],
|
||||
[4, "周四"],
|
||||
[5, "周五"],
|
||||
[6, "周六"],
|
||||
]);
|
||||
|
||||
function generateTimeMsg(game_info) {
|
||||
const { start_time, end_time } = game_info;
|
||||
const startTime = dayjs(start_time);
|
||||
const endTime = dayjs(end_time);
|
||||
const diffDay = startTime.startOf("day").diff(dayjs().startOf("day"), "day");
|
||||
|
||||
const dayofWeek = startTime.day();
|
||||
const gameLength = `${endTime.diff(startTime, "hour")}小时`;
|
||||
return (
|
||||
<>
|
||||
<Text>
|
||||
{diffDay <= 2 && diffDay >= 0
|
||||
? diffDayMap.get(diffDay)
|
||||
: startTime.format("YYYY-MM-DD")}
|
||||
</Text>
|
||||
<Text>({DayOfWeekMap.get(dayofWeek)})</Text>
|
||||
<Text>{startTime.format("ah")}点</Text>
|
||||
<Text>{gameLength}</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const OrderList = () => {
|
||||
const [list, setList] = useState([]);
|
||||
const [list, setList] = useState<any[][]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const refundRef = useRef(null);
|
||||
|
||||
useDidShow(() => {
|
||||
getOrders();
|
||||
});
|
||||
const end = list.length * PAGESIZE >= total;
|
||||
|
||||
async function getOrders() {
|
||||
const res = await orderService.getOrderList();
|
||||
console.log(res);
|
||||
useEffect(() => {
|
||||
getOrders(1);
|
||||
}, []);
|
||||
|
||||
function addPageInfo(arr, page) {
|
||||
return arr.map((item) => ({ ...item, page }));
|
||||
}
|
||||
|
||||
// clear 是否清除当前页后面的数据(如果有的话,没有也不影响)
|
||||
async function getOrders(page, clear = true) {
|
||||
const res = await orderService.getOrderList({ page, pageSize: PAGESIZE });
|
||||
if (res.code === 0) {
|
||||
setList(res.data.rows);
|
||||
setTotal(res.data.count);
|
||||
setList((prev) => {
|
||||
const newList = [...prev];
|
||||
const index = page - 1;
|
||||
newList.splice(
|
||||
index,
|
||||
clear ? newList.length - index : 1,
|
||||
addPageInfo(res.data.rows, page)
|
||||
);
|
||||
return newList;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePayNow(gameId) {
|
||||
function handleFetchNext() {
|
||||
console.log("scroll");
|
||||
if (!end) {
|
||||
getOrders(list.length + 1);
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePayNow(item) {
|
||||
try {
|
||||
const unPaidRes = await orderService.getUnpaidOrder(gameId);
|
||||
const unPaidRes = await orderService.getUnpaidOrder(item.game_info?.id);
|
||||
if (unPaidRes.code === 0 && unPaidRes.data.has_unpaid_order) {
|
||||
await payOrder(unPaidRes.data.payment_params);
|
||||
getOrders();
|
||||
getOrders(item.page, false);
|
||||
} else {
|
||||
throw new Error("支付调用失败");
|
||||
}
|
||||
@@ -43,51 +116,72 @@ const OrderList = () => {
|
||||
}
|
||||
}
|
||||
|
||||
function renderCancelContent(checkOrderInfo) {
|
||||
const { refund_policy = [] } = checkOrderInfo;
|
||||
const policyList = [
|
||||
{
|
||||
time: "申请退款时间",
|
||||
rule: "退款规则",
|
||||
},
|
||||
...refund_policy.map((item, index) => {
|
||||
const [, theTime] = item.application_time.split("undefined ");
|
||||
const theTimeObj = dayjs(theTime);
|
||||
const year = theTimeObj.format("YYYY");
|
||||
const month = theTimeObj.format("M");
|
||||
const day = theTimeObj.format("D");
|
||||
const time = theTimeObj.format("HH:MM");
|
||||
return {
|
||||
time: `${year}年${month}月${day}日${time}${index === 0 ? "前" : "后"}`,
|
||||
rule: item.refund_rule,
|
||||
function handleViewGame(gameId) {
|
||||
if (!gameId) {
|
||||
Taro.showToast({ title: "球局未找到", icon: "error" });
|
||||
return;
|
||||
}
|
||||
Taro.navigateTo({
|
||||
url: `/game_pages/detail/index?id=${gameId}&from=orderList`,
|
||||
});
|
||||
}
|
||||
|
||||
async function handleDeleteOrder(item) {
|
||||
const { id: order_id } = item;
|
||||
// TODO:删除订单,刷新这一页,然后后面的全清除掉
|
||||
const onCancel = () => {
|
||||
Dialog.close("cancelOrder");
|
||||
};
|
||||
}),
|
||||
];
|
||||
return (
|
||||
<View className={styles.refundPolicy}>
|
||||
<View className={styles.moduleTitle}>
|
||||
<Text>退款政策</Text>
|
||||
const onConfirm = async () => {
|
||||
try {
|
||||
const deleteRes = await orderService.deleteOrder({
|
||||
order_id,
|
||||
});
|
||||
if (deleteRes.code !== 0) {
|
||||
throw new Error(deleteRes.message);
|
||||
}
|
||||
getOrders(item.page);
|
||||
Taro.showToast({
|
||||
title: "删除成功",
|
||||
icon: "none",
|
||||
});
|
||||
} catch (e) {
|
||||
Taro.showToast({
|
||||
title: e.message,
|
||||
icon: "error",
|
||||
});
|
||||
} finally {
|
||||
Dialog.close("cancelOrder");
|
||||
}
|
||||
};
|
||||
Dialog.open("cancelOrder", {
|
||||
title: "确定删除订单吗?",
|
||||
content: (
|
||||
<View className={styles.cancelTip}>
|
||||
<Text>删除订单后,您将无法恢复订单。请确认是否继续取消?</Text>
|
||||
</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>
|
||||
),
|
||||
footer: (
|
||||
<View className={styles.dialogFooter}>
|
||||
<Button className={styles.cancel} onClick={onCancel}>
|
||||
取消
|
||||
</Button>
|
||||
<Button className={styles.confirm} type="primary" onClick={onConfirm}>
|
||||
确认
|
||||
</Button>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
),
|
||||
onConfirm,
|
||||
onCancel,
|
||||
});
|
||||
}
|
||||
|
||||
async function handleCancelOrder(item) {
|
||||
const { order_no, order_status, game_info, amount } = item;
|
||||
if (order_status === OrderStatus.PENDING) {
|
||||
Dialog.open("cancelOrder", {
|
||||
title: "确定取消订单吗?",
|
||||
content: "取消订单后,您将无法恢复订单。请确认是否继续取消?",
|
||||
onConfirm: async () => {
|
||||
const { order_no } = item;
|
||||
const onCancel = () => {
|
||||
Dialog.close("cancelOrder");
|
||||
};
|
||||
const onConfirm = async () => {
|
||||
try {
|
||||
const cancelRes = await orderService.cancelUnpaidOrder({
|
||||
order_no,
|
||||
@@ -96,7 +190,11 @@ const OrderList = () => {
|
||||
if (cancelRes.code !== 0) {
|
||||
throw new Error(cancelRes.message);
|
||||
}
|
||||
getOrders();
|
||||
getOrders(item.page, false);
|
||||
Taro.showToast({
|
||||
title: "取消成功",
|
||||
icon: "none",
|
||||
});
|
||||
} catch (e) {
|
||||
Taro.showToast({
|
||||
title: e.message,
|
||||
@@ -105,42 +203,38 @@ const OrderList = () => {
|
||||
} finally {
|
||||
Dialog.close("cancelOrder");
|
||||
}
|
||||
},
|
||||
onCancel: () => {
|
||||
Dialog.close("cancelOrder");
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
const res = await orderService.getCheckOrderInfo(game_info.id);
|
||||
};
|
||||
Dialog.open("cancelOrder", {
|
||||
title: "确定取消订单吗?",
|
||||
content: renderCancelContent(res.data),
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
const refundRes = await orderService.applicateRefund({
|
||||
order_no,
|
||||
refund_amount: amount,
|
||||
refund_reason: "用户主动退款",
|
||||
content: (
|
||||
<View className={styles.cancelTip}>
|
||||
<Text>取消订单后,您将无法恢复订单。请确认是否继续取消?</Text>
|
||||
</View>
|
||||
),
|
||||
footer: (
|
||||
<View className={styles.dialogFooter}>
|
||||
<Button className={styles.cancel} onClick={onCancel}>
|
||||
取消
|
||||
</Button>
|
||||
<Button className={styles.confirm} type="primary" onClick={onConfirm}>
|
||||
确认
|
||||
</Button>
|
||||
</View>
|
||||
),
|
||||
onConfirm,
|
||||
onCancel,
|
||||
});
|
||||
if (refundRes.code !== 0) {
|
||||
throw new Error(refundRes.message);
|
||||
}
|
||||
getOrders();
|
||||
} catch (e) {
|
||||
Taro.showToast({
|
||||
title: e.message,
|
||||
icon: "error",
|
||||
});
|
||||
} finally {
|
||||
Dialog.close("cancelOrder");
|
||||
|
||||
function handleQuit(item) {
|
||||
if (refundRef.current) {
|
||||
refundRef.current.show(item, (result) => {
|
||||
if (result) {
|
||||
getOrders(item.page);
|
||||
}
|
||||
},
|
||||
onCancel: () => {
|
||||
Dialog.close("cancelOrder");
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleViewOrderDetail(orderId) {
|
||||
Taro.navigateTo({
|
||||
@@ -148,92 +242,164 @@ const OrderList = () => {
|
||||
});
|
||||
}
|
||||
|
||||
const flatList = list.flat()
|
||||
|
||||
return (
|
||||
<View className={styles.container}>
|
||||
{list.map((item) => {
|
||||
const unPay = item.order_status === OrderStatus.PENDING;
|
||||
const expired =
|
||||
item.order_status === OrderStatus.FINISHED ||
|
||||
[CancelType.TIMEOUT, CancelType.USER].includes(item.cancel_type);
|
||||
const expiredTime = dayjs(item.expire_time).isSame(dayjs(), "day")
|
||||
? dayjs(item.expire_time).format("HH:mm:ss")
|
||||
: dayjs(item.expire_time).format("YYYY-MM-DD HH:mm:ss");
|
||||
const showCancel =
|
||||
item.order_status !== OrderStatus.FINISHED &&
|
||||
<ScrollView
|
||||
scrollY
|
||||
scrollWithAnimation
|
||||
lowerThreshold={20}
|
||||
onScrollToLower={handleFetchNext}
|
||||
enhanced
|
||||
showScrollbar={false}
|
||||
className={styles.list}
|
||||
>
|
||||
{/* <View className={styles.bg} /> */}
|
||||
{flatList.map((item) => {
|
||||
const unPay =
|
||||
item.order_status === OrderStatus.PENDING &&
|
||||
item.cancel_type === CancelType.NONE;
|
||||
const { game_info } = item;
|
||||
|
||||
const {
|
||||
skill_level_max,
|
||||
skill_level_min,
|
||||
play_type,
|
||||
participants,
|
||||
location_name,
|
||||
current_players,
|
||||
max_players,
|
||||
court_type,
|
||||
} = game_info || {};
|
||||
|
||||
return (
|
||||
<View key={item.id} className={styles.orderItem}>
|
||||
<View className={styles.orderTitle}>
|
||||
<View className={styles.userInfo}>
|
||||
<Avatar
|
||||
className={styles.avatar}
|
||||
src="https://img.yzcdn.cn/vant/cat.jpeg"
|
||||
/>
|
||||
<View className={styles.nickName}>
|
||||
<Text className={styles.nickNameText}>Light</Text>
|
||||
<Image
|
||||
className={styles.arrowRight}
|
||||
src={orderListArrowRight}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
{expired ? (
|
||||
""
|
||||
) : (
|
||||
<View className={styles.paidInfo}>
|
||||
<Text
|
||||
className={classnames(
|
||||
styles.payTime,
|
||||
styles[unPay ? "pending" : "paid"],
|
||||
)}
|
||||
>
|
||||
{unPay
|
||||
? `请在 ${expiredTime} 前支付`
|
||||
: dayjs(item.pay_time).format("YYYY-MM-DD HH:mm:ss")}
|
||||
</Text>
|
||||
<Text
|
||||
className={classnames(
|
||||
styles.payNum,
|
||||
styles[unPay ? "pending" : "paid"],
|
||||
)}
|
||||
>
|
||||
{unPay ? "待支付" : "已支付"} ¥ {item.amount}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<View
|
||||
className={styles.gameInfo}
|
||||
onClick={handleViewOrderDetail.bind(null, item.id)}
|
||||
>
|
||||
{item?.game_info?.title}
|
||||
<View className={styles.gameTitle}>
|
||||
<View className={styles.title}>{item?.game_info?.title}</View>
|
||||
<View
|
||||
className={classnames(
|
||||
styles.payNum,
|
||||
styles[unPay ? "pending" : "paid"]
|
||||
)}
|
||||
>
|
||||
<Text>{unPay ? "待支付" : refundTextMap.get(item.refund_status)}</Text> ¥{" "}
|
||||
<Text>{item.amount}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className={styles.gameTime}>
|
||||
{generateTimeMsg(item.game_info || {})}
|
||||
</View>
|
||||
<View className={styles.address}>
|
||||
{insertDotInTags([location_name, court_type, "3.5km"]).map(
|
||||
(text, index) => (
|
||||
<Text key={index}>{text}</Text>
|
||||
)
|
||||
)}
|
||||
</View>
|
||||
<View className={styles.gameOtherInfo}>
|
||||
{participants?.length >= 0 ? (
|
||||
<View className={styles.avatarCards}>
|
||||
{
|
||||
/* participants */ [
|
||||
{
|
||||
user: {
|
||||
avatar_url: "https://img.yzcdn.cn/vant/cat.jpeg",
|
||||
id: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
user: {
|
||||
avatar_url: "https://img.yzcdn.cn/vant/cat.jpeg",
|
||||
id: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
user: {
|
||||
avatar_url: "https://img.yzcdn.cn/vant/cat.jpeg",
|
||||
id: 3,
|
||||
},
|
||||
},
|
||||
].map((participant) => {
|
||||
const {
|
||||
user: { avatar_url, id },
|
||||
} = participant;
|
||||
return (
|
||||
<Image
|
||||
className={styles.avatar}
|
||||
mode="aspectFill"
|
||||
key={id}
|
||||
src={avatar_url}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</View>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<View className={styles.participantProgress}>
|
||||
<Text className={styles.current}>
|
||||
报名人数 {current_players}
|
||||
</Text>
|
||||
<Text>/</Text>
|
||||
<Text>{max_players}</Text>
|
||||
</View>
|
||||
<View className={styles.levelReq}>
|
||||
{skill_level_max !== skill_level_min
|
||||
? `${skill_level_min || "-"} 至 ${skill_level_max || "-"}`
|
||||
: skill_level_min === 1
|
||||
? "无要求"
|
||||
: `${skill_level_min} 以上`}
|
||||
</View>
|
||||
<View className={styles.playType}>{play_type}</View>
|
||||
</View>
|
||||
</View>
|
||||
<View className={styles.orderActions}>
|
||||
<View className={styles.extraActions}></View>
|
||||
<View className={styles.mainActions}>
|
||||
{showCancel && (
|
||||
{generateOrderActions(
|
||||
item,
|
||||
{
|
||||
handleDeleteOrder,
|
||||
handleCancelOrder,
|
||||
handleQuit,
|
||||
handlePayNow,
|
||||
handleViewGame,
|
||||
},
|
||||
"list"
|
||||
)?.map((obj) => (
|
||||
<Button
|
||||
className={classnames(styles.button, styles.cancelOrder)}
|
||||
onClick={handleCancelOrder.bind(null, item)}
|
||||
>
|
||||
取消订单
|
||||
</Button>
|
||||
className={classnames(
|
||||
styles.button,
|
||||
styles[obj.className]
|
||||
)}
|
||||
{unPay && !expired && (
|
||||
<Button
|
||||
className={classnames(styles.button, styles.payNow)}
|
||||
onClick={handlePayNow.bind(null, item.game_info.id)}
|
||||
onClick={obj.action}
|
||||
>
|
||||
立即支付
|
||||
{obj.text}
|
||||
</Button>
|
||||
)}
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
{flatList.length > 0 && end && (
|
||||
<View className={styles.endTips}>已经到底了~</View>
|
||||
)}
|
||||
{flatList.length === 0 && (
|
||||
<View className={styles.emptyNotice}>
|
||||
<Image src={emptyContent} />
|
||||
<Text className={styles.emptyTip}>暂时没有订单</Text>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
<Dialog id="cancelOrder" />
|
||||
<RefundPopup ref={refundRef} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,12 +9,13 @@
|
||||
|
||||
// 导航栏
|
||||
.navbar {
|
||||
height: 100px;
|
||||
height: 56px;
|
||||
background: #FFFFFF;
|
||||
padding-top: 44px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.06);
|
||||
|
||||
.navbar-content {
|
||||
height: 56px;
|
||||
@@ -50,13 +51,13 @@
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
margin-bottom:100px;
|
||||
// margin-bottom:100px;
|
||||
background-color: none !important;
|
||||
|
||||
.message-list-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 12px;
|
||||
padding: 12px 12px 112px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ const Message = () => {
|
||||
</View>
|
||||
|
||||
{/* 消息列表 */}
|
||||
<ScrollView scrollY className="message-list">
|
||||
<ScrollView scrollY className="message-list" scrollWithAnimation enhanced showScrollbar={false} >
|
||||
{filteredMessages.length > 0 ? (
|
||||
<View className="message-list-content">
|
||||
{filteredMessages.map(renderMessageItem)}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: "NTRP 评测",
|
||||
// navigationBarTitleText: "NTRP 评测",
|
||||
// navigationBarBackgroundColor: '#FAFAFA',
|
||||
// navigationStyle: 'custom',
|
||||
navigationStyle: 'custom',
|
||||
enableShareAppMessage: true,
|
||||
});
|
||||
|
||||
@@ -62,3 +62,625 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 44px;
|
||||
padding: 46px 42px 0 10px;
|
||||
|
||||
.closeIcon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-right: auto;
|
||||
|
||||
.closeImg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
flex: 1;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.introContainer {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: radial-gradient(227.15% 100% at 50% 0%, #BFFFEF 0%, #FFF 36.58%), #FAFAFA;
|
||||
|
||||
.result {
|
||||
|
||||
.avatarWrap {
|
||||
width: 200px;
|
||||
height: 100px;
|
||||
padding: 30px 0 0 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
@include commonAvatarStyle(1);
|
||||
}
|
||||
|
||||
.tip {
|
||||
padding: 0 30px;
|
||||
|
||||
.tipImage {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.lastResult {
|
||||
margin: 40px 22px;
|
||||
display: flex;
|
||||
padding: 16px 20px 20px 20px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
background: #FFF;
|
||||
box-shadow: 0 8px 64px 0 rgba(0, 0, 0, 0.10);
|
||||
|
||||
.tipAndTime {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
font-feature-settings: 'liga' off, 'clig' off;
|
||||
font-family: "Noto Sans SC";
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.levelWrap {
|
||||
color: #000;
|
||||
font-feature-settings: 'liga' off, 'clig' off;
|
||||
text-overflow: ellipsis;
|
||||
font-family: "Noto Sans SC";
|
||||
font-size: 32px;
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
line-height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
|
||||
.level {
|
||||
color: #00E5AD;
|
||||
}
|
||||
}
|
||||
|
||||
.slogan {
|
||||
color: #000;
|
||||
font-family: "Noto Sans SC";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin: 0 22px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
.buttonWrap {
|
||||
width: 100%;
|
||||
height: 52px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
overflow: hidden;
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
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;
|
||||
|
||||
.arrowImage {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.guide {
|
||||
.tip {
|
||||
padding: 0 30px;
|
||||
|
||||
.tipImage {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.radar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.radarImage {
|
||||
width: 320px;
|
||||
transform: scale(1.8);
|
||||
}
|
||||
}
|
||||
|
||||
.desc {
|
||||
padding: 0 30px;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
font-family: "PingFang SC";
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin: 74px 22px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
.buttonWrap {
|
||||
width: 100%;
|
||||
height: 52px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
overflow: hidden;
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
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;
|
||||
|
||||
.arrowImage {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.testContainer {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: radial-gradient(227.15% 100% at 50% 0%, #BFFFEF 0%, #FFF 36.58%), #FAFAFA;
|
||||
|
||||
.bar {
|
||||
margin: 12px 20px 36px;
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
position: relative;
|
||||
|
||||
.progressBar {
|
||||
height: 8px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
border-radius: 999px;
|
||||
background-color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
.notice {
|
||||
padding: 0 20px 20px;
|
||||
color: #000;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 18px;
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.question {
|
||||
padding: 0 20px 48px;
|
||||
box-sizing: border-box;
|
||||
height: 502px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
|
||||
.content {
|
||||
color: #000;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 36px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
|
||||
.optionItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
display: flex;
|
||||
padding: 14px 20px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border-radius: 16px;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.12);
|
||||
background: #fff;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.optionText {
|
||||
color: #000;
|
||||
font-feature-settings: 'liga' off, 'clig' off;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.optionIcon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 22px;
|
||||
gap: 24px;
|
||||
|
||||
.next {
|
||||
width: 100%;
|
||||
height: 52px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
background: rgba(0, 0, 0, 0.20);
|
||||
box-shadow: 0 8px 64px 0 rgba(0, 0, 0, 0.10);
|
||||
overflow: hidden;
|
||||
|
||||
.nextBtn {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
background: rgba(0, 0, 0, 0.20);
|
||||
box-shadow: 0 8px 64px 0 rgba(0, 0, 0, 0.10);
|
||||
|
||||
.nextBtn {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.20);
|
||||
color: #fff;
|
||||
border-radius: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.prev {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
color: #000;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: normal;
|
||||
|
||||
.backIcon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.resultContainer {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: radial-gradient(227.15% 100% at 50% 0%, #BFFFEF 0%, #FFF 36.58%), #FAFAFA;
|
||||
|
||||
.card {
|
||||
margin: 10px 20px 0;
|
||||
|
||||
padding: 24px 28px 0;
|
||||
position: relative;
|
||||
display: flex;
|
||||
// height: px;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
border-radius: 26px;
|
||||
border: 4px solid #FFF;
|
||||
background: linear-gradient(180deg, #BFFFEF 0%, #F2FFFC 100%), #FFF;
|
||||
box-shadow: 0 8px 64px 0 rgba(0, 0, 0, 0.10);
|
||||
|
||||
.avatarWrap {
|
||||
padding-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
@include commonAvatarStyle(0.5);
|
||||
}
|
||||
|
||||
.desc {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
|
||||
.tip {
|
||||
color: #000;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.levelWrap {
|
||||
color: #000;
|
||||
font-feature-settings: 'liga' off, 'clig' off;
|
||||
text-overflow: ellipsis;
|
||||
font-family: "Noto Sans SC";
|
||||
font-size: 36px;
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
line-height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
|
||||
.level {
|
||||
color: #00E5AD;
|
||||
}
|
||||
}
|
||||
|
||||
.slogan {
|
||||
color: #000;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.retest {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 12px;
|
||||
display: flex;
|
||||
padding: 6px 10px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border-radius: 12px;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.12);
|
||||
background: #FFF;
|
||||
box-shadow: 0 8px 64px 0 rgba(0, 0, 0, 0.10);
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
font-feature-settings: 'liga' off, 'clig' off;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
|
||||
.re_actIcon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.updateTip {
|
||||
color: #000;
|
||||
font-feature-settings: 'liga' off, 'clig' off;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
|
||||
text-align: center;
|
||||
padding: 24px 0;
|
||||
|
||||
.grayTip {
|
||||
color: rgba(60, 60, 67, 0.60);
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
padding: 0 28px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
|
||||
.viewGame {
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.viewGameBtn {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
background: #000;
|
||||
color: #FFF;
|
||||
font-feature-settings: 'liga' off, 'clig' off;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.otherActions {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
|
||||
.share, .saveImage {
|
||||
width: 50%;
|
||||
height: 50px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
overflow: hidden;
|
||||
|
||||
.shareBtn, .saveImageBtn {
|
||||
background: #FFF;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
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;
|
||||
|
||||
.downloadIcon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.wechatIcon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,624 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { View, Text, Image, Button } from "@tarojs/components";
|
||||
import Taro, { useRouter } from "@tarojs/taro";
|
||||
import { withAuth } from "@/components";
|
||||
import evaluateService from "@/services/evaluateService";
|
||||
import { useUserActions } from "@/store/userStore";
|
||||
import { delay } from "@/utils";
|
||||
import { useState, useEffect, useRef, useId } from "react";
|
||||
import { View, Text, Image, Button, Canvas } from "@tarojs/components";
|
||||
import Taro, { useRouter, useShareAppMessage } from "@tarojs/taro";
|
||||
import dayjs from "dayjs";
|
||||
import classnames from "classnames";
|
||||
import { withAuth, RadarChart } from "@/components";
|
||||
import evaluateService, {
|
||||
LastTimeTestResult,
|
||||
Question,
|
||||
TestResultData,
|
||||
} from "@/services/evaluateService";
|
||||
import { useUserInfo, useUserActions } from "@/store/userStore";
|
||||
import { delay, getCurrentFullPath } from "@/utils";
|
||||
import CloseIcon from "@/static/ntrp/ntrp_close_icon.svg";
|
||||
import DocCopy from "@/static/ntrp/ntrp_doc_copy.svg";
|
||||
import ArrowRight from "@/static/ntrp/ntrp_arrow_right.svg";
|
||||
import ArrowBack from "@/static/ntrp/ntrp_arrow_back.svg";
|
||||
import CircleChecked from "@/static/ntrp/ntrp_circle_checked.svg";
|
||||
import CircleUnChecked from "@/static/ntrp/ntrp_circle_unchecked.svg";
|
||||
import WechatIcon from "@/static/ntrp/ntrp_wechat.svg";
|
||||
import DownloadIcon from "@/static/ntrp/ntrp_download.svg";
|
||||
import ReTestIcon from "@/static/ntrp/ntrp_re-action.svg";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
enum StageType {
|
||||
INTRO = "intro",
|
||||
TEST = "test",
|
||||
RESULT = "result",
|
||||
}
|
||||
|
||||
enum SourceType {
|
||||
DETAIL = 'detail',
|
||||
PUBLISH = 'publish',
|
||||
}
|
||||
|
||||
const sourceTypeToTextMap = new Map([
|
||||
[SourceType.DETAIL, '继续加入球局'],
|
||||
[SourceType.PUBLISH, '继续发布球局'],
|
||||
])
|
||||
|
||||
function adjustRadarLabels(
|
||||
source: [string, number][],
|
||||
topK: number = 4 // 默认挑前4个最长的标签保护
|
||||
): [string, number][] {
|
||||
if (source.length === 0) return source;
|
||||
|
||||
// 复制并按长度排序(降序)
|
||||
let sorted = [...source].sort((a, b) => b[0].length - a[0].length);
|
||||
|
||||
// 取出前 K 个最长标签
|
||||
let protectedLabels = sorted.slice(0, topK);
|
||||
// 其他标签(保持原始顺序,但排除掉 protected)
|
||||
let protectedSet = new Set(protectedLabels.map(([l]) => l));
|
||||
let others = source.filter(([l]) => !protectedSet.has(l));
|
||||
|
||||
let n = source.length;
|
||||
let result: ([string, number] | undefined)[] = new Array(n);
|
||||
|
||||
// 放首尾
|
||||
result[0] = protectedLabels.shift() || others.shift();
|
||||
result[n - 1] = protectedLabels.shift() || others.shift();
|
||||
|
||||
// 放中间(支持偶数两个位置)
|
||||
if (n % 2 === 0) {
|
||||
let mid1 = n / 2 - 1;
|
||||
let mid2 = n / 2;
|
||||
result[mid1] = protectedLabels.shift() || others.shift();
|
||||
result[mid2] = protectedLabels.shift() || others.shift();
|
||||
} else {
|
||||
let mid = Math.floor(n / 2);
|
||||
result[mid] = protectedLabels.shift() || others.shift();
|
||||
}
|
||||
|
||||
// 把剩余标签按顺序塞进空位
|
||||
let pool = [...protectedLabels, ...others];
|
||||
for (let i = 0; i < n; i++) {
|
||||
if (!result[i]) result[i] = pool.shift();
|
||||
}
|
||||
|
||||
return result as [string, number][];
|
||||
}
|
||||
|
||||
function CommonGuideBar(props) {
|
||||
const { title, confirm } = props;
|
||||
const { params } = useRouter();
|
||||
const { redirect } = params;
|
||||
|
||||
function handleClose() {
|
||||
//TODO: 二次确认
|
||||
if (confirm) {
|
||||
}
|
||||
Taro.redirectTo({
|
||||
url: redirect ? redirect : "/game_pages/list/index",
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<View className={styles.header}>
|
||||
<View className={styles.closeIcon} onClick={handleClose}>
|
||||
<Image className={styles.closeImg} src={CloseIcon} />
|
||||
</View>
|
||||
<View className={styles.title}>
|
||||
<Text>{title}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function Intro(props) {
|
||||
const { redirect } = props;
|
||||
const [ntrpData, setNtrpData] = useState<LastTimeTestResult>();
|
||||
const userInfo = useUserInfo();
|
||||
const { fetchUserInfo } = useUserActions();
|
||||
const [ready, setReady] = useState(false);
|
||||
|
||||
const { last_test_result: { ntrp_level, create_time, id } = {} } =
|
||||
ntrpData || {};
|
||||
const lastTestTime = dayjs(create_time).format("YYYY年M月D日");
|
||||
|
||||
useEffect(() => {
|
||||
getLastResult();
|
||||
}, []);
|
||||
|
||||
async function getLastResult() {
|
||||
const res = await evaluateService.getLastResult();
|
||||
if (res.code === 0) {
|
||||
setNtrpData(res.data);
|
||||
if (res.data.has_ntrp_level) {
|
||||
fetchUserInfo();
|
||||
}
|
||||
setReady(true);
|
||||
}
|
||||
}
|
||||
if (!ready) {
|
||||
return "";
|
||||
}
|
||||
|
||||
function handleNext(type) {
|
||||
Taro.redirectTo({
|
||||
url: `/other_pages/ntrp-evaluate/index?stage=${type}${
|
||||
type === StageType.RESULT ? `&id=${id}` : ""
|
||||
}${redirect ? `&redirect=${redirect}` : ""}`,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<View className={styles.introContainer}>
|
||||
<CommonGuideBar />
|
||||
{ntrpData?.has_ntrp_level ? (
|
||||
<View className={styles.result}>
|
||||
<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>
|
||||
{/* tip */}
|
||||
<View className={styles.tip}>
|
||||
<Image
|
||||
className={styles.tipImage}
|
||||
src="http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/b7cb47aa-b609-4112-899f-3fde02ed2431.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</View>
|
||||
<View className={styles.lastResult}>
|
||||
<View className={styles.tipAndTime}>
|
||||
<Text>上次测试结果</Text>
|
||||
<Text>{lastTestTime}</Text>
|
||||
</View>
|
||||
<View className={styles.levelWrap}>
|
||||
<Text>NTRP</Text>
|
||||
<Text className={styles.level}>{ntrp_level}</Text>
|
||||
</View>
|
||||
<View className={styles.slogan}>
|
||||
<Text>变线+网前,下一步就是赢比赛!</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className={styles.actions}>
|
||||
<View className={styles.buttonWrap}>
|
||||
<Button
|
||||
className={classnames(styles.button, styles.primary)}
|
||||
type="primary"
|
||||
onClick={() => handleNext(StageType.TEST)}
|
||||
>
|
||||
<Text>再次测试</Text>
|
||||
<Image className={styles.arrowImage} src={ArrowRight} />
|
||||
</Button>
|
||||
</View>
|
||||
<View className={styles.buttonWrap}>
|
||||
<Button
|
||||
className={styles.button}
|
||||
onClick={() => handleNext(StageType.RESULT)}
|
||||
>
|
||||
<Text>继续使用上次测试结果</Text>
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<View className={styles.guide}>
|
||||
{/* tip */}
|
||||
<View className={styles.tip}>
|
||||
<Image
|
||||
className={styles.tipImage}
|
||||
src="http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/b7cb47aa-b609-4112-899f-3fde02ed2431.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</View>
|
||||
{/* radar */}
|
||||
<View className={styles.radar}>
|
||||
<Image
|
||||
className={styles.radarImage}
|
||||
src="http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/a2e1b639-82a9-4ab8-b767-8605556eafcb.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</View>
|
||||
<View className={styles.desc}>
|
||||
<Text>
|
||||
NTRP(National Tennis Rating
|
||||
Program)是一种常用的网球水平分级系统,这不是绝对精准的“分数”,而是一个参考标准,能够帮助你更清晰地了解自己的网球水平,从而在训练、比赛或娱乐活动中找到「难度合适」的球友,避免过度碾压或被碾压。
|
||||
</Text>
|
||||
</View>
|
||||
<View className={styles.actions}>
|
||||
<View className={styles.buttonWrap}>
|
||||
<Button
|
||||
className={classnames(styles.button, styles.primary)}
|
||||
type="primary"
|
||||
onClick={() => handleNext(StageType.TEST)}
|
||||
>
|
||||
<Text>开始测试</Text>
|
||||
<Image className={styles.arrowImage} src={ArrowRight} />
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function Test(props) {
|
||||
const { redirect } = props;
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
const [index, setIndex] = useState(0);
|
||||
const [questions, setQuestions] = useState<
|
||||
(Question & { choosen: number })[]
|
||||
>([]);
|
||||
const startTimeRef = useRef<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
startTimeRef.current = Date.now();
|
||||
getQUestions();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setDisabled(questions[index]?.choosen === -1);
|
||||
}, [index, questions]);
|
||||
|
||||
async function getQUestions() {
|
||||
const res = await evaluateService.getQuestions();
|
||||
if (res.code === 0) {
|
||||
setQuestions(res.data.map((item) => ({ ...item, choosen: 3 })));
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelect(i) {
|
||||
setQuestions((prev) =>
|
||||
prev.map((item, pIndex) => ({
|
||||
...item,
|
||||
...(pIndex === index ? { choosen: i } : {}),
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
setDisabled(true);
|
||||
try {
|
||||
const res = await evaluateService.submit({
|
||||
answers: questions.map((item) => ({
|
||||
question_id: item.id,
|
||||
answer_index: item.choosen,
|
||||
})),
|
||||
test_duration: (Date.now() - startTimeRef.current) / 1000,
|
||||
});
|
||||
if (res.code === 0) {
|
||||
Taro.redirectTo({
|
||||
url: `/other_pages/ntrp-evaluate/index?stage=${StageType.RESULT}&id=${
|
||||
res.data.record_id
|
||||
}${redirect ? `&redirect=${redirect}` : ""}`,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
Taro.showToast({ title: e.message, icon: "error" });
|
||||
} finally {
|
||||
setDisabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handIndexChange(direction) {
|
||||
console.log(disabled, direction);
|
||||
if (disabled && direction > 0) {
|
||||
return;
|
||||
}
|
||||
if (index === questions.length - 1 && direction > 0) {
|
||||
handleSubmit();
|
||||
return;
|
||||
}
|
||||
setIndex((prev) => prev + direction);
|
||||
}
|
||||
|
||||
const question = questions[index];
|
||||
if (!question) {
|
||||
return "";
|
||||
}
|
||||
return (
|
||||
<View className={styles.testContainer}>
|
||||
<CommonGuideBar confirm title={`${index + 1} / ${questions.length}`} />
|
||||
<View className={styles.bar}>
|
||||
<View
|
||||
className={styles.progressBar}
|
||||
style={{ width: `${100 * ((index + 1) / questions.length)}%` }}
|
||||
/>
|
||||
</View>
|
||||
<View className={styles.notice}>
|
||||
<Text>根据近3个月实际表现勾选最符合项</Text>
|
||||
</View>
|
||||
<View className={styles.question}>
|
||||
<View className={styles.content}>{question.question_content}</View>
|
||||
<View className={styles.options}>
|
||||
{question.options.map((item, i) => {
|
||||
const checked = question.choosen === i;
|
||||
return (
|
||||
<View
|
||||
key={i}
|
||||
className={styles.optionItem}
|
||||
onClick={() => handleSelect(i)}
|
||||
>
|
||||
<View className={styles.optionText}>{item.text}</View>
|
||||
<View className={styles.optionIcon}>
|
||||
<Image
|
||||
className={styles.icon}
|
||||
src={checked ? CircleChecked : CircleUnChecked}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
<View className={styles.actions}>
|
||||
<View
|
||||
className={classnames(styles.next, disabled ? styles.disabled : "")}
|
||||
onClick={() => handIndexChange(1)}
|
||||
>
|
||||
<Button className={styles.nextBtn} type="primary">
|
||||
{index === questions.length - 1 ? "完成测试" : "继续"}
|
||||
</Button>
|
||||
</View>
|
||||
{index !== 0 && (
|
||||
<View className={styles.prev} onClick={() => handIndexChange(-1)}>
|
||||
<Image className={styles.backIcon} src={ArrowBack} />
|
||||
<Text>返回</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function Result(props) {
|
||||
const { params } = useRouter();
|
||||
const { id, type, redirect } = params;
|
||||
const userInfo = useUserInfo();
|
||||
const { fetchUserInfo } = useUserActions();
|
||||
const radarRef = useRef();
|
||||
|
||||
const [result, setResult] = useState<TestResultData>();
|
||||
const [radarData, setRadarData] = useState<
|
||||
[propName: string, prop: number][]
|
||||
>([]);
|
||||
|
||||
useEffect(() => {
|
||||
getResultById();
|
||||
fetchUserInfo();
|
||||
}, []);
|
||||
|
||||
async function getResultById() {
|
||||
const res = await evaluateService.getTestResult({ record_id: Number(id) });
|
||||
if (res.code === 0) {
|
||||
setResult(res.data);
|
||||
setRadarData(
|
||||
adjustRadarLabels(
|
||||
Object.entries(res.data.radar_data.abilities).map(([key, value]) => [
|
||||
key,
|
||||
value.current_score,
|
||||
])
|
||||
)
|
||||
);
|
||||
updateUserLevel(res.data.record_id, res.data.ntrp_level);
|
||||
}
|
||||
}
|
||||
|
||||
function updateUserLevel(record_id, ntrp_level) {
|
||||
try {
|
||||
evaluateService.updateNtrp({
|
||||
record_id,
|
||||
ntrp_level,
|
||||
update_type: "test_result",
|
||||
});
|
||||
} catch (e) {
|
||||
Taro.showToast({ title: e.message, icon: "none" });
|
||||
}
|
||||
}
|
||||
|
||||
function handleReTest() {
|
||||
Taro.redirectTo({
|
||||
url: `/other_pages/ntrp-evaluate/index?stage=${StageType.TEST}${
|
||||
redirect ? `&redirect=${redirect}` : ""
|
||||
}`,
|
||||
});
|
||||
}
|
||||
|
||||
function handleViewGames() {
|
||||
Taro.redirectTo({
|
||||
url: "/game_pages/list/index",
|
||||
});
|
||||
}
|
||||
|
||||
function handleGoon () {
|
||||
if (type) {
|
||||
Taro.redirectTo({ url: redirect })
|
||||
} else {
|
||||
handleViewGames()
|
||||
}
|
||||
}
|
||||
|
||||
async function genCardImage() {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const url = await radarRef.current.generateImage();
|
||||
const query = Taro.createSelectorQuery();
|
||||
query
|
||||
.select("#exportCanvas")
|
||||
.fields({ node: true, size: true })
|
||||
.exec((res2) => {
|
||||
const canvas = res2[0].node;
|
||||
const ctx = canvas.getContext("2d");
|
||||
const dpr = Taro.getSystemInfoSync().pixelRatio;
|
||||
const width = 300;
|
||||
const height = 400;
|
||||
canvas.width = width * dpr;
|
||||
canvas.height = height * dpr;
|
||||
ctx.scale(dpr, dpr);
|
||||
|
||||
// 背景
|
||||
ctx.fillStyle = "#e9fdf8";
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
// 标题文字
|
||||
ctx.fillStyle = "#000";
|
||||
ctx.font = "16px sans-serif";
|
||||
ctx.fillText("你的 NTRP 测试结果为", 20, 40);
|
||||
ctx.fillStyle = "#00E5AD";
|
||||
ctx.font = "bold 22px sans-serif";
|
||||
ctx.fillText(`NTRP ${result?.ntrp_level}`, 20, 70);
|
||||
|
||||
// 绘制雷达图
|
||||
const img = canvas.createImage();
|
||||
img.src = url;
|
||||
img.onload = () => {
|
||||
ctx.drawImage(img, 20, 100, 260, 260);
|
||||
|
||||
// 第三步:导出最终卡片
|
||||
Taro.canvasToTempFilePath({
|
||||
canvas,
|
||||
success: (res3) => {
|
||||
console.log("导出成功:", res3.tempFilePath);
|
||||
resolve(res3.tempFilePath);
|
||||
},
|
||||
});
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function handleSaveImage() {
|
||||
if (!userInfo.id) {
|
||||
return
|
||||
}
|
||||
const url = await genCardImage();
|
||||
Taro.saveImageToPhotosAlbum({ filePath: url });
|
||||
}
|
||||
|
||||
useShareAppMessage(async (res) => {
|
||||
const url = await genCardImage();
|
||||
console.log(res, "res");
|
||||
return {
|
||||
title: "分享",
|
||||
imageUrl: url,
|
||||
path: `/other_pages/ntrp-evaluate/index?stage=${StageType.INTRO}`,
|
||||
};
|
||||
});
|
||||
|
||||
function handleAuth () {
|
||||
if (userInfo.id) {
|
||||
return true
|
||||
}
|
||||
const currentPage = getCurrentFullPath()
|
||||
Taro.redirectTo({
|
||||
url: `/login_pages/index/index${
|
||||
currentPage ? `?redirect=${encodeURIComponent(currentPage)}` : ""
|
||||
}`,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<View className={styles.resultContainer}>
|
||||
<CommonGuideBar />
|
||||
<View className={styles.card}>
|
||||
<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.desc}>
|
||||
<View className={styles.tip}>
|
||||
<Text>你的 NTRP 测试结果为</Text>
|
||||
</View>
|
||||
<View className={styles.levelWrap}>
|
||||
<Text>NTRP</Text>
|
||||
<Text className={styles.level}>{result?.ntrp_level}</Text>
|
||||
</View>
|
||||
<View className={styles.slogan}>
|
||||
<Text>变线+网前,下一步就是赢比赛!</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View>
|
||||
<RadarChart ref={radarRef} data={radarData} />
|
||||
</View>
|
||||
<View className={styles.retest} onClick={handleReTest}>
|
||||
<Image className={styles.re_actIcon} src={ReTestIcon} />
|
||||
<Text>重新测试</Text>
|
||||
</View>
|
||||
</View>
|
||||
{userInfo.id ? (
|
||||
<View className={styles.updateTip}>
|
||||
<Text>你的 NTRP 水平已更新为 {result?.ntrp_level} </Text>
|
||||
<Text className={styles.grayTip}>(可在个人信息中修改)</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View className={styles.updateTip}>
|
||||
<Text>登录「有场」小程序,查看匹配你的球局</Text>
|
||||
</View>
|
||||
)}
|
||||
<View className={styles.actions}>
|
||||
<View className={styles.viewGame} onClick={handleGoon}>
|
||||
<Button className={styles.viewGameBtn}>{sourceTypeToTextMap.get(type) || '去看看球局'}</Button>
|
||||
</View>
|
||||
<View className={styles.otherActions}>
|
||||
<View className={styles.share}>
|
||||
<Button className={styles.shareBtn} openType={userInfo.id ? 'share' : undefined} onClick={handleAuth}>
|
||||
<Image className={styles.wechatIcon} src={WechatIcon} />
|
||||
<Text>邀请好友测试</Text>
|
||||
</Button>
|
||||
</View>
|
||||
<View className={styles.saveImage} onClick={handleSaveImage}>
|
||||
<Button className={styles.saveImageBtn}>
|
||||
<Image className={styles.downloadIcon} src={DownloadIcon} />
|
||||
<Text>保存图片</Text>
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<Canvas
|
||||
type="2d"
|
||||
id="exportCanvas"
|
||||
style={{
|
||||
width: "0px",
|
||||
height: "0px",
|
||||
position: "absolute",
|
||||
left: "-9999px",
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const ComponentsMap = {
|
||||
[StageType.INTRO]: Intro,
|
||||
[StageType.TEST]: Test,
|
||||
[StageType.RESULT]: Result,
|
||||
};
|
||||
|
||||
function NtrpEvaluate() {
|
||||
const { updateUserInfo } = useUserActions();
|
||||
const { params } = useRouter();
|
||||
const { redirect } = params;
|
||||
|
||||
useEffect(() => {
|
||||
evaluateService.getEvaluateQuestions().then((data) => {
|
||||
console.log(data);
|
||||
});
|
||||
}, []);
|
||||
const stage = params.stage as StageType;
|
||||
|
||||
async function handleUpdateNtrp() {
|
||||
await updateUserInfo({
|
||||
@@ -33,21 +635,9 @@ function NtrpEvaluate() {
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View className={styles.container}>
|
||||
<View className={styles.title}>NTRP评分</View>
|
||||
<View className={styles.content}>
|
||||
<Image
|
||||
className={styles.image}
|
||||
src="https://img.yzcdn.cn/vant/cat.jpeg"
|
||||
/>
|
||||
<Text className={styles.description}>您的NTRP评分是 4.0 分。</Text>
|
||||
</View>
|
||||
<Button className={styles.button} onClick={handleUpdateNtrp}>
|
||||
评测通过
|
||||
</Button>
|
||||
</View>
|
||||
);
|
||||
const Component = ComponentsMap[stage];
|
||||
|
||||
return <Component redirect={redirect} />;
|
||||
}
|
||||
|
||||
export default withAuth(NtrpEvaluate);
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, Textarea, Image } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { ConfigProvider, Loading, Popup, Toast } from '@nutui/nutui-react-taro'
|
||||
import styles from './index.module.scss'
|
||||
import uploadFiles from '@/services/uploadFiles'
|
||||
import publishService from '@/services/publishService'
|
||||
import { usePublishBallActions } from '@/store/publishBallStore'
|
||||
import { useKeyboardHeight } from '@/store/keyboardStore'
|
||||
import images from '@/config/images'
|
||||
|
||||
export interface AiImportPopupProps {
|
||||
visible: boolean
|
||||
onClose: () => void
|
||||
onManualPublish?: () => void
|
||||
}
|
||||
|
||||
const AiImportPopup: React.FC<AiImportPopupProps> = ({
|
||||
visible,
|
||||
onClose,
|
||||
onManualPublish,
|
||||
}) => {
|
||||
const [text, setText] = useState('')
|
||||
const [uploadFailCount, setUploadFailCount] = useState(0)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [uploadLoading, setUploadLoading] = useState(false)
|
||||
const maxFailCount = 3
|
||||
|
||||
// 获取 actions(在组件顶层调用 Hook)
|
||||
const { setPublishData } = usePublishBallActions()
|
||||
|
||||
// 使用全局键盘状态
|
||||
const { keyboardHeight, isKeyboardVisible, addListener, initializeKeyboardListener } = useKeyboardHeight()
|
||||
|
||||
const textIdentification = async (text: string) => {
|
||||
setLoading(true)
|
||||
const res = await publishService.extract_tennis_activity({text})
|
||||
const { data } = res
|
||||
if (data && data?.length > 0) {
|
||||
navigateToPublishBall(data)
|
||||
} else {
|
||||
Taro.showToast({
|
||||
title: '未识别到球局信息',
|
||||
icon: 'error'
|
||||
})
|
||||
setUploadFailCount(prev => prev + 1)
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const initAiPopup = () => {
|
||||
setText('')
|
||||
setUploadFailCount(0)
|
||||
setLoading(false)
|
||||
setUploadLoading(false)
|
||||
}
|
||||
const handlePasteAndRecognize = async () => {
|
||||
if (text) {
|
||||
textIdentification(text)
|
||||
} else {
|
||||
getClipboardData()
|
||||
}
|
||||
}
|
||||
const getClipboardData = async () => {
|
||||
try {
|
||||
const res = await Taro.getClipboardData()
|
||||
if (res.data && res.data.trim()) {
|
||||
setText(res.data)
|
||||
Toast.show('toast', {
|
||||
content: '有场读取了你的剪切板信息',
|
||||
duration: 2,
|
||||
wordBreak:'break-word'
|
||||
})
|
||||
textIdentification(res.data)
|
||||
// Taro.showToast({
|
||||
// title: '已读取你的剪切板信息',
|
||||
// icon: 'success',
|
||||
// duration: 2000
|
||||
// })
|
||||
} else {
|
||||
Taro.showToast({
|
||||
title: '剪切板为空,请手动输入',
|
||||
icon: 'none',
|
||||
duration: 2
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取剪切板失败:', error)
|
||||
Taro.showToast({
|
||||
title: '读取剪切板失败,请手动输入',
|
||||
icon: 'error',
|
||||
duration: 2
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const navigateToPublishBall = (data: any) => {
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
setPublishData(data)
|
||||
initAiPopup()
|
||||
onClose()
|
||||
Taro.navigateTo({
|
||||
url: '/publish_pages/publishBall/index?type=ai'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleTextChange = (e: any) => {
|
||||
setText(e.detail.value)
|
||||
}
|
||||
|
||||
// 使用全局键盘状态监听
|
||||
useEffect(() => {
|
||||
// 初始化全局键盘监听器
|
||||
initializeKeyboardListener()
|
||||
|
||||
// 添加本地监听器
|
||||
const removeListener = addListener((height, visible) => {
|
||||
console.log('AiImportPopup 收到键盘变化:', height, visible)
|
||||
})
|
||||
|
||||
return () => {
|
||||
removeListener()
|
||||
}
|
||||
}, [initializeKeyboardListener, addListener])
|
||||
|
||||
const handleImageRecognition = async () => {
|
||||
try {
|
||||
const res = await Taro.chooseMedia({
|
||||
count: 1,
|
||||
mediaType: ['image'],
|
||||
sourceType: ['album', 'camera'],
|
||||
camera: 'back'
|
||||
})
|
||||
|
||||
if (res.tempFiles && res.tempFiles.length > 0) {
|
||||
// 这里可以调用图片识别API
|
||||
setUploadLoading(false)
|
||||
setLoading(true)
|
||||
const res_upload = await uploadFiles.upload_oss_img(res.tempFiles[0].tempFilePath)
|
||||
const {ossPath} = res_upload;
|
||||
if (ossPath) {
|
||||
setUploadLoading(true)
|
||||
const publishData = await publishService.extract_tennis_activity_from_image({image_url: ossPath})
|
||||
const { data } = publishData
|
||||
if (data && data?.length > 0) {
|
||||
navigateToPublishBall(data)
|
||||
} else {
|
||||
Taro.showToast({
|
||||
title: '未识别到球局信息',
|
||||
icon: 'error'
|
||||
})
|
||||
setUploadFailCount(prev => prev + 1)
|
||||
setUploadLoading(false)
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('选择图片失败:', error)
|
||||
if (!(typeof error === 'object' && error.errMsg && error.errMsg.includes('fail cancel'))) {
|
||||
setUploadFailCount(prev => prev + 1)
|
||||
Taro.showToast({
|
||||
title: '上传失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const handleManualPublish = () => {
|
||||
if (onManualPublish) {
|
||||
onManualPublish()
|
||||
}
|
||||
onClose()
|
||||
}
|
||||
|
||||
const showManualButton = uploadFailCount >= maxFailCount
|
||||
|
||||
return (
|
||||
<Popup
|
||||
visible={visible}
|
||||
position="bottom"
|
||||
round={true}
|
||||
closeable={false}
|
||||
onClose={onClose}
|
||||
className={styles.aiImportPopup}
|
||||
style={{ paddingBottom: isKeyboardVisible ? `${keyboardHeight}px` : undefined }}
|
||||
>
|
||||
<View className={styles.popupContent}>
|
||||
{/* 头部 */}
|
||||
<View className={styles.header}>
|
||||
<View className={styles.titleContainer}>
|
||||
<Image src={images.ICON_IMPORTANT_BLACK} className={styles.lightningIcon} />
|
||||
<Text className={styles.title}>智能导入球局信息</Text>
|
||||
</View>
|
||||
<View className={styles.closeButton} onClick={onClose}>
|
||||
<Image src={images.ICON_CLOSE} className={styles.lightningIcon} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 文本域 */}
|
||||
<View className={styles.textAreaContainer}>
|
||||
<Textarea
|
||||
className={styles.textArea}
|
||||
value={text}
|
||||
onInput={handleTextChange}
|
||||
onFocus={() => {}}
|
||||
onBlur={() => {}}
|
||||
placeholder="在此「粘贴识别」或输入文本,智能拆分球局时间、费用、地点和其他信息,并帮你智能生成球局标题"
|
||||
maxlength={100}
|
||||
showConfirmBar={false}
|
||||
placeholderClass={styles.textArea_placeholder}
|
||||
autoHeight
|
||||
// 关闭系统自动上推,改为手动根据键盘高度加内边距
|
||||
adjustPosition={false}
|
||||
/>
|
||||
<View className={styles.charCount}>
|
||||
<Text className={styles.charCountText}>{text.length}/100</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 图片识别按钮 */}
|
||||
<View className={styles.imageRecognitionContainer}>
|
||||
<View className={`${styles.imageRecognitionButton} ${uploadLoading ? styles.uploadLoadingContainer : ''}`} onClick={handleImageRecognition}>
|
||||
{
|
||||
uploadLoading ? (<Image src={images.ICON_UPLOAD_SUCCESS} className={styles.cameraIcon} />) : (<Image src={images.ICON_UPLOAD_IMG} className={styles.cameraIcon} />)
|
||||
}
|
||||
<Text className={styles.imageRecognitionText}>图片识别</Text>
|
||||
<Text className={styles.imageRecognitionDesc}>{uploadLoading ? '已上传 1 张图片' : '支持订场截图/小红书笔记截图等图片'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 底部按钮 */}
|
||||
<View className={styles.bottomButtons}>
|
||||
{showManualButton && (
|
||||
<View className={styles.manualButton} onClick={handleManualPublish}>
|
||||
<Text className={styles.manualButtonText}>手动发布球局</Text>
|
||||
</View>
|
||||
)}
|
||||
<View className={styles.pasteButton} onClick={handlePasteAndRecognize}>
|
||||
{
|
||||
loading ? (
|
||||
<View className={styles.loadingContainer}>
|
||||
<ConfigProvider theme={{ nutuiLoadingIconColor: '#fff', nutuiLoadingIconSize: '20px' }}>
|
||||
<Loading type="circular" />
|
||||
</ConfigProvider>
|
||||
<Text className={styles.pasteButtonText}>识别中</Text>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<Image src={images.ICON_COPY} className={styles.clipboardIcon} />
|
||||
<Text className={styles.pasteButtonText}>粘贴并识别</Text>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<Toast id="toast" />
|
||||
</Popup>
|
||||
)
|
||||
}
|
||||
|
||||
export default AiImportPopup
|
||||
@@ -0,0 +1,201 @@
|
||||
@use '~@/scss/themeColor.scss' as theme;
|
||||
|
||||
.aiImportPopup {
|
||||
.popupContent {
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border-radius: 16px 16px 0 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
|
||||
.titleContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.lightningIcon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
}
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
box-sizing: border-box;
|
||||
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);
|
||||
.lightningIcon{
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.textAreaContainer {
|
||||
position: relative;
|
||||
padding: 0 16px 12px 16px;
|
||||
|
||||
.textArea {
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
padding: 12px;
|
||||
border: 1px solid #e5e6eb;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: #1f2329;
|
||||
background: #fff;
|
||||
box-sizing: border-box;
|
||||
resize: none;
|
||||
|
||||
.textArea_placeholder{
|
||||
color: rgba(60, 60, 67, 0.30);
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.charCount {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 32px;
|
||||
|
||||
.charCountText {
|
||||
font-size: 12px;
|
||||
color: #8a8a8a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.imageRecognitionContainer {
|
||||
padding: 0 20px;
|
||||
|
||||
.imageRecognitionButton {
|
||||
display: flex;
|
||||
height: 40px;
|
||||
padding: 2px 16px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
align-self: stretch;
|
||||
border-radius: 999px;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.16);
|
||||
background: #FFF;
|
||||
|
||||
.cameraIcon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.imageRecognitionText {
|
||||
font-size: 14px;
|
||||
color: #1f2329;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.imageRecognitionDesc {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
}
|
||||
.uploadLoadingContainer{
|
||||
border-radius: 999px;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.16);
|
||||
background: rgba(52, 199, 89, 0.10);
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.bottomButtons {
|
||||
padding: 28px 16px 16px 16px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding-bottom: calc(20px + env(safe-area-inset-bottom));
|
||||
|
||||
.manualButton {
|
||||
flex: 1;
|
||||
height: 44px;
|
||||
background: #f5f6f7;
|
||||
border: 1px solid #e5e6eb;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:active {
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
.manualButtonText {
|
||||
font-size: 14px;
|
||||
color: #1f2329;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.pasteButton {
|
||||
flex: 1;
|
||||
height: 44px;
|
||||
background: #000;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
.loadingContainer{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
}
|
||||
&:active {
|
||||
background: #333;
|
||||
}
|
||||
|
||||
.clipboardIcon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.pasteButtonText {
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.nut-toast-inner{
|
||||
max-width: 95%;
|
||||
width: auto;
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from './AiImportPopup'
|
||||
export type { AiImportPopupProps } from './AiImportPopup'
|
||||
@@ -50,9 +50,9 @@ const FormBasicInfo: React.FC<FormBasicInfoProps> = ({
|
||||
// 处理场馆选择
|
||||
const handleStadiumSelect = (stadium: Stadium | null) => {
|
||||
console.log(stadium,'stadiumstadium');
|
||||
const { address, name, latitude, longitude, court_type, court_surface, description, description_tag, venue_image_list} = stadium || {};
|
||||
const { address, name, venue_id, latitude, longitude, court_type, court_surface, description, description_tag, venue_image_list} = stadium || {};
|
||||
onChange({...value,
|
||||
venue_id: stadium?.id,
|
||||
venue_id,
|
||||
location_name: name,
|
||||
location: address,
|
||||
latitude,
|
||||
@@ -96,8 +96,8 @@ const FormBasicInfo: React.FC<FormBasicInfoProps> = ({
|
||||
onChange({...value, [key]: ''});
|
||||
return;
|
||||
}
|
||||
if (numValue < 0) {
|
||||
onChange({...value, [key]: '0'});
|
||||
if (numValue <= 0) {
|
||||
onChange({...value, [key]: '1'});
|
||||
return;
|
||||
}
|
||||
if (numValue > 9999.99) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { View, Text, Input, ScrollView, Image } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { Loading } from '@nutui/nutui-react-taro'
|
||||
import StadiumDetail, { StadiumDetailRef } from './StadiumDetail'
|
||||
import { CommonPopup } from '@/components'
|
||||
import { CommonPopup } from '../../../../components'
|
||||
import { getLocation } from '@/utils/locationUtils'
|
||||
import PublishService from '@/services/publishService'
|
||||
import images from '@/config/images'
|
||||
@@ -135,6 +135,12 @@ const SelectStadium: React.FC<SelectStadiumProps> = ({
|
||||
setSearchValue('')
|
||||
}
|
||||
|
||||
const handleDetailCancel = () => {
|
||||
setShowDetail(false)
|
||||
setSelectedStadium(null)
|
||||
setSearchValue('')
|
||||
}
|
||||
|
||||
const handleItemLocation = (stadium: Stadium) => {
|
||||
if (stadium.latitude && stadium.longitude) {
|
||||
Taro.openLocation({
|
||||
@@ -169,7 +175,7 @@ const SelectStadium: React.FC<SelectStadiumProps> = ({
|
||||
cancelText="返回"
|
||||
confirmText="确认"
|
||||
className="select-stadium-popup"
|
||||
onCancel={handleCancel}
|
||||
onCancel={handleDetailCancel}
|
||||
onConfirm={handleConfirm}
|
||||
position="bottom"
|
||||
round
|
||||
|
||||
@@ -10,6 +10,7 @@ import './StadiumDetail.scss'
|
||||
|
||||
export interface Stadium {
|
||||
id?: string
|
||||
venue_id?: string
|
||||
name: string
|
||||
address?: string
|
||||
longitude?: number
|
||||
@@ -108,7 +109,8 @@ const StadiumDetail = forwardRef<StadiumDetailRef, StadiumDetailProps>(({
|
||||
description:{
|
||||
description: '',
|
||||
description_tag: []
|
||||
}
|
||||
},
|
||||
venue_id: stadium.id
|
||||
})
|
||||
|
||||
// 暴露方法给父组件
|
||||
|
||||
@@ -1,50 +1,74 @@
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import React, { useCallback, useState, useEffect } from 'react'
|
||||
import { View, Text, Input } from '@tarojs/components'
|
||||
import { Checkbox } from '@nutui/nutui-react-taro'
|
||||
import styles from './index.module.scss'
|
||||
interface FormSwitchProps {
|
||||
value: boolean
|
||||
onChange: (checked: boolean) => void
|
||||
subTitle: string
|
||||
wechatId?: string
|
||||
|
||||
type WechatContactValue = {
|
||||
is_wechat_contact: boolean
|
||||
wechat_contact: string
|
||||
default_wechat_contact: string
|
||||
}
|
||||
|
||||
const FormSwitch: React.FC<FormSwitchProps> = ({ value, onChange, subTitle, wechatId }) => {
|
||||
interface FormSwitchProps {
|
||||
value: WechatContactValue
|
||||
wechatId: string
|
||||
onChange: (val: WechatContactValue) => void
|
||||
subTitle: string
|
||||
}
|
||||
|
||||
const FormSwitch: React.FC<FormSwitchProps> = ({ value, onChange, subTitle }) => {
|
||||
const [editWechat, setEditWechat] = useState(false)
|
||||
const [wechatIdValue, setWechatIdValue] = useState('')
|
||||
const [wechat, setWechat] = useState(wechatId)
|
||||
const [defaultWechat, setDefaultWechat] = useState('')
|
||||
const [isWechatContact, setIsWechatContact] = useState(false)
|
||||
const editWechatId = () => {
|
||||
setEditWechat(true)
|
||||
}
|
||||
const setWechatId = useCallback((e: any) => {
|
||||
const value = e.target.value
|
||||
onChange && onChange(value)
|
||||
setWechatIdValue(value)
|
||||
}, [onChange])
|
||||
const valueStr = e.target.value
|
||||
onChange && onChange({ is_wechat_contact: isWechatContact, wechat_contact: valueStr, default_wechat_contact: defaultWechat })
|
||||
setWechatIdValue(valueStr)
|
||||
}, [onChange, isWechatContact])
|
||||
|
||||
const fillWithPhone = () => {
|
||||
if (wechat) {
|
||||
setWechatIdValue(wechat)
|
||||
if (defaultWechat) {
|
||||
setWechatIdValue(defaultWechat)
|
||||
}
|
||||
}
|
||||
|
||||
const handleChange = (checked: boolean) => {
|
||||
setIsWechatContact(checked)
|
||||
onChange({ is_wechat_contact: checked, wechat_contact: wechatIdValue, default_wechat_contact: defaultWechat })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const { is_wechat_contact, default_wechat_contact } = value || {} as any
|
||||
if (is_wechat_contact) {
|
||||
setIsWechatContact(is_wechat_contact)
|
||||
}
|
||||
if (default_wechat_contact) {
|
||||
setDefaultWechat(default_wechat_contact)
|
||||
}
|
||||
}, [value])
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<View className={styles['wechat-contact-section']}>
|
||||
<View className={styles['wechat-contact-item']}>
|
||||
<Checkbox
|
||||
className={styles['wechat-contact-checkbox']}
|
||||
checked={value}
|
||||
onChange={onChange}
|
||||
checked={isWechatContact}
|
||||
onChange={(checked) => handleChange(checked)}
|
||||
/>
|
||||
<View className={styles['wechat-contact-content']}>
|
||||
<Text className={styles['wechat-contact-text']}>{subTitle}</Text>
|
||||
</View>
|
||||
</View>
|
||||
{
|
||||
!editWechat && wechatId && (
|
||||
!editWechat && (
|
||||
<View className={styles['wechat-contact-id']}>
|
||||
<Text className={styles['wechat-contact-text']}>微信号: {wechatId.replace(/(\d{3})(\d{4})(\d{4})/, '$1 $2 $3')}</Text>
|
||||
<Text className={styles['wechat-contact-text']}>微信号: {defaultWechat.replace(/(\d{3})(\d{4})(\d{4})/, '$1 $2 $3')}</Text>
|
||||
<View className={styles['wechat-contact-edit']} onClick={editWechatId}>修改</View>
|
||||
</View>
|
||||
)
|
||||
@@ -55,7 +79,9 @@ const FormSwitch: React.FC<FormSwitchProps> = ({ value, onChange, subTitle, wech
|
||||
<View className={styles['wechat-contact-edit-input']}>
|
||||
<Input value={wechatIdValue} onInput={setWechatId} placeholder='请输入正确微信号' />
|
||||
</View>
|
||||
<View className={styles['wechat-contact-edit']} onClick={fillWithPhone}>手机号填入:{wechat}</View>
|
||||
{defaultWechat && (
|
||||
<View className={styles['wechat-contact-edit']} onClick={fillWithPhone}>手机号填入:{defaultWechat}</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '约球规则',
|
||||
navigationBarBackgroundColor: '#ffffff',
|
||||
navigationBarTextStyle: 'black',
|
||||
backgroundColor: '#f5f5f5',
|
||||
enablePullDownRefresh: false,
|
||||
disableScroll: false
|
||||
})
|
||||
245
src/publish_pages/publishBall/footballRules/index.scss
Normal file
@@ -0,0 +1,245 @@
|
||||
// 条款页面样式
|
||||
.terms_page {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #FAFAFA;
|
||||
box-sizing: border-box;
|
||||
|
||||
}
|
||||
|
||||
|
||||
// 状态栏样式
|
||||
.status_bar {
|
||||
position: absolute;
|
||||
top: 21px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 33px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
z-index: 10;
|
||||
|
||||
.time {
|
||||
color: #000000;
|
||||
font-family: 'SF Pro';
|
||||
font-weight: 590;
|
||||
font-size: 17px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.status_icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
|
||||
.signal_icon,
|
||||
.wifi_icon,
|
||||
.battery_icon {
|
||||
width: 20px;
|
||||
height: 12px;
|
||||
background: #000000;
|
||||
border-radius: 2px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.signal_icon {
|
||||
width: 19px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.wifi_icon {
|
||||
width: 17px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.battery_icon {
|
||||
width: 27px;
|
||||
height: 13px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.35);
|
||||
background: #000000;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -3px;
|
||||
top: 4px;
|
||||
width: 1px;
|
||||
height: 4px;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border-radius: 0 1px 1px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导航栏样式
|
||||
.navigation_bar {
|
||||
position: absolute;
|
||||
top: 54px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 44px;
|
||||
background: #FFFFFF;
|
||||
border-radius: 44px 44px 0 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
|
||||
.nav_content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.back_button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
|
||||
.back_icon {
|
||||
width: 8px;
|
||||
height: 16px;
|
||||
background: #000000;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: #000000;
|
||||
transform: translateY(-50%) rotate(45deg);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: #000000;
|
||||
transform: translateY(-50%) rotate(-45deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page_title {
|
||||
font-family: 'PingFang SC';
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
color: #000000;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nav_placeholder {
|
||||
width: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 主要内容区域
|
||||
.main_content {
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
flex: 1;
|
||||
|
||||
box-sizing: border-box;
|
||||
|
||||
overflow-y: auto;
|
||||
|
||||
// 条款标题
|
||||
.terms_title {
|
||||
font-family: 'PingFang SC';
|
||||
font-weight: 600;
|
||||
font-size: 20px;
|
||||
line-height: 1.6em;
|
||||
text-align: center;
|
||||
color: #000000;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
// 条款简介
|
||||
.terms_intro {
|
||||
font-family: 'PingFang SC';
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 1.43em;
|
||||
color: #000000;
|
||||
margin-bottom: 24px;
|
||||
|
||||
}
|
||||
|
||||
// 条款详细内容
|
||||
.terms_content {
|
||||
font-family: 'PingFang SC';
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 1.43em;
|
||||
color: #000000;
|
||||
margin-bottom: 40px;
|
||||
white-space: pre-line;
|
||||
padding: 0px 24px;
|
||||
|
||||
.terms_first_line,
|
||||
span.terms_first_line {
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
// 底部按钮
|
||||
.bottom_actions {
|
||||
margin-bottom: 40px;
|
||||
|
||||
.agree_button {
|
||||
width: 100%;
|
||||
height: 52px;
|
||||
background: #07C160;
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
color: #FFFFFF;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
font-family: 'PingFang SC';
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 底部指示器
|
||||
.home_indicator {
|
||||
position: absolute;
|
||||
bottom: 21px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 140px;
|
||||
height: 5px;
|
||||
background: #000000;
|
||||
border-radius: 2.5px;
|
||||
z-index: 10;
|
||||
}
|
||||
40
src/publish_pages/publishBall/footballRules/index.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { View, ScrollView } from '@tarojs/components';
|
||||
import './index.scss';
|
||||
|
||||
const footballRules: React.FC = () => {
|
||||
// 获取页面参数
|
||||
|
||||
const [termsContent, setTermsContent] = React.useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setTermsContent(`<span class="terms_first_line">欢迎使用本平台(以下简称"本平台")的微信绑定服务。为保障您的权益,请您务必仔细阅读并理解以下协议内容。</span>
|
||||
|
||||
一、绑定服务说明
|
||||
1. 本平台提供微信账号绑定服务,用户可通过微信快捷登录方式使用平台功能。
|
||||
2. 绑定微信账号后,用户可使用微信登录、微信支付、微信分享等功能。
|
||||
3. 本平台承诺保护用户微信账号信息安全,不会泄露给第三方。`)
|
||||
}, [])
|
||||
return (
|
||||
<View className="terms_page">
|
||||
{/* 主要内容 */}
|
||||
<ScrollView className="main_content" scrollY>
|
||||
{/* 条款标题 */}
|
||||
<View className="terms_title">
|
||||
约球规则
|
||||
</View>
|
||||
|
||||
{/* 条款详细内容 */}
|
||||
<View
|
||||
className="terms_content"
|
||||
dangerouslySetInnerHTML={{ __html: termsContent }}
|
||||
/>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default footballRules;
|
||||
@@ -1,4 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '发布',
|
||||
navigationBarBackgroundColor: '#FAFAFA'
|
||||
navigationStyle: 'custom'
|
||||
})
|
||||
@@ -1,14 +1,25 @@
|
||||
@use '~@/scss/themeColor.scss' as theme;
|
||||
|
||||
.publish-ball-container{
|
||||
position: relative;
|
||||
&.publish-ball-container-keyboard{
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 9999;
|
||||
}
|
||||
}
|
||||
.publish-ball {
|
||||
padding-top: 0;
|
||||
min-height: 100vh;
|
||||
background: theme.$page-background-color;
|
||||
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
&__scroll {
|
||||
height: calc(100vh - 120px);
|
||||
overflow: auto;
|
||||
padding: 4px 16px 72px 16px;
|
||||
padding: 4px 16px 20px 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -166,12 +177,8 @@
|
||||
|
||||
// 提交区域
|
||||
.submit-section {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 16px;
|
||||
|
||||
padding: 16px;
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
color: white;
|
||||
@@ -207,6 +214,8 @@
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
:global(.nut-icon-Checked){
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
background: rgba(22, 24, 35, 0.75)!important;
|
||||
}
|
||||
}
|
||||
@@ -255,3 +264,12 @@
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.publish-ball-navbar{
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
z-index: 9999 !important;
|
||||
width: 100% !important;
|
||||
box-shadow: none!important;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import { View, Text, Button, Image } from '@tarojs/components'
|
||||
import { Checkbox } from '@nutui/nutui-react-taro'
|
||||
import dayjs from 'dayjs'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { type ActivityType } from '../../components/ActivityTypeSwitch'
|
||||
import CommonDialog from '../../components/CommonDialog'
|
||||
@@ -10,9 +11,14 @@ import { FormFieldConfig, publishBallFormSchema } from '../../config/formSchema/
|
||||
import { PublishBallFormData } from '../../../types/publishBall';
|
||||
import PublishService from '@/services/publishService';
|
||||
import { getNextHourTime, getEndTime, delay } from '@/utils';
|
||||
import { useGlobalState } from "@/store/global"
|
||||
import GeneralNavbar from "@/components/GeneralNavbar"
|
||||
import images from '@/config/images'
|
||||
import { useUserInfo } from '@/store/userStore'
|
||||
import styles from './index.module.scss'
|
||||
import dayjs from 'dayjs'
|
||||
import { usePublishBallData } from '@/store/publishBallStore'
|
||||
import { useKeyboardHeight } from '@/store/keyboardStore'
|
||||
import DetailService from "@/services/detailService";
|
||||
|
||||
const defaultFormData: PublishBallFormData = {
|
||||
title: '',
|
||||
@@ -35,26 +41,41 @@ const defaultFormData: PublishBallFormData = {
|
||||
venue_description: '',
|
||||
venue_image_list: [],
|
||||
},
|
||||
players: [1, 1],
|
||||
players: {
|
||||
min: 1,
|
||||
max: 1,
|
||||
organizer_joined: true
|
||||
},
|
||||
skill_level: [1.0, 5.0],
|
||||
descriptionInfo: {
|
||||
description: '',
|
||||
description_tag: [],
|
||||
},
|
||||
is_substitute_supported: true,
|
||||
wechat: {
|
||||
is_wechat_contact: true,
|
||||
wechat_contact: '14223332214'
|
||||
wechat_contact: '',
|
||||
default_wechat_contact: ''
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const PublishBall: React.FC = () => {
|
||||
const [activityType, setActivityType] = useState<ActivityType>('individual')
|
||||
const [isSubmitDisabled, setIsSubmitDisabled] = useState(false)
|
||||
const userInfo = useUserInfo();
|
||||
const publishAiData = usePublishBallData()
|
||||
const { statusNavbarHeightInfo } = useGlobalState();
|
||||
|
||||
// 使用全局键盘状态
|
||||
const { keyboardHeight, isKeyboardVisible, addListener, initializeKeyboardListener } = useKeyboardHeight()
|
||||
// 获取页面参数并设置导航标题
|
||||
const [optionsConfig, setOptionsConfig] = useState<FormFieldConfig[]>(publishBallFormSchema)
|
||||
const [formData, setFormData] = useState<PublishBallFormData[]>([
|
||||
defaultFormData
|
||||
])
|
||||
console.log(userInfo, 'userInfo');
|
||||
const [formData, setFormData] = useState<PublishBallFormData[]>([defaultFormData])
|
||||
const [checked, setChecked] = useState(true)
|
||||
const [titleBar, setTitleBar] = useState('发布')
|
||||
const scrollViewRef = useRef<any>(null)
|
||||
|
||||
// 删除确认弹窗状态
|
||||
const [deleteConfirm, setDeleteConfirm] = useState<{
|
||||
@@ -77,14 +98,6 @@ const PublishBall: React.FC = () => {
|
||||
}
|
||||
|
||||
|
||||
// 处理活动类型变化
|
||||
const handleActivityTypeChange = (type: ActivityType) => {
|
||||
if (type === 'group') {
|
||||
setFormData([defaultFormData])
|
||||
} else {
|
||||
setFormData([defaultFormData])
|
||||
}
|
||||
}
|
||||
|
||||
// 检查相邻两组数据是否相同
|
||||
const checkAdjacentDataSame = (formDataArray: PublishBallFormData[]) => {
|
||||
@@ -165,9 +178,11 @@ const PublishBall: React.FC = () => {
|
||||
}
|
||||
|
||||
const validateFormData = (formData: PublishBallFormData, isOnSubmit: boolean = false) => {
|
||||
const { activityInfo, image_list, title, timeRange } = formData;
|
||||
const { activityInfo, title, timeRange, image_list, players, current_players } = formData;
|
||||
const { play_type, price, location_name } = activityInfo;
|
||||
if (!image_list?.length) {
|
||||
|
||||
const { max } = players;
|
||||
if (!image_list?.length && activityType === 'group') {
|
||||
if (!isOnSubmit) {
|
||||
Taro.showToast({
|
||||
title: `请上传活动封面`,
|
||||
@@ -216,6 +231,7 @@ const PublishBall: React.FC = () => {
|
||||
if (timeRange?.start_time && timeRange?.end_time) {
|
||||
const start = dayjs(timeRange.start_time)
|
||||
const end = dayjs(timeRange.end_time)
|
||||
const currentTime = dayjs()
|
||||
if (!end.isAfter(start)) {
|
||||
if (!isOnSubmit) {
|
||||
Taro.showToast({
|
||||
@@ -234,6 +250,33 @@ const PublishBall: React.FC = () => {
|
||||
}
|
||||
return false
|
||||
}
|
||||
if (start.isBefore(currentTime)) {
|
||||
if (!isOnSubmit) {
|
||||
Taro.showToast({
|
||||
title: `开始时间需晚于当前时间`,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
return false
|
||||
}
|
||||
if (end.isBefore(currentTime)) {
|
||||
if (!isOnSubmit) {
|
||||
Taro.showToast({
|
||||
title: `结束时间需晚于当前时间`,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (current_players && (current_players > max)) {
|
||||
if (!isOnSubmit) {
|
||||
Taro.showToast({
|
||||
title: `最大人数不能小于当前参与人数${current_players}`,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
@@ -246,39 +289,54 @@ const PublishBall: React.FC = () => {
|
||||
return true
|
||||
}
|
||||
|
||||
const getParams = () => {
|
||||
const currentInstance = Taro.getCurrentInstance()
|
||||
const params = currentInstance.router?.params
|
||||
return params
|
||||
}
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
// 基础验证
|
||||
console.log(formData, 'formData');
|
||||
const params = getParams()
|
||||
const { republish } = params || {};
|
||||
if (activityType === 'individual') {
|
||||
const isValid = validateFormData(formData[0])
|
||||
if (!isValid) {
|
||||
return
|
||||
}
|
||||
const { activityInfo, descriptionInfo, timeRange, players, skill_level,image_list, ...rest } = formData[0];
|
||||
const { activityInfo, descriptionInfo,is_substitute_supported, timeRange, players, skill_level, image_list, wechat, id, ...rest } = formData[0];
|
||||
const { min, max, organizer_joined } = players;
|
||||
const options = {
|
||||
...rest,
|
||||
...activityInfo,
|
||||
...descriptionInfo,
|
||||
...timeRange,
|
||||
max_players: players[1],
|
||||
current_players: players[0],
|
||||
max_players: max,
|
||||
min_players: min,
|
||||
organizer_joined: organizer_joined === true ? 1 : 0,
|
||||
skill_level_min: skill_level[0],
|
||||
skill_level_max: skill_level[1],
|
||||
image_list: image_list.map(item => item.url)
|
||||
image_list: image_list.map(item => item.url),
|
||||
is_wechat_contact: wechat.is_wechat_contact ? 1 : 0,
|
||||
wechat_contact: wechat.wechat_contact || wechat.default_wechat_contact,
|
||||
is_substitute_supported: is_substitute_supported ? '1' : '0',
|
||||
...(republish === '0' ? { id } : {}),
|
||||
}
|
||||
const res = await PublishService.createPersonal(options);
|
||||
const res = republish === '0' ? await PublishService.gamesUpdate(options) : await PublishService.createPersonal(options);
|
||||
const successText = republish === '0' ? '更新成功' : '发布成功';
|
||||
if (res.code === 0 && res.data) {
|
||||
Taro.showToast({
|
||||
title: '发布成功',
|
||||
title: successText,
|
||||
icon: 'success'
|
||||
})
|
||||
delay(1000)
|
||||
// 如果是个人球局,则跳转到详情页,并自动分享
|
||||
// 如果是畅打,则跳转第一个球局详情页,并自动分享 @刘杰
|
||||
const id = (res as any).data?.id;
|
||||
Taro.navigateTo({
|
||||
// @ts-expect-error: id
|
||||
url: `/pages/detail/index?id=${res.data.id || 1}&from=publish&autoShare=1`
|
||||
url: `/game_pages/detail/index?id=${id || 1}&from=publish&autoShare=1`
|
||||
})
|
||||
} else {
|
||||
Taro.showToast({
|
||||
@@ -300,31 +358,37 @@ const PublishBall: React.FC = () => {
|
||||
return
|
||||
}
|
||||
const options = formData.map((item) => {
|
||||
const { activityInfo, descriptionInfo, timeRange, players, skill_level, ...rest } = item;
|
||||
const { activityInfo, descriptionInfo, timeRange, players, skill_level, is_substitute_supported, id, ...rest } = item;
|
||||
const { min, max, organizer_joined } = players;
|
||||
return {
|
||||
...rest,
|
||||
...activityInfo,
|
||||
...descriptionInfo,
|
||||
...timeRange,
|
||||
max_players: players[1],
|
||||
current_players: players[0],
|
||||
max_players: max,
|
||||
min_players: min,
|
||||
organizer_joined: organizer_joined === true ? 1 : 0,
|
||||
skill_level_min: skill_level[0],
|
||||
skill_level_max: skill_level[1],
|
||||
image_list: item.image_list.map(img => img.url)
|
||||
is_substitute_supported: is_substitute_supported ? '1' : '0',
|
||||
image_list: item.image_list.map(img => img.url),
|
||||
...(republish === '0' ? { id } : {}),
|
||||
}
|
||||
})
|
||||
const res = await PublishService.create_play_pmoothlys({rows: options});
|
||||
const successText = republish === '0' ? '更新成功' : '发布成功';
|
||||
const res = republish === '0' ? await PublishService.gamesUpdate(options[0]) : await PublishService.create_play_pmoothlys({rows: options});
|
||||
if (res.code === 0 && res.data) {
|
||||
Taro.showToast({
|
||||
title: '发布成功',
|
||||
title: successText,
|
||||
icon: 'success'
|
||||
})
|
||||
delay(1000)
|
||||
// 如果是个人球局,则跳转到详情页,并自动分享
|
||||
// 如果是畅打,则跳转第一个球局详情页,并自动分享 @刘杰
|
||||
const id = republish === '0' ? (res as any).data?.id : (res as any).data?.[0]?.id;
|
||||
Taro.navigateTo({
|
||||
// @ts-expect-error: id
|
||||
url: `/pages/detail/index?id=${res.data?.[0].id || 1}&from=publish&autoShare=1`
|
||||
url: `/game_pages/detail/index?id=${id || 1}&from=publish&autoShare=1`
|
||||
})
|
||||
} else {
|
||||
Taro.showToast({
|
||||
@@ -335,16 +399,77 @@ const PublishBall: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const initFormData = () => {
|
||||
const currentInstance = Taro.getCurrentInstance()
|
||||
const params = currentInstance.router?.params
|
||||
if (params?.type) {
|
||||
const type = params.type as ActivityType
|
||||
if (type === 'individual' || type === 'group') {
|
||||
setActivityType(type)
|
||||
if (type === 'group') {
|
||||
const mergeWithDefault = (data: any, isDetail: boolean = false): PublishBallFormData => {
|
||||
// ai导入与详情数据处理
|
||||
const { start_time, end_time, play_type, price, venue_id, location_name, location, latitude,
|
||||
longitude, court_type, court_surface, venue_description_tag, venue_description, venue_image_list,
|
||||
description, description_tag, max_players, min_players, skill_level_max, skill_level_min,
|
||||
venueDtl, wechat_contact, image_list, id: publish_id, is_wechat_contact,
|
||||
is_substitute_supported, title, current_players, organizer_joined
|
||||
} = data;
|
||||
const level_max = skill_level_max ? Number(skill_level_max) : 5.0;
|
||||
const level_min = skill_level_min ? Number(skill_level_min) : 1.0;
|
||||
const userPhone = wechat_contact || (userInfo as any)?.phone || ''
|
||||
let activityInfo = {};
|
||||
if (venueDtl) {
|
||||
const { latitude, longitude,venue_type, surface_type, facilities, name, id } = venueDtl;
|
||||
activityInfo = {
|
||||
latitude,
|
||||
longitude,
|
||||
court_type: venue_type,
|
||||
court_surface: surface_type,
|
||||
venue_description: facilities,
|
||||
location_name: name,
|
||||
venue_id: id
|
||||
}
|
||||
}
|
||||
if (isDetail) {
|
||||
activityInfo = {
|
||||
venue_id,
|
||||
location_name,
|
||||
location,
|
||||
latitude,
|
||||
longitude,
|
||||
court_type,
|
||||
court_surface,
|
||||
venue_description_tag,
|
||||
venue_description,
|
||||
venue_image_list
|
||||
}
|
||||
}
|
||||
return {
|
||||
...defaultFormData,
|
||||
title,
|
||||
...(is_substitute_supported === '0' ? { is_substitute_supported: false } : {}),
|
||||
...(publish_id ? { id: publish_id } : {}),
|
||||
timeRange: {
|
||||
...defaultFormData.timeRange,
|
||||
start_time,
|
||||
end_time,
|
||||
},
|
||||
activityInfo: {
|
||||
...defaultFormData.activityInfo,
|
||||
...(play_type ? { play_type } : {}),
|
||||
...((price) ? { price } : {}),
|
||||
...activityInfo
|
||||
},
|
||||
descriptionInfo: {
|
||||
...defaultFormData.descriptionInfo,
|
||||
...(description ? { description } : {}),
|
||||
...(description_tag ? { description_tag } : {}),
|
||||
},
|
||||
...(level_max && level_min ? { skill_level: [level_min, level_max] } : {}),
|
||||
...(max_players && min_players ? { players: { min: min_players, max: max_players, organizer_joined: organizer_joined === 0 ? false : true } } : {}),
|
||||
wechat: { ...defaultFormData.wechat, default_wechat_contact: userPhone, is_wechat_contact: is_wechat_contact === 0 ? false : true},
|
||||
image_list: image_list?.map(item => ({ url: item, id: item })) || [],
|
||||
...(current_players ? { current_players } : {}),
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const formatConfig = () => {
|
||||
const newFormSchema = publishBallFormSchema.reduce((acc, item) => {
|
||||
if (item.prop === 'is_wechat_contact') {
|
||||
if (item.prop === 'wechat') {
|
||||
return acc
|
||||
}
|
||||
if (item.prop === 'image_list') {
|
||||
@@ -361,22 +486,75 @@ const PublishBall: React.FC = () => {
|
||||
return acc
|
||||
}, [] as FormFieldConfig[])
|
||||
setOptionsConfig(newFormSchema)
|
||||
setFormData([defaultFormData])
|
||||
}
|
||||
// 根据type设置导航标题
|
||||
const initFormData = () => {
|
||||
const params = getParams()
|
||||
const userPhone = (userInfo as any)?.phone || ''
|
||||
if (params?.type) {
|
||||
const type = params.type as ActivityType
|
||||
if (type === 'individual' || type === 'group') {
|
||||
setActivityType(type)
|
||||
if (type === 'group') {
|
||||
Taro.setNavigationBarTitle({
|
||||
title: '发布畅打活动'
|
||||
})
|
||||
formatConfig()
|
||||
setFormData([defaultFormData])
|
||||
setTitleBar('发布畅打活动')
|
||||
} else {
|
||||
Taro.setNavigationBarTitle({
|
||||
title: '发布'
|
||||
setTitleBar('发布')
|
||||
setFormData([{...defaultFormData, wechat: { ...defaultFormData.wechat, default_wechat_contact: userPhone } }])
|
||||
}
|
||||
} else if (type === 'ai') {
|
||||
// 从 Store 注入 AI 生成的表单 JSON
|
||||
if (publishAiData && Array.isArray(publishAiData) && publishAiData.length > 0) {
|
||||
Taro.showToast({
|
||||
title: '智能识别成功,请完善剩余信息',
|
||||
icon: 'none'
|
||||
})
|
||||
const merged = publishAiData.map(item => mergeWithDefault(item))
|
||||
setFormData(merged.length ? merged : [defaultFormData])
|
||||
if (merged.length === 1) {
|
||||
setTitleBar('发布')
|
||||
setActivityType('individual')
|
||||
} else {
|
||||
formatConfig()
|
||||
setTitleBar('发布畅打活动')
|
||||
setActivityType('group')
|
||||
}
|
||||
} else {
|
||||
setFormData([defaultFormData])
|
||||
setTitleBar('发布')
|
||||
setActivityType('individual')
|
||||
}
|
||||
}
|
||||
}
|
||||
if (params?.gameId) {
|
||||
getGameDetail(params.gameId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const getGameDetail = async (gameId) => {
|
||||
if (!gameId) return;
|
||||
try {
|
||||
const res = await DetailService.getDetail(Number(gameId));
|
||||
if (res.code === 0) {
|
||||
const merged = mergeWithDefault(res.data, true)
|
||||
setFormData([merged])
|
||||
if (res.data.game_type === '个人球局') {
|
||||
setTitleBar('发布')
|
||||
setActivityType('individual')
|
||||
} else {
|
||||
setTitleBar('发布畅打活动')
|
||||
setActivityType('group')
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Taro.showToast({
|
||||
title: e.message,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
handleActivityTypeChange(type)
|
||||
}
|
||||
}
|
||||
};
|
||||
const onCheckedChange = (checked: boolean) => {
|
||||
setChecked(checked)
|
||||
}
|
||||
@@ -394,8 +572,28 @@ const PublishBall: React.FC = () => {
|
||||
initFormData()
|
||||
}, [])
|
||||
|
||||
// 使用全局键盘状态监听
|
||||
useEffect(() => {
|
||||
// 初始化全局键盘监听器
|
||||
initializeKeyboardListener()
|
||||
|
||||
// 添加本地监听器
|
||||
const removeListener = addListener((height, visible) => {
|
||||
console.log('PublishBall 收到键盘变化:', height, visible)
|
||||
|
||||
})
|
||||
|
||||
return () => {
|
||||
removeListener()
|
||||
}
|
||||
}, [initializeKeyboardListener, addListener])
|
||||
|
||||
console.log(isKeyboardVisible, 'isKeyboardVisible');
|
||||
console.log(keyboardHeight, 'keyboardHeight');
|
||||
return (
|
||||
<View className={styles['publish-ball']}>
|
||||
<View className={`${styles['publish-ball-container']} ${isKeyboardVisible ? styles['publish-ball-container-keyboard'] : ''}`} style={{ bottom: isKeyboardVisible ? `${keyboardHeight - 124}px` : 0 }}>
|
||||
<GeneralNavbar title={titleBar} backgroundColor="#FAFAFA" className={styles['publish-ball-navbar']} />
|
||||
<View className={styles['publish-ball']} style={{ paddingTop: `${statusNavbarHeightInfo.totalHeight}px` }}>
|
||||
{/* 活动类型切换 */}
|
||||
<View className={styles['activity-type-switch']}>
|
||||
{/* <ActivityTypeSwitch
|
||||
@@ -404,7 +602,7 @@ const PublishBall: React.FC = () => {
|
||||
/> */}
|
||||
</View>
|
||||
|
||||
<View className={styles['publish-ball__scroll']}>
|
||||
<View className={styles['publish-ball__scroll']} style={{ height: `calc(100vh - ${statusNavbarHeightInfo.totalHeight+120}px)`, overflow: 'auto' }}>
|
||||
{
|
||||
formData.map((item, index) => (
|
||||
<View key={index}>
|
||||
@@ -471,7 +669,7 @@ const PublishBall: React.FC = () => {
|
||||
activityType === 'individual' && (
|
||||
<Text className={styles['submit-tip']}>
|
||||
点击确定发布约球,即表示已经同意条款
|
||||
<Text className={styles['link']}>《约球规则》</Text>
|
||||
<Text className={styles['link']} onClick={() => Taro.navigateTo({url: '/publish_pages/publishBall/footballRules/index'})}>《约球规则》</Text>
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
@@ -489,6 +687,7 @@ const PublishBall: React.FC = () => {
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { ImageUpload, Range, TimeSelector, TextareaTag, NumberInterval, TitleTextarea, FormSwitch, UploadCover } from '@/components'
|
||||
import { ImageUpload, Range, TimeSelector, TextareaTag, NumberInterval, TitleTextarea, FormSwitch, UploadCover } from '../../components'
|
||||
import FormBasicInfo from './components/FormBasicInfo'
|
||||
import { type CoverImage } from '../../components/index.types'
|
||||
import { FormFieldConfig, FieldType } from '../../config/formSchema/publishBallFormSchema'
|
||||
import { PublishBallFormData } from '../../../types/publishBall';
|
||||
import WechatSwitch from './components/WechatSwitch/WechatSwitch'
|
||||
import styles from './index.module.scss'
|
||||
import { useDictionaryActions } from '@/store/dictionaryStore'
|
||||
import { useDictionaryActions } from '../../store/dictionaryStore'
|
||||
|
||||
// 组件映射器
|
||||
const componentMap = {
|
||||
@@ -128,14 +128,14 @@ const PublishForm: React.FC<{
|
||||
return '';
|
||||
}
|
||||
|
||||
const getPlayersText = (players: [number, number] | any) => {
|
||||
const getPlayersText = (players: { min: number, max: number, organizer_joined: boolean } | any) => {
|
||||
// 检查 players 是否为数组
|
||||
if (!Array.isArray(players) || players.length !== 2) {
|
||||
if (!players.min || !players.max) {
|
||||
console.warn('getPlayersText: players 不是有效的数组格式:', players);
|
||||
return '未设置';
|
||||
}
|
||||
|
||||
const [min, max] = players;
|
||||
const { min, max } = players;
|
||||
|
||||
// 检查 min 和 max 是否为有效数字
|
||||
if (typeof min !== 'number' || typeof max !== 'number') {
|
||||
@@ -174,7 +174,7 @@ const PublishForm: React.FC<{
|
||||
...item.props,
|
||||
...(item.type === FieldType.TEXTAREATAG ? { options: item.options } : {}),
|
||||
...(item.props?.className ? { className: styles[item.props.className] } : {}),
|
||||
...(item.type === FieldType.WECHATCONTACT ? { wechatId: formData.wechat_contact } : {})
|
||||
// ...(item.type === FieldType.WECHATCONTACT ? { wechatId: formData.wechat.wechat_contact } : {})
|
||||
}
|
||||
if (item.type === FieldType.UPLOADIMAGE) {
|
||||
/* 活动封面 */
|
||||
@@ -188,8 +188,7 @@ const PublishForm: React.FC<{
|
||||
return <>
|
||||
<View className={styles['activity-description']}>
|
||||
<Text className={styles['description-text']}>
|
||||
活动开始前2小时未达到最低人数,活动自动取消;活动
|
||||
结束后,报名者累计到达最低人数时,一旦到达期即有序。
|
||||
活动开始前2小时未达到最低人数,活动自动取消;活动结束后,报名者累计到达最低人数时,一旦到达期即有序。
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
|
||||
85
src/services/commentServices.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import httpService from "./httpService";
|
||||
import type { ApiResponse } from "./httpService";
|
||||
|
||||
export interface CommentResponse {
|
||||
rows: Comment[]
|
||||
count: number
|
||||
}
|
||||
|
||||
export interface UserInfo {
|
||||
id: number
|
||||
nickname: string
|
||||
avatar_url: string
|
||||
}
|
||||
|
||||
export type BaseComment<T = {}> = {
|
||||
create_time: string
|
||||
last_modify_time: string
|
||||
id: number
|
||||
game_id: number
|
||||
user_id: number
|
||||
parent_id: number | null
|
||||
reply_to_user_id: number | null
|
||||
content: string
|
||||
like_count: number
|
||||
reply_count: number
|
||||
user: UserInfo
|
||||
is_liked?: boolean
|
||||
} & T
|
||||
|
||||
export type ReplyComment = BaseComment<{
|
||||
parent_id: number
|
||||
reply_to_user_id: number
|
||||
reply_to_user: UserInfo
|
||||
}>
|
||||
|
||||
export type Comment = BaseComment<{
|
||||
replies: ReplyComment[]
|
||||
}>
|
||||
|
||||
// 接口响应
|
||||
export interface ReplyCommentResponse {
|
||||
count: number
|
||||
rows: ReplyComment[]
|
||||
}
|
||||
|
||||
export interface ToggleLikeType {
|
||||
is_liked: boolean,
|
||||
like_count: number,
|
||||
message: string
|
||||
}
|
||||
|
||||
// 评论管理类
|
||||
class CommentService {
|
||||
// 查询评论列表
|
||||
async getComments(req: { game_id: number, page: number, pageSize: number }): Promise<ApiResponse<CommentResponse>> {
|
||||
return httpService.post("/comments/list", req, { showLoading: true });
|
||||
}
|
||||
|
||||
// 发表评论
|
||||
async createComment(req: { game_id: number, content: string }): Promise<ApiResponse<BaseComment>> {
|
||||
return httpService.post("/comments/create", req, { showLoading: true });
|
||||
}
|
||||
|
||||
// 回复评论
|
||||
async replyComment(req: { parent_id: number, reply_to_user_id: number, content: string }): Promise<ApiResponse<ReplyComment>> {
|
||||
return httpService.post("/comments/reply", req, { showLoading: true });
|
||||
}
|
||||
|
||||
// 点赞取消点赞评论
|
||||
async toggleLike(req: { comment_id: number }): Promise<ApiResponse<ToggleLikeType>> {
|
||||
return httpService.post("/comments/like", req, { showLoading: true });
|
||||
}
|
||||
|
||||
// 删除评论
|
||||
async deleteComment(req: { comment_id: number }): Promise<ApiResponse<any>> {
|
||||
return httpService.post("/comments/delete", req, { showLoading: true });
|
||||
}
|
||||
|
||||
// 获取评论的所有回复
|
||||
async getReplies(req: { comment_id: number, page: number, pageSize: number }): Promise<ApiResponse<ReplyCommentResponse>> {
|
||||
return httpService.post("/comments/replies", req, { showLoading: true });
|
||||
}
|
||||
}
|
||||
|
||||
export default new CommentService();
|
||||
@@ -70,7 +70,7 @@ class CommonApiService {
|
||||
|
||||
// 获取字典数据
|
||||
async getDictionaryManyKey(keys: string): Promise<ApiResponse<any>> {
|
||||
return httpService.get('/parameter/many_key', { keys }, {
|
||||
return httpService.post('/parameter/many_key', { keys }, {
|
||||
showLoading: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
import httpService from './httpService'
|
||||
import type { ApiResponse } from './httpService'
|
||||
|
||||
// 用户信息接口
|
||||
export interface UserProfile {
|
||||
id: string
|
||||
nickname: string
|
||||
avatar?: string
|
||||
age?: number
|
||||
gender: 'male' | 'female'
|
||||
interests: string[]
|
||||
acceptNotification: boolean
|
||||
}
|
||||
|
||||
// 反馈评价接口
|
||||
export interface Feedback {
|
||||
id: string
|
||||
matchId: string
|
||||
userId: string
|
||||
photos: string[]
|
||||
rating: number
|
||||
recommend: 'yes' | 'no' | 'neutral'
|
||||
aspects: string[]
|
||||
comments: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
// DynamicFormDemo页面API服务类
|
||||
class DemoApiService {
|
||||
// ==================== 用户信息相关接口 ====================
|
||||
|
||||
// 获取用户信息
|
||||
async getUserProfile(): Promise<ApiResponse<UserProfile>> {
|
||||
return httpService.get('/user/profile')
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
async updateUserProfile(data: Partial<UserProfile>): Promise<ApiResponse<UserProfile>> {
|
||||
return httpService.put('/user/profile', data, {
|
||||
showLoading: true,
|
||||
loadingText: '保存中...'
|
||||
})
|
||||
}
|
||||
|
||||
// 上传头像
|
||||
async uploadAvatar(filePath: string): Promise<ApiResponse<{ url: string }>> {
|
||||
return httpService.post('/user/avatar', { filePath }, {
|
||||
showLoading: true,
|
||||
loadingText: '上传头像中...'
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 反馈评价相关接口 ====================
|
||||
|
||||
// 提交活动评价
|
||||
async submitFeedback(data: {
|
||||
matchId: string
|
||||
photos?: string[]
|
||||
rating: number
|
||||
recommend: 'yes' | 'no' | 'neutral'
|
||||
aspects: string[]
|
||||
comments: string
|
||||
}): Promise<ApiResponse<Feedback>> {
|
||||
return httpService.post('/feedback', data, {
|
||||
showLoading: true,
|
||||
loadingText: '提交评价中...'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取我的评价列表
|
||||
async getMyFeedbacks(params?: {
|
||||
page?: number
|
||||
limit?: number
|
||||
}): Promise<ApiResponse<{ list: Feedback[]; total: number }>> {
|
||||
return httpService.get('/feedback/my', params)
|
||||
}
|
||||
|
||||
// 获取活动的所有评价
|
||||
async getMatchFeedbacks(matchId: string, params?: {
|
||||
page?: number
|
||||
limit?: number
|
||||
}): Promise<ApiResponse<{ list: Feedback[]; total: number }>> {
|
||||
return httpService.get(`/feedback/match/${matchId}`, params)
|
||||
}
|
||||
|
||||
// ==================== 通用表单提交接口 ====================
|
||||
|
||||
// 提交表单数据(通用接口)
|
||||
async submitForm(formType: string, formData: any[]): Promise<ApiResponse<any>> {
|
||||
return httpService.post('/forms/submit', {
|
||||
type: formType,
|
||||
data: formData
|
||||
}, {
|
||||
showLoading: true,
|
||||
loadingText: '提交中...'
|
||||
})
|
||||
}
|
||||
|
||||
// 保存表单草稿
|
||||
async saveFormDraft(formType: string, formData: any[]): Promise<ApiResponse<{ id: string }>> {
|
||||
return httpService.post('/forms/draft', {
|
||||
type: formType,
|
||||
data: formData
|
||||
}, {
|
||||
showLoading: true,
|
||||
loadingText: '保存中...'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取表单草稿
|
||||
async getFormDrafts(formType: string): Promise<ApiResponse<{ id: string; data: any[]; createdAt: string }[]>> {
|
||||
return httpService.get('/forms/drafts', { type: formType })
|
||||
}
|
||||
|
||||
// 删除表单草稿
|
||||
async deleteFormDraft(id: string): Promise<ApiResponse<{ success: boolean }>> {
|
||||
return httpService.delete(`/forms/draft/${id}`)
|
||||
}
|
||||
|
||||
// ==================== 兴趣爱好相关接口 ====================
|
||||
|
||||
// 获取兴趣爱好选项
|
||||
async getInterestOptions(): Promise<ApiResponse<{ label: string; value: string }[]>> {
|
||||
return httpService.get('/interests')
|
||||
}
|
||||
|
||||
// 获取推荐的兴趣爱好
|
||||
async getRecommendedInterests(): Promise<ApiResponse<string[]>> {
|
||||
return httpService.get('/interests/recommended')
|
||||
}
|
||||
}
|
||||
|
||||
// 导出API服务实例
|
||||
export default new DemoApiService()
|
||||
@@ -82,6 +82,13 @@ export enum MATCH_STATUS {
|
||||
NOT_STARTED = 0, // 未开始
|
||||
IN_PROGRESS = 1, //进行中
|
||||
FINISHED = 2, //已结束
|
||||
CANCELED = 3, // 已取消
|
||||
}
|
||||
|
||||
// 是否支持候补
|
||||
export enum IsSubstituteSupported {
|
||||
SUPPORT = '0', // 支持
|
||||
NOTSUPPORT = '1', // 不支持
|
||||
}
|
||||
|
||||
export interface UpdateLocationRes {
|
||||
@@ -92,10 +99,8 @@ export interface UpdateLocationRes {
|
||||
city: string;
|
||||
district: string;
|
||||
}
|
||||
|
||||
// 发布球局类
|
||||
class GameDetailService {
|
||||
// 用户登录
|
||||
// 查询球局详情
|
||||
async getDetail(id: number): Promise<ApiResponse<GameData>> {
|
||||
return httpService.post(
|
||||
"/games/detail",
|
||||
@@ -114,6 +119,27 @@ class GameDetailService {
|
||||
showLoading: true,
|
||||
});
|
||||
}
|
||||
|
||||
// 组织者加入球局
|
||||
async organizerJoin(game_id: number): Promise<ApiResponse<any>> {
|
||||
return httpService.post("/games/organizer_join", { game_id }, {
|
||||
showLoading: true,
|
||||
});
|
||||
}
|
||||
|
||||
// 组织者退出球局
|
||||
async organizerQuit(req: { game_id: number, quit_reason: string }): Promise<ApiResponse<any>> {
|
||||
return httpService.post("/games/organizer_quit", req, {
|
||||
showLoading: true,
|
||||
});
|
||||
}
|
||||
|
||||
// 组织者解散球局
|
||||
async disbandGame(req: { game_id: number, settle_reason: string }): Promise<ApiResponse<any>> {
|
||||
return httpService.post("/games/settle_game", req, {
|
||||
showLoading: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 导出认证服务实例
|
||||
|
||||
@@ -1,71 +1,123 @@
|
||||
import httpService from "./httpService";
|
||||
import type { ApiResponse } from "./httpService";
|
||||
|
||||
export interface AnswerResItem {
|
||||
question_id: number;
|
||||
answer_index: number;
|
||||
// 单个选项类型
|
||||
interface Option {
|
||||
text: string;
|
||||
score: number;
|
||||
}
|
||||
|
||||
// 单个问题类型
|
||||
export interface Question {
|
||||
id: number;
|
||||
question_title: string;
|
||||
question_content: string;
|
||||
options: Option[];
|
||||
radar_mapping: string[];
|
||||
}
|
||||
|
||||
// 单项能力分数
|
||||
interface AbilityScore {
|
||||
current_score: number;
|
||||
max_score: number;
|
||||
percentage: number;
|
||||
}
|
||||
|
||||
// 雷达图数据
|
||||
interface RadarData {
|
||||
abilities: Record<string, AbilityScore>; // key 是能力名称,如 "正手球质"
|
||||
summary: {
|
||||
total_questions: number;
|
||||
calculation_time: string; // ISO 字符串
|
||||
};
|
||||
}
|
||||
|
||||
// 单题答案
|
||||
interface Answer {
|
||||
question_id: number;
|
||||
question_title: string;
|
||||
answer_index: number;
|
||||
selected_option: string;
|
||||
score: number;
|
||||
}
|
||||
|
||||
export type AnswerItem = Pick<AnswerResItem, "question_id" | "answer_index">;
|
||||
|
||||
export interface Answers {
|
||||
answers: AnswerItem[];
|
||||
test_duration: number;
|
||||
}
|
||||
|
||||
export interface QuestionItem {
|
||||
id: number;
|
||||
question_title: string;
|
||||
question_content: string;
|
||||
options: string[];
|
||||
scores: number[];
|
||||
}
|
||||
|
||||
export interface SubmitAnswerRes {
|
||||
// 提交测试结果 对象类型
|
||||
export interface TestResultData {
|
||||
record_id: number;
|
||||
total_score: number;
|
||||
ntrp_level: string;
|
||||
is_coverage: boolean;
|
||||
old_ntrp_level: string;
|
||||
level_description: string;
|
||||
answers: AnswerResItem[];
|
||||
radar_data: RadarData;
|
||||
answers: Answer[];
|
||||
}
|
||||
|
||||
// 单条测试记录
|
||||
interface TestRecord {
|
||||
id: number;
|
||||
total_score: number;
|
||||
ntrp_level: string;
|
||||
level_description: string;
|
||||
test_duration: number; // 单位:秒
|
||||
create_time: string; // 时间字符串
|
||||
}
|
||||
|
||||
// 测试历史对象类型
|
||||
export interface TestResultList {
|
||||
count: number;
|
||||
rows: TestRecord[];
|
||||
}
|
||||
|
||||
// 单次测试结果
|
||||
interface TestResult {
|
||||
id: number;
|
||||
total_score: number;
|
||||
ntrp_level: string;
|
||||
level_description: string;
|
||||
radar_data: RadarData;
|
||||
test_duration: number; // 单位秒
|
||||
create_time: string; // 时间字符串
|
||||
}
|
||||
|
||||
// data 对象
|
||||
// 上一次测试结果
|
||||
export interface LastTimeTestResult {
|
||||
has_test_record: boolean;
|
||||
has_ntrp_level: boolean;
|
||||
user_ntrp_level: string;
|
||||
last_test_result: TestResult;
|
||||
}
|
||||
|
||||
// 发布球局类
|
||||
class EvaluateService {
|
||||
async getEvaluateQuestions(): Promise<ApiResponse<QuestionItem[]>> {
|
||||
return httpService.post("/ntrp/questions", {
|
||||
showLoading: true,
|
||||
});
|
||||
// 获取测试题目
|
||||
async getQuestions(): Promise<ApiResponse<Question[]>> {
|
||||
return httpService.post("/ntrp/questions", {}, { showLoading: true });
|
||||
}
|
||||
|
||||
async submitEvaluateAnswers({
|
||||
answers,
|
||||
}: Answers): Promise<ApiResponse<SubmitAnswerRes>> {
|
||||
return httpService.post(
|
||||
"/ntrp/submit",
|
||||
{ answers },
|
||||
{
|
||||
showLoading: true,
|
||||
},
|
||||
);
|
||||
// 提交答案
|
||||
async submit(req: { answers: { question_id: number, answer_index: number }[], test_duration: number }): Promise<ApiResponse<TestResultData>> {
|
||||
return httpService.post("/ntrp/submit", req, { showLoading: true });
|
||||
}
|
||||
|
||||
async getHistoryNtrp(): Promise<ApiResponse<any>> {
|
||||
return httpService.post("/ntrp/history", {
|
||||
showLoading: true,
|
||||
});
|
||||
// 获取测试历史
|
||||
async getResultList(): Promise<ApiResponse<TestResultList>> {
|
||||
return httpService.post("/ntrp/history", {}, { showLoading: true });
|
||||
}
|
||||
|
||||
async getNtrpDetail(record_id: number): Promise<ApiResponse<any>> {
|
||||
return httpService.post(
|
||||
"/ntrp/detail",
|
||||
{ record_id },
|
||||
{
|
||||
showLoading: true,
|
||||
},
|
||||
);
|
||||
// 获取测试详情
|
||||
async getTestResult(req: { record_id: number }): Promise<ApiResponse<TestResultData>> {
|
||||
return httpService.post("/ntrp/detail", req, { showLoading: true });
|
||||
}
|
||||
|
||||
// 获取最后一次(最新)测试结果
|
||||
async getLastResult(): Promise<ApiResponse<LastTimeTestResult>> {
|
||||
return httpService.post("/ntrp/last_result", {}, { showLoading: true });
|
||||
}
|
||||
|
||||
// 更新NTRP等级
|
||||
async updateNtrp(req: { record_id: number, ntrp_level: string, update_type: string }): Promise<ApiResponse<any>> {
|
||||
return httpService.post("/ntrp/update_user_level", req, { showLoading: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
232
src/services/followService.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import { API_CONFIG } from '@/config/api';
|
||||
import httpService, { ApiResponse } from './httpService';
|
||||
|
||||
// 球友信息接口
|
||||
export interface FollowUser {
|
||||
id: number;
|
||||
nickname: string;
|
||||
avatar_url: string;
|
||||
personal_profile?: string;
|
||||
gender?: string;
|
||||
city?: string;
|
||||
ntrp_level?: number;
|
||||
// 关注状态:mutual_follow=互相关注, following=已关注, follower=粉丝, recommend=推荐
|
||||
follow_status: 'mutual_follow' | 'following' | 'follower' | 'recommend';
|
||||
follow_time?: string; // 关注时间
|
||||
is_mutual?: boolean; // 是否互关
|
||||
is_following?: boolean; // 是否已关注
|
||||
common_games_count?: number; // 共同参与球局数量
|
||||
}
|
||||
|
||||
// 关注列表响应接口
|
||||
export interface FollowListResponse {
|
||||
list: FollowUser[];
|
||||
total: number;
|
||||
current_city?: string; // 当前用户城市(推荐接口返回)
|
||||
}
|
||||
|
||||
// 球友关注服务类
|
||||
export class FollowService {
|
||||
|
||||
// 获取互关用户列表
|
||||
static async get_mutual_follow_list(
|
||||
page: number = 1,
|
||||
page_size: number = 20
|
||||
): Promise<FollowListResponse> {
|
||||
try {
|
||||
const response = await httpService.post<FollowListResponse>(
|
||||
'/user_follow/mutual_follow_list',
|
||||
{ page, page_size },
|
||||
{ showLoading: false }
|
||||
);
|
||||
|
||||
if (response.code === 0) {
|
||||
// 为数据添加 follow_status 标识
|
||||
const list = response.data.list.map(user => ({
|
||||
...user,
|
||||
follow_status: 'mutual_follow' as const
|
||||
}));
|
||||
return { ...response.data, list };
|
||||
} else {
|
||||
throw new Error(response.message || '获取互关列表失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取互关列表失败:', error);
|
||||
return { list: [], total: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
// 获取我的粉丝列表
|
||||
static async get_fans_list(
|
||||
page: number = 1,
|
||||
page_size: number = 20
|
||||
): Promise<FollowListResponse> {
|
||||
try {
|
||||
const response = await httpService.post<FollowListResponse>(
|
||||
'/user_follow/my_fans_list',
|
||||
{ page, page_size },
|
||||
{ showLoading: false }
|
||||
);
|
||||
|
||||
if (response.code === 0) {
|
||||
// 为数据添加 follow_status 标识
|
||||
const list = response.data.list.map(user => ({
|
||||
...user,
|
||||
follow_status: user.is_mutual ? 'mutual_follow' as const : 'follower' as const
|
||||
}));
|
||||
return { ...response.data, list };
|
||||
} else {
|
||||
throw new Error(response.message || '获取粉丝列表失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取粉丝列表失败:', error);
|
||||
return { list: [], total: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
// 获取我的关注列表
|
||||
static async get_following_list(
|
||||
page: number = 1,
|
||||
page_size: number = 20
|
||||
): Promise<FollowListResponse> {
|
||||
try {
|
||||
const response = await httpService.post<FollowListResponse>(
|
||||
'/user_follow/my_following_list',
|
||||
{ page, page_size },
|
||||
{ showLoading: false }
|
||||
);
|
||||
|
||||
if (response.code === 0) {
|
||||
// 为数据添加 follow_status 标识
|
||||
const list = response.data.list.map(user => ({
|
||||
...user,
|
||||
follow_status: user.is_mutual ? 'mutual_follow' as const : 'following' as const
|
||||
}));
|
||||
return { ...response.data, list };
|
||||
} else {
|
||||
throw new Error(response.message || '获取关注列表失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取关注列表失败:', error);
|
||||
return { list: [], total: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
// 获取关注列表(统一入口)
|
||||
static async get_follow_list(
|
||||
type: 'mutual_follow' | 'following' | 'follower' | 'recommend',
|
||||
page: number = 1,
|
||||
page_size: number = 20
|
||||
): Promise<FollowListResponse> {
|
||||
switch (type) {
|
||||
case 'mutual_follow':
|
||||
return this.get_mutual_follow_list(page, page_size);
|
||||
case 'following':
|
||||
return this.get_following_list(page, page_size);
|
||||
case 'follower':
|
||||
return this.get_fans_list(page, page_size);
|
||||
case 'recommend':
|
||||
return this.get_recommend_users(page, page_size);
|
||||
default:
|
||||
return { list: [], total: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
// 关注用户
|
||||
static async follow_user(user_id: number): Promise<boolean> {
|
||||
try {
|
||||
const response = await httpService.post<any>(
|
||||
API_CONFIG.USER.FOLLOW,
|
||||
{ following_id: user_id },
|
||||
{
|
||||
showLoading: false,
|
||||
loadingText: '关注中...'
|
||||
}
|
||||
);
|
||||
|
||||
if (response.code === 0) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(response.message || '关注失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('关注失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 取消关注用户
|
||||
static async unfollow_user(user_id: number): Promise<boolean> {
|
||||
try {
|
||||
const response = await httpService.post<any>(
|
||||
API_CONFIG.USER.UNFOLLOW,
|
||||
{ following_id: user_id },
|
||||
{
|
||||
showLoading: false,
|
||||
loadingText: '取消关注中...'
|
||||
}
|
||||
);
|
||||
|
||||
if (response.code === 0) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(response.message || '取消关注失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('取消关注失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 回关用户(关注粉丝)
|
||||
static async follow_back(user_id: number): Promise<boolean> {
|
||||
return this.follow_user(user_id);
|
||||
}
|
||||
|
||||
// 获取同城推荐用户
|
||||
static async get_recommend_users(page: number = 1, page_size: number = 10): Promise<FollowListResponse> {
|
||||
try {
|
||||
const response = await httpService.post<FollowListResponse>(
|
||||
'/user_follow/recommend_same_city',
|
||||
{ page, page_size },
|
||||
{ showLoading: false }
|
||||
);
|
||||
|
||||
if (response.code === 0) {
|
||||
// 为数据添加 follow_status 标识
|
||||
const list = response.data.list.map(user => ({
|
||||
...user,
|
||||
follow_status: 'recommend' as const
|
||||
}));
|
||||
return { ...response.data, list };
|
||||
} else {
|
||||
throw new Error(response.message || '获取推荐用户失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取推荐用户失败:', error);
|
||||
return { list: [], total: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
// 检查关注状态
|
||||
static async check_follow_status(user_id: number): Promise<'mutual_follow' | 'following' | 'follower' | 'none'> {
|
||||
try {
|
||||
const response = await httpService.post<{ status: string }>(
|
||||
'/wch_users/follow_status',
|
||||
{ user_id },
|
||||
{ showLoading: false }
|
||||
);
|
||||
|
||||
if (response.code === 0) {
|
||||
return response.data.status as 'mutual_follow' | 'following' | 'follower' | 'none';
|
||||
} else {
|
||||
return 'none';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查关注状态失败:', error);
|
||||
return 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default FollowService;
|
||||
@@ -105,7 +105,7 @@ class HttpService {
|
||||
}
|
||||
|
||||
// 处理响应
|
||||
private handleResponse<T>(response: any): Promise<ApiResponse<T>> {
|
||||
private handleResponse<T>(response: any, showToast: boolean): Promise<ApiResponse<T>> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { statusCode, data } = response
|
||||
|
||||
@@ -121,8 +121,12 @@ class HttpService {
|
||||
// 业务状态码检查
|
||||
if (data && typeof data === 'object') {
|
||||
if (data.success === false || (data.code && data.code !== 0 && data.code !== 200)) {
|
||||
if (showToast) {
|
||||
this.handleBusinessError(data)
|
||||
reject(new Error(data.message || '请求失败'))
|
||||
} else {
|
||||
reject(response.data)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -191,7 +195,8 @@ class HttpService {
|
||||
data,
|
||||
params,
|
||||
showLoading = false,
|
||||
loadingText = '请求中...'
|
||||
loadingText = '请求中...',
|
||||
showToast = true,
|
||||
} = config
|
||||
|
||||
let fullUrl = this.buildUrl(url, method === 'GET' ? params : undefined)
|
||||
@@ -238,11 +243,12 @@ class HttpService {
|
||||
method: method,
|
||||
data: method !== 'GET' ? data : undefined,
|
||||
header: reqHeader,
|
||||
timeout: this.timeout
|
||||
timeout: this.timeout,
|
||||
showToast,
|
||||
}
|
||||
|
||||
const response = await Taro.request(requestConfig)
|
||||
return this.handleResponse<T>(response)
|
||||
return this.handleResponse<T>(response, showToast)
|
||||
} catch (error) {
|
||||
this.log('error', '请求失败', error)
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ export const getGamesList = async (params?: Record<string, any>) => {
|
||||
*/
|
||||
// const isIntegrate = params?.order === '0';
|
||||
// const fetchApi = isIntegrate ? '/games/integrate_list' : '/games/list'
|
||||
return httpService.post('/games/list', params, { showLoading: true })
|
||||
return httpService.post('/games/list', params, { showLoading: false })
|
||||
} catch (error) {
|
||||
console.error("列表数据获取失败:", error);
|
||||
throw error;
|
||||
@@ -39,7 +39,7 @@ export const getGamesList = async (params?: Record<string, any>) => {
|
||||
*/
|
||||
export const getGamesIntegrateList = async (params?: Record<string, any>) => {
|
||||
try {
|
||||
return httpService.post('/games/integrate_list', params, { showLoading: true })
|
||||
return httpService.post('/games/integrate_list', params, { showLoading: false })
|
||||
} catch (error) {
|
||||
console.error("列表数据获取失败:", error);
|
||||
throw error;
|
||||
@@ -53,7 +53,7 @@ export const getGamesIntegrateList = async (params?: Record<string, any>) => {
|
||||
*/
|
||||
export const getGamesCount = async (params?: Record<string, any>) => {
|
||||
try {
|
||||
return httpService.post('/games/count', params, { showLoading: true })
|
||||
return httpService.post('/games/count', params, { showLoading: false })
|
||||
} catch (error) {
|
||||
console.error("列表数量获取失败:", error);
|
||||
throw error;
|
||||
@@ -68,7 +68,7 @@ export const getGamesCount = async (params?: Record<string, any>) => {
|
||||
export const getSearchHistory = async (params) => {
|
||||
try {
|
||||
// 调用HTTP服务获取搜索历史记录
|
||||
return httpService.post('/games/search_history', params, { showLoading: true })
|
||||
return httpService.post('/games/search_history', params, { showLoading: false })
|
||||
} catch (error) {
|
||||
// 捕获并打印错误信息
|
||||
console.error("历史记录获取失败:", error);
|
||||
@@ -84,7 +84,7 @@ export const getSearchHistory = async (params) => {
|
||||
export const clearHistory = async (params) => {
|
||||
try {
|
||||
// 调用HTTP服务清除搜索历史记录
|
||||
return httpService.post('/games/search_history/delete_all', params, { showLoading: true })
|
||||
return httpService.post('/games/search_history/delete_all', params, { showLoading: false })
|
||||
} catch (error) {
|
||||
// 捕获并打印错误信息
|
||||
console.error("清除历史记录失败:", error);
|
||||
|
||||
@@ -40,23 +40,23 @@ export interface UnreadMountResponse {
|
||||
class NoticeService {
|
||||
// 获取用户消息通知列表
|
||||
async getNotificationList({ notification_type, is_read }: NoticeListParams): Promise<ApiResponse<NoticeListResponse>> {
|
||||
return httpService.post("/notifications/list", { notification_type, is_read }, { showLoading: true });
|
||||
return httpService.post("/notifications/list", { notification_type, is_read }, { showLoading: false });
|
||||
}
|
||||
// 获取消息通知详情
|
||||
async getNotificationDetail(notification_id: number): Promise<ApiResponse<Notice>> {
|
||||
return httpService.post("/notifications/detail", { notification_id }, { showLoading: true });
|
||||
return httpService.post("/notifications/detail", { notification_id }, { showLoading: false });
|
||||
}
|
||||
// 标记消息为已读
|
||||
async markNotificationRead({ notification_ids, mark_all }: MarkReadParams): Promise<ApiResponse<{ marked_count: number }>> {
|
||||
return httpService.post("/notifications/mark_read", { notification_ids, mark_all }, { showLoading: true });
|
||||
return httpService.post("/notifications/mark_read", { notification_ids, mark_all }, { showLoading: false });
|
||||
}
|
||||
// 删除消息通知
|
||||
async delNotification({ notification_ids, delete_all }: DeleteParams): Promise<ApiResponse<{ deleted_count: number }>> {
|
||||
return httpService.post("/notifications/delete", { notification_ids, delete_all }, { showLoading: true });
|
||||
return httpService.post("/notifications/delete", { notification_ids, delete_all }, { showLoading: false });
|
||||
}
|
||||
// 获取未读消息数量
|
||||
async getNotificationUnreadCount(): Promise<ApiResponse<UnreadMountResponse>> {
|
||||
return httpService.post("/notifications/unread_count", {}, { showLoading: true });
|
||||
return httpService.post("/notifications/unread_count", {}, { showLoading: false });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,18 @@ export enum CancelType {
|
||||
TIMEOUT, // 超时
|
||||
}
|
||||
|
||||
export enum RefundStatus {
|
||||
NONE = 0, // 无退款
|
||||
PENDING, // 退款中
|
||||
SUCCESS, // 已退款
|
||||
}
|
||||
|
||||
export const refundTextMap = new Map([
|
||||
[RefundStatus.NONE, "已支付"],
|
||||
[RefundStatus.PENDING, "退款中"],
|
||||
[RefundStatus.SUCCESS, "已退款"],
|
||||
]);
|
||||
|
||||
export interface PayMentParams {
|
||||
order_id: number;
|
||||
order_no: string;
|
||||
@@ -78,8 +90,8 @@ export interface GameOrderRes {
|
||||
// 发布球局类
|
||||
class OrderService {
|
||||
// 查询订单列表
|
||||
async getOrderList() {
|
||||
return httpService.post("/user/orders", {}, { showLoading: true });
|
||||
async getOrderList(pagination: { page: number, pageSize: number }) {
|
||||
return httpService.post("/user/orders", pagination, { showLoading: true });
|
||||
}
|
||||
|
||||
// 获取订单详情
|
||||
@@ -161,6 +173,21 @@ class OrderService {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 删除订单
|
||||
async deleteOrder({
|
||||
order_id,
|
||||
}: {
|
||||
order_id: number;
|
||||
}): Promise<ApiResponse<any>> {
|
||||
return httpService.post(
|
||||
"/payment/delete_order",
|
||||
{ order_id },
|
||||
{
|
||||
showLoading: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 导出认证服务实例
|
||||
|
||||
@@ -20,7 +20,7 @@ export interface PublishBallData {
|
||||
venue_description?: string // 场地描述
|
||||
venue_image_list?: string[] // 场地图片
|
||||
max_players: number // 人数要求
|
||||
current_players: number // 人数要求
|
||||
min_players: number // 人数要求
|
||||
skill_level_min: number // 水平要求(NTRP)
|
||||
skill_level_max: number // 水平要求(NTRP)
|
||||
description: string // 备注
|
||||
@@ -142,6 +142,24 @@ class PublishService {
|
||||
showToast: false,
|
||||
})
|
||||
}
|
||||
async extract_tennis_activity(req: {text: string}): Promise<getPicturesRes> {
|
||||
return httpService.post('/ai/extract_tennis_activity', req, {
|
||||
showLoading: false,
|
||||
showToast: false,
|
||||
})
|
||||
}
|
||||
async extract_tennis_activity_from_image(req: {image_url: string}): Promise<getPicturesRes> {
|
||||
return httpService.post('/ai/extract_tennis_activity_from_image', req, {
|
||||
showLoading: false,
|
||||
showToast: false,
|
||||
})
|
||||
}
|
||||
async gamesUpdate(data: PublishBallData): Promise<ApiResponse<createGameData>> {
|
||||
return httpService.post('/games/update', data, {
|
||||
showLoading: true,
|
||||
loadingText: '发布中...'
|
||||
})
|
||||
}
|
||||
async getPictures(req) {
|
||||
const { type, tag, otherReq = {} } = req
|
||||
if (type === 'history') {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { API_CONFIG } from '@/config/api';
|
||||
import httpService, { ApiResponse } from './httpService';
|
||||
import uploadFiles from './uploadFiles';
|
||||
import Taro from '@tarojs/taro';
|
||||
import getCurrentConfig from '@/config/env';
|
||||
|
||||
|
||||
// 用户详情接口
|
||||
@@ -28,6 +29,10 @@ interface UserDetailData {
|
||||
last_login_time: string;
|
||||
create_time: string;
|
||||
last_modify_time: string;
|
||||
personal_profile: string;
|
||||
occupation: string;
|
||||
birthday: string;
|
||||
ntrp_level: string,
|
||||
stats: {
|
||||
followers_count: number;
|
||||
following_count: number;
|
||||
@@ -36,6 +41,17 @@ interface UserDetailData {
|
||||
};
|
||||
}
|
||||
|
||||
export interface PickerOption {
|
||||
text: string | number;
|
||||
value: string | number;
|
||||
children?: PickerOption[];
|
||||
}
|
||||
|
||||
export interface Profession {
|
||||
name: string;
|
||||
children: Profession[] | [];
|
||||
}
|
||||
|
||||
// 用户详细信息接口(从 loginService 移过来)
|
||||
export interface UserInfoType {
|
||||
id: number
|
||||
@@ -108,6 +124,26 @@ interface BackendGameData {
|
||||
venue_type: string;
|
||||
surface_type: string;
|
||||
};
|
||||
participants: {
|
||||
user: {
|
||||
avatar_url: string;
|
||||
};
|
||||
}[];
|
||||
}
|
||||
|
||||
const formatOptions = (data: Profession[]): PickerOption[] => {
|
||||
return data.map((item: Profession) => {
|
||||
const { name: text, children } = item;
|
||||
const itm: PickerOption = {
|
||||
text,
|
||||
value: text,
|
||||
children: children ? formatOptions(children) : []
|
||||
}
|
||||
if (!itm.children!.length) {
|
||||
delete itm.children
|
||||
}
|
||||
return itm
|
||||
})
|
||||
}
|
||||
|
||||
// 用户服务类
|
||||
@@ -116,7 +152,7 @@ export class UserService {
|
||||
private static transform_game_data(backend_data: BackendGameData[]): any[] {
|
||||
return backend_data.map(game => {
|
||||
// 处理时间格式
|
||||
const start_time = new Date(game.start_time);
|
||||
const start_time = new Date(game.start_time.replace(/\s/, 'T'));
|
||||
const date_time = this.format_date_time(start_time);
|
||||
|
||||
// 处理图片数组 - 兼容两种数据格式
|
||||
@@ -153,6 +189,8 @@ export class UserService {
|
||||
id: game.id,
|
||||
title: game.title || '未命名球局',
|
||||
start_time: date_time,
|
||||
original_start_time: game.start_time,
|
||||
end_time: game.end_time || '',
|
||||
location: location,
|
||||
distance_km: parseFloat(distance.replace('km', '')) || 0,
|
||||
current_players: registered_count,
|
||||
@@ -163,30 +201,42 @@ export class UserService {
|
||||
image_list: images,
|
||||
court_type: game.court_type || '未知',
|
||||
matchType: game.play_type || '不限',
|
||||
shinei: game.court_type || '未知'
|
||||
shinei: game.court_type || '未知',
|
||||
participants: game.participants || [],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private static is_date_in_this_week(date: Date): boolean {
|
||||
const today = new Date();
|
||||
const firstDayOfWeek = new Date(today.setDate(today.getDate() - today.getDay()));
|
||||
const lastDayOfWeek = new Date(firstDayOfWeek.setDate(firstDayOfWeek.getDate() + 6));
|
||||
|
||||
return date >= firstDayOfWeek && date <= lastDayOfWeek;
|
||||
}
|
||||
|
||||
// 格式化时间显示
|
||||
private static format_date_time(start_time: Date): string {
|
||||
const now = new Date();
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const today = new Date(now.getFullYear(), now.getMonth() + 1, now.getDate());
|
||||
const tomorrow = new Date(today.getTime() + 24 * 60 * 60 * 1000);
|
||||
const day_after_tomorrow = new Date(today.getTime() + 2 * 24 * 60 * 60 * 1000);
|
||||
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
|
||||
|
||||
const start_date = new Date(start_time.getFullYear(), start_time.getMonth(), start_time.getDate());
|
||||
const start_date = new Date(start_time.getFullYear(), start_time.getMonth() + 1, start_time.getDate());
|
||||
const weekday = weekdays[start_time.getDay()];
|
||||
|
||||
let date_str = '';
|
||||
if (start_date.getTime() === today.getTime()) {
|
||||
date_str = '今天';
|
||||
} else if (start_date.getTime() === tomorrow.getTime()) {
|
||||
date_str = '明天';
|
||||
date_str = `明天(${weekday})`;
|
||||
} else if (start_date.getTime() === day_after_tomorrow.getTime()) {
|
||||
date_str = '后天';
|
||||
date_str = `后天(${weekday})`;
|
||||
} else if (this.is_date_in_this_week(start_time)) {
|
||||
date_str = weekday;
|
||||
} else {
|
||||
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
|
||||
date_str = weekdays[start_time.getDay()];
|
||||
date_str = `${start_time.getFullYear()}-${(start_time.getMonth() + 1).toString().padStart(2, '0')}-${start_time.getDate().toString().padStart(2, '0')}(${weekday})`;
|
||||
}
|
||||
|
||||
const time_str = `${start_time.getHours().toString().padStart(2, '0')}:${start_time.getMinutes().toString().padStart(2, '0')}`;
|
||||
@@ -215,7 +265,7 @@ export class UserService {
|
||||
if (response.code === 0) {
|
||||
const userData = response.data;
|
||||
return {
|
||||
id: userData.user_code || user_id || '',
|
||||
id: userData.id || '',
|
||||
nickname: userData.nickname || '',
|
||||
avatar: userData.avatar_url || '',
|
||||
join_date: userData.subscribe_time ? `${new Date(userData.subscribe_time).getFullYear()}年${new Date(userData.subscribe_time).getMonth() + 1}月加入` : '',
|
||||
@@ -226,12 +276,15 @@ export class UserService {
|
||||
participated: userData.stats?.participated_games_count || 0
|
||||
},
|
||||
|
||||
personal_profile: '',
|
||||
location: userData.city + userData.district|| '',
|
||||
occupation: '',
|
||||
ntrp_level: '',
|
||||
personal_profile: userData.personal_profile || '',
|
||||
occupation: userData.occupation || '',
|
||||
ntrp_level: userData.ntrp_level || '',
|
||||
phone: userData.phone || '',
|
||||
gender: userData.gender || ''
|
||||
gender: userData.gender || '',
|
||||
birthday: userData.birthday || '',
|
||||
country: userData.country || '',
|
||||
province: userData.province || '',
|
||||
city: userData.city || '',
|
||||
};
|
||||
} else {
|
||||
throw new Error(response.message || '获取用户信息失败');
|
||||
@@ -260,7 +313,6 @@ export class UserService {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 如果没有需要更新的字段,直接返回
|
||||
if (Object.keys(filtered_data).length === 0) {
|
||||
console.log('没有需要更新的字段');
|
||||
@@ -281,10 +333,10 @@ export class UserService {
|
||||
}
|
||||
|
||||
// 获取用户主办的球局
|
||||
static async get_hosted_games(user_id: string): Promise<any[]> {
|
||||
static async get_hosted_games(userId: string | number): Promise<any[]> {
|
||||
try {
|
||||
const response = await httpService.post<any>(API_CONFIG.USER.HOSTED_GAMES, {
|
||||
user_id
|
||||
userId
|
||||
}, {
|
||||
|
||||
showLoading: false
|
||||
@@ -304,10 +356,10 @@ export class UserService {
|
||||
}
|
||||
|
||||
// 获取用户参与的球局
|
||||
static async get_participated_games(user_id: string): Promise<any[]> {
|
||||
static async get_participated_games(userId: string | number): Promise<any[]> {
|
||||
try {
|
||||
const response = await httpService.post<any>(API_CONFIG.USER.PARTICIPATED_GAMES, {
|
||||
user_id
|
||||
userId
|
||||
}, {
|
||||
|
||||
showLoading: false
|
||||
@@ -328,7 +380,7 @@ export class UserService {
|
||||
}
|
||||
|
||||
// 获取用户球局记录(兼容旧方法)
|
||||
static async get_user_games(user_id: string, type: 'hosted' | 'participated'): Promise<any[]> {
|
||||
static async get_user_games(user_id: string | number, type: 'hosted' | 'participated'): Promise<any[]> {
|
||||
if (type === 'hosted') {
|
||||
return this.get_hosted_games(user_id);
|
||||
} else {
|
||||
@@ -337,10 +389,10 @@ export class UserService {
|
||||
}
|
||||
|
||||
// 关注/取消关注用户
|
||||
static async toggle_follow(user_id: string, is_following: boolean): Promise<boolean> {
|
||||
static async toggle_follow(following_id: string | number, is_following: boolean): Promise<boolean> {
|
||||
try {
|
||||
const endpoint = is_following ? API_CONFIG.USER.UNFOLLOW : API_CONFIG.USER.FOLLOW;
|
||||
const response = await httpService.post<any>(endpoint, { user_id }, {
|
||||
const response = await httpService.post<any>(endpoint, { following_id }, {
|
||||
|
||||
showLoading: true,
|
||||
loadingText: is_following ? '取消关注中...' : '关注中...'
|
||||
@@ -449,6 +501,57 @@ export class UserService {
|
||||
return require('../static/userInfo/default_avatar.svg');
|
||||
}
|
||||
}
|
||||
|
||||
// 解析用户手机号
|
||||
static async parse_phone(phone_code: string): Promise<string> {
|
||||
try {
|
||||
const response = await httpService.post<{ phone: string }>(API_CONFIG.USER.PARSE_PHONE, { phone_code }, {
|
||||
showLoading: true,
|
||||
loadingText: '获取手机号中...'
|
||||
});
|
||||
if (response.code === 0) {
|
||||
return response.data.phone || '';
|
||||
} else {
|
||||
throw new Error(response.message || '获取手机号失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取手机号失败:', error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// 获取职业树
|
||||
static async getProfessions(): Promise<[] | PickerOption[]> {
|
||||
try {
|
||||
const response = await httpService.post<any>(API_CONFIG.PROFESSIONS);
|
||||
const { code, data, message } = response;
|
||||
if (code === 0) {
|
||||
return formatOptions(data || []);
|
||||
} else {
|
||||
throw new Error(message || '获取职业树失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取职业树失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 获取城市树
|
||||
static async getCities(): Promise<[] | PickerOption[]> {
|
||||
try {
|
||||
const response = await httpService.post<any>(API_CONFIG.CITIS);
|
||||
const { code, data, message } = response;
|
||||
if (code === 0) {
|
||||
return formatOptions(data || []);
|
||||
} else {
|
||||
throw new Error(message || '获取城市树失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取职业树失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 从 loginService 移过来的用户相关方法
|
||||
@@ -501,3 +604,86 @@ export const get_user_info = (): any | null => {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 客服中心处理函数
|
||||
export const handleCustomerService = async (): Promise<void> => {
|
||||
try {
|
||||
// 获取当前环境的客服配置
|
||||
const config = getCurrentConfig;
|
||||
const { customerService } = config;
|
||||
|
||||
console.log('打开客服中心,配置信息:', customerService);
|
||||
|
||||
// 使用微信官方客服能力
|
||||
await Taro.openCustomerServiceChat({
|
||||
extInfo: {
|
||||
url: customerService.serviceUrl
|
||||
},
|
||||
corpId: customerService.corpId,
|
||||
success: (res) => {
|
||||
console.log('打开客服成功:', res);
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error('打开客服失败:', error);
|
||||
// 如果官方客服不可用,显示备用联系方式
|
||||
showCustomerServiceFallback(customerService);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('客服功能异常:', error);
|
||||
// 备用方案:显示联系信息
|
||||
showCustomerServiceFallback();
|
||||
}
|
||||
};
|
||||
|
||||
// 客服备用方案
|
||||
const showCustomerServiceFallback = (customerInfo?: any) => {
|
||||
const options = ['拨打客服电话', '复制邮箱地址'];
|
||||
|
||||
// 如果没有客服信息,只显示通用提示
|
||||
if (!customerInfo?.phoneNumber && !customerInfo?.email) {
|
||||
Taro.showModal({
|
||||
title: '联系客服',
|
||||
content: '如需帮助,请通过其他方式联系我们',
|
||||
showCancel: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Taro.showActionSheet({
|
||||
itemList: options,
|
||||
success: async (res) => {
|
||||
if (res.tapIndex === 0 && customerInfo?.phoneNumber) {
|
||||
// 拨打客服电话
|
||||
try {
|
||||
await Taro.makePhoneCall({
|
||||
phoneNumber: customerInfo.phoneNumber
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('拨打电话失败:', error);
|
||||
Taro.showToast({
|
||||
title: '拨打电话失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
} else if (res.tapIndex === 1 && customerInfo?.email) {
|
||||
// 复制邮箱地址
|
||||
try {
|
||||
await Taro.setClipboardData({
|
||||
data: customerInfo.email
|
||||
});
|
||||
Taro.showToast({
|
||||
title: '邮箱地址已复制',
|
||||
icon: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('复制邮箱失败:', error);
|
||||
Taro.showToast({
|
||||
title: '复制失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
115
src/services/walletService.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import httpService from "./httpService";
|
||||
|
||||
// 钱包信息接口
|
||||
export interface WalletInfo {
|
||||
balance: number;
|
||||
total_income: number;
|
||||
total_withdraw: number;
|
||||
}
|
||||
|
||||
// 交易记录接口
|
||||
export interface TransactionRecord {
|
||||
id: string;
|
||||
type: "withdraw" | "income" | "refund";
|
||||
amount: number;
|
||||
description: string;
|
||||
created_time: string;
|
||||
status: "pending" | "success" | "failed";
|
||||
}
|
||||
|
||||
// 提现请求接口
|
||||
export interface WithdrawRequest {
|
||||
amount: number;
|
||||
bank_id: string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export class WalletService {
|
||||
/**
|
||||
* 获取钱包信息
|
||||
*/
|
||||
static async get_wallet_info(): Promise<WalletInfo> {
|
||||
try {
|
||||
const response = await httpService.post("/api/wallet/get_info", {});
|
||||
|
||||
if (response.code === 200) {
|
||||
return response.data;
|
||||
} else {
|
||||
throw new Error(response.message || "获取钱包信息失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取钱包信息失败:", error);
|
||||
// 返回模拟数据
|
||||
return {
|
||||
balance: 1588.80,
|
||||
total_income: 3200.00,
|
||||
total_withdraw: 1611.20,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取交易记录
|
||||
*/
|
||||
static async get_transactions(): Promise<TransactionRecord[]> {
|
||||
try {
|
||||
const response = await httpService.post("/api/wallet/get_transactions", {});
|
||||
|
||||
if (response.code === 200) {
|
||||
return response.data;
|
||||
} else {
|
||||
throw new Error(response.message || "获取交易记录失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取交易记录失败:", error);
|
||||
// 返回模拟数据
|
||||
return [
|
||||
{
|
||||
id: "1",
|
||||
type: "income",
|
||||
amount: 150.00,
|
||||
description: "球局收入",
|
||||
created_time: "2024-12-20 14:30:00",
|
||||
status: "success",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
type: "withdraw",
|
||||
amount: 200.00,
|
||||
description: "提现",
|
||||
created_time: "2024-12-19 10:15:00",
|
||||
status: "success",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
type: "refund",
|
||||
amount: 80.00,
|
||||
description: "球局退款",
|
||||
created_time: "2024-12-18 16:45:00",
|
||||
status: "success",
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交提现申请
|
||||
*/
|
||||
static async submit_withdraw(request: WithdrawRequest): Promise<void> {
|
||||
try {
|
||||
const response = await httpService.post("/wallet/withdraw", {
|
||||
amount: request.amount,
|
||||
transfer_remark: "用户申请提现"
|
||||
});
|
||||
|
||||
if (response.code !== 200) {
|
||||
throw new Error(response.message || "提现申请提交失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("提现申请提交失败:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
4
src/static/detail/icon-sendup.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.0042 6.05029V18" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6 12L12 6L18 12" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 322 B |
4
src/static/detail/icon-write.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.625 15.75H16.125" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4.125 10.02V12.75H6.86895L14.625 4.99054L11.8857 2.25L4.125 10.02Z" stroke="white" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 354 B |
4
src/static/ntrp/ntrp_arrow_back.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.50002 10L17.5 10" stroke="black" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.5 15L2.5 10L7.5 5" stroke="black" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 338 B |
4
src/static/ntrp/ntrp_arrow_right.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.5 10H2.5" stroke="white" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12.5 5L17.5 10L12.5 15" stroke="white" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 334 B |
4
src/static/ntrp/ntrp_arrow_right_color.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="13" viewBox="0 0 12 13" fill="none">
|
||||
<path d="M10.5 6.5H1.5" stroke="#5CA693" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.5 3.5L10.5 6.5L7.5 9.5" stroke="#5CA693" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 298 B |
4
src/static/ntrp/ntrp_circle_checked.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="10" cy="10" r="10" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.36695 10.343C5.75135 9.94642 6.38444 9.93658 6.78099 10.321L8.84309 12.3199L13.1831 6.17329C13.5016 5.72214 14.1256 5.61464 14.5768 5.93319C15.0279 6.25174 15.1354 6.87571 14.8169 7.32687L9.80293 14.428C9.6344 14.6667 9.3699 14.8197 9.07898 14.8469C8.78805 14.8741 8.4998 14.7726 8.29001 14.5692L5.38894 11.757C4.99239 11.3726 4.98254 10.7395 5.36695 10.343Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 576 B |
3
src/static/ntrp/ntrp_circle_unchecked.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="10" cy="10" r="9.25" stroke="#161823" stroke-opacity="0.34" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 196 B |
4
src/static/ntrp/ntrp_close_icon.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.33325 9.33334L22.6666 22.6667" stroke="black" stroke-width="2.66667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.33325 22.6667L22.6666 9.33334" stroke="black" stroke-width="2.66667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 363 B |
12
src/static/ntrp/ntrp_doc_copy.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<svg width="55" height="55" viewBox="0 0 55 55" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_4962_28921)">
|
||||
<path d="M14.462 7.25007C14.6157 6.15625 15.6271 5.39415 16.7209 5.54788L36.5263 8.33134L45.0372 19.6258L41.1404 47.3533C40.9866 48.4471 39.9753 49.2092 38.8815 49.0554L11.154 45.1586C10.0601 45.0049 9.29805 43.9936 9.45178 42.8997L14.462 7.25007Z" stroke="#00E6AD" stroke-width="4" stroke-linejoin="round"/>
|
||||
<path d="M20.4364 22.2275L36.2807 24.4543" stroke="#00E6AD" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M19.3226 30.1494L35.1669 32.3762" stroke="#00E6AD" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_4962_28921">
|
||||
<rect width="48" height="48" fill="white" transform="translate(7.37561 0.195312) rotate(8)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 862 B |
5
src/static/ntrp/ntrp_download.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.5 10.0035V17.5H17.5V10" stroke="black" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13.75 9.58301L10 13.333L6.25 9.58301" stroke="black" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.99658 2.5V13.3333" stroke="black" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 479 B |
4
src/static/ntrp/ntrp_popup_close.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M7 7L17 17" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7 17L17 7" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 313 B |
4
src/static/ntrp/ntrp_re-action.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.18198 9.18198C8.36765 9.99633 7.24265 10.5 6 10.5C3.51472 10.5 1.5 8.48528 1.5 6C1.5 3.51472 3.51472 1.5 6 1.5C7.24265 1.5 8.36765 2.00368 9.18198 2.81803C9.59648 3.23253 10.5 4.25 10.5 4.25" stroke="black" stroke-opacity="0.85" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.5 2V4.25H8.25" stroke="black" stroke-opacity="0.85" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 507 B |
22
src/static/ntrp/ntrp_wechat.svg
Normal file
@@ -0,0 +1,22 @@
|
||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.52734 11.4374C1.52734 13.7338 2.74202 15.8003 4.6428 17.1972C4.79619 17.3071 4.89569 17.4895 4.89569 17.6946C4.89569 17.761 4.88118 17.8252 4.86459 17.8895C4.71329 18.4636 4.47075 19.3838 4.45831 19.4253C4.43967 19.4979 4.41063 19.5725 4.41063 19.6492C4.41063 19.817 4.54539 19.9538 4.7112 19.9538C4.77753 19.9538 4.82936 19.9289 4.88531 19.8979L6.85865 18.7413C7.00789 18.6543 7.16542 18.6004 7.33747 18.6004C7.43074 18.6004 7.51782 18.6149 7.60278 18.6398C8.52312 18.9092 9.516 19.0584 10.5462 19.0584C15.5251 19.0584 19.563 15.6469 19.563 11.4395C19.563 7.22993 15.5251 3.81836 10.5462 3.81836C5.56314 3.81629 1.52734 7.22784 1.52734 11.4374Z" fill="white"/>
|
||||
<path d="M1.52734 11.4374C1.52734 13.7338 2.74202 15.8003 4.6428 17.1972C4.79619 17.3071 4.89569 17.4895 4.89569 17.6946C4.89569 17.761 4.88118 17.8252 4.86459 17.8895C4.71329 18.4636 4.47075 19.3838 4.45831 19.4253C4.43967 19.4979 4.41063 19.5725 4.41063 19.6492C4.41063 19.817 4.54539 19.9538 4.7112 19.9538C4.77753 19.9538 4.82936 19.9289 4.88531 19.8979L6.85865 18.7413C7.00789 18.6543 7.16542 18.6004 7.33747 18.6004C7.43074 18.6004 7.51782 18.6149 7.60278 18.6398C8.52312 18.9092 9.516 19.0584 10.5462 19.0584C15.5251 19.0584 19.563 15.6469 19.563 11.4395C19.563 7.22993 15.5251 3.81836 10.5462 3.81836C5.5652 3.81629 1.52734 7.22784 1.52734 11.4374Z" fill="url(#paint0_linear_4962_26946)"/>
|
||||
<path d="M11.4062 17.0935C11.4062 20.5983 14.7684 23.4378 18.914 23.4378C19.7701 23.4378 20.5971 23.3134 21.3641 23.0896C21.4346 23.0689 21.5071 23.0565 21.5838 23.0565C21.7268 23.0565 21.8595 23.1 21.9818 23.1725L23.6256 24.1342C23.6711 24.1612 23.7147 24.1819 23.7706 24.1819C23.9095 24.1819 24.0215 24.0679 24.0215 23.929C24.0215 23.8669 23.9966 23.8026 23.9821 23.7425C23.9717 23.7073 23.7706 22.9404 23.6442 22.4616C23.6297 22.4077 23.6173 22.3559 23.6173 22.2999C23.6173 22.13 23.7002 21.9787 23.8266 21.8875C25.4102 20.7247 26.4218 19.0024 26.4218 17.0914C26.4218 13.5866 23.0597 10.7471 18.914 10.7471C14.7663 10.7492 11.4062 13.5887 11.4062 17.0935Z" fill="url(#paint1_linear_4962_26946)"/>
|
||||
<path d="M20.4937 15.1574C20.4937 15.7212 20.9434 16.1772 21.4989 16.1772C22.0543 16.1772 22.5041 15.7212 22.5041 15.1574C22.5041 14.5937 22.0543 14.1377 21.4989 14.1377C20.9434 14.1377 20.4937 14.5937 20.4937 15.1574Z" fill="#919191"/>
|
||||
<path d="M15.4692 15.1574C15.4692 15.7212 15.919 16.1772 16.4745 16.1772C17.0299 16.1772 17.4797 15.7212 17.4797 15.1574C17.4797 14.5937 17.0299 14.1377 16.4745 14.1377C15.919 14.1377 15.4692 14.5937 15.4692 15.1574Z" fill="#919191"/>
|
||||
<path d="M8.73456 8.98751C8.73456 9.6632 8.19561 10.2104 7.52817 10.2104C6.86279 10.2104 6.32178 9.6632 6.32178 8.98751C6.32178 8.31185 6.86073 7.76465 7.52817 7.76465C8.19561 7.76465 8.73456 8.31185 8.73456 8.98751Z" fill="#168743"/>
|
||||
<path d="M14.7639 8.98751C14.7639 9.6632 14.2228 10.2104 13.5575 10.2104C12.8921 10.2104 12.3511 9.6632 12.3511 8.98751C12.3511 8.31185 12.8921 7.76465 13.5575 7.76465C14.2228 7.76465 14.7639 8.31185 14.7639 8.98751Z" fill="#168743"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_4962_26946" x1="10.5439" y1="19.9517" x2="10.5439" y2="3.81689" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.0602" stop-color="#05CD66"/>
|
||||
<stop offset="0.2202" stop-color="#0ED169"/>
|
||||
<stop offset="0.4805" stop-color="#26DB6F"/>
|
||||
<stop offset="0.8069" stop-color="#4DEB7A"/>
|
||||
<stop offset="0.9517" stop-color="#61F380"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_4962_26946" x1="18.9141" y1="24.1841" x2="18.9141" y2="10.7484" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.081" stop-color="#D9D9D9"/>
|
||||
<stop offset="1" stop-color="#F0F0F0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
4
src/static/order/orderListClose.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.83325 5.83325L14.1666 14.1666" stroke="#A0A0A0" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5.83325 14.1666L14.1666 5.83325" stroke="#A0A0A0" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 367 B |