ci: Prettier纳入两层防御体系 (#520)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Successful in 1m6s
CI Build & Deploy Pipeline / Build Staging Web Image (push) Successful in 1m49s
CI Build & Deploy Pipeline / Build Production API Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Production Web Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Production Worker Image (push) Has been skipped
CI Build & Deploy Pipeline / Deploy Production (push) Has been skipped
CI Build & Deploy Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Validate Code Quality And Tests (push) Successful in 3m1s
CI/CD Pipeline / Unit Tests (push) Successful in 3m20s
CI/CD Pipeline / Integration Tests (push) Successful in 1m29s
CI Build & Deploy Pipeline / Build Staging API Image (push) Successful in 7m4s
CI Build & Deploy Pipeline / Build Staging Worker Image (push) Successful in 7m54s
CI Build & Deploy Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 45s
CI Build & Deploy Pipeline / Staging E2E Tests (push) Failing after 2m50s
CI Build & Deploy Pipeline / Staging API Integration Tests (push) Successful in 3m28s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Successful in 1m6s
CI Build & Deploy Pipeline / Build Staging Web Image (push) Successful in 1m49s
CI Build & Deploy Pipeline / Build Production API Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Production Web Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Production Worker Image (push) Has been skipped
CI Build & Deploy Pipeline / Deploy Production (push) Has been skipped
CI Build & Deploy Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Validate Code Quality And Tests (push) Successful in 3m1s
CI/CD Pipeline / Unit Tests (push) Successful in 3m20s
CI/CD Pipeline / Integration Tests (push) Successful in 1m29s
CI Build & Deploy Pipeline / Build Staging API Image (push) Successful in 7m4s
CI Build & Deploy Pipeline / Build Staging Worker Image (push) Successful in 7m54s
CI Build & Deploy Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 45s
CI Build & Deploy Pipeline / Staging E2E Tests (push) Failing after 2m50s
CI Build & Deploy Pipeline / Staging API Integration Tests (push) Successful in 3m28s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
This commit was merged in pull request #520.
This commit is contained in:
@@ -1,94 +1,94 @@
|
||||
/**
|
||||
* 认证相关 Hooks
|
||||
*/
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import * as authApi from "@/api/auth";
|
||||
import { useAuthStore } from "@/store/authStore";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import * as authApi from "@/api/auth"
|
||||
import { useAuthStore } from "@/store/authStore"
|
||||
|
||||
// 登录 Hook
|
||||
export const useLogin = () => {
|
||||
const navigate = useNavigate();
|
||||
const setAuth = useAuthStore((state) => state.setAuth);
|
||||
const navigate = useNavigate()
|
||||
const setAuth = useAuthStore((state) => state.setAuth)
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: authApi.login,
|
||||
});
|
||||
})
|
||||
|
||||
// 手动处理成功后的逻辑
|
||||
const login = async (credentials: Parameters<typeof authApi.login>[0]) => {
|
||||
const data = await mutation.mutateAsync(credentials);
|
||||
const refreshToken = data.refresh_token ?? null;
|
||||
const data = await mutation.mutateAsync(credentials)
|
||||
const refreshToken = data.refresh_token ?? null
|
||||
|
||||
// 先保存 token
|
||||
localStorage.setItem("access_token", data.access_token);
|
||||
localStorage.setItem("access_token", data.access_token)
|
||||
if (refreshToken) {
|
||||
localStorage.setItem("refresh_token", refreshToken);
|
||||
localStorage.setItem("refresh_token", refreshToken)
|
||||
} else {
|
||||
localStorage.removeItem("refresh_token");
|
||||
localStorage.removeItem("refresh_token")
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
const user = await authApi.getCurrentUser();
|
||||
setAuth(user, data.access_token, refreshToken);
|
||||
navigate("/");
|
||||
return data;
|
||||
};
|
||||
const user = await authApi.getCurrentUser()
|
||||
setAuth(user, data.access_token, refreshToken)
|
||||
navigate("/")
|
||||
return data
|
||||
}
|
||||
|
||||
return { ...mutation, mutateAsync: login };
|
||||
};
|
||||
return { ...mutation, mutateAsync: login }
|
||||
}
|
||||
|
||||
// 注册 Hook
|
||||
export const useRegister = () => {
|
||||
const navigate = useNavigate();
|
||||
const navigate = useNavigate()
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: authApi.register,
|
||||
});
|
||||
})
|
||||
|
||||
const register = async (data: Parameters<typeof authApi.register>[0]) => {
|
||||
const result = await mutation.mutateAsync(data);
|
||||
const result = await mutation.mutateAsync(data)
|
||||
navigate("/login", {
|
||||
state: { message: "注册成功!请查收验证邮件。" },
|
||||
});
|
||||
return result;
|
||||
};
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
return { ...mutation, mutateAsync: register };
|
||||
};
|
||||
return { ...mutation, mutateAsync: register }
|
||||
}
|
||||
|
||||
// 登出 Hook
|
||||
export const useLogout = () => {
|
||||
const navigate = useNavigate();
|
||||
const clearAuth = useAuthStore((state) => state.clearAuth);
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate()
|
||||
const clearAuth = useAuthStore((state) => state.clearAuth)
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: authApi.logout,
|
||||
});
|
||||
})
|
||||
|
||||
const logout = async () => {
|
||||
try {
|
||||
await mutation.mutateAsync();
|
||||
await mutation.mutateAsync()
|
||||
} catch (error) {
|
||||
// 即使登出失败也清除本地状态
|
||||
} finally {
|
||||
clearAuth();
|
||||
queryClient.clear();
|
||||
navigate("/");
|
||||
clearAuth()
|
||||
queryClient.clear()
|
||||
navigate("/")
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return { ...mutation, mutateAsync: logout };
|
||||
};
|
||||
return { ...mutation, mutateAsync: logout }
|
||||
}
|
||||
|
||||
// 获取当前用户 Hook
|
||||
export const useCurrentUser = () => {
|
||||
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
||||
const isAuthenticated = useAuthStore((state) => state.isAuthenticated)
|
||||
|
||||
return useQuery({
|
||||
queryKey: ["currentUser"],
|
||||
queryFn: authApi.getCurrentUser,
|
||||
enabled: isAuthenticated,
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,76 +4,76 @@
|
||||
* 当存在 processing 状态的克隆条目时,每 3 秒自动拉取最新列表;
|
||||
* 全部完成(ready / failed)后停止轮询。
|
||||
*/
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { getVoiceClones } from "@/api/voiceClone";
|
||||
import type { VoiceClone } from "@/api/voiceClone";
|
||||
import { useState, useEffect, useCallback, useRef } from "react"
|
||||
import { getVoiceClones } from "@/api/voiceClone"
|
||||
import type { VoiceClone } from "@/api/voiceClone"
|
||||
|
||||
const POLL_INTERVAL = 3000; // 3 秒
|
||||
const POLL_INTERVAL = 3000 // 3 秒
|
||||
|
||||
export const useCloneProgress = () => {
|
||||
const [clones, setClones] = useState<VoiceClone[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const timerRef = useRef<ReturnType<typeof setInterval>>(undefined);
|
||||
const [clones, setClones] = useState<VoiceClone[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const timerRef = useRef<ReturnType<typeof setInterval>>(undefined)
|
||||
|
||||
const fetchClones = useCallback(async (silent = false) => {
|
||||
if (!silent) setLoading(true);
|
||||
if (!silent) setLoading(true)
|
||||
try {
|
||||
const data = await getVoiceClones();
|
||||
setClones(data);
|
||||
const data = await getVoiceClones()
|
||||
setClones(data)
|
||||
} catch {
|
||||
// 静默失败,下次轮询重试
|
||||
} finally {
|
||||
if (!silent) setLoading(false);
|
||||
if (!silent) setLoading(false)
|
||||
}
|
||||
}, []);
|
||||
}, [])
|
||||
|
||||
/* 初始加载 */
|
||||
useEffect(() => {
|
||||
fetchClones();
|
||||
}, [fetchClones]);
|
||||
fetchClones()
|
||||
}, [fetchClones])
|
||||
|
||||
/* 轮询:有 processing 条目时启动,全部结束时停止 */
|
||||
useEffect(() => {
|
||||
const hasProcessing = clones.some((c) => c.status === "processing");
|
||||
const hasProcessing = clones.some((c) => c.status === "processing")
|
||||
|
||||
if (hasProcessing) {
|
||||
if (!timerRef.current) {
|
||||
timerRef.current = setInterval(() => {
|
||||
fetchClones(true);
|
||||
}, POLL_INTERVAL);
|
||||
fetchClones(true)
|
||||
}, POLL_INTERVAL)
|
||||
}
|
||||
} else {
|
||||
if (timerRef.current) {
|
||||
clearInterval(timerRef.current);
|
||||
timerRef.current = undefined;
|
||||
clearInterval(timerRef.current)
|
||||
timerRef.current = undefined
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timerRef.current) {
|
||||
clearInterval(timerRef.current);
|
||||
timerRef.current = undefined;
|
||||
clearInterval(timerRef.current)
|
||||
timerRef.current = undefined
|
||||
}
|
||||
};
|
||||
}, [clones, fetchClones]);
|
||||
}
|
||||
}, [clones, fetchClones])
|
||||
|
||||
/** 手动刷新 */
|
||||
const refresh = useCallback(() => fetchClones(), [fetchClones]);
|
||||
const refresh = useCallback(() => fetchClones(), [fetchClones])
|
||||
|
||||
/** 克隆成功后追加到列表 */
|
||||
const addClone = useCallback((voice: VoiceClone) => {
|
||||
setClones((prev) => [voice, ...prev]);
|
||||
}, []);
|
||||
setClones((prev) => [voice, ...prev])
|
||||
}, [])
|
||||
|
||||
/** 删除后从列表移除 */
|
||||
const removeClone = useCallback((id: string) => {
|
||||
setClones((prev) => prev.filter((c) => c.id !== id));
|
||||
}, []);
|
||||
setClones((prev) => prev.filter((c) => c.id !== id))
|
||||
}, [])
|
||||
|
||||
/** 更新某条克隆(如改名) */
|
||||
const updateClone = useCallback((updated: VoiceClone) => {
|
||||
setClones((prev) => prev.map((c) => (c.id === updated.id ? updated : c)));
|
||||
}, []);
|
||||
setClones((prev) => prev.map((c) => (c.id === updated.id ? updated : c)))
|
||||
}, [])
|
||||
|
||||
return {
|
||||
clones,
|
||||
@@ -84,5 +84,5 @@ export const useCloneProgress = () => {
|
||||
updateClone,
|
||||
/** 是否有正在处理中的克隆 */
|
||||
hasProcessing: clones.some((c) => c.status === "processing"),
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user