From ec8ee300d2a377fde8bafa2f3ce1c36167e95cca Mon Sep 17 00:00:00 2001 From: GaoNeng-wWw Date: Mon, 27 May 2024 14:09:46 +0800 Subject: [PATCH 1/4] feat(cashier): cashier modal --- app/(dashboard)/components/side-bar.tsx | 2 + app/(dashboard)/components/user-group.tsx | 11 ++ app/(dashboard)/dashboard/page.tsx | 9 +- app/(dashboard)/layout.tsx | 28 +++- app/(root)/authenticate/page.tsx | 17 ++- app/(root)/page.tsx | 6 +- app/store.ts | 3 +- components/pay-modal.tsx | 159 ++++++++++++++++++++++ components/primitives.ts | 1 + config/prices.tsx | 10 ++ hooks/useModal.tsx | 28 ++++ interface/userAPI.ts | 1 - providers/index.ts | 4 +- providers/pay.ts | 17 +++ styles/globals.css | 12 ++ 15 files changed, 292 insertions(+), 16 deletions(-) create mode 100644 app/(dashboard)/components/user-group.tsx create mode 100644 components/pay-modal.tsx create mode 100644 hooks/useModal.tsx create mode 100644 providers/pay.ts diff --git a/app/(dashboard)/components/side-bar.tsx b/app/(dashboard)/components/side-bar.tsx index bc20c20..5f8e940 100644 --- a/app/(dashboard)/components/side-bar.tsx +++ b/app/(dashboard)/components/side-bar.tsx @@ -10,6 +10,7 @@ import { IconType } from "react-icons"; import { FaImage, FaFolder } from "react-icons/fa"; import { HiOutlineArrowsUpDown } from "react-icons/hi2"; import { UploadProgress } from "./upload-progress"; +import UserGroup from "./user-group"; export interface SideBarItem { href: string; @@ -72,6 +73,7 @@ export function SideBar(){ : null } + ) diff --git a/app/(dashboard)/components/user-group.tsx b/app/(dashboard)/components/user-group.tsx new file mode 100644 index 0000000..851710f --- /dev/null +++ b/app/(dashboard)/components/user-group.tsx @@ -0,0 +1,11 @@ +import { showPayModal } from '@/app/store'; +import {Button} from '@nextui-org/button'; +import { useAtom } from 'jotai'; +export default function UserGroup(){ + const [isShow, setShow] = useAtom(showPayModal); + return ( + + ) +} \ No newline at end of file diff --git a/app/(dashboard)/dashboard/page.tsx b/app/(dashboard)/dashboard/page.tsx index d09feea..0acedcb 100644 --- a/app/(dashboard)/dashboard/page.tsx +++ b/app/(dashboard)/dashboard/page.tsx @@ -6,11 +6,12 @@ import { useEffect, useMemo, useState } from "react" import { PictureList } from "./components/picture-list"; import { Folder } from "./components/folder"; import { useAtom, useAtomValue, useSetAtom } from "jotai"; -import { currentFolderId, folderId } from "@/app/store"; +import { currentFolderId, folderId, profile } from "@/app/store"; import { Fold as IFold } from "@/providers/folder"; import { AxiosResponse } from "axios"; import { Upload } from "@/components/upload"; import { Message } from "@/components/message"; +import { UserAPI } from "@/interface/userAPI"; const foldCache = new Map(); const cache = new Map(); @@ -20,8 +21,8 @@ export default function Files(){ const setFoldIdStack = useSetAtom(folderId); const [folderInfo, setFolderInfo] = useState([]) const [children, setChildren] = useState([]) + const [,setProfile] = useAtom(profile); useEffect(()=>{ - Message.success('test') if (cache.has(id)){ setChildren(cache.get(id)!); return; @@ -38,6 +39,10 @@ export default function Files(){ }) } }, [id]) + useEffect(()=>{ + UserAPI.getExtendedInformation() + .then((val) => val ? setProfile(val) : null) + }, []) useEffect(()=>{ const folderInfo = []; const promiseStack = []; diff --git a/app/(dashboard)/layout.tsx b/app/(dashboard)/layout.tsx index f876aff..38def58 100644 --- a/app/(dashboard)/layout.tsx +++ b/app/(dashboard)/layout.tsx @@ -1,4 +1,5 @@ 'use client' +import "@/styles/globals.css"; import { BreadcrumbItem, Breadcrumbs, @@ -21,6 +22,9 @@ import IOC from "@/providers"; import { Message } from "@/components/message"; import { usePathname } from "next/navigation"; import { ToastProvider } from "../providers"; +import { PayModal } from "@/components/pay-modal"; +import { Toaster } from "react-hot-toast"; +import { rgba } from "color2k"; export default function DashboardLayout({ children, }: { @@ -59,10 +63,22 @@ export default function DashboardLayout({ }) } return ( -
@@ -79,7 +95,7 @@ export default function DashboardLayout({ } - {children} + {/* {children} */}
@@ -121,6 +137,8 @@ export default function DashboardLayout({ +
+ ) } \ No newline at end of file diff --git a/app/(root)/authenticate/page.tsx b/app/(root)/authenticate/page.tsx index 84953e9..acf6e50 100644 --- a/app/(root)/authenticate/page.tsx +++ b/app/(root)/authenticate/page.tsx @@ -12,6 +12,8 @@ import { useRouter } from 'next/navigation'; import { SetLoggedInState } from '@/interface/hooks'; import { UserAPI } from '@/interface/userAPI'; import { useDebounceFn } from 'ahooks'; +import { useAtom } from 'jotai'; +import { profile } from '@/app/store'; export type Colors = "default" | "primary" | "secondary" | "success" | "warning" | "danger" | undefined; export type PageType = 'wait-check' | 'login' | 'register' @@ -20,6 +22,7 @@ export default function Page(){ const [account, setAccount] = useState('') const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); + const [,setProfile] = useAtom(profile); const [accountExists, setAccountExists] = useState(false); const [code, setCode] = useState(''); const [passwordRobustness, setPasswordRobustness] = useState(new Array(6).fill(false)); @@ -127,13 +130,19 @@ export default function Page(){ UserAPI.login(account, password) .then((r) => { const [state, text] = r; - if (state) { + if (!state) { + Message.error(text); + setLoading(false) + } + return state + }) + .then((val) => val ? UserAPI.getExtendedInformation() : null) + .then((val) => { + if (val){ Message.success("登录成功"); SetLoggedInState(true); + setProfile(val); router.push("/dashboard"); - } else { - Message.error(text); - setLoading(false) } }) } diff --git a/app/(root)/page.tsx b/app/(root)/page.tsx index b63e993..620a671 100644 --- a/app/(root)/page.tsx +++ b/app/(root)/page.tsx @@ -5,13 +5,15 @@ import {Link} from "@nextui-org/link"; import {button as buttonStyles} from "@nextui-org/theme"; import {siteConfig} from "@/config/site"; import {subtitle, title} from "@/components/primitives"; -import {Card, CardFooter, CardHeader, Image} from "@nextui-org/react"; +import {Card, CardFooter, CardHeader, Image, useDisclosure, useModal} from "@nextui-org/react"; import {Button} from "@nextui-org/button"; import {HiOutlineNewspaper} from "react-icons/hi"; import {FaLocationArrow} from "react-icons/fa6"; -import {BsLightbulb} from "react-icons/bs"; +import {BsLightbulb, BsOpencollective} from "react-icons/bs"; import {IsLoggedIn} from "@/interface/hooks"; import ClientOnly from "@/components/ClientOnly"; +import { PayModal } from "@/components/pay-modal"; +import React, { useEffect } from "react"; export default function Home() { return ( diff --git a/app/store.ts b/app/store.ts index d99771a..bdca9e4 100644 --- a/app/store.ts +++ b/app/store.ts @@ -15,4 +15,5 @@ export const profile = atom(null); export const verify = atom(true); export const uploadStack = atom([]); export const currentFolderId = atom('1'); -export const folderId = atom([{id: '1', name: 'root'}]) \ No newline at end of file +export const folderId = atom([{id: '1', name: 'root'}]) +export const showPayModal = atom(true); \ No newline at end of file diff --git a/components/pay-modal.tsx b/components/pay-modal.tsx new file mode 100644 index 0000000..414891c --- /dev/null +++ b/components/pay-modal.tsx @@ -0,0 +1,159 @@ +'use client'; + +import { profile as ProfileAtom, showPayModal } from "@/app/store"; +import { Button } from "@nextui-org/button"; +import { Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from "@nextui-org/modal"; +import {Dropdown,DropdownTrigger,DropdownItem, DropdownMenu} from '@nextui-org/dropdown'; +import { useAtom, useAtomValue } from "jotai"; +import React, { useEffect, useMemo, useState } from "react"; +import { PriceInfo } from "./price"; +import { getNumberId, idTable, maxId, priceAdvanced, priceFree, priceProfessional, priceStarted } from "@/config/prices"; +import { title } from "./primitives"; +import { Message } from "./message"; +import IOC from "@/providers"; +interface PriceListProps { + onSelect?: ( + level: string, + )=>void, + pricesList: PriceInfo[] +} +const PriceList = (props: PriceListProps) => { + const profile = useAtomValue(ProfileAtom); + const {pricesList, onSelect} = props; + const [selectedId, setId] = useState( + Math.min(idTable[profile?.level.level.toUpperCase() ?? 'FREE']+1, maxId) + ); + const [qrVisibility, setVisibility] = useState(false); + const onWheel = (ev: React.WheelEvent) => { + ev.stopPropagation(); + ev.currentTarget.scrollTo({ + left: ev.currentTarget.scrollLeft + (70*(ev.deltaY / 100)), + behavior: "smooth" + }) + } + const selectLevel = (price: PriceInfo) => { + const {id} = price; + const priceId = idTable[id]; + const profileId = idTable[profile?.level.level.toUpperCase() ?? 'FREE']; + onSelect?.(price.id) + if (profileId >= priceId){ + Message.error('您不能降级购买用户组'); + return; + } + setId(priceId); + } + useEffect(()=>{ + setId( + Math.min(idTable[profile?.level.level.toUpperCase() ?? 'FREE'] + 1, maxId) + ) + }, [profile]); + return ( +
+ { + pricesList.map((price) => ( +
selectLevel(price)} + className=" + p-4 rounded-md flex flex-col items-center gap-2 bg-default/80 aria-disabled:bg-default/50 + aria-selected:bg-primary aria-[disabled=false]:cursor-pointer + group + " + > +

{price.plainName}

+

+ { price.price === 0 ? '免费' : `${price.price}元/月` } +

+
+ )) + } +
+ ) +} + +type PayQrProps = { + period: string; + level: string; +} + +const PayQr = (props:PayQrProps) => { + const {period:rawPeriod, level} = props; + const period = Number.isNaN(parseInt(rawPeriod)) ? 1 : parseInt(rawPeriod); + const date = new Date().toString(); + const [qrUrl, setQRUrl] = useState(''); + useEffect(()=>{ + IOC.pay.wechat({ + level, + period, + start_date: date.toString() + }) + .then((res) => {console.log(res)}) + },[]) + return (<>) +} + +export function PayModal(){ + const [isOpen, setOpen] = useAtom(showPayModal); + const profile = useAtomValue(ProfileAtom); + const [level, setLevel] = useState(profile?.level.level ?? 'FREE'); + const [qrVisibility, setQRVisibility] = useState(false); + const [periods, setPeriods] = React.useState(new Set(["1"])); + const period = useMemo(() => Array.from(periods)[0], [periods]); + useEffect(()=>{ + setQRVisibility( + getNumberId(profile?.level.level ?? 'FREE') < getNumberId(level) + ) + }, [level, profile?.level.level]); + const close = () => { + setOpen(false) + } + const onSelectLevel = (id: string) => setLevel(id); + const prices = [priceFree, priceStarted, priceAdvanced, priceProfessional] + return ( + + + + 定价 + + + + { + qrVisibility ? : null + } + + + + + + + + + + 1月 + + + 3月 + + + 6月 + + + 12月 + + + + + + + ) +} \ No newline at end of file diff --git a/components/primitives.ts b/components/primitives.ts index 76c41a3..bbd2444 100644 --- a/components/primitives.ts +++ b/components/primitives.ts @@ -13,6 +13,7 @@ export const title = tv({ foreground: "dark:from-[#FFFFFF] dark:to-[#4B4B4B]", }, size: { + xs: 'text-2xl', sm: "text-3xl lg:text-4xl", md: "text-[2.3rem] lg:text-5xl leading-9", lg: "text-4xl lg:text-6xl", diff --git a/config/prices.tsx b/config/prices.tsx index 753b2d0..160de25 100644 --- a/config/prices.tsx +++ b/config/prices.tsx @@ -1,6 +1,13 @@ import {PriceInfo} from '@/components/price'; import {PiFireDuotone} from "react-icons/pi"; +export const idTable:{[x:string]: number} = { + FREE: 0, + STARTED: 1, + ADVANCED: 2, + PROFESSIONAL: 3 +} + export const priceFree: PriceInfo = { id: 'FREE', name: "免费", @@ -51,6 +58,9 @@ export function getDisabledById(id: string){ const disabled = item.disabled ?? ['all']; return disabled.map((disabledFeature) => disabledFeature.toUpperCase()).filter((feature)=>feature.toLowerCase()!=='none'); } +export const getNameById = (id: string) => prices.filter((p) => p.id === id.toUpperCase())[0].plainName; +export const getNumberId = (id: string) => idTable[id.toUpperCase() ?? 'FREE'] +export const maxId = Object.entries(idTable).sort((a,b) => b[1] - a[1])[0][1] export function getGroupPrice(name: string): PriceInfo { let lower = name.toLowerCase(); diff --git a/hooks/useModal.tsx b/hooks/useModal.tsx new file mode 100644 index 0000000..50387a5 --- /dev/null +++ b/hooks/useModal.tsx @@ -0,0 +1,28 @@ +import {Modal, ModalBody, ModalContent,ModalFooter, useDisclosure} from '@nextui-org/modal'; +import React, { useEffect, useState } from "react"; + +export type UseModalBaseProps = { + visible: boolean, + props: T +} + +export function useModal(options: Partial> | null, Slot: React.FC){ + const {visible:_visible, props} = options ?? {visible: true, props: {}}; + const {isOpen, onOpenChange, onClose} = useDisclosure({isOpen: _visible}); + const Component = () => { + const ref = React.useRef(); + const close = () => { + console.log('close'); + onClose() + console.log(isOpen) + }; + return ( + + + + ); + } + return { + Component + } +} \ No newline at end of file diff --git a/interface/userAPI.ts b/interface/userAPI.ts index a69b642..ce28633 100644 --- a/interface/userAPI.ts +++ b/interface/userAPI.ts @@ -66,7 +66,6 @@ export class UserAPI { let requestOptions: RequestInit = { method: 'GET', - credentials: 'include', redirect: 'follow', headers: headers }; diff --git a/providers/index.ts b/providers/index.ts index 5633f5a..bc74af3 100644 --- a/providers/index.ts +++ b/providers/index.ts @@ -1,6 +1,7 @@ import { Certify } from './certify'; import { Folder } from './folder'; import {http} from './http'; +import { Pay } from './pay'; import {Picture} from './picture'; import { Share } from './share'; import {User} from './user'; @@ -10,7 +11,8 @@ const IOC = { picture: new Picture(http), certify: new Certify(http), share: new Share(http), - fold: new Folder(http) + fold: new Folder(http), + pay: new Pay(http) } export default IOC; diff --git a/providers/pay.ts b/providers/pay.ts new file mode 100644 index 0000000..2ef920a --- /dev/null +++ b/providers/pay.ts @@ -0,0 +1,17 @@ +import { Axios } from "axios"; + +interface WechatPayParams { + level: string, + period: number, + start_date: string; +} + +export class Pay { + private axios: Axios; + constructor(axios: Axios){ + this.axios = axios; + } + wechat(param: WechatPayParams){ + return this.axios.post('/pay/wechat', param) + } +} \ No newline at end of file diff --git a/styles/globals.css b/styles/globals.css index b5c61c9..ee0d77b 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -1,3 +1,15 @@ @tailwind base; @tailwind components; @tailwind utilities; +@layer utilities { + /* Hide scrollbar for Chrome, Safari and Opera */ + .no-scrollbar::-webkit-scrollbar { + display: none; + } + + /* Hide scrollbar for IE, Edge and Firefox */ + .no-scrollbar { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } +} From c2e78827cf9eb4fb69188d3ccfee0011a7e0ef93 Mon Sep 17 00:00:00 2001 From: GaoNeng-wWw Date: Wed, 29 May 2024 11:06:48 +0800 Subject: [PATCH 2/4] fix: periods select --- app/store.ts | 2 +- components/pay-modal.tsx | 52 +++++++++++++++++++++++++++------------- hooks/useCamelCase.ts | 15 ++++++++++++ 3 files changed, 51 insertions(+), 18 deletions(-) create mode 100644 hooks/useCamelCase.ts diff --git a/app/store.ts b/app/store.ts index bdca9e4..e425cc5 100644 --- a/app/store.ts +++ b/app/store.ts @@ -16,4 +16,4 @@ export const verify = atom(true); export const uploadStack = atom([]); export const currentFolderId = atom('1'); export const folderId = atom([{id: '1', name: 'root'}]) -export const showPayModal = atom(true); \ No newline at end of file +export const showPayModal = atom(false); \ No newline at end of file diff --git a/components/pay-modal.tsx b/components/pay-modal.tsx index 414891c..b200cdc 100644 --- a/components/pay-modal.tsx +++ b/components/pay-modal.tsx @@ -5,15 +5,16 @@ import { Button } from "@nextui-org/button"; import { Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from "@nextui-org/modal"; import {Dropdown,DropdownTrigger,DropdownItem, DropdownMenu} from '@nextui-org/dropdown'; import { useAtom, useAtomValue } from "jotai"; -import React, { useEffect, useMemo, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react" import { PriceInfo } from "./price"; import { getNumberId, idTable, maxId, priceAdvanced, priceFree, priceProfessional, priceStarted } from "@/config/prices"; import { title } from "./primitives"; import { Message } from "./message"; import IOC from "@/providers"; +import { camelCase } from "@/hooks/useCamelCase"; interface PriceListProps { onSelect?: ( - level: string, + price: PriceInfo )=>void, pricesList: PriceInfo[] } @@ -35,11 +36,11 @@ const PriceList = (props: PriceListProps) => { const {id} = price; const priceId = idTable[id]; const profileId = idTable[profile?.level.level.toUpperCase() ?? 'FREE']; - onSelect?.(price.id) if (profileId >= priceId){ Message.error('您不能降级购买用户组'); return; } + onSelect?.(price) setId(priceId); } useEffect(()=>{ @@ -47,6 +48,10 @@ const PriceList = (props: PriceListProps) => { Math.min(idTable[profile?.level.level.toUpperCase() ?? 'FREE'] + 1, maxId) ) }, [profile]); + useEffect(()=>{ + const price = pricesList[Math.min(selectedId, maxId)] + selectLevel(price); + }, []); return (
{ @@ -79,36 +84,44 @@ type PayQrProps = { const PayQr = (props:PayQrProps) => { const {period:rawPeriod, level} = props; - const period = Number.isNaN(parseInt(rawPeriod)) ? 1 : parseInt(rawPeriod); - const date = new Date().toString(); + const period = useMemo(() => Number.isNaN(parseInt(rawPeriod)) ? 1 : parseInt(rawPeriod), [rawPeriod]); + // const period = + const date = new Date(); const [qrUrl, setQRUrl] = useState(''); useEffect(()=>{ - IOC.pay.wechat({ - level, - period, - start_date: date.toString() - }) - .then((res) => {console.log(res)}) - },[]) + // IOC.pay.wechat({ + // level, + // period, + // start_date: date.toISOString() + // }) + // .then((res) => {console.log(res)}) + },[level, period]) return (<>) } export function PayModal(){ const [isOpen, setOpen] = useAtom(showPayModal); const profile = useAtomValue(ProfileAtom); - const [level, setLevel] = useState(profile?.level.level ?? 'FREE'); + const [level, setLevel] = useState(profile?.level.level ?? 'Free'); const [qrVisibility, setQRVisibility] = useState(false); const [periods, setPeriods] = React.useState(new Set(["1"])); - const period = useMemo(() => Array.from(periods)[0], [periods]); + const period = useMemo(() => Array.from(periods).join(''), [periods]); + useEffect(()=>{ + console.log(period); + },[period]) useEffect(()=>{ setQRVisibility( - getNumberId(profile?.level.level ?? 'FREE') < getNumberId(level) + getNumberId(profile?.level.level ?? 'Free') < getNumberId(level) ) }, [level, profile?.level.level]); const close = () => { setOpen(false) } - const onSelectLevel = (id: string) => setLevel(id); + const onSelectLevel = (price: PriceInfo) => { + setLevel( + camelCase(price.id, true) + ) + }; const prices = [priceFree, priceStarted, priceAdvanced, priceProfessional] return ( @@ -134,7 +147,12 @@ export function PayModal(){ { + setPeriods( + new Set(Array.from(val as Set)) + ) + }} selectedKeys={periods} selectionMode="single" > diff --git a/hooks/useCamelCase.ts b/hooks/useCamelCase.ts new file mode 100644 index 0000000..5f5dace --- /dev/null +++ b/hooks/useCamelCase.ts @@ -0,0 +1,15 @@ +export const camelCase = (val: string, pascalCase:boolean=false) => { + if (!val.includes('-')){ + if (!pascalCase){ + return val.toLowerCase(); + } + return `${val[0].toUpperCase()}${val.toLowerCase().slice(1)}` + } + const arr = val.split('-'); + for (let i=0;i 0) + } + return arr.join(''); +} \ No newline at end of file From ca0db3cc9a7e0e5ae1a54138b0c3d58ae1daca74 Mon Sep 17 00:00:00 2001 From: GaoNeng-wWw Date: Wed, 29 May 2024 14:17:08 +0800 Subject: [PATCH 3/4] feat: pay modal --- components/pay-modal.tsx | 169 +++++++++++++++++++++++++-------------- components/primitives.ts | 3 +- config/prices.tsx | 2 +- hooks/useDate.ts | 25 ++++++ package.json | 1 + providers/http.ts | 34 ++++++++ providers/user.ts | 4 +- 7 files changed, 175 insertions(+), 63 deletions(-) create mode 100644 hooks/useDate.ts diff --git a/components/pay-modal.tsx b/components/pay-modal.tsx index b200cdc..116228f 100644 --- a/components/pay-modal.tsx +++ b/components/pay-modal.tsx @@ -1,26 +1,34 @@ 'use client'; -import { profile as ProfileAtom, showPayModal } from "@/app/store"; +import { profile, profile as ProfileAtom, showPayModal } from "@/app/store"; import { Button } from "@nextui-org/button"; import { Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from "@nextui-org/modal"; import {Dropdown,DropdownTrigger,DropdownItem, DropdownMenu} from '@nextui-org/dropdown'; import { useAtom, useAtomValue } from "jotai"; import React, { useEffect, useMemo, useState } from "react" import { PriceInfo } from "./price"; -import { getNumberId, idTable, maxId, priceAdvanced, priceFree, priceProfessional, priceStarted } from "@/config/prices"; +import { getNumberId, idTable, maxId, priceAdvanced, priceFree, priceProfessional, prices, priceStarted } from "@/config/prices"; import { title } from "./primitives"; import { Message } from "./message"; import IOC from "@/providers"; import { camelCase } from "@/hooks/useCamelCase"; +import { FaAngleDown } from "react-icons/fa"; +import { useDate } from "@/hooks/useDate"; +import {QRCodeCanvas} from 'qrcode.react'; + interface PriceListProps { onSelect?: ( - price: PriceInfo + price: PriceInfo | null, + period: string )=>void, pricesList: PriceInfo[] } const PriceList = (props: PriceListProps) => { const profile = useAtomValue(ProfileAtom); const {pricesList, onSelect} = props; + const [periods, setPeriods] = useState(new Set(["1"])); + const [activePrice, setActivePrice] = useState(null) + const period = useMemo(() => Array.from(periods).join(''), [periods]); const [selectedId, setId] = useState( Math.min(idTable[profile?.level.level.toUpperCase() ?? 'FREE']+1, maxId) ); @@ -40,7 +48,8 @@ const PriceList = (props: PriceListProps) => { Message.error('您不能降级购买用户组'); return; } - onSelect?.(price) + setActivePrice(price); + // onSelect?.(price) setId(priceId); } useEffect(()=>{ @@ -48,9 +57,13 @@ const PriceList = (props: PriceListProps) => { Math.min(idTable[profile?.level.level.toUpperCase() ?? 'FREE'] + 1, maxId) ) }, [profile]); + useEffect(()=>{ + onSelect?.(activePrice, period) + }, [activePrice, period]); useEffect(()=>{ const price = pricesList[Math.min(selectedId, maxId)] selectLevel(price); + setActivePrice(price) }, []); return (
@@ -63,13 +76,47 @@ const PriceList = (props: PriceListProps) => { className=" p-4 rounded-md flex flex-col items-center gap-2 bg-default/80 aria-disabled:bg-default/50 aria-selected:bg-primary aria-[disabled=false]:cursor-pointer - group + group h-fit " >

{price.plainName}

-

- { price.price === 0 ? '免费' : `${price.price}元/月` } -

+ { + selectedId === idTable[price.id] ? ( +
+ + + + + { + setPeriods( + new Set(Array.from(val as Set)) + ) + }} + selectedKeys={periods} + selectionMode="single" + > + + 1月 + + + 3月 + + + 6月 + + + 12月 + + + +
+ ) : null + }
)) } @@ -85,18 +132,58 @@ type PayQrProps = { const PayQr = (props:PayQrProps) => { const {period:rawPeriod, level} = props; const period = useMemo(() => Number.isNaN(parseInt(rawPeriod)) ? 1 : parseInt(rawPeriod), [rawPeriod]); - // const period = - const date = new Date(); const [qrUrl, setQRUrl] = useState(''); + const date = useDate(); + const [money, setMoney] = useState( + prices.filter((p) => p.id.toLowerCase() === level.toLowerCase())[0].price * period + ) + const [,setProfile] = useAtom(profile) + const updateUserInfo = () => { + IOC.user.getExtendedInformation() + .then((res) => res.data) + .then((res)=>{ + console.log(res); + return res; + }) + .then((data) => setProfile(data)) + } useEffect(()=>{ - // IOC.pay.wechat({ - // level, - // period, - // start_date: date.toISOString() - // }) - // .then((res) => {console.log(res)}) + setMoney( + prices.filter((p) => p.id.toLowerCase() === level.toLowerCase())[0].price * period + ) + const {year, month, day} = date.getDateObject() + IOC.pay.wechat({ + level, + period, + start_date: date.toString('-', `${year}`, `${month}`, `${day}`) + }) + .then((res) => { + setQRUrl(res.data) + }) },[level, period]) - return (<>) + return ( +
+
+ +
+ 总计: {money}元 +
+ +
+
+ + 支付后则表示您知晓且同意 + +
+ +
+
+
+ ) } export function PayModal(){ @@ -104,8 +191,7 @@ export function PayModal(){ const profile = useAtomValue(ProfileAtom); const [level, setLevel] = useState(profile?.level.level ?? 'Free'); const [qrVisibility, setQRVisibility] = useState(false); - const [periods, setPeriods] = React.useState(new Set(["1"])); - const period = useMemo(() => Array.from(periods).join(''), [periods]); + const [period, setPeriods] = useState('1'); useEffect(()=>{ console.log(period); },[period]) @@ -114,13 +200,14 @@ export function PayModal(){ getNumberId(profile?.level.level ?? 'Free') < getNumberId(level) ) }, [level, profile?.level.level]); - const close = () => { - setOpen(false) - } - const onSelectLevel = (price: PriceInfo) => { + const onSelectLevel = (price: PriceInfo | null, period: string) => { + if (!price){ + return; + } setLevel( camelCase(price.id, true) ) + setPeriods(period) }; const prices = [priceFree, priceStarted, priceAdvanced, priceProfessional] return ( @@ -135,42 +222,6 @@ export function PayModal(){ qrVisibility ? : null } - - - - - - - { - setPeriods( - new Set(Array.from(val as Set)) - ) - }} - selectedKeys={periods} - selectionMode="single" - > - - 1月 - - - 3月 - - - 6月 - - - 12月 - - - -
) diff --git a/components/primitives.ts b/components/primitives.ts index bbd2444..7ad2c12 100644 --- a/components/primitives.ts +++ b/components/primitives.ts @@ -6,13 +6,14 @@ export const title = tv({ color: { violet: "from-[#FF1CF7] to-[#b249f8]", yellow: "from-[#FF705B] to-[#FFB457]", - blue: "from-[#5EA2EF] to-[#0072F5]", + blue: "from-[#2E8FFF] to-[#0072F5]", cyan: "from-[#00b7fa] to-[#01cfea]", green: "from-[#6FEE8D] to-[#17c964]", pink: "from-[#FF72E1] to-[#F54C7A]", foreground: "dark:from-[#FFFFFF] dark:to-[#4B4B4B]", }, size: { + base: 'text-base', xs: 'text-2xl', sm: "text-3xl lg:text-4xl", md: "text-[2.3rem] lg:text-5xl leading-9", diff --git a/config/prices.tsx b/config/prices.tsx index 160de25..23d6a13 100644 --- a/config/prices.tsx +++ b/config/prices.tsx @@ -51,7 +51,7 @@ export const priceProfessional: PriceInfo = { disabled: ["none"] } -const prices = [priceFree,priceStarted,priceAdvanced,priceProfessional] as const; +export const prices = [priceFree,priceStarted,priceAdvanced,priceProfessional] as const; export function getDisabledById(id: string){ const item = prices.filter((price) => price.id === id.toUpperCase())[0]; diff --git a/hooks/useDate.ts b/hooks/useDate.ts new file mode 100644 index 0000000..e9b56a1 --- /dev/null +++ b/hooks/useDate.ts @@ -0,0 +1,25 @@ +export const useDate = () => { + const toString = ( + splitor: string = '-', + year: string, + month: string, + day: string + ) => { + return [year, month.padStart(2,'0'), day.padStart(2,'0')].join(splitor); + } + const getDateObject = () => { + const date = new Date(); + const year = date.getFullYear(); + const month = date.getMonth()+1; + const day = date.getDate(); + return { + year, + month, + day + } + } + return { + getDateObject, + toString, + } +} \ No newline at end of file diff --git a/package.json b/package.json index 89eac94..f9bcd82 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "next": "13.5.1", "next-themes": "^0.2.1", "postcss": "8.4.31", + "qs": "^6.12.1", "react": "18.2.0", "react-cookies": "^0.1.1", "react-device-detect": "^2.2.3", diff --git a/providers/http.ts b/providers/http.ts index d7d6f80..0f0f119 100644 --- a/providers/http.ts +++ b/providers/http.ts @@ -1,6 +1,38 @@ import {Message} from "@/components/message"; import axios, {AxiosError} from "axios"; import cookie from 'react-cookies'; +import qs from 'qs'; + +// 用于存储pending的请求(处理多条相同请求) +const pendingRequest = new Map() + +// 生成request的唯一key +const generateRequestKey = (config: any) => { + // 通过url,method,params,data生成唯一key,用于判断是否重复请求 + // params为get请求参数,data为post请求参数 + const { url, method, params, data } = config + return [url, method, qs.stringify(params), qs.stringify(data)].join('&') +} + +// 将重复请求添加到pendingRequest中 +const addPendingRequest = (config: any) => { + const key = generateRequestKey(config) + if (!pendingRequest.has(key)) { + config.cancelToken = new axios.CancelToken(cancel => { + pendingRequest.set(key, cancel) + }) + } +} + +// 取消重复请求 +const removePendingRequest = (config: any) => { + const key = generateRequestKey(config) + if (pendingRequest.has(key)) { + const cancelToken = pendingRequest.get(key) + cancelToken(key) // 取消之前发送的请求 + pendingRequest.delete(key)// 请求对象中删除requestKey + } +} export const http = axios.create({ baseURL: process.env.NODE_ENV === 'development' ? '/api' : process.env.NEXT_PUBLIC_API_SERVER, @@ -16,6 +48,8 @@ http.interceptors.request.use( config.headers.token = token; } } + removePendingRequest(config); + addPendingRequest(config); return config; }, (err) => { diff --git a/providers/user.ts b/providers/user.ts index ab728e9..9ec7cea 100644 --- a/providers/user.ts +++ b/providers/user.ts @@ -1,4 +1,4 @@ -import {CheckCodeData, LoginData, RegisterRequestData} from "@/interface/model/user"; +import {CheckCodeData, LoginData, RegisterRequestData, UserModel} from "@/interface/model/user"; import {Axios} from "axios"; export class User { @@ -25,7 +25,7 @@ export class User { } async getExtendedInformation() { - return this.axios.get('/user?extended=true'); + return this.axios.get('/user?extended=true'); } async loginSession() { From 2f67e27d41fc207c8d6a672d9a65d13fb751cace Mon Sep 17 00:00:00 2001 From: GaoNeng-wWw Date: Sun, 16 Jun 2024 21:46:05 +0800 Subject: [PATCH 4/4] feat(cashier): redesign --- app/(dashboard)/layout.tsx | 2 +- components/pay-modal.tsx | 137 ++++++++++++++++--------------------- components/primitives.ts | 42 ++++++++++++ 3 files changed, 101 insertions(+), 80 deletions(-) diff --git a/app/(dashboard)/layout.tsx b/app/(dashboard)/layout.tsx index 38def58..ed18b15 100644 --- a/app/(dashboard)/layout.tsx +++ b/app/(dashboard)/layout.tsx @@ -139,6 +139,6 @@ export default function DashboardLayout({ - + ) } \ No newline at end of file diff --git a/components/pay-modal.tsx b/components/pay-modal.tsx index 116228f..b6f28a9 100644 --- a/components/pay-modal.tsx +++ b/components/pay-modal.tsx @@ -1,14 +1,14 @@ 'use client'; import { profile, profile as ProfileAtom, showPayModal } from "@/app/store"; -import { Button } from "@nextui-org/button"; +import { Button, ButtonGroup } from "@nextui-org/button"; import { Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from "@nextui-org/modal"; import {Dropdown,DropdownTrigger,DropdownItem, DropdownMenu} from '@nextui-org/dropdown'; import { useAtom, useAtomValue } from "jotai"; import React, { useEffect, useMemo, useState } from "react" import { PriceInfo } from "./price"; import { getNumberId, idTable, maxId, priceAdvanced, priceFree, priceProfessional, prices, priceStarted } from "@/config/prices"; -import { title } from "./primitives"; +import { bg, title } from "./primitives"; import { Message } from "./message"; import IOC from "@/providers"; import { camelCase } from "@/hooks/useCamelCase"; @@ -19,7 +19,6 @@ import {QRCodeCanvas} from 'qrcode.react'; interface PriceListProps { onSelect?: ( price: PriceInfo | null, - period: string )=>void, pricesList: PriceInfo[] } @@ -58,7 +57,7 @@ const PriceList = (props: PriceListProps) => { ) }, [profile]); useEffect(()=>{ - onSelect?.(activePrice, period) + onSelect?.(activePrice) }, [activePrice, period]); useEffect(()=>{ const price = pricesList[Math.min(selectedId, maxId)] @@ -66,57 +65,23 @@ const PriceList = (props: PriceListProps) => { setActivePrice(price) }, []); return ( -
+
{ pricesList.map((price) => (
selectLevel(price)} - className=" - p-4 rounded-md flex flex-col items-center gap-2 bg-default/80 aria-disabled:bg-default/50 - aria-selected:bg-primary aria-[disabled=false]:cursor-pointer - group h-fit - " + className={ + ` + border p-4 rounded-md flex flex-col items-center gap-2 border-default bg-default-100 aria-disabled:border-default/50 + aria-[disabled=false]:cursor-pointer aria-selected:bg-primary aria-selected:border-primary-100 + group h-fit aria-selected:text-white + ` + } > -

{price.plainName}

- { - selectedId === idTable[price.id] ? ( -
- - - - - { - setPeriods( - new Set(Array.from(val as Set)) - ) - }} - selectedKeys={periods} - selectionMode="single" - > - - 1月 - - - 3月 - - - 6月 - - - 12月 - - - -
- ) : null - } +

{price.plainName}

+

¥{price.price}元/月

)) } @@ -125,19 +90,27 @@ const PriceList = (props: PriceListProps) => { } type PayQrProps = { - period: string; level: string; + onClickPayDone?: () => void } const PayQr = (props:PayQrProps) => { - const {period:rawPeriod, level} = props; - const period = useMemo(() => Number.isNaN(parseInt(rawPeriod)) ? 1 : parseInt(rawPeriod), [rawPeriod]); + const {level} = props; + const [period, setPeriods] = useState<1|4|12>(1); + const periodString = { + [1]: '月', + [4]: '季', + [12]: '年' + } const [qrUrl, setQRUrl] = useState(''); const date = useDate(); - const [money, setMoney] = useState( - prices.filter((p) => p.id.toLowerCase() === level.toLowerCase())[0].price * period - ) - const [,setProfile] = useAtom(profile) + const money = useMemo(()=>{ + return prices.filter((p) => p.id.toLowerCase() === level.toLowerCase())[0].price * period; + }, [period, level]); + const moneyPerMonth = useMemo(()=>{ + return prices.filter((p) => p.id.toLowerCase() === level.toLowerCase())[0].price; + }, [level]); + const [profileData,setProfile] = useAtom(profile) const updateUserInfo = () => { IOC.user.getExtendedInformation() .then((res) => res.data) @@ -145,12 +118,15 @@ const PayQr = (props:PayQrProps) => { console.log(res); return res; }) - .then((data) => setProfile(data)) + .then((data) => { + if (data.level.level.toLowerCase() === profileData?.level.level.toLowerCase()){ + Message.error('支付失败') + } + setProfile(data) + props.onClickPayDone?.(); + }) } useEffect(()=>{ - setMoney( - prices.filter((p) => p.id.toLowerCase() === level.toLowerCase())[0].price * period - ) const {year, month, day} = date.getDateObject() IOC.pay.wechat({ level, @@ -163,24 +139,30 @@ const PayQr = (props:PayQrProps) => { },[level, period]) return (
-
- -
- 总计: {money}元 -
- -
-
- - 支付后则表示您知晓且同意 - +
+
+
+ ¥{moneyPerMonth}/月
-
- ToYou 虚拟产品购买协议 + + + + + +
+ 合计¥{money}/{periodString[period]}
+
+ + 请示用微信扫码支付 +
) @@ -192,15 +174,12 @@ export function PayModal(){ const [level, setLevel] = useState(profile?.level.level ?? 'Free'); const [qrVisibility, setQRVisibility] = useState(false); const [period, setPeriods] = useState('1'); - useEffect(()=>{ - console.log(period); - },[period]) useEffect(()=>{ setQRVisibility( getNumberId(profile?.level.level ?? 'Free') < getNumberId(level) ) }, [level, profile?.level.level]); - const onSelectLevel = (price: PriceInfo | null, period: string) => { + const onSelectLevel = (price: PriceInfo | null) => { if (!price){ return; } @@ -219,7 +198,7 @@ export function PayModal(){ { - qrVisibility ? : null + qrVisibility ? setOpen(false)} /> : null } diff --git a/components/primitives.ts b/components/primitives.ts index 7ad2c12..16299e8 100644 --- a/components/primitives.ts +++ b/components/primitives.ts @@ -1,5 +1,47 @@ import {tv} from "tailwind-variants"; +export const bg = tv({ + base: "tracking-tight inline font-bold", + variants: { + color: { + violet: "bg-gradient-to-r from-[#FF1CF7] to-[#b249f8]", + yellow: "bg-gradient-to-r from-[#FF705B] to-[#FFB457]", + blue: "bg-gradient-to-r from-[#2E8FFF] to-[#0072F5]", + cyan: "bg-gradient-to-r from-[#00b7fa] to-[#01cfea]", + green: "bg-gradient-to-r from-[#6FEE8D] to-[#17c964]", + pink: "bg-gradient-to-r from-[#FF72E1] to-[#F54C7A]", + foreground: "bg-gradient-to-r dark:from-[#FFFFFF] dark:to-[#4B4B4B]", + }, + size: { + base: 'text-base', + xs: 'text-2xl', + sm: "text-3xl lg:text-4xl", + md: "text-[2.3rem] lg:text-5xl leading-9", + lg: "text-4xl lg:text-6xl", + }, + fullWidth: { + true: "w-full block", + }, + }, + defaultVariants: { + size: "md", + }, + compoundVariants: [ + { + color: [ + "violet", + "yellow", + "blue", + "cyan", + "green", + "pink", + "foreground", + ], + // class: "bg-gradient-to-b", + }, + ], +}) + export const title = tv({ base: "tracking-tight inline font-bold", variants: {