fix(#1718): 昵称页提交按钮加同步防连点守卫(连点致 PATCH/me 重复提交)、补 /vite.svg favicon 消除 404
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 1m58s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 2m2s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m8s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 2m11s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m10s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m18s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 2m19s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m21s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 4m8s
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 2m33s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 7s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 27s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 1m58s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 2m2s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m8s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 2m11s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m10s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m18s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 2m19s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m21s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 4m8s
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 2m33s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 7s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 27s
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<rect width="64" height="64" rx="14" fill="#3b82f6"/>
|
||||
<text x="32" y="44" font-size="34" text-anchor="middle">🦐</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 194 B |
@@ -2,11 +2,12 @@
|
||||
* 微信新用户昵称引导页
|
||||
* 新微信用户首次登录后强制填写昵称,完成后才进入主界面
|
||||
*/
|
||||
import React from "react"
|
||||
import React, { useRef } from "react"
|
||||
import { Form, Input, message } from "antd"
|
||||
import { Navigate, useNavigate } from "react-router-dom"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { updateProfile } from "@/api/auth"
|
||||
import { getErrorMessage, isErrorMsgShown } from "@/api/errors"
|
||||
import { useAuthStore } from "@/store/authStore"
|
||||
import Button from "@/components/ui/Button"
|
||||
import "./Login.css"
|
||||
@@ -22,6 +23,10 @@ const WechatOnboarding: React.FC = () => {
|
||||
const user = useAuthStore((state) => state.user)
|
||||
const hasAccessToken = Boolean(localStorage.getItem("access_token"))
|
||||
const [form] = Form.useForm<OnboardingFormValues>()
|
||||
// 同步防连点守卫:antd loading 要等 React 重渲染后才禁用按钮,
|
||||
// 连点两次时第一次的 mutation 刚触发、重渲染未发生,第二次 click 仍会进来
|
||||
// (截图里 PATCH /me 405 出现两次就是连点导致的重复提交)
|
||||
const submittingRef = useRef(false)
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: (displayName: string) => updateProfile({ display_name: displayName }),
|
||||
@@ -37,6 +42,8 @@ const WechatOnboarding: React.FC = () => {
|
||||
}
|
||||
|
||||
const onFinish = async (values: OnboardingFormValues) => {
|
||||
if (submittingRef.current) return
|
||||
submittingRef.current = true
|
||||
try {
|
||||
const updated = await saveMutation.mutateAsync(values.display_name.trim())
|
||||
// 后端返回的 profile_completed 以最新资料为准,前端同步标记完善
|
||||
@@ -45,9 +52,14 @@ const WechatOnboarding: React.FC = () => {
|
||||
const redirect = localStorage.getItem("login_redirect") || "/app/dashboard"
|
||||
localStorage.removeItem("login_redirect")
|
||||
navigate(redirect, { replace: true })
|
||||
} catch {
|
||||
message.error("保存失败,请重试")
|
||||
} catch (err) {
|
||||
// 透传后端真实原因(如接口异常/校验失败);拦截器已弹过的不重复弹
|
||||
if (!isErrorMsgShown(err)) {
|
||||
message.error(`昵称保存失败:${getErrorMessage(err, "请稍后重试")}`)
|
||||
}
|
||||
submittingRef.current = false
|
||||
}
|
||||
// 成功时页面跳走,不复位
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -89,6 +101,7 @@ const WechatOnboarding: React.FC = () => {
|
||||
buttonSize="lg"
|
||||
htmlType="submit"
|
||||
loading={saveMutation.isPending}
|
||||
disabled={saveMutation.isPending}
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
{saveMutation.isPending ? "保存中..." : "进入小虾智剪"}
|
||||
|
||||
@@ -122,6 +122,54 @@ describe("WechatOnboarding 昵称引导页", () => {
|
||||
)
|
||||
})
|
||||
|
||||
it("连点提交按钮只触发一次请求(防重复提交)", async () => {
|
||||
// mutation 挂起不立即完成,模拟慢网络下连续双击
|
||||
let resolveSubmit: (v: unknown) => void = () => {}
|
||||
updateProfileMock = vi.fn(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
resolveSubmit = resolve
|
||||
}),
|
||||
)
|
||||
renderPage()
|
||||
fireEvent.change(screen.getByPlaceholderText("请输入您的昵称"), {
|
||||
target: { value: "小虾用户" },
|
||||
})
|
||||
const btn = screen.getByText("进入小虾智剪")
|
||||
fireEvent.click(btn)
|
||||
// 第一次点击后立即再点(此时重渲染/loading 可能还没生效)
|
||||
fireEvent.click(btn)
|
||||
fireEvent.click(btn)
|
||||
await waitFor(() => {
|
||||
expect(updateProfileMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
// 释放挂起的 Promise,避免泄漏
|
||||
resolveSubmit({ id: "u1", display_name: "小虾用户", profile_completed: true })
|
||||
})
|
||||
|
||||
it("提交失败后守卫复位,允许再次提交", async () => {
|
||||
updateProfileMock = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce({
|
||||
isAxiosError: true,
|
||||
response: { status: 500, data: { detail: "服务内部错误" } },
|
||||
})
|
||||
.mockResolvedValueOnce({ id: "u1", display_name: "小虾用户", profile_completed: true })
|
||||
renderPage()
|
||||
fireEvent.change(screen.getByPlaceholderText("请输入您的昵称"), {
|
||||
target: { value: "小虾用户" },
|
||||
})
|
||||
fireEvent.click(screen.getByText("进入小虾智剪"))
|
||||
await waitFor(() => {
|
||||
expect(updateProfileMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
// 失败后再点一次,应能重新提交
|
||||
fireEvent.click(screen.getByText("进入小虾智剪"))
|
||||
await waitFor(() => {
|
||||
expect(updateProfileMock).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
|
||||
it("提交失败显示错误且不跳转", async () => {
|
||||
updateProfileMock = vi.fn(async () => {
|
||||
throw new Error("500")
|
||||
|
||||
Reference in New Issue
Block a user