b2700b85ff
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 25s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 32s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m25s
CI/CD Pipeline / Integration Tests (push) Successful in 1m53s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 3m7s
CI/CD Pipeline / Frontend Lint (push) Successful in 55s
CI/CD Pipeline / Unit Tests (push) Successful in 2m55s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m32s
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
181 lines
5.6 KiB
TypeScript
181 lines
5.6 KiB
TypeScript
import React, { useState, useEffect, useRef } from "react"
|
|
import { Modal, Tabs, Form, Input, Button, message } from "antd"
|
|
import { sendVerificationCode, bindContact, type BindContactResponse } from "@/api/auth"
|
|
|
|
interface BindContactModalProps {
|
|
open: boolean
|
|
onSuccess?: (user: BindContactResponse["user"]) => void
|
|
onCancel?: () => void
|
|
}
|
|
|
|
const BindContactModal: React.FC<BindContactModalProps> = ({ open, onSuccess, onCancel }) => {
|
|
const [activeTab, setActiveTab] = useState<"email" | "phone">("email")
|
|
const [form] = Form.useForm()
|
|
const [loading, setLoading] = useState(false)
|
|
const [codeLoading, setCodeLoading] = useState(false)
|
|
const [countdown, setCountdown] = useState(0)
|
|
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
|
|
|
useEffect(() => {
|
|
if (countdown > 0) {
|
|
timerRef.current = setInterval(() => {
|
|
setCountdown((prev) => prev - 1)
|
|
}, 1000)
|
|
} else if (timerRef.current) {
|
|
clearInterval(timerRef.current)
|
|
timerRef.current = null
|
|
}
|
|
return () => {
|
|
if (timerRef.current) clearInterval(timerRef.current)
|
|
}
|
|
}, [countdown])
|
|
|
|
useEffect(() => {
|
|
if (open) {
|
|
form.resetFields()
|
|
setCountdown(0)
|
|
}
|
|
}, [open, form])
|
|
|
|
const handleSendCode = async () => {
|
|
try {
|
|
const value = form.getFieldValue(activeTab === "email" ? "email" : "phone")
|
|
if (!value) {
|
|
message.warning(activeTab === "email" ? "请输入邮箱" : "请输入手机号")
|
|
return
|
|
}
|
|
setCodeLoading(true)
|
|
await sendVerificationCode({
|
|
target: activeTab,
|
|
value,
|
|
purpose: "bind",
|
|
})
|
|
message.success("验证码已发送")
|
|
setCountdown(60)
|
|
} catch (error) {
|
|
// error handled by interceptor
|
|
} finally {
|
|
setCodeLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleSubmit = async () => {
|
|
try {
|
|
const values = await form.validateFields()
|
|
setLoading(true)
|
|
|
|
const payload =
|
|
activeTab === "email"
|
|
? { email: values.email, email_code: values.code }
|
|
: { phone: values.phone, phone_code: values.code }
|
|
|
|
const result = await bindContact(payload)
|
|
|
|
message.success("绑定成功")
|
|
onSuccess?.(result.user)
|
|
} catch (error) {
|
|
// error handled by interceptor
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Modal
|
|
title="绑定联系方式"
|
|
open={open}
|
|
onCancel={onCancel}
|
|
footer={null}
|
|
destroyOnHidden
|
|
maskClosable={false}
|
|
>
|
|
<p style={{ color: "#666", marginBottom: 16 }}>为了保障账号安全,请绑定您的邮箱或手机号</p>
|
|
|
|
<Tabs
|
|
activeKey={activeTab}
|
|
onChange={(key) => setActiveTab(key as "email" | "phone")}
|
|
items={[
|
|
{
|
|
key: "email",
|
|
label: "邮箱绑定",
|
|
children: (
|
|
<Form form={form} layout="vertical">
|
|
<Form.Item
|
|
name="email"
|
|
label="邮箱"
|
|
rules={[
|
|
{ required: true, message: "请输入邮箱" },
|
|
{ type: "email", message: "请输入有效的邮箱地址" },
|
|
]}
|
|
>
|
|
<Input placeholder="请输入邮箱地址" size="large" />
|
|
</Form.Item>
|
|
<Form.Item
|
|
name="code"
|
|
label="验证码"
|
|
rules={[{ required: true, message: "请输入验证码" }]}
|
|
>
|
|
<div style={{ display: "flex", gap: 8 }}>
|
|
<Input placeholder="请输入验证码" size="large" style={{ flex: 1 }} />
|
|
<Button
|
|
size="large"
|
|
onClick={handleSendCode}
|
|
loading={codeLoading}
|
|
disabled={countdown > 0}
|
|
>
|
|
{countdown > 0 ? `${countdown}s 后重发` : "发送验证码"}
|
|
</Button>
|
|
</div>
|
|
</Form.Item>
|
|
</Form>
|
|
),
|
|
},
|
|
{
|
|
key: "phone",
|
|
label: "手机绑定",
|
|
children: (
|
|
<Form form={form} layout="vertical">
|
|
<Form.Item
|
|
name="phone"
|
|
label="手机号"
|
|
rules={[
|
|
{ required: true, message: "请输入手机号" },
|
|
{ pattern: /^1[3-9]\d{9}$/, message: "请输入有效的手机号" },
|
|
]}
|
|
>
|
|
<Input placeholder="请输入手机号" size="large" />
|
|
</Form.Item>
|
|
<Form.Item
|
|
name="code"
|
|
label="验证码"
|
|
rules={[{ required: true, message: "请输入验证码" }]}
|
|
>
|
|
<div style={{ display: "flex", gap: 8 }}>
|
|
<Input placeholder="请输入验证码" size="large" style={{ flex: 1 }} />
|
|
<Button
|
|
size="large"
|
|
onClick={handleSendCode}
|
|
loading={codeLoading}
|
|
disabled={countdown > 0}
|
|
>
|
|
{countdown > 0 ? `${countdown}s 后重发` : "发送验证码"}
|
|
</Button>
|
|
</div>
|
|
</Form.Item>
|
|
</Form>
|
|
),
|
|
},
|
|
]}
|
|
/>
|
|
|
|
<div style={{ marginTop: 16 }}>
|
|
<Button type="primary" block size="large" loading={loading} onClick={handleSubmit}>
|
|
确认绑定
|
|
</Button>
|
|
</div>
|
|
</Modal>
|
|
)
|
|
}
|
|
|
|
export default BindContactModal
|