/** 与后端 `SMS_RESEND_INTERVAL_SECONDS` 一致,用于「获取验证码」按钮倒计时(秒);可在 .env 设 `VITE_SMS_RESEND_INTERVAL_SECONDS`) */ export function getSmsResendCooldownSeconds(): number { const raw = import.meta.env.VITE_SMS_RESEND_INTERVAL_SECONDS as string | undefined if (raw != null && String(raw).trim() !== '') { const n = parseInt(String(raw).trim(), 10) if (Number.isFinite(n) && n >= 1) return Math.min(n, 600) } return 60 } export const PARTICIPANT_SMS_SEND_PATH = '/api/auth/sms/send' as const export const PARTICIPANT_SMS_LOGIN_PATH = '/api/auth/sms/login' as const export const TOKEN_KEY = 'cxxfds_token' as const /** 选手端最近一次访问的赛事 slug(用于 / 与旧链接跳转) */ export const PARTICIPANT_COMPETITION_SLUG_KEY = 'cxxfds_participant_competition_slug' as const /** 管理端 Bearer Token(与选手端分离) */ export const ADMIN_TOKEN_KEY = 'cxxfds_admin_token' as const /** 当前选中赛事(顶栏切换器),仅存 id */ export const ADMIN_COMPETITION_ID_KEY = 'cxxfds_admin_competition_id' as const /** 评审端 Bearer Token(与选手端、管理端分离) */ export const REVIEWER_TOKEN_KEY = 'cxxfds_reviewer_token' as const /** 评审端会话绑定的赛事 slug */ export const REVIEWER_COMPETITION_SLUG_KEY = 'cxxfds_reviewer_competition_slug' as const export const REVIEW_AUTH_LOGIN_PATH = '/api/v1/review/auth/login' as const export const REVIEW_ME_PATH = '/api/v1/review/me' as const export const REVIEW_APPLICATIONS_INDEX_PATH = '/api/v1/review/applications' as const export function reviewApplicationDetailUrl(applicationId: number, competitionSlug: string): string { const qs = new URLSearchParams({ competition_slug: competitionSlug }) return `${getApiBase()}/api/v1/review/applications/${applicationId}?${qs.toString()}` } /** 提交/更新本人评审打分(`Authorization: Bearer` + JSON body:`competition_slug`、`payload`) */ export function reviewApplicationSubmitScoreUrl(applicationId: number): string { return `${getApiBase()}/api/v1/review/applications/${applicationId}/score` } /** 管理端 REST 前缀(相对 `/api`),与 Laravel `routes/api.php` 分组对齐 */ export const ADMIN_API_PREFIX = '/v1/admin' as const /** 管理端登录页在浏览器中的 pathname(生产环境 base 为 `/admin/` 时为 `/admin/admin/login`) */ export function pathnameForAdminLogin(): string { const baseUrl = import.meta.env.BASE_URL || '/' if (baseUrl === '/') { return '/admin/login' } const prefix = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl return `${prefix}/admin/login` } /** 是否启用管理端 Mock(仅读 `VITE_` 变量;改后需重启 `npm run dev`) */ export function adminUseMock(): boolean { const raw = import.meta.env.VITE_ADMIN_USE_MOCK if (raw == null) return false const s = String(raw).trim().toLowerCase().replace(/^['"]|['"]$/g, '') return s === 'true' || s === '1' || s === 'yes' || s === 'on' } /** 后端 API 根地址(无尾部斜杠)。本地开发/预览默认空字符串,走 Vite 代理的 `/api`,避免跨域。 */ export function getApiBase(): string { const fromEnv = import.meta.env.VITE_API_BASE as string | undefined if (fromEnv?.trim()) { return fromEnv.trim().replace(/\/$/, '') } const host = window.location.hostname if (host === 'localhost' || host === '127.0.0.1' || host === '') { return '' } return '' } /** 评审端附件下载 URL(需请求头 `Authorization: Bearer`,见详情页 `downloadAttachment`) */ export function reviewApplicationFileDownloadUrl( applicationId: number, fileId: number, competitionSlug: string, ): string { const qs = new URLSearchParams({ competition_slug: competitionSlug }) return `${getApiBase()}/api/v1/review/applications/${applicationId}/files/${fileId}/download?${qs.toString()}` } /** 管理端接口根路径(无尾部斜杠),形如 `{origin}/api/v1/admin` */ export function getAdminApiRoot(): string { const fromEnv = import.meta.env.VITE_ADMIN_API_BASE?.trim() if (fromEnv) { return fromEnv.replace(/\/$/, '') } return `${getApiBase()}/api${ADMIN_API_PREFIX}` } export function readReviewerSession(): { token: string; competitionSlug: string } { return { token: localStorage.getItem(REVIEWER_TOKEN_KEY) ?? '', competitionSlug: localStorage.getItem(REVIEWER_COMPETITION_SLUG_KEY) ?? '', } } export function writeReviewerSession(token: string, competitionSlug: string): void { localStorage.setItem(REVIEWER_TOKEN_KEY, token) localStorage.setItem(REVIEWER_COMPETITION_SLUG_KEY, competitionSlug) } export function clearReviewerSession(): void { localStorage.removeItem(REVIEWER_TOKEN_KEY) localStorage.removeItem(REVIEWER_COMPETITION_SLUG_KEY) } /** `/c/:slug/review…` — 不向选手 slug 写入 localStorage */ export function pathIsReviewerZone(fullPath: string): boolean { return /^\/c\/[^/]+\/review(?:\/|$)/.test(fullPath) }