补充个人中心页面api & 首页弹唱查看活动 & 联系客服
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
@@ -7,6 +8,11 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
|
||||
// 参考 juipnet Host/Views/Home/Index.cshtml 公告弹窗:
|
||||
// 查看活动 -> /product 产品页;联系客服 -> 企点QQ客服二维码页
|
||||
const CUSTOMER_SERVICE_URL =
|
||||
"https://admin.qidian.qq.com/template/blue/mp/menu/qr-code-jump.html?linkType=0&env=ol&kfuin=2852138148&fid=299&key=f377ec024ca45115a03a7632c7bda230&cate=1&source=&isLBS=&isCustomEntry=&type=16&ftype=1&_type=wpa&qidian=true&_pid=l1gunh.657dox.kjy6xnny"
|
||||
|
||||
interface AnnouncementModalProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
@@ -16,6 +22,18 @@ export default function AnnouncementModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: AnnouncementModalProps) {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const handleViewActivity = () => {
|
||||
onOpenChange(false)
|
||||
navigate("/product")
|
||||
}
|
||||
|
||||
const handleContactService = () => {
|
||||
onOpenChange(false)
|
||||
window.open(CUSTOMER_SERVICE_URL, "_blank", "noopener,noreferrer")
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
@@ -50,13 +68,19 @@ export default function AnnouncementModal({
|
||||
|
||||
<div className="flex mt-2">
|
||||
<DialogClose asChild className="flex-1">
|
||||
<Button className="w-full h-14 rounded-none bg-linear-to-r from-blue-500 to-cyan-400 hover:opacity-90 text-white text-lg font-medium border-0 rounded-bl-xl">
|
||||
<Button
|
||||
onClick={handleViewActivity}
|
||||
className="w-full h-14 rounded-none bg-linear-to-r from-blue-500 to-cyan-400 hover:opacity-90 text-white text-lg font-medium border-0 rounded-bl-xl"
|
||||
>
|
||||
查看活动
|
||||
</Button>
|
||||
</DialogClose>
|
||||
|
||||
<DialogClose asChild className="flex-1">
|
||||
<Button className="w-full h-14 rounded-none bg-linear-to-r from-cyan-400 to-blue-500 hover:opacity-90 text-white text-lg font-medium border-0 rounded-br-xl">
|
||||
<Button
|
||||
onClick={handleContactService}
|
||||
className="w-full h-14 rounded-none bg-linear-to-r from-cyan-400 to-blue-500 hover:opacity-90 text-white text-lg font-medium border-0 rounded-br-xl"
|
||||
>
|
||||
联系客服
|
||||
</Button>
|
||||
</DialogClose>
|
||||
|
||||
212
src/components/composites/dialogs/cashOutDialog.tsx
Normal file
212
src/components/composites/dialogs/cashOutDialog.tsx
Normal file
@@ -0,0 +1,212 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
import { submitCashOut } from "@/api/cashOut"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
|
||||
const ALIPAY_ACCOUNT_RE =
|
||||
/^(?:1[3-9]\d{9}|[a-zA-Z\d._-]*@[a-zA-Z\d.-]{1,10}\.[a-zA-Z\d]{1,20})$/
|
||||
|
||||
|
||||
const CUSTOMER_SERVICE_URL =
|
||||
"https://admin.qidian.qq.com/template/blue/mp/menu/qr-code-jump.html?linkType=0&env=ol&kfuin=2852138148&fid=299&key=f377ec024ca45115a03a7632c7bda230&cate=1&source=&isLBS=&isCustomEntry=&type=16&ftype=1&_type=wpa&qidian=true"
|
||||
|
||||
interface CashOutDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
balance: number
|
||||
onSubmitted?: () => void
|
||||
}
|
||||
|
||||
export function CashOutDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
balance,
|
||||
onSubmitted,
|
||||
}: CashOutDialogProps) {
|
||||
const [step, setStep] = useState<"notice" | "form">("notice")
|
||||
const [money, setMoney] = useState("")
|
||||
const [reason, setReason] = useState("")
|
||||
const [alipayAccount, setAlipayAccount] = useState("")
|
||||
const [realName, setRealName] = useState("")
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setSubmitting(false)
|
||||
// setStep("notice")
|
||||
}
|
||||
}, [open])
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const cashOutMoney = Number(money)
|
||||
if (!Number.isFinite(cashOutMoney) || cashOutMoney <= 0) {
|
||||
toast.error("抱歉!请重新输入提现金额!")
|
||||
return
|
||||
}
|
||||
if (cashOutMoney > balance) {
|
||||
toast.error("抱歉!您提现的额度大于余额,请重新输入提现金额!")
|
||||
return
|
||||
}
|
||||
if (!realName.trim()) {
|
||||
toast.error("抱歉!请重新输入姓名!")
|
||||
return
|
||||
}
|
||||
if (
|
||||
!alipayAccount.trim() ||
|
||||
!ALIPAY_ACCOUNT_RE.test(alipayAccount.trim())
|
||||
) {
|
||||
toast.error("抱歉!请重新输入支付宝账号!")
|
||||
return
|
||||
}
|
||||
|
||||
setSubmitting(true)
|
||||
const res = await submitCashOut({
|
||||
cash_out_money: money,
|
||||
reason: reason.trim(),
|
||||
alipay_account: alipayAccount.trim(),
|
||||
real_name: realName.trim(),
|
||||
})
|
||||
setSubmitting(false)
|
||||
|
||||
if (!res.success) {
|
||||
toast.error(res.message || "提现异常!请重试或联系客服。")
|
||||
return
|
||||
}
|
||||
if (!res.data) {
|
||||
toast.error("提现异常!请重试或联系客服。")
|
||||
return
|
||||
}
|
||||
toast.success(
|
||||
"提现请求发送成功,我们会在5到7个工作日进行处理,请注意查看资金明细。",
|
||||
)
|
||||
setMoney("")
|
||||
setReason("")
|
||||
setAlipayAccount("")
|
||||
setRealName("")
|
||||
onOpenChange(false)
|
||||
onSubmitted?.()
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-h-[85dvh] overflow-y-auto sm:max-w-sm p-6">
|
||||
{step === "notice" ? (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl font-semibold text-center">
|
||||
余额提现
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="mt-3 space-y-2 text-sm text-gray-600">
|
||||
<p>聚IP涵盖十余种全网知名旗舰产品,总有一款适合您!</p>
|
||||
<p>联系客服帮您推荐合适的产品,我们将最大努力满足您的需求!</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex gap-3">
|
||||
<Button
|
||||
type="button"
|
||||
className="flex-1 bg-blue-600 hover:bg-blue-700 text-white"
|
||||
onClick={() =>
|
||||
window.open(
|
||||
CUSTOMER_SERVICE_URL,
|
||||
"_blank",
|
||||
"noopener,noreferrer",
|
||||
)
|
||||
}
|
||||
>
|
||||
联系客服
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
onClick={() => setStep("form")}
|
||||
>
|
||||
我不需要
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl font-semibold text-center">
|
||||
余额提现
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 mt-2">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
当前可用余额:
|
||||
<span className="font-bold text-orange-500">
|
||||
¥{balance.toFixed(2)}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-sm text-gray-600">提现金额</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
value={money}
|
||||
onChange={e => setMoney(e.target.value)}
|
||||
className="h-11 bg-gray-50 border-gray-200"
|
||||
placeholder="请输入提现金额"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-sm text-gray-600">提现理由</Label>
|
||||
<Input
|
||||
value={reason}
|
||||
onChange={e => setReason(e.target.value)}
|
||||
className="h-11 bg-gray-50 border-gray-200"
|
||||
placeholder="请输入提现理由"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-sm text-gray-600">
|
||||
提现支付宝账号(目前仅支持支付宝)
|
||||
</Label>
|
||||
<Input
|
||||
value={alipayAccount}
|
||||
onChange={e => setAlipayAccount(e.target.value)}
|
||||
className="h-11 bg-gray-50 border-gray-200"
|
||||
placeholder="请输入支付宝账号"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-sm text-gray-600">支付宝真实姓名</Label>
|
||||
<Input
|
||||
value={realName}
|
||||
onChange={e => setRealName(e.target.value)}
|
||||
className="h-11 bg-gray-50 border-gray-200"
|
||||
placeholder="请输入与支付宝关联的真实姓名"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
disabled={submitting}
|
||||
onClick={() => void handleSubmit()}
|
||||
className="w-full h-11 mt-2 bg-blue-600 hover:bg-blue-700 text-white font-medium"
|
||||
>
|
||||
{submitting ? "提交中..." : "确认提现"}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -18,14 +18,14 @@ import { Label } from "@/components/ui/label"
|
||||
export type EditInfoValues = {
|
||||
qq: string
|
||||
wechat: string
|
||||
taobao: string
|
||||
// taobao: string
|
||||
email: string
|
||||
}
|
||||
|
||||
const editInfoSchema = z.object({
|
||||
qq: z.string(),
|
||||
wechat: z.string(),
|
||||
taobao: z.string(),
|
||||
// taobao: z.string(),
|
||||
email: z
|
||||
.string()
|
||||
.refine(
|
||||
@@ -44,7 +44,7 @@ interface EditInfoDialogProps {
|
||||
const DEFAULT_INITIAL_DATA: EditInfoValues = {
|
||||
qq: "",
|
||||
wechat: "",
|
||||
taobao: "",
|
||||
// taobao: "",
|
||||
email: "",
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ export function EditInfoDialog({
|
||||
const res = await updateUserInfo({
|
||||
QQ: data.qq,
|
||||
Wx: data.wechat,
|
||||
TaoBao: data.taobao,
|
||||
// TaoBao: data.taobao,
|
||||
Email: data.email,
|
||||
})
|
||||
setSubmitting(false)
|
||||
@@ -118,7 +118,7 @@ export function EditInfoDialog({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
{/* <div className="space-y-1.5">
|
||||
<Label className="text-sm text-gray-600">淘宝会员名</Label>
|
||||
<Controller
|
||||
name="taobao"
|
||||
@@ -127,7 +127,7 @@ export function EditInfoDialog({
|
||||
<Input {...field} className="h-11 bg-gray-50 border-gray-200" />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-sm text-gray-600">邮箱</Label>
|
||||
|
||||
112
src/components/composites/dialogs/rechargeDialog.tsx
Normal file
112
src/components/composites/dialogs/rechargeDialog.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
import alipay from "@/home/_assets/alipay.svg"
|
||||
import wechat from "@/home/_assets/wechat.svg"
|
||||
|
||||
type PayChannel = "50" | "30"
|
||||
|
||||
interface RechargeDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
export function RechargeDialog({ open, onOpenChange }: RechargeDialogProps) {
|
||||
const [amount, setAmount] = useState("100")
|
||||
const [payChannel, setPayChannel] = useState<PayChannel>("50")
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setAmount("100")
|
||||
setPayChannel("50")
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) reset()
|
||||
}, [open, reset])
|
||||
|
||||
const handleGoPay = () => {
|
||||
const chargeAmount = Number(amount)
|
||||
if (!Number.isFinite(chargeAmount) || chargeAmount <= 0) {
|
||||
toast.error("请输入正确的充值金额")
|
||||
return
|
||||
}
|
||||
|
||||
window.open(
|
||||
`/pay?amount=${chargeAmount}&channel=${payChannel}`,
|
||||
"_blank",
|
||||
"noopener,noreferrer",
|
||||
)
|
||||
onOpenChange(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-center">账户充值</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 mt-1">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-sm text-gray-600">充值金额</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={amount}
|
||||
onChange={e => setAmount(e.target.value)}
|
||||
className="h-11 bg-gray-50 border-gray-200"
|
||||
placeholder="请输入充值金额"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm text-gray-600">支付方式</Label>
|
||||
<RadioGroup
|
||||
value={payChannel}
|
||||
onValueChange={value => setPayChannel(value as PayChannel)}
|
||||
className="flex gap-6"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="50" id="recharge-alipay" />
|
||||
<Label
|
||||
htmlFor="recharge-alipay"
|
||||
className="cursor-pointer text-sm text-gray-700 flex items-center gap-1.5"
|
||||
>
|
||||
<img src={alipay} alt="支付宝" className="w-5 h-5" />
|
||||
支付宝
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="30" id="recharge-wechat" />
|
||||
<Label
|
||||
htmlFor="recharge-wechat"
|
||||
className="cursor-pointer text-sm text-gray-700 flex items-center gap-1.5"
|
||||
>
|
||||
<img src={wechat} alt="微信" className="w-5 h-5" />
|
||||
微信
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleGoPay}
|
||||
className="w-full h-11 bg-blue-600 hover:bg-blue-700 text-white font-medium"
|
||||
>
|
||||
充值
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
121
src/components/composites/orderFilterBar.tsx
Normal file
121
src/components/composites/orderFilterBar.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { SearchIcon } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import {
|
||||
ORDER_TYPE_LABELS,
|
||||
type OrderFilters,
|
||||
PACKAGE_OPTIONS,
|
||||
} from "@/lib/models/order"
|
||||
import type { ProductItem } from "@/lib/models/product"
|
||||
|
||||
// 订单管理/退货订单共用的筛选栏
|
||||
export default function OrderFilterBar(props: {
|
||||
filters: OrderFilters
|
||||
products: ProductItem[]
|
||||
showOrderType?: boolean
|
||||
onPatch: (patch: Partial<OrderFilters>) => void
|
||||
onSearch: () => void
|
||||
}) {
|
||||
const { filters, products, showOrderType = true, onPatch, onSearch } = props
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg p-4 flex flex-wrap items-end gap-x-4 gap-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-500 whitespace-nowrap">
|
||||
日期筛选:
|
||||
</span>
|
||||
<Input
|
||||
type="date"
|
||||
value={filters.bTime}
|
||||
onChange={e => onPatch({ bTime: e.target.value })}
|
||||
className="w-36"
|
||||
aria-label="开始时间"
|
||||
/>
|
||||
<span className="text-sm text-gray-400">至</span>
|
||||
<Input
|
||||
type="date"
|
||||
value={filters.eTime}
|
||||
onChange={e => onPatch({ eTime: e.target.value })}
|
||||
className="w-36"
|
||||
aria-label="结束时间"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showOrderType && (
|
||||
<Select
|
||||
value={filters.orderType}
|
||||
onValueChange={value => onPatch({ orderType: value })}
|
||||
>
|
||||
<SelectTrigger className="w-30">
|
||||
<SelectValue placeholder="全部类型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="0">全部类型</SelectItem>
|
||||
{Object.entries(ORDER_TYPE_LABELS).map(([value, label]) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
|
||||
<Select
|
||||
value={filters.productId}
|
||||
onValueChange={value => onPatch({ productId: value })}
|
||||
>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue placeholder="全部产品" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="0">全部产品</SelectItem>
|
||||
{products.map(item => (
|
||||
<SelectItem key={item.Product.Id} value={String(item.Product.Id)}>
|
||||
{item.Product.Name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
value={filters.packageName}
|
||||
onValueChange={value => onPatch({ packageName: value })}
|
||||
>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue placeholder="全部套餐" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="0">全部套餐</SelectItem>
|
||||
{PACKAGE_OPTIONS.map(pkg => (
|
||||
<SelectItem key={pkg} value={pkg}>
|
||||
{pkg}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
value={filters.keyword}
|
||||
onChange={e => onPatch({ keyword: e.target.value })}
|
||||
onKeyDown={e => {
|
||||
if (e.key === "Enter") onSearch()
|
||||
}}
|
||||
placeholder="账号"
|
||||
className="w-40"
|
||||
/>
|
||||
<Button onClick={onSearch}>
|
||||
<SearchIcon data-icon="inline-start" />
|
||||
查询
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { create } from "zustand"
|
||||
|
||||
|
||||
type ClientState = {
|
||||
breakpoint: {
|
||||
sm: boolean
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { create } from "zustand"
|
||||
|
||||
|
||||
|
||||
type LayoutState = {
|
||||
navbar: boolean
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
type UserInfoRecord,
|
||||
} from "@/api/token"
|
||||
|
||||
|
||||
export type ProfileState = {
|
||||
token: string | null
|
||||
userInfo: UserInfoRecord | null
|
||||
|
||||
Reference in New Issue
Block a user