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 忘记密码流程标记已完成 + 短信人机验证列入后期优化
This commit is contained in:
2026-06-08 11:58:54 +08:00
parent 7032d1c3f2
commit 0f137b21fa

View File

@@ -12,21 +12,21 @@
// - 用户可读错误信息errorMessage // - 用户可读错误信息errorMessage
// ============================================================ // ============================================================
import { useMemo } from 'react'; import {useMemo} from 'react';
import { useAppContext } from '@/contexts/app-context'; import {useAppContext} from '@/contexts/app-context';
import { import {
TaskAPI, TaskAPI,
ModelAPI, ModelAPI,
fetchBanners, fetchBanners,
MODEL_CATEGORY_LABELS, MODEL_CATEGORY_LABELS,
type TaskListRequestParams, type TaskListRequestParams,
type TaskInfoItemResponseBody, type TaskInfoItemResponseBody,
type CreatTaskRequestBody, type CreatTaskRequestBody,
type ModelsListResponseBody, type ModelsListResponseBody,
type Banner, type Banner,
type ModelCategory, type ModelCategory,
} from '@/services/modules'; } from '@/services/modules';
import { useAsyncData, useAsyncMutation } from './use-async'; import {useAsyncData, useAsyncMutation} from './use-async';
// ============================================================ // ============================================================
// Banner // Banner
@@ -34,13 +34,13 @@ import { useAsyncData, useAsyncMutation } from './use-async';
/** 首页轮播图数据 Hook */ /** 首页轮播图数据 Hook */
export function useBannerList() { export function useBannerList() {
const { isLoggedIn } = useAppContext(); const {isLoggedIn} = useAppContext();
return useAsyncData<Banner[]>( return useAsyncData<Banner[]>(
fetchBanners, fetchBanners,
[], [],
{ enabled: isLoggedIn, label: 'banner-list' }, {enabled: isLoggedIn, label: 'banner-list'},
); );
} }
// ============================================================ // ============================================================
@@ -49,53 +49,53 @@ export function useBannerList() {
/** 模型列表数据 Hook按类别分别请求后合并 */ /** 模型列表数据 Hook按类别分别请求后合并 */
export function useModelList() { export function useModelList() {
const { isLoggedIn } = useAppContext(); const {isLoggedIn} = useAppContext();
// 使用单个 fetcher 返回合并后的结果 // 使用单个 fetcher 返回合并后的结果
const result = useAsyncData<ModelsListResponseBody[]>( const result = useAsyncData<ModelsListResponseBody[]>(
async () => { async () => {
const categories: ModelCategory[] = ['txt2img', 'img2img', 'img2video', 'txt2audio']; const categories: ModelCategory[] = ["image_to_image", "text_to_image", "image_to_video", "text_to_audio"];
const results = await Promise.allSettled( const results = await Promise.allSettled(
categories.map((category) => categories.map((category) =>
ModelAPI.fetchModels({ ModelAPI.fetchModels({
provider_name: '', provider_name: '',
category, category,
page: 1, page: 1,
page_size: 50, page_size: 50,
}), }),
), ),
); );
const allModels: ModelsListResponseBody[] = []; const allModels: ModelsListResponseBody[] = [];
results.forEach((r) => { results.forEach((r) => {
if (r.status === 'fulfilled') { if (r.status === 'fulfilled') {
allModels.push(...r.value); allModels.push(...r.value);
} }
}); });
// 按优先级降序排列 // 按优先级降序排列
allModels.sort((a, b) => b.priority - a.priority); allModels.sort((a, b) => b.priority - a.priority);
return allModels; return allModels;
}, },
[], [],
{ enabled: isLoggedIn, label: 'model-list' }, {enabled: isLoggedIn, label: 'model-list'},
); );
// 按类别分组 // 按类别分组
const groupedModels = useMemo(() => { const groupedModels = useMemo(() => {
const groups = new Map<string, ModelsListResponseBody[]>(); const groups = new Map<string, ModelsListResponseBody[]>();
result.data?.forEach((model) => { result.data?.forEach((model) => {
const cat = model.category_name || 'other'; const cat = model.category_name || 'other';
if (!groups.has(cat)) groups.set(cat, []); if (!groups.has(cat)) groups.set(cat, []);
groups.get(cat)!.push(model); groups.get(cat)!.push(model);
}); });
return groups; return groups;
}, [result.data]); }, [result.data]);
return { return {
...result, ...result,
/** 按类别分组后的模型 */ /** 按类别分组后的模型 */
groupedModels, groupedModels,
}; };
} }
// ============================================================ // ============================================================
@@ -103,10 +103,10 @@ export function useModelList() {
// ============================================================ // ============================================================
export interface UseTaskListParams { export interface UseTaskListParams {
/** 完整的 API 请求参数(由调用方根据 antd Table onChange 构建) */ /** 完整的 API 请求参数(由调用方根据 antd Table onChange 构建) */
requestParams: TaskListRequestParams; requestParams: TaskListRequestParams;
/** 外部传入的刷新信号(任意变化触发重新请求) */ /** 外部传入的刷新信号(任意变化触发重新请求) */
refreshSignal?: number; refreshSignal?: number;
} }
/** /**
@@ -116,14 +116,14 @@ export interface UseTaskListParams {
* 本 Hook 仅负责数据获取、竞态控制、错误日志。 * 本 Hook 仅负责数据获取、竞态控制、错误日志。
*/ */
export function useTaskList(params: UseTaskListParams) { export function useTaskList(params: UseTaskListParams) {
const { isLoggedIn } = useAppContext(); const {isLoggedIn} = useAppContext();
const { requestParams, refreshSignal = 0 } = params; const {requestParams, refreshSignal = 0} = params;
return useAsyncData( return useAsyncData(
() => TaskAPI.getTaskList(requestParams), () => TaskAPI.getTaskList(requestParams),
[requestParams, refreshSignal], [requestParams, refreshSignal],
{ enabled: isLoggedIn, label: 'task-list' }, {enabled: isLoggedIn, label: 'task-list'},
); );
} }
// ============================================================ // ============================================================
@@ -132,13 +132,13 @@ export function useTaskList(params: UseTaskListParams) {
/** 任务详情 HooktaskId 为 null 时不请求) */ /** 任务详情 HooktaskId 为 null 时不请求) */
export function useTaskDetail(taskId: string | null | undefined) { export function useTaskDetail(taskId: string | null | undefined) {
const { isLoggedIn } = useAppContext(); const {isLoggedIn} = useAppContext();
return useAsyncData<TaskInfoItemResponseBody>( return useAsyncData<TaskInfoItemResponseBody>(
() => TaskAPI.getTaskInfo(taskId!), () => TaskAPI.getTaskInfo(taskId!),
[taskId], [taskId],
{ enabled: isLoggedIn && !!taskId, label: 'task-detail' }, {enabled: isLoggedIn && !!taskId, label: 'task-detail'},
); );
} }
// ============================================================ // ============================================================
@@ -147,20 +147,20 @@ export function useTaskDetail(taskId: string | null | undefined) {
/** 提交任务 Mutation Hook */ /** 提交任务 Mutation Hook */
export function useSubmitTask(options?: { export function useSubmitTask(options?: {
onSuccess?: (data: TaskInfoItemResponseBody) => void; onSuccess?: (data: TaskInfoItemResponseBody) => void;
onError?: (err: Error) => void; onError?: (err: Error) => void;
}) { }) {
return useAsyncMutation<TaskInfoItemResponseBody, [CreatTaskRequestBody]>( return useAsyncMutation<TaskInfoItemResponseBody, [CreatTaskRequestBody]>(
(body) => TaskAPI.submitTask(body), (body) => TaskAPI.submitTask(body),
{ {
label: 'submit-task', label: 'submit-task',
onSuccess: options?.onSuccess, onSuccess: options?.onSuccess,
onError: options?.onError, onError: options?.onError,
}, },
); );
} }
// ---------- 重新导出类型和工具 ---------- // ---------- 重新导出类型和工具 ----------
export { MODEL_CATEGORY_LABELS }; export {MODEL_CATEGORY_LABELS};
export type { ModelCategory, ModelsListResponseBody, TaskInfoItemResponseBody, Banner }; export type {ModelCategory, ModelsListResponseBody, TaskInfoItemResponseBody, Banner};