Files
ele-HeiXiu/src/pages/settings/blocks/UpdateSetting.tsx
YoungestSongMo 7032d1c3f2 feat: 忘记密码/修改密码 + 设置页结构重组 + barrel cleanup + LoginPage 版本统一
新增:
  - ForgotPasswordModal — 手机号+短信验证码重置密码 (AuthAPI.resetPassword)
  - ChangePasswordModal — 当前密码验证后修改密码 (AuthAPI.changePassword)
  - useSmsCooldown Hook — 通用短信发送冷却逻辑,供注册/忘记密码复用
  - PasswordSetting — 设置页账号安全区块,双选项:修改密码 / 短信验证码重置

  修改:
  - LoginForm \"忘记密码?\" 链接接入 ForgotPasswordModal(替换 TODO)
  - LoginPage 移除 edition 业务版本 UI 区分,登录/注册 Tab 全局统一
  - 设置页 Section 组件移入 blocks/ 子目录(git mv 保留历史)
  - auth.ts sendSms URL 修正: /sms/send → /send-sms

  清理:
  - settings/index.ts 移除 4 个未使用 re-export,精简为仅 SettingsPage
  - navbar/index.ts 移除未使用 re-export,精简为仅 NavBar

  文档:
  - WORKLOG.md 新增 2026-06-08 工作日志
  - ARCHITECTURE.md / PROJECT.md 更新设置页目录结构与新组件
  - TODO.md 忘记密码流程标记已完成 + 短信人机验证列入后期优化
2026-06-08 11:57:35 +08:00

220 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// UpdateSetting — 版本更新区块
// ============================================================
import { useState } from 'react';
import { Card, Button, Typography, Progress, Tag, Modal, Divider } from 'antd';
import {
SyncOutlined,
CheckCircleOutlined,
ExclamationCircleOutlined,
RocketOutlined,
DownloadOutlined,
FileTextOutlined,
} from '@ant-design/icons';
import { useUpdater } from '@/hooks/use-updater';
import { isDev } from '@/utils/platform';
const { Text, Title } = Typography;
/** 简单渲染 changelogMarkdown → JSX支持 ## / - / --- */
function renderChangelog(markdown: string) {
const lines = markdown.split('\n');
const elements: React.ReactNode[] = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const key = `cl-${i}`;
if (line.startsWith('## ')) {
elements.push(
<Title key={key} level={5} className="!mt-3! !mb-1!">
{line.slice(3)}
</Title>,
);
continue;
}
if (/^-{3,}$/.test(line.trim())) {
elements.push(<Divider key={key} className="!my-2!" />);
continue;
}
if (line.trim().startsWith('- ')) {
elements.push(
<Text key={key} className="block pl-3 text-sm leading-relaxed">
{line.trim().slice(2)}
</Text>,
);
continue;
}
if (line.trim() === '') {
elements.push(<div key={key} className="h-2" />);
continue;
}
elements.push(
<Text key={key} className="block text-sm leading-relaxed">
{line}
</Text>,
);
}
return elements.length > 0 ? elements : <Text type="secondary"></Text>;
}
export function UpdateSetting() {
const { status, info, progress, error, checkForUpdates, downloadUpdate, installUpdate } =
useUpdater();
const [detailOpen, setDetailOpen] = useState(false);
return (
<Card size="small" title="版本更新" className="rounded-md!">
<div className="flex flex-col gap-3">
{/* ---- idle ---- */}
{status === 'idle' && (
<Text type="secondary" className="text-sm">
</Text>
)}
{/* ---- checking ---- */}
{status === 'checking' && (
<div className="flex items-center gap-2">
<SyncOutlined spin style={{ color: 'var(--color-primary, #4F46E5)' }} />
<Text type="secondary">...</Text>
</div>
)}
{/* ---- no-update ---- */}
{status === 'no-update' && (
<div className="flex items-center gap-2">
<CheckCircleOutlined style={{ color: 'var(--color-success, #52c41a)' }} />
<Text style={{ color: 'var(--color-success, #52c41a)' }}></Text>
</div>
)}
{/* ---- error ---- */}
{status === 'error' && (
<div className="flex items-center gap-2">
<ExclamationCircleOutlined style={{ color: 'var(--color-error, #ff4d4f)' }} />
<Text type="danger">{error?.message || '检查更新失败'}</Text>
</div>
)}
{/* ---- 发现新版本(等待用户决定)---- */}
{status === 'available' && (
<>
<div className="flex items-center gap-2">
<RocketOutlined style={{ color: 'var(--color-primary, #4F46E5)' }} />
<Text strong>{info?.version}</Text>
{info?.forceUpdate && <Tag color="red"></Tag>}
</div>
{info?.releaseNotes && (
<div className="flex items-start gap-2">
<Text
type="secondary"
className="text-xs whitespace-pre-line line-clamp-2 flex-1"
>
{info.releaseNotes
.replace(/^## .+?\n\n?/, '')
.replace(/\n---\n?[\s\S]*$/, '')
.trim()}
</Text>
<Button
type="link"
size="small"
icon={<FileTextOutlined />}
onClick={() => setDetailOpen(true)}
className="!px-1! flex-shrink-0"
>
</Button>
</div>
)}
<Button
type="primary"
size="small"
icon={<DownloadOutlined />}
onClick={downloadUpdate}
className="self-start"
>
</Button>
</>
)}
{/* ---- 下载中 ---- */}
{status === 'downloading' && (
<>
<div className="flex items-center gap-2">
<DownloadOutlined style={{ color: 'var(--color-primary, #4F46E5)' }} />
<Text> v{info?.version}...</Text>
</div>
<Progress
percent={progress.percent}
strokeColor={{ '0%': '#4F46E5', '100%': '#7C3AED' }}
size="small"
/>
</>
)}
{/* ---- 下载完成 ---- */}
{status === 'downloaded' && (
<>
<div className="flex items-center gap-2">
<CheckCircleOutlined style={{ color: 'var(--color-success, #52c41a)' }} />
<Text></Text>
</div>
<Button
type="primary"
icon={<DownloadOutlined />}
onClick={installUpdate}
size="small"
className="self-start"
>
</Button>
</>
)}
{/* ---- 操作按钮 ---- */}
<div className="flex items-center gap-3">
<Button
size="small"
icon={<SyncOutlined spin={status === 'checking'} />}
onClick={checkForUpdates}
disabled={status === 'checking' || status === 'downloading'}
>
</Button>
{isDev() && (
<Text type="secondary" className="text-xs">
5
</Text>
)}
</div>
</div>
{/* ---- 更新详情弹窗 ---- */}
<Modal
title={`更新详情 — v${info?.version || ''}`}
open={detailOpen}
onCancel={() => setDetailOpen(false)}
footer={<Button onClick={() => setDetailOpen(false)}></Button>}
width={520}
>
<div className="max-h-96 overflow-y-auto py-2">
{info?.releaseNotes ? (
renderChangelog(info.releaseNotes)
) : (
<Text type="secondary"></Text>
)}
</div>
</Modal>
</Card>
);
}