feat(web): 工单页与升级面板,修掉视图竞态与标签缺失导致的崩溃
- 新增 /w/:wsId/tickets:派给我的 / 我发起的、状态过滤、列表内联快操作 - 详情抽屉按角色显示受理/开工/提交/验收/驳回/改派/取消;任务态底部可升级成工单 - 导航抽成 WorkspaceNav,带未读角标(30 秒对齐,操作后立刻刷新) - 修竞态:切视图/切 tab 连发请求时,慢的回包会把新结果盖掉(reqId 序号丢弃过期响应) - 登录支持 ?next= 站内回跳,邀请链接不再'登录完掉回首页' - e2e/tickets.mjs:双人双上下文的完整闭环,8 项断言全绿
This commit is contained in:
+14
-2
@@ -1,13 +1,25 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { useAuth } from "../App";
|
||||
import { Layout } from "../components/Layout";
|
||||
import { ApiError, api, session, type Tokens } from "../lib/client";
|
||||
import type { User } from "../lib/types";
|
||||
|
||||
/**
|
||||
* 登录后回哪儿:`?next=` 给邀请链接用。
|
||||
* 只接受站内路径(以单个 `/` 开头),避免被拼成 `//evil.com` 的外跳。
|
||||
*/
|
||||
function safeNext(raw: string | null): string | null {
|
||||
if (!raw) return null;
|
||||
if (!raw.startsWith("/") || raw.startsWith("//")) return null;
|
||||
return raw;
|
||||
}
|
||||
|
||||
export function LoginPage() {
|
||||
const { reload } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [params] = useSearchParams();
|
||||
const next = safeNext(params.get("next"));
|
||||
const [phone, setPhone] = useState("");
|
||||
const [code, setCode] = useState("");
|
||||
const [devCode, setDevCode] = useState<string | null>(null);
|
||||
@@ -45,7 +57,7 @@ export function LoginPage() {
|
||||
const data = await api.post<{ user: User; tokens: Tokens }>("/auth/login", { phone, code });
|
||||
session.save(data.tokens);
|
||||
await reload();
|
||||
navigate("/", { replace: true });
|
||||
navigate(next ?? "/", { replace: true });
|
||||
} catch (err) {
|
||||
setError(err instanceof ApiError ? err.message : "登录失败");
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user