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

This commit is contained in:
xiaoxia
2026-09-06 11:19:33 +08:00
parent a83ed58864
commit b27ff1323d
3 changed files with 68 additions and 3 deletions
+4
View File
@@ -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

+16 -3
View File
@@ -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")