feat: 构建系统重构 + VCHSM 架构迁移 + 右键菜单修复 + 尺寸参数统一
## 构建、打包与分发系统重构 (build/) - 构建系统收敛至 build/ 目录:build.mjs + electron-builder.js + 6 个脚本 - electron-builder 配置从 package.json 提取为 build/electron-builder.js - 新增 --edition (personal/enterprise) 和 --channel (stable/beta/alpha) 参数 - OSS 路径按渠道隔离 - 新增 pre-build-check.mjs:Git/CHANGELOG/版本/环境变量校验 - 新增 bump-version.mjs:版本号管理 + CHANGELOG 自动生成 - updater.ts 发送 channel/edition/deviceFingerprint - 新增 src/shared/utils/rollout.ts 灰度测试工具 - 新增 10 条 NPM 脚本 ## VCHSM 五层架构迁移 - 7 个业务模块 + ~500 处导入路径同步 ## 修复 - MediaCardContextMenu 改用共享 ContextMenu 组件 - ComboBox 尺寸参数显示统一 (size-format.ts) ## 文档 - UpdateA.md / build/README.md / README.md / .env.example 更新 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
257
src/modules/auth/views/components/ForgotPasswordModal.tsx
Normal file
257
src/modules/auth/views/components/ForgotPasswordModal.tsx
Normal file
@@ -0,0 +1,257 @@
|
||||
// ============================================================
|
||||
// ForgotPasswordModal — 忘记密码弹窗
|
||||
//
|
||||
// 场景:未登录用户通过手机号 + 短信验证码重置密码
|
||||
// 复用:可在登录页"忘记密码?"入口使用,也可在其他位置独立调用
|
||||
//
|
||||
// API:
|
||||
// - AuthAPI.sendSms({ phone }) 发送短信验证码
|
||||
// - AuthAPI.resetPassword({ phone, sms_code, new_password }) 重置密码
|
||||
// ============================================================
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { logger } from '@/shared/utils/bus';
|
||||
import { Modal, Input, Button, Typography, App } from 'antd';
|
||||
import {
|
||||
PhoneOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
LockOutlined,
|
||||
KeyOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
import { useTheme } from '@/app/providers/ThemeProvider';
|
||||
import { AuthAPI } from '@/modules/auth/controllers/auth-api';
|
||||
import {
|
||||
validatePhone,
|
||||
validateSmsCode,
|
||||
validatePassword,
|
||||
validateConfirmPassword,
|
||||
runFieldChecks,
|
||||
} from '../../settings/validators';
|
||||
import { useSmsCooldown } from '../../hooks/use-sms-cooldown';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
interface ForgotPasswordModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function ForgotPasswordModal({ open, onClose }: ForgotPasswordModalProps) {
|
||||
const { isDark } = useTheme();
|
||||
const { message } = App.useApp();
|
||||
|
||||
// ---- 表单字段 ----
|
||||
const [phone, setPhone] = useState('');
|
||||
const [smsCode, setSmsCode] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
|
||||
// ---- 错误信息 ----
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
// ---- 提交状态 ----
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
// ---- 短信冷却 ----
|
||||
const { cooldown, secondsLeft, startCooldown } = useSmsCooldown(60);
|
||||
|
||||
// ---- 清除单个字段错误 ----
|
||||
const clearError = useCallback((name: string) => {
|
||||
setErrors((p) => {
|
||||
if (!(name in p)) return p;
|
||||
const n = { ...p };
|
||||
delete n[name];
|
||||
return n;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// ---- 发送验证码 ----
|
||||
const handleSendSms = useCallback(() => {
|
||||
const err = validatePhone(phone);
|
||||
if (err) {
|
||||
setErrors((p) => ({ ...p, phone: err }));
|
||||
return;
|
||||
}
|
||||
clearError('phone');
|
||||
startCooldown();
|
||||
AuthAPI.sendSms({ phone })
|
||||
.then(() => message.success('验证码已发送'))
|
||||
.catch((e) => { logger.warn('auth', '忘记密码-验证码发送失败', e instanceof Error ? e : undefined); message.error((e as Error).message || '验证码发送失败'); });
|
||||
}, [phone, startCooldown, clearError, message]);
|
||||
|
||||
// ---- 提交重置 ----
|
||||
const handleSubmit = useCallback(async () => {
|
||||
if (runFieldChecks(setErrors,
|
||||
{ field: 'phone', error: validatePhone(phone) },
|
||||
{ field: 'smsCode', error: validateSmsCode(smsCode) },
|
||||
{ field: 'newPassword', error: validatePassword(newPassword) },
|
||||
{ field: 'confirmPassword', error: validateConfirmPassword(confirmPassword, newPassword) },
|
||||
)) return;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await AuthAPI.resetPassword({
|
||||
phone,
|
||||
sms_code: smsCode,
|
||||
new_password: newPassword,
|
||||
});
|
||||
message.success('密码重置成功,请使用新密码登录');
|
||||
// 重置表单状态
|
||||
setPhone('');
|
||||
setSmsCode('');
|
||||
setNewPassword('');
|
||||
setConfirmPassword('');
|
||||
onClose();
|
||||
} catch (err) {
|
||||
logger.warn('auth', '密码重置失败', err instanceof Error ? err : undefined);
|
||||
message.error((err as Error).message || '密码重置失败');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}, [phone, smsCode, newPassword, confirmPassword, onClose, message]);
|
||||
|
||||
// ---- 关闭时重置 ----
|
||||
const handleClose = useCallback(() => {
|
||||
setErrors({});
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onCancel={handleClose}
|
||||
footer={null}
|
||||
width={440}
|
||||
centered
|
||||
destroyOnHidden={true}
|
||||
mask={{ closable: false }}
|
||||
styles={{
|
||||
body: {
|
||||
padding: '24px 32px 16px',
|
||||
background: isDark ? '#1A1730' : '#FFFFFF',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* 标题 */}
|
||||
<div className="text-center mb-5">
|
||||
<Title level={5} className="!mb-1 gradient-primary-text">
|
||||
重置密码
|
||||
</Title>
|
||||
<Text type="secondary" className="text-xs">
|
||||
通过手机短信验证码重置您的密码
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* 表单 */}
|
||||
<div className="space-y-4">
|
||||
{/* 手机号 */}
|
||||
<div>
|
||||
<Input
|
||||
size="large"
|
||||
prefix={<PhoneOutlined className="opacity-40" />}
|
||||
placeholder="请输入手机号"
|
||||
value={phone}
|
||||
onChange={(e) => {
|
||||
setPhone(e.target.value);
|
||||
clearError('phone');
|
||||
}}
|
||||
status={errors.phone ? 'error' : undefined}
|
||||
allowClear
|
||||
/>
|
||||
{errors.phone && (
|
||||
<Text type="danger" className="text-xs ml-1">
|
||||
{errors.phone}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 短信验证码 */}
|
||||
<div>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
size="large"
|
||||
prefix={<SafetyCertificateOutlined className="opacity-40" />}
|
||||
placeholder="请输入验证码"
|
||||
value={smsCode}
|
||||
onChange={(e) => {
|
||||
setSmsCode(e.target.value);
|
||||
clearError('smsCode');
|
||||
}}
|
||||
status={errors.smsCode ? 'error' : undefined}
|
||||
className="flex-1"
|
||||
allowClear
|
||||
/>
|
||||
<Button
|
||||
size="large"
|
||||
type="primary"
|
||||
disabled={cooldown}
|
||||
onClick={handleSendSms}
|
||||
className="text-xs! shrink-0 px-3!"
|
||||
>
|
||||
{cooldown ? `${secondsLeft}s` : '发送验证码'}
|
||||
</Button>
|
||||
</div>
|
||||
{errors.smsCode && (
|
||||
<Text type="danger" className="text-xs ml-1">
|
||||
{errors.smsCode}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 新密码 */}
|
||||
<div>
|
||||
<Input.Password
|
||||
size="large"
|
||||
prefix={<LockOutlined className="opacity-40" />}
|
||||
placeholder="新密码(6~128位)"
|
||||
value={newPassword}
|
||||
onChange={(e) => {
|
||||
setNewPassword(e.target.value);
|
||||
clearError('newPassword');
|
||||
}}
|
||||
status={errors.newPassword ? 'error' : undefined}
|
||||
allowClear
|
||||
/>
|
||||
{errors.newPassword && (
|
||||
<Text type="danger" className="text-xs ml-1">
|
||||
{errors.newPassword}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 确认新密码 */}
|
||||
<div>
|
||||
<Input.Password
|
||||
size="large"
|
||||
prefix={<KeyOutlined className="opacity-40" />}
|
||||
placeholder="请再次输入新密码"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => {
|
||||
setConfirmPassword(e.target.value);
|
||||
clearError('confirmPassword');
|
||||
}}
|
||||
status={errors.confirmPassword ? 'error' : undefined}
|
||||
allowClear
|
||||
/>
|
||||
{errors.confirmPassword && (
|
||||
<Text type="danger" className="text-xs ml-1">
|
||||
{errors.confirmPassword}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 提交按钮 */}
|
||||
<Button
|
||||
type="primary"
|
||||
block
|
||||
size="large"
|
||||
loading={submitting}
|
||||
onClick={handleSubmit}
|
||||
className="h-11! text-base! font-medium! mt-2!"
|
||||
>
|
||||
重置密码
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user