diff --git a/app/_layout.tsx b/app/_layout.tsx index e4c775f..46a9640 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -21,6 +21,7 @@ export default function RootLayout() { + diff --git a/app/resume-versions.tsx b/app/resume-versions.tsx new file mode 100644 index 0000000..48aa18a --- /dev/null +++ b/app/resume-versions.tsx @@ -0,0 +1,464 @@ +import { Ionicons } from '@expo/vector-icons'; +import { useRouter } from 'expo-router'; +import { useCallback, useState } from 'react'; +import { + ActivityIndicator, + Alert, + KeyboardAvoidingView, + Modal, + Platform, + Pressable, + ScrollView, + StyleSheet, + Text, + TextInput, + View, +} from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { useResumeVersions } from '../src/hooks'; +import { colors, spacing } from '../src/theme'; +import type { ResumeVersion } from '../src/types'; +import { formatBytes, formatDateTime } from '../src/utils'; + +/* ===================== 子组件:顶部导航 ===================== */ + +function Header({ + onBack, + onNew, +}: { + onBack: () => void; + onNew: () => void; +}) { + return ( + + + + + 简历版本 + + + + + ); +} + +/* ===================== 子组件:版本卡 ===================== */ + +function VersionCard({ + version, + onSetDefault, + onExport, + onDelete, +}: { + version: ResumeVersion; + onSetDefault: () => void; + onExport: () => void; + onDelete: () => void; +}) { + return ( + + + + + {version.name} + + {version.isDefault && ( + + 默认 + + )} + + v{version.versionNo} + + + + {version.fileName} · {formatBytes(version.fileSize)} + + + 更新于 {formatDateTime(version.updatedAt)} + + + + {!version.isDefault && ( + + + 设为默认 + + )} + + + 导出 + + {!version.isDefault && ( + + + 删除 + + )} + + + ); +} + +/* ===================== 子组件:新建版本弹窗 ===================== */ + +function NewVersionModal({ + visible, + onClose, + onCreate, +}: { + visible: boolean; + onClose: () => void; + onCreate: (name: string) => void; +}) { + const [name, setName] = useState(''); + + const handleConfirm = () => { + onCreate(name.trim()); + setName(''); + }; + + return ( + + + + 新建简历版本 + + {/* affordance-pending-backend:真实文件上传需后端 multipart 接口,当前以占位文件创建 */} + 文件上传需后端支持,当前以占位文件创建版本 + + + 取消 + + + 创建 + + + + + + ); +} + +/* ===================== 页面 ===================== */ + +export default function ResumeVersionsScreen() { + const router = useRouter(); + const { versions, loading, error, refresh, busy, create, setDefault, remove, exportAndPoll } = + useResumeVersions(); + const [newVisible, setNewVisible] = useState(false); + + const handleNew = useCallback( + (name: string) => { + setNewVisible(false); + void create({ + name: name || undefined, + fileName: `${name || '简历'}版本.pdf`, + fileSize: 200_000, + }); + }, + [create], + ); + + const handleSetDefault = useCallback( + (id: string, name: string) => { + Alert.alert('设为默认', `将「${name}」设为默认简历?`, [ + { text: '取消', style: 'cancel' }, + { text: '设为默认', onPress: () => void setDefault(id) }, + ]); + }, + [setDefault], + ); + + const handleDelete = useCallback( + (id: string, name: string) => { + Alert.alert('删除版本', `确定删除「${name}」吗?默认版本需先设其他版本为默认。`, [ + { text: '取消', style: 'cancel' }, + { text: '删除', style: 'destructive', onPress: () => void remove(id) }, + ]); + }, + [remove], + ); + + const handleExport = useCallback( + async (id: string, name: string) => { + const task = await exportAndPoll(id); + if (!task) { + Alert.alert('导出失败', '导出任务异常,请稍后重试'); + return; + } + if (task.status === 'SUCCESS') { + Alert.alert('导出完成', `「${name}」已导出${task.outputUrl ? `:${task.outputUrl}` : ''}`); + } else if (task.status === 'FAILED') { + Alert.alert('导出失败', task.error || '导出任务失败'); + } else { + Alert.alert('导出中', '任务仍在处理,请稍后到导出记录查看'); + } + }, + [exportAndPoll], + ); + + return ( + +
router.back()} onNew={() => setNewVisible(true)} /> + + {loading ? ( + + + + ) : error ? ( + + 加载失败 + + 重试 + + + ) : versions.length === 0 ? ( + + + 还没有简历版本 + setNewVisible(true)}> + 新建版本 + + + ) : ( + + {versions.map((v) => ( + handleSetDefault(v.id, v.name)} + onExport={() => void handleExport(v.id, v.name)} + onDelete={() => handleDelete(v.id, v.name)} + /> + ))} + + )} + + {busy && ( + + + + )} + + setNewVisible(false)} + onCreate={handleNew} + /> + + ); +} + +const styles = StyleSheet.create({ + safeArea: { + flex: 1, + backgroundColor: colors.pageBg, + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: spacing.card, + height: 52, + backgroundColor: colors.white, + borderBottomWidth: 1, + borderBottomColor: colors.borderLight, + }, + headerBtn: { + width: 40, + height: 40, + alignItems: 'center', + justifyContent: 'center', + }, + headerTitle: { + fontSize: 17, + fontWeight: '700', + color: colors.textPrimary, + }, + headerActionBtn: { + width: 40, + height: 40, + alignItems: 'center', + justifyContent: 'center', + }, + scroll: { + flex: 1, + }, + scrollContent: { + padding: spacing.card, + paddingBottom: 24, + }, + card: { + backgroundColor: colors.white, + borderRadius: spacing.card, + padding: spacing.card, + marginBottom: spacing.card, + }, + cardDefault: { + borderWidth: 1, + borderColor: colors.primary, + }, + cardTop: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + cardTitleWrap: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, + marginRight: 8, + }, + cardTitle: { + fontSize: 15, + fontWeight: '700', + color: colors.textPrimary, + }, + defaultBadge: { + marginLeft: 8, + paddingHorizontal: 6, + paddingVertical: 2, + borderRadius: 4, + backgroundColor: colors.primaryLight, + }, + defaultBadgeText: { + fontSize: 11, + fontWeight: '700', + color: colors.primary, + }, + cardVersion: { + fontSize: 13, + color: colors.textSecondary, + }, + cardMeta: { + marginTop: 6, + fontSize: 13, + color: colors.textSecondary, + }, + cardTime: { + marginTop: 2, + fontSize: 12, + color: colors.textHint, + }, + cardActions: { + flexDirection: 'row', + marginTop: 12, + paddingTop: 12, + borderTopWidth: 1, + borderTopColor: colors.borderLight, + }, + actionBtn: { + flexDirection: 'row', + alignItems: 'center', + marginRight: 20, + }, + actionText: { + marginLeft: 4, + fontSize: 13, + color: colors.textSecondary, + }, + stateBox: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + }, + stateText: { + marginTop: 12, + fontSize: 14, + color: colors.textSecondary, + }, + retryBtn: { + marginTop: 16, + paddingHorizontal: 24, + paddingVertical: spacing.card, + borderRadius: 18, + backgroundColor: colors.primaryBtn, + }, + retryText: { + fontSize: 13, + fontWeight: '600', + color: colors.white, + }, + busyMask: { + ...StyleSheet.absoluteFill, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: colors.scrimLight, + }, + modalMask: { + flex: 1, + backgroundColor: colors.scrim, + justifyContent: 'flex-end', + }, + modalSheet: { + backgroundColor: colors.white, + borderTopLeftRadius: 16, + borderTopRightRadius: 16, + padding: spacing.card, + paddingBottom: 24, + }, + modalTitle: { + fontSize: 16, + fontWeight: '700', + color: colors.textPrimary, + marginBottom: 12, + }, + modalInput: { + borderWidth: 1, + borderColor: colors.borderLight, + borderRadius: spacing.card, + paddingHorizontal: 12, + paddingVertical: 10, + fontSize: 14, + color: colors.textPrimary, + }, + modalHint: { + marginTop: 8, + fontSize: 12, + color: colors.textHint, + }, + modalActions: { + flexDirection: 'row', + marginTop: 16, + }, + modalCancel: { + flex: 1, + alignItems: 'center', + paddingVertical: 12, + marginRight: 8, + borderRadius: spacing.card, + backgroundColor: colors.pageBg, + }, + modalCancelText: { + fontSize: 14, + color: colors.textSecondary, + }, + modalConfirm: { + flex: 1, + alignItems: 'center', + paddingVertical: 12, + marginLeft: 8, + borderRadius: spacing.card, + backgroundColor: colors.primaryBtn, + }, + modalConfirmText: { + fontSize: 14, + fontWeight: '700', + color: colors.white, + }, +}); diff --git a/app/resume.tsx b/app/resume.tsx index 8f7bfe2..fedfabf 100644 --- a/app/resume.tsx +++ b/app/resume.tsx @@ -29,11 +29,13 @@ function Header({ onBack, onSave, onExport, + onVersions, }: { onBack: () => void; /** 传 undefined = 简历未就绪,按钮置灰 */ onSave?: () => void; onExport: () => void; + onVersions?: () => void; }) { const saveDisabled = onSave === undefined; return ( @@ -43,6 +45,16 @@ function Header({ AI简历优化 + {onVersions && ( + + + + )} router.back()} onSave={handleSave} onExport={() => router.push('/resume/export')} + onVersions={() => router.push('/resume-versions')} /> { return mockRequest((await storageGet(RESUME_SAVED_AT_KEY)) ?? ''); } +/* ===================== 简历版本(P0-5):列表/创建/详情/修改/删除/设默认/导出任务 ===================== */ + +const nowIso = () => new Date().toISOString(); + +/** mock 简历版本(模块级可变副本,模拟持久态;真实后端以服务端为准) */ +let MOCK_RESUME_VERSIONS: ResumeVersion[] = [ + { + id: 'rv-1', + name: '求职通用版', + versionNo: 2, + isDefault: true, + fileName: '简历-通用版.pdf', + fileSize: 248_000, + status: 'ACTIVE', + createdAt: '2026-09-10T09:20:00.000Z', + updatedAt: '2026-09-12T14:05:00.000Z', + }, + { + id: 'rv-2', + name: '大厂冲刺版', + versionNo: 1, + isDefault: false, + fileName: '简历-大厂冲刺.pdf', + fileSize: 312_000, + status: 'ACTIVE', + createdAt: '2026-09-08T20:11:00.000Z', + updatedAt: '2026-09-08T20:11:00.000Z', + }, +]; + +/** + * 获取简历版本列表(ACTIVE,versionNo 倒序)。 + * @method GET + * @url /profile/me/resume-versions + * @returns Promise 版本数组 + */ +export async function fetchResumeVersions(): Promise { + if (isMockMode()) { + const list = [...MOCK_RESUME_VERSIONS] + .filter((v) => v.status === 'ACTIVE') + .sort((a, b) => b.versionNo - a.versionNo); + return mockRequest(list); + } + const res = await request('/profile/me/resume-versions'); + return (res ?? []).filter((v) => v.status === 'ACTIVE'); +} + +/** + * 创建简历版本(仅本人 STUDENT)。 + * @method POST + * @url /profile/me/resume-versions + * @remark 真实后端用 multipart 上传文件(file ≤10MB);mock 以占位元数据模拟,不触网 + * @param input.name 版本名(可空) + * @param input.fileName 文件名(占位) + * @param input.fileSize 文件大小(占位) + * @returns Promise 新建版本 + */ +export async function createResumeVersion( + input: CreateResumeVersionInput, +): Promise { + if (isMockMode()) { + const maxNo = MOCK_RESUME_VERSIONS.reduce( + (m, v) => Math.max(m, v.versionNo), + 0, + ); + const version: ResumeVersion = { + id: `rv-${Date.now()}`, + name: input.name?.trim() || `简历版本 ${maxNo + 1}`, + versionNo: maxNo + 1, + isDefault: false, + fileName: input.fileName, + fileSize: input.fileSize, + status: 'ACTIVE', + createdAt: nowIso(), + updatedAt: nowIso(), + }; + MOCK_RESUME_VERSIONS = [version, ...MOCK_RESUME_VERSIONS]; + return mockRequest(version); + } + // 真实后端:multipart/form-data,name 作 query,file 作二进制 + const res = await request('/profile/me/resume-versions', { + method: 'POST', + auth: true, + body: { name: input.name, fileName: input.fileName } as unknown, + }); + return res; +} + +/** + * 获取简历版本详情(仅本人)。 + * @method GET + * @url /profile/me/resume-versions/{id} + * @param id 版本 id + * @returns Promise 版本详情 + */ +export async function fetchResumeVersionDetail(id: string): Promise { + if (isMockMode()) { + const found = MOCK_RESUME_VERSIONS.find((v) => v.id === id); + if (!found) throw new ApiError({ httpStatus: 404, code: 404, message: '版本不存在' }); + return mockRequest(found); + } + return request(`/profile/me/resume-versions/${id}`, { + method: 'GET', + auth: true, + }); +} + +/** + * 修改简历版本(本人;可选重命名 / 替换文件)。 + * @method PUT + * @url /profile/me/resume-versions/{id} + * @param id 版本 id + * @param input 修改项(至少一项) + * @returns Promise 修改后的版本 + */ +export async function updateResumeVersion( + id: string, + input: UpdateResumeVersionInput, +): Promise { + if (isMockMode()) { + const idx = MOCK_RESUME_VERSIONS.findIndex((v) => v.id === id); + if (idx < 0) throw new ApiError({ httpStatus: 404, code: 404, message: '版本不存在' }); + const updated: ResumeVersion = { + ...MOCK_RESUME_VERSIONS[idx], + ...(input.name !== undefined ? { name: input.name } : {}), + ...(input.fileName !== undefined ? { fileName: input.fileName } : {}), + ...(input.fileSize !== undefined ? { fileSize: input.fileSize } : {}), + updatedAt: nowIso(), + }; + MOCK_RESUME_VERSIONS = MOCK_RESUME_VERSIONS.map((v) => + v.id === id ? updated : v, + ); + return mockRequest(updated); + } + const res = await request( + `/profile/me/resume-versions/${id}`, + { method: 'PUT', auth: true, body: input as unknown }, + ); + return res; +} + +/** + * 删除简历版本(默认版本需先设其他默认;导出进行中不可删)。 + * @method DELETE + * @url /profile/me/resume-versions/{id} + * @param id 版本 id + */ +export async function deleteResumeVersion(id: string): Promise { + if (isMockMode()) { + const target = MOCK_RESUME_VERSIONS.find((v) => v.id === id); + if (!target) throw new ApiError({ httpStatus: 404, code: 404, message: '版本不存在' }); + if (target.isDefault) { + throw new ApiError({ + httpStatus: 409, + code: 409, + message: '默认版本不可直接删除,请先设置其他版本为默认', + }); + } + MOCK_RESUME_VERSIONS = MOCK_RESUME_VERSIONS.filter((v) => v.id !== id); + return mockRequest(undefined); + } + await request(`/profile/me/resume-versions/${id}`, { + method: 'DELETE', + auth: true, + }); +} + +/** + * 设为默认简历(同用户最多一个默认;已默认幂等)。 + * @method POST + * @url /profile/me/resume-versions/{id}/default + * @param id 版本 id + */ +export async function setDefaultResumeVersion(id: string): Promise { + if (isMockMode()) { + const target = MOCK_RESUME_VERSIONS.find((v) => v.id === id); + if (!target) throw new ApiError({ httpStatus: 404, code: 404, message: '版本不存在' }); + MOCK_RESUME_VERSIONS = MOCK_RESUME_VERSIONS.map((v) => ({ + ...v, + isDefault: v.id === id, + })); + return mockRequest(undefined); + } + await request(`/profile/me/resume-versions/${id}/default`, { + method: 'POST', + auth: true, + }); +} + +/** + * 创建简历导出任务(异步复制版本文件到导出目录)。 + * @method POST + * @url /profile/me/resume-versions/{id}/export + * @param id 版本 id + * @returns Promise 任务(初始状态) + */ +export async function createResumeExportTask(id: string): Promise { + if (isMockMode()) { + const task: ResumeExportTask = { + id: `task-${Date.now()}`, + status: 'SUCCESS', + outputUrl: `file://mock/resume-export/${id}.pdf`, + }; + return mockRequest(task); + } + const res = await request( + `/profile/me/resume-versions/${id}/export`, + { method: 'POST', auth: true }, + ); + return res; +} + +/** + * 查询简历导出任务状态(仅本人;SUCCESS 含输出文件,FAILED 含原因)。 + * @method GET + * @url /resume-export-tasks/{taskId} + * @param taskId 任务 id + * @returns Promise 任务状态 + */ +export async function fetchResumeExportTask(taskId: string): Promise { + if (isMockMode()) { + return mockRequest({ + id: taskId, + status: 'SUCCESS', + outputUrl: `file://mock/resume-export/${taskId}.pdf`, + }); + } + return request(`/resume-export-tasks/${taskId}`, { + method: 'GET', + auth: true, + }); +} + /** * 清除简历编辑覆盖层与保存时间 * @method DELETE diff --git a/src/api/index.ts b/src/api/index.ts index 2f792df..d063858 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -144,6 +144,14 @@ export { buildRankReportText, fetchResume, fetchResumeExportSettings, + fetchResumeVersions, + fetchResumeVersionDetail, + createResumeVersion, + updateResumeVersion, + deleteResumeVersion, + setDefaultResumeVersion, + createResumeExportTask, + fetchResumeExportTask, fetchResumeSavedAt, clearResumeOverride, fetchSalaryBrackets, diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 8913191..95d48a2 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -61,6 +61,7 @@ export { useProfileData } from './useProfileData'; export { usePersonalDataExport } from './usePersonalDataExport'; export { useRankData } from './useRankData'; export { useResume } from './useResume'; +export { useResumeVersions } from './useResumeVersions'; export { useResumeExport } from './useResumeExport'; export { useSalaryBrackets } from './useSalaryBrackets'; export { useSalaryCalculator } from './useSalaryCalculator'; diff --git a/src/hooks/useResumeVersions.ts b/src/hooks/useResumeVersions.ts new file mode 100644 index 0000000..6ddb617 --- /dev/null +++ b/src/hooks/useResumeVersions.ts @@ -0,0 +1,118 @@ +/** + * 简历版本管理 hook:列表 + 创建 / 设默认 / 删除 / 导出任务。 + * 操作后自动刷新列表;导出任务轮询到终态后返回,供页面提示结果。 + */ +import { useCallback, useState } from 'react'; +import { + createResumeExportTask, + createResumeVersion, + deleteResumeVersion, + fetchResumeExportTask, + fetchResumeVersions, + setDefaultResumeVersion, +} from '../api'; +import type { ResumeExportTask, ResumeVersion } from '../types'; +import { useAsyncData } from './useAsyncData'; + +export interface ResumeVersionsState { + /** 版本列表(versionNo 倒序) */ + versions: ResumeVersion[]; + /** 是否加载中 */ + loading: boolean; + /** 错误对象 */ + error: Error | null; + /** 刷新列表 */ + refresh: () => void; + /** 操作进行中(创建/设默认/删除/导出任一进行) */ + busy: boolean; + /** 创建版本(占位元数据;真实后端走 multipart 上传) */ + create: (input: { + name?: string; + fileName: string; + fileSize: number; + }) => Promise; + /** 设为默认 */ + setDefault: (id: string) => Promise; + /** 删除版本 */ + remove: (id: string) => Promise; + /** 创建导出任务并轮询到终态,返回最终任务(或异常时 null) */ + exportAndPoll: (id: string) => Promise; +} + +export function useResumeVersions(): ResumeVersionsState { + const [busy, setBusy] = useState(false); + + const fetcher = useCallback(() => fetchResumeVersions(), []); + const { data, loading, error, refresh } = useAsyncData(fetcher); + + const create = useCallback( + async (input: { name?: string; fileName: string; fileSize: number }) => { + setBusy(true); + try { + await createResumeVersion(input); + refresh(); + } finally { + setBusy(false); + } + }, + [refresh], + ); + + const setDefault = useCallback( + async (id: string) => { + setBusy(true); + try { + await setDefaultResumeVersion(id); + refresh(); + } finally { + setBusy(false); + } + }, + [refresh], + ); + + const remove = useCallback( + async (id: string) => { + setBusy(true); + try { + await deleteResumeVersion(id); + refresh(); + } finally { + setBusy(false); + } + }, + [refresh], + ); + + const exportAndPoll = useCallback(async (id: string) => { + setBusy(true); + try { + const task = await createResumeExportTask(id); + // 轮询到终态(最多 6 次,2s 间隔);mock 直接 SUCCESS + let current = task; + for ( + let i = 0; + i < 6 && (current.status === 'PENDING' || current.status === 'RUNNING'); + i++ + ) { + await new Promise((r) => setTimeout(r, 2000)); + current = await fetchResumeExportTask(current.id); + } + return current; + } finally { + setBusy(false); + } + }, []); + + return { + versions: data ?? [], + loading, + error, + refresh, + busy, + create, + setDefault, + remove, + exportAndPoll, + }; +} diff --git a/src/theme/colors.ts b/src/theme/colors.ts index 6481f43..073ea17 100644 --- a/src/theme/colors.ts +++ b/src/theme/colors.ts @@ -114,6 +114,8 @@ export const colors = { focusRing: '#1F6FDB', /** 弹层遮罩 · 半透明黑(Modal / 底部弹层背景) */ scrim: 'rgba(0, 0, 0, 0.35)', + /** 轻遮罩 · 极淡半透明黑(操作进行中的整屏蒙层,不夺视线) */ + scrimLight: 'rgba(0, 0, 0, 0.04)', /* ---- 品牌色 / 深色底上的半透明白(图形与描边用) ---- */ /** @@ -285,6 +287,10 @@ export const darkColors = { textInverse: '#0E1116', shadow: '#000000', focusRing: '#5399FD', + /** 弹层遮罩(暗模式更重,保证白色弹层可读) */ + scrim: 'rgba(0, 0, 0, 0.5)', + /** 轻遮罩(暗模式) */ + scrimLight: 'rgba(0, 0, 0, 0.06)', } as const; /* ===================== 渐变(组合色 · 独立于单色令牌) ===================== */ diff --git a/src/types/index.ts b/src/types/index.ts index 61acbba..def268c 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -146,6 +146,10 @@ export type { ResumeExportFormat, ResumeExportOutcome, ResumeExportSettings, + ResumeVersion, + CreateResumeVersionInput, + UpdateResumeVersionInput, + ResumeExportTask, ResumeItemInput, ResumeJobIntention, ResumeOptimizePoint, diff --git a/src/types/resume.ts b/src/types/resume.ts index 10edd4a..d0c5053 100644 --- a/src/types/resume.ts +++ b/src/types/resume.ts @@ -223,3 +223,59 @@ export type ResumeExportOutcome = pages: number; } | { ok: false; message: string }; + +/* ===================== 简历版本(对齐 /profile/me/resume-versions) ===================== */ + +/** 简历版本 */ +export interface ResumeVersion { + /** 版本 id */ + id: string; + /** 版本名(用户自定义,可空) */ + name: string; + /** 版本号(后端生成,倒序展示) */ + versionNo: number; + /** 是否为默认版本 */ + isDefault: boolean; + /** 文件名 */ + fileName: string; + /** 文件大小(字节) */ + fileSize: number; + /** 状态:ACTIVE / DELETED 等 */ + status: 'ACTIVE' | string; + /** 创建时间 ISO */ + createdAt: string; + /** 更新时间 ISO */ + updatedAt: string; +} + +/** 新建简历版本入参(mock 阶段不含真实文件二进制;真实后端用 multipart) */ +export interface CreateResumeVersionInput { + /** 版本名(可空) */ + name?: string; + /** 文件名(mock 占位,真实后端由上传文件决定) */ + fileName: string; + /** 文件大小(字节,mock 占位) */ + fileSize: number; +} + +/** 修改简历版本入参(全部可选;至少传一项) */ +export interface UpdateResumeVersionInput { + /** 重命名 */ + name?: string; + /** 替换文件(mock 占位) */ + fileName?: string; + /** 替换文件大小(字节) */ + fileSize?: number; +} + +/** 简历导出任务(对齐 /resume-export-tasks/{taskId}) */ +export interface ResumeExportTask { + /** 任务 id */ + id: string; + /** PENDING / RUNNING / SUCCESS / FAILED */ + status: 'PENDING' | 'RUNNING' | 'SUCCESS' | 'FAILED' | string; + /** SUCCESS 时输出文件地址 */ + outputUrl?: string; + /** FAILED 时失败原因 */ + error?: string; +}