fix: gate Taro routes before rendering

This commit is contained in:
Codex
2026-07-02 01:55:07 +08:00
parent d8e3db79d2
commit 685856e02e
7 changed files with 354 additions and 12 deletions

View File

@@ -79,3 +79,53 @@ textarea {
border-radius: 999px;
background: rgba(255, 255, 255, 0.13);
}
.route-guard-page {
min-height: 100vh;
padding: 28px;
background: var(--tiku-page);
}
.route-guard-shell {
width: 100%;
max-width: 760px;
margin: 0 auto;
padding-top: 52px;
}
.route-guard-panel {
display: flex;
flex-direction: column;
gap: 14px;
padding: 38px;
border: 1px solid #dbeafe;
border-radius: 30px;
background: linear-gradient(135deg, #1152d4 0%, #2563eb 62%, #60a5fa 100%);
box-shadow: var(--tiku-shadow-blue);
color: #fff;
}
.route-guard-kicker {
display: block;
color: rgba(255, 255, 255, 0.78);
font-size: 22px;
font-weight: 800;
letter-spacing: 0;
}
.route-guard-title {
display: block;
color: #fff;
font-size: 40px;
font-weight: 900;
line-height: 1.16;
letter-spacing: 0;
}
.route-guard-subtitle {
display: block;
color: rgba(255, 255, 255, 0.86);
font-size: 24px;
line-height: 1.6;
letter-spacing: 0;
}

View File

@@ -1,12 +1,72 @@
import { PropsWithChildren } from 'react';
import { PropsWithChildren, useEffect, useState } from 'react';
import { Text, View } from '@tarojs/components';
import { useDidShow } from '@tarojs/taro';
import 'katex/dist/katex.min.css';
import { guardCurrentRoute } from '@/services/routeGuard';
import { appEnv } from '@/env';
import { currentPagePath, guardCurrentRoute, shouldGuardPath } from '@/services/routeGuard';
import './app.css';
export default function App({ children }: PropsWithChildren) {
const [routeReady, setRouteReady] = useState(() => !shouldGuardPath(currentPagePath()));
function verifyCurrentRoute() {
const path = currentPagePath();
const protectedRoute = shouldGuardPath(path);
setRouteReady(!protectedRoute);
guardCurrentRoute()
.then(ok => setRouteReady(Boolean(ok)))
.catch(() => setRouteReady(false));
}
useEffect(() => {
verifyCurrentRoute();
}, []);
useDidShow(() => {
guardCurrentRoute().catch(() => undefined);
verifyCurrentRoute();
});
if (!routeReady && appEnv.portal === 'tenant-admin') {
return (
<View className='route-guard-page'>
<View className='route-guard-shell'>
<View className='route-guard-panel'>
<Text className='route-guard-kicker'>Tenant Admin</Text>
<Text className='route-guard-title'></Text>
<Text className='route-guard-subtitle'></Text>
</View>
</View>
</View>
);
}
if (!routeReady && appEnv.portal === 'platform-admin') {
return (
<View className='route-guard-page'>
<View className='route-guard-shell'>
<View className='route-guard-panel'>
<Text className='route-guard-kicker'>Platform Admin</Text>
<Text className='route-guard-title'></Text>
<Text className='route-guard-subtitle'></Text>
</View>
</View>
</View>
);
}
if (!routeReady) {
return (
<View className='route-guard-page'>
<View className='route-guard-shell'>
<View className='route-guard-panel'>
<Text className='route-guard-kicker'>Student</Text>
<Text className='route-guard-title'></Text>
<Text className='route-guard-subtitle'></Text>
</View>
</View>
</View>
);
}
return children;
}

View File

@@ -354,9 +354,64 @@
color: var(--tiku-primary);
}
.platform-chip.readonly {
border-color: #bfdbfe;
background: #eff6ff;
color: #1d4ed8;
}
.platform-error {
display: block;
margin-top: 12px;
color: #be123c;
font-size: 22px;
}
@media (min-width: 980px) {
.platform-page {
max-width: 1180px;
margin: 0 auto;
padding: 38px;
}
.platform-shell {
gap: 30px;
}
.platform-header {
padding: 38px;
}
.platform-title {
font-size: 44px;
}
.platform-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.platform-grid.three {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.platform-module-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
@media (max-width: 520px) {
.platform-page {
padding: 20px;
}
.platform-grid,
.platform-grid.three,
.platform-module-grid,
.platform-form {
grid-template-columns: minmax(0, 1fr);
}
.platform-title {
font-size: 34px;
}
}

View File

@@ -323,3 +323,46 @@
font-size: 42px;
line-height: 1;
}
@media (min-width: 900px) {
.student-page {
max-width: 980px;
margin: 0 auto;
padding: 36px;
}
.hero-band {
min-height: 320px;
padding: 44px;
}
.hero-title {
font-size: 52px;
}
.entry-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 420px) {
.student-page {
padding: 20px;
}
.brand {
font-size: 32px;
}
.hero-title {
font-size: 38px;
}
.home-stat-grid {
gap: 10px;
}
.entry-grid {
gap: 12px;
}
}

View File

@@ -1301,3 +1301,68 @@
grid-template-columns: minmax(0, 1fr);
}
}
.points-list {
gap: 16px;
}
.points-list .list-row {
border-color: #bfdbfe;
background: linear-gradient(135deg, #eff6ff, #fff);
}
.points-list .list-row:nth-child(2n) {
border-color: #bbf7d0;
background: linear-gradient(135deg, #ecfdf5, #fff);
}
.scoreline-page .student-topbar {
align-items: flex-start;
}
.scoreline-page .student-title {
font-size: 40px;
}
.scoreline-page .secondary-button {
flex: 0 0 auto;
}
@media (min-width: 900px) {
.student-page {
max-width: 900px;
margin: 0 auto;
padding: 36px;
}
.student-topbar {
margin-bottom: 28px;
}
.student-title {
font-size: 42px;
}
.answer-sheet {
grid-template-columns: repeat(10, minmax(0, 1fr));
}
.toolbar.wrap {
gap: 14px;
}
}
@media (max-width: 420px) {
.student-page {
padding: 20px;
}
.student-title {
font-size: 34px;
}
.student-subtitle,
.row-meta {
font-size: 21px;
}
}

View File

@@ -355,6 +355,13 @@
background: #fff;
}
.admin-row.nested {
margin-top: 14px;
border-color: #bfdbfe;
background: linear-gradient(135deg, #eff6ff, #fff);
box-shadow: none;
}
.admin-error {
display: block;
margin-top: 12px;
@@ -421,3 +428,57 @@
overflow-wrap: anywhere;
white-space: pre-wrap;
}
@media (min-width: 980px) {
.admin-page {
max-width: 1180px;
margin: 0 auto;
padding: 38px;
}
.admin-shell {
gap: 30px;
}
.admin-header {
padding: 38px;
}
.admin-title {
font-size: 44px;
}
.admin-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.admin-grid.three,
.admin-metric-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.admin-module-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.admin-form-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 520px) {
.admin-page {
padding: 20px;
}
.admin-grid,
.admin-grid.three,
.admin-metric-grid,
.admin-module-grid {
grid-template-columns: minmax(0, 1fr);
}
.admin-title {
font-size: 34px;
}
}

View File

@@ -23,17 +23,23 @@ function forbiddenUrl(reason: string, redirectPath: string) {
return `/pages/student/login/index?reason=${encodeURIComponent(reason)}&redirect=${encodeURIComponent(safeRedirectPath(redirectPath))}`;
}
function currentPagePath() {
export function currentPagePath() {
const instance = Taro.getCurrentInstance();
const path = instance.router?.path || '';
return path.startsWith('/') ? path : `/${path}`;
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
if (normalizedPath !== '/') return normalizedPath;
if (isH5Runtime() && typeof window !== 'undefined') {
const pathname = window.location.pathname || '';
if (pathname.startsWith('/pages/')) return pathname;
}
return normalizedPath;
}
function isPublicPath(path: string) {
return path === '/pages/bootstrap/index' || path === '/pages/student/login/index';
}
function shouldGuardPath(path: string) {
export function shouldGuardPath(path: string) {
if (isPublicPath(path)) return false;
if (appEnv.portal === 'tenant-admin') return path.startsWith('/pages/tenant-admin/');
if (appEnv.portal === 'platform-admin') return path.startsWith('/pages/platform-admin/');
@@ -93,18 +99,20 @@ export async function requireTenantAdmin(redirectPath: string) {
export async function guardCurrentRoute() {
await ensureRuntimeConfigLoaded();
const path = currentPagePath();
if (!path || !shouldGuardPath(path) || pendingGuardPath === path) return;
if (!path || !shouldGuardPath(path)) return true;
if (pendingGuardPath === path) return false;
pendingGuardPath = path;
try {
if (appEnv.portal === 'platform-admin') {
await requirePlatformAdmin(path);
return;
const payload = await requirePlatformAdmin(path);
return Boolean(payload);
}
if (appEnv.portal === 'tenant-admin') {
await requireTenantAdmin(path);
return;
const payload = await requireTenantAdmin(path);
return Boolean(payload);
}
await requireSignedIn(path);
const payload = await requireSignedIn(path);
return Boolean(payload);
} finally {
pendingGuardPath = '';
}