|
|
|
|
|
/** 与后端 `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)
|
|
|
|
|
|
}
|