You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

166 lines
6.6 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/** 与后端 `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
/** 管理员代登选手端标记(sessionStorage) */
export const ADMIN_IMPERSONATION_FLAG_KEY = 'cxxfds_admin_impersonation' 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
/** 管理端登录用户摘要(username / name,用于顶栏展示) */
export const ADMIN_USER_KEY = 'cxxfds_admin_user' 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`
}
/**
* 本地开发(Vite base=/)时,将生产构建的 pathname(/admin/admin/...)折叠为 /admin/...,
* 避免登录后 redirect 落到无匹配路由的地址。
*/
export function collapseDevAdminPath(path: string): string {
const baseUrl = import.meta.env.BASE_URL || '/'
if (baseUrl !== '/') return path
if (!path.startsWith('/admin/admin')) return path
const collapsed = path.replace(/^\/admin\/admin(?=\/|$)/, '/admin')
return collapsed || '/admin'
}
/** 登录后 redirect 参数规整(去掉生产 base 前缀,并折叠 dev 下的 /admin/admin) */
export function normalizeAdminRedirectPath(path: string): string {
const trimmed = path.trim()
if (!trimmed) return ''
const baseUrl = import.meta.env.BASE_URL || '/'
const basePrefix = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl
if (basePrefix && basePrefix !== '/' && trimmed.startsWith(`${basePrefix}/admin`)) {
return collapseDevAdminPath(trimmed.slice(basePrefix.length) || '/admin')
}
return collapseDevAdminPath(trimmed)
}
/** 后端 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,避免 Laravel `APP_URL` 带尾斜杠时出现 `//storage/...` 导致 404 */
export function normalizePublicAssetUrl(raw: string | null | undefined): string {
const s = String(raw ?? '').trim()
if (!s) return s
if (/^https?:\/\//i.test(s)) {
try {
const u = new URL(s)
const collapsed = u.pathname.replace(/\/{2,}/g, '/')
u.pathname = collapsed.startsWith('/') ? collapsed : `/${collapsed}`
return u.toString()
} catch {
return s.replace(/([^:])\/\/+/g, '$1/')
}
}
return s.replace(/\/{2,}/g, '/')
}
/** 评审端附件下载 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)
}
/** `/c/:slug/manage…` — 赛事管理端(管理员) */
export function pathIsManageZone(fullPath: string): boolean {
return /^\/c\/[^/]+\/manage(?:\/|$)/.test(fullPath)
}