feat(resume): admin bulk resume import

Adds the dashboard surface for the backend's /api/admin/v1/resume/import-batches
endpoints, and the multipart support it needed.

Client:
- rawRequest passes a FormData body through untouched and skips Content-Type,
  so the browser sets multipart/form-data *with the boundary*. Setting it by
  hand stripped the boundary and the server saw an empty upload — verified
  against the backend: 202 with the fix, 422 VALIDATION_ERROR without it.
  This was the first admin endpoint in the codebase to accept a file.
- New api.upload(). fetch reports no upload progress, so upload UI stays
  indeterminate rather than faking a percentage.

UI (/resume/import-batches, gated on admin:resume:read; writes on :manage):
- Upload dialog with countryCode and dry-run checked by default — seeing the
  collision report before creating accounts is almost always what you want.
- Detail sheet polls every 2s and stops once the batch is terminal. Clickable
  count tiles filter the per-resume report; step statuses surface `命中缓存` /
  `复用结果` so the dedup is visible.
- Confirm a dry run, retry failed rows, and resolve a missing contact.
  IMPORT_IDENTITY_TAKEN is shown inline — a collision is expected input, not
  an error toast.
- progress.total is 0 until unpacking finishes, so both views render 准备中…
  rather than "0 / 0", which would read as an empty batch.

types.gen.ts regenerated from the backend's admin spec.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-24 23:21:15 +08:00
co-authored by Claude Opus 5
parent 2c6fed7149
commit 78c28ed1bd
14 changed files with 2243 additions and 13 deletions
+22
View File
@@ -32,6 +32,7 @@ import { Route as AuthedStudioRobotAuthorsRouteImport } from './routes/_authed/s
import { Route as AuthedStudioCategoryBriefsRouteImport } from './routes/_authed/studio/category-briefs'
import { Route as AuthedStudioArticlesRouteImport } from './routes/_authed/studio/articles'
import { Route as AuthedStudioAgentTokensRouteImport } from './routes/_authed/studio/agent-tokens'
import { Route as AuthedResumeImportBatchesRouteImport } from './routes/_authed/resume/import-batches'
import { Route as AuthedExcProvidersRouteImport } from './routes/_authed/exc/providers'
import { Route as AuthedExcOrdersRouteImport } from './routes/_authed/exc/orders'
import { Route as AuthedExcDispatchesRouteImport } from './routes/_authed/exc/dispatches'
@@ -158,6 +159,12 @@ const AuthedStudioAgentTokensRoute = AuthedStudioAgentTokensRouteImport.update({
path: '/studio/agent-tokens',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedResumeImportBatchesRoute =
AuthedResumeImportBatchesRouteImport.update({
id: '/resume/import-batches',
path: '/resume/import-batches',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedExcProvidersRoute = AuthedExcProvidersRouteImport.update({
id: '/exc/providers',
path: '/exc/providers',
@@ -216,6 +223,7 @@ export interface FileRoutesByFullPath {
'/exc/dispatches': typeof AuthedExcDispatchesRoute
'/exc/orders': typeof AuthedExcOrdersRoute
'/exc/providers': typeof AuthedExcProvidersRoute
'/resume/import-batches': typeof AuthedResumeImportBatchesRoute
'/studio/agent-tokens': typeof AuthedStudioAgentTokensRoute
'/studio/articles': typeof AuthedStudioArticlesRoute
'/studio/category-briefs': typeof AuthedStudioCategoryBriefsRoute
@@ -247,6 +255,7 @@ export interface FileRoutesByTo {
'/exc/dispatches': typeof AuthedExcDispatchesRoute
'/exc/orders': typeof AuthedExcOrdersRoute
'/exc/providers': typeof AuthedExcProvidersRoute
'/resume/import-batches': typeof AuthedResumeImportBatchesRoute
'/studio/agent-tokens': typeof AuthedStudioAgentTokensRoute
'/studio/articles': typeof AuthedStudioArticlesRoute
'/studio/category-briefs': typeof AuthedStudioCategoryBriefsRoute
@@ -280,6 +289,7 @@ export interface FileRoutesById {
'/_authed/exc/dispatches': typeof AuthedExcDispatchesRoute
'/_authed/exc/orders': typeof AuthedExcOrdersRoute
'/_authed/exc/providers': typeof AuthedExcProvidersRoute
'/_authed/resume/import-batches': typeof AuthedResumeImportBatchesRoute
'/_authed/studio/agent-tokens': typeof AuthedStudioAgentTokensRoute
'/_authed/studio/articles': typeof AuthedStudioArticlesRoute
'/_authed/studio/category-briefs': typeof AuthedStudioCategoryBriefsRoute
@@ -313,6 +323,7 @@ export interface FileRouteTypes {
| '/exc/dispatches'
| '/exc/orders'
| '/exc/providers'
| '/resume/import-batches'
| '/studio/agent-tokens'
| '/studio/articles'
| '/studio/category-briefs'
@@ -344,6 +355,7 @@ export interface FileRouteTypes {
| '/exc/dispatches'
| '/exc/orders'
| '/exc/providers'
| '/resume/import-batches'
| '/studio/agent-tokens'
| '/studio/articles'
| '/studio/category-briefs'
@@ -376,6 +388,7 @@ export interface FileRouteTypes {
| '/_authed/exc/dispatches'
| '/_authed/exc/orders'
| '/_authed/exc/providers'
| '/_authed/resume/import-batches'
| '/_authed/studio/agent-tokens'
| '/_authed/studio/articles'
| '/_authed/studio/category-briefs'
@@ -556,6 +569,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthedStudioAgentTokensRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/resume/import-batches': {
id: '/_authed/resume/import-batches'
path: '/resume/import-batches'
fullPath: '/resume/import-batches'
preLoaderRoute: typeof AuthedResumeImportBatchesRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/exc/providers': {
id: '/_authed/exc/providers'
path: '/exc/providers'
@@ -627,6 +647,7 @@ interface AuthedRouteChildren {
AuthedExcDispatchesRoute: typeof AuthedExcDispatchesRoute
AuthedExcOrdersRoute: typeof AuthedExcOrdersRoute
AuthedExcProvidersRoute: typeof AuthedExcProvidersRoute
AuthedResumeImportBatchesRoute: typeof AuthedResumeImportBatchesRoute
AuthedStudioAgentTokensRoute: typeof AuthedStudioAgentTokensRoute
AuthedStudioArticlesRoute: typeof AuthedStudioArticlesRoute
AuthedStudioCategoryBriefsRoute: typeof AuthedStudioCategoryBriefsRoute
@@ -657,6 +678,7 @@ const AuthedRouteChildren: AuthedRouteChildren = {
AuthedExcDispatchesRoute: AuthedExcDispatchesRoute,
AuthedExcOrdersRoute: AuthedExcOrdersRoute,
AuthedExcProvidersRoute: AuthedExcProvidersRoute,
AuthedResumeImportBatchesRoute: AuthedResumeImportBatchesRoute,
AuthedStudioAgentTokensRoute: AuthedStudioAgentTokensRoute,
AuthedStudioArticlesRoute: AuthedStudioArticlesRoute,
AuthedStudioCategoryBriefsRoute: AuthedStudioCategoryBriefsRoute,