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

5 months ago
/** 与后端 `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
2 months ago
/** 管理员代登选手端标记(sessionStorage) */
export const ADMIN_IMPERSONATION_FLAG_KEY = 'cxxfds_admin_impersonation' as const
5 months ago
/** 选手端最近一次访问的赛事 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
4 months ago
/** 管理端登录用户摘要(username / name,用于顶栏展示) */
export const ADMIN_USER_KEY = 'cxxfds_admin_user' as const
5 months ago
/** 当前选中赛事(顶栏切换器),仅存 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`
}
4 months ago
/**
* 本地开发(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)
}
5 months ago
/** 后端 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 ''
}
5 months ago
/** 规整附件公开 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, '/')
}
5 months ago
/** 评审端附件下载 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)
}
3 months ago
/** `/c/:slug/manage…` — 赛事管理端(管理员) */
export function pathIsManageZone(fullPath: string): boolean {
return /^\/c\/[^/]+\/manage(?:\/|$)/.test(fullPath)
}