Files
xiaoxia-saas/apps/web/src/components/auth/BindContactModal.tsx
T
xiaoxia 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
fix(web): #558 修复bind-contact接口字段名,对齐后端BindContactRequest (#708)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-07-22 14:12:56 +08:00

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