补充个人中心页面api & 首页弹唱查看活动 & 联系客服

This commit is contained in:
Eamon
2026-09-08 16:19:40 +08:00
parent f59e929843
commit 7f03dbfb56
45 changed files with 2146 additions and 1359 deletions

View File

@@ -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>

View 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>
)
}

View File

@@ -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>

View 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>
)
}

View 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>
)
}

View File

@@ -1,6 +1,5 @@
import { create } from "zustand"
type ClientState = {
breakpoint: {
sm: boolean

View File

@@ -1,7 +1,5 @@
import { create } from "zustand"
type LayoutState = {
navbar: boolean
}

View File

@@ -6,7 +6,6 @@ import {
type UserInfoRecord,
} from "@/api/token"
export type ProfileState = {
token: string | null
userInfo: UserInfoRecord | null