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:
2026-06-29 18:41:07 +08:00
parent e0d91335c7
commit 337d6c1205
187 changed files with 5252 additions and 1702 deletions

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