fix: tighten H5 portal guard and legacy home stats

This commit is contained in:
Codex
2026-07-02 03:12:30 +08:00
parent 25ccaea873
commit e6735b58da
5 changed files with 156 additions and 12 deletions

View File

@@ -4,7 +4,7 @@ import 'katex/dist/katex.min.css';
import { AdminLegacyShell } from '@/components/AdminLegacyShell';
import { StudentLegacyShell } from '@/components/StudentLegacyShell';
import { appEnv, isH5Runtime } from '@/env';
import { currentPagePath, guardCurrentRoute, shouldGuardPath } from '@/services/routeGuard';
import { currentPagePath, guardCurrentRoute, isPathAllowedForPortal, shouldGuardPath } from '@/services/routeGuard';
import './app.css';
const routeChangeEvent = 'tiku-route-change';
@@ -61,14 +61,18 @@ function shouldUseAdminShell(path: string) {
}
export default function App({ children }: PropsWithChildren) {
const [routeReady, setRouteReady] = useState(() => !shouldGuardPath(currentPagePath()));
const [routeReady, setRouteReady] = useState(() => {
const initialPath = currentPagePath();
return isPathAllowedForPortal(initialPath) && !shouldGuardPath(initialPath);
});
const [path, setPath] = useState(() => currentPagePath());
function verifyCurrentRoute() {
const nextPath = currentPagePath();
setPath(nextPath);
const allowedRoute = isPathAllowedForPortal(nextPath);
const protectedRoute = shouldGuardPath(nextPath);
setRouteReady(!protectedRoute);
setRouteReady(allowedRoute && !protectedRoute);
guardCurrentRoute()
.then(ok => setRouteReady(Boolean(ok)))
.catch(() => setRouteReady(false));

View File

@@ -25,6 +25,18 @@
window.location.replace('/pages/bootstrap/index?redirect=' + encodeURIComponent('/'));
return;
}
if (portal === 'student' && path.indexOf('/pages/') === 0 && path.indexOf('/pages/student/') !== 0 && path.indexOf('/pages/bootstrap/') !== 0) {
window.location.replace('/pages/bootstrap/index?redirect=' + encodeURIComponent('/pages/student/home/index'));
return;
}
if (portal === 'tenant-admin' && path.indexOf('/pages/') === 0 && path.indexOf('/pages/tenant-admin/') !== 0 && path.indexOf('/pages/bootstrap/') !== 0 && path.indexOf('/pages/student/login/') !== 0) {
window.location.replace('/pages/bootstrap/index?redirect=' + encodeURIComponent('/pages/tenant-admin/workbench/index'));
return;
}
if (portal === 'platform-admin' && path.indexOf('/pages/') === 0 && path.indexOf('/pages/platform-admin/') !== 0 && path.indexOf('/pages/bootstrap/') !== 0 && path.indexOf('/pages/student/login/') !== 0) {
window.location.replace('/pages/bootstrap/index?redirect=' + encodeURIComponent('/pages/platform-admin/workbench/index'));
return;
}
if (path.indexOf('/pages/student/') === 0 && !publicStudentPaths[path]) {
window.location.replace('/pages/bootstrap/index?redirect=' + encodeURIComponent(redirect));
return;

View File

@@ -179,20 +179,28 @@
.home-stat-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
margin-top: 20px;
}
.home-stat {
min-height: 126px;
padding: 20px 18px;
min-height: 160px;
padding: 20px;
border: 1px solid #f1f5f9;
border-radius: 16px;
background: #fff;
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04);
}
.home-stat-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
margin-bottom: 14px;
}
.home-stat-value {
display: block;
color: var(--tiku-text);
@@ -203,10 +211,83 @@
.home-stat-label {
display: block;
margin-top: 12px;
color: var(--tiku-muted);
font-size: 21px;
font-weight: 700;
font-size: 18px;
font-weight: 760;
}
.home-stat-meta {
display: block;
margin-top: 8px;
color: #94a3b8;
font-size: 17px;
font-weight: 720;
line-height: 1.3;
}
.home-stat-badge {
flex: 0 0 auto;
padding: 4px 9px;
border-radius: 999px;
font-size: 14px;
font-weight: 900;
line-height: 1.2;
}
.home-stat-badge.green {
background: #ecfdf5;
color: #059669;
}
.home-stat-badge.amber {
background: #fffbeb;
color: #b45309;
}
.home-stat-badge.red {
background: #fff1f2;
color: #e11d48;
}
.home-stat-badge.blue {
background: #eff6ff;
color: #1152d4;
}
.home-progress-track {
height: 8px;
margin-top: 16px;
overflow: hidden;
border-radius: 999px;
background: #f1f5f9;
}
.home-progress-fill {
height: 100%;
border-radius: inherit;
}
.home-progress-fill.blue {
background: #1152d4;
}
.home-mini-bars {
display: flex;
align-items: flex-end;
gap: 5px;
height: 34px;
margin-top: 14px;
}
.home-mini-bar {
flex: 1;
min-width: 0;
border-radius: 4px 4px 0 0;
background: rgba(17, 82, 212, 0.18);
}
.home-mini-bar.active {
background: #1152d4;
}
.section {
@@ -373,6 +454,11 @@
.entry-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.home-stat-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 18px;
}
}
@media (max-width: 420px) {

View File

@@ -26,6 +26,9 @@ export default function StudentHomePage() {
const totalAnswered = Number(answerStats.totalAnswered || 0);
const accuracy = Math.round(Number(answerStats.accuracy || 0) * 100);
const wrongCount = Number((stats.wrongBook as Record<string, unknown> | undefined)?.unresolvedWrong || 0);
const streakDays = Number((stats.checkin as Record<string, unknown> | undefined)?.streakDays || 0);
const accuracyPercent = Math.max(0, Math.min(100, accuracy || 0));
const answeredPercent = Math.max(12, Math.min(100, Math.round(totalAnswered / 20)));
const quickActions = [
{ name: '文化课', path: '/pages/student/catalog/index?entryType=subjects', meta: '必刷好题', tone: 'red' },
{ name: '专业课', path: '/pages/student/catalog/index?entryType=exam_bank', meta: '专项练习', tone: 'blue' },
@@ -70,16 +73,42 @@ export default function StudentHomePage() {
<View className='home-stat-grid'>
<View className='home-stat'>
<View className='home-stat-head'>
<Text className='home-stat-label'></Text>
<Text className='home-stat-badge green'></Text>
</View>
<Text className='home-stat-value'>{String(totalAnswered)}</Text>
<Text className='home-stat-label'></Text>
<Text className='home-stat-meta'></Text>
<View className='home-progress-track'><View className='home-progress-fill blue' style={{ width: `${answeredPercent}%` }} /></View>
</View>
<View className='home-stat'>
<View className='home-stat-head'>
<Text className='home-stat-label'></Text>
<Text className='home-stat-badge amber'>{accuracy ? '良好' : '待开始'}</Text>
</View>
<Text className='home-stat-value'>{accuracy ? `${accuracy}%` : '--'}</Text>
<Text className='home-stat-label'></Text>
<Text className='home-stat-meta'></Text>
<View className='home-mini-bars'>
{[42, 56, 50, 68, 62, accuracyPercent || 18].map((height, itemIndex) => (
<View className={`home-mini-bar ${itemIndex === 5 ? 'active' : ''}`} style={{ height: `${Math.max(14, height)}%` }} key={itemIndex} />
))}
</View>
</View>
<View className='home-stat' onClick={() => Taro.navigateTo({ url: '/pages/student/review/index?type=wrong' })}>
<View className='home-stat-head'>
<Text className='home-stat-label'></Text>
<Text className='home-stat-badge red'></Text>
</View>
<Text className='home-stat-value'>{String(wrongCount)}</Text>
<Text className='home-stat-label'></Text>
<Text className='home-stat-meta'> </Text>
</View>
<View className='home-stat'>
<View className='home-stat-head'>
<Text className='home-stat-label'></Text>
<Text className='home-stat-badge blue'></Text>
</View>
<Text className='home-stat-value'>{String(streakDays)}</Text>
<Text className='home-stat-meta'></Text>
</View>
</View>

View File

@@ -50,8 +50,17 @@ function isPublicPath(path: string) {
return path === '/pages/bootstrap/index' || path === '/pages/student/login/index';
}
export function isPathAllowedForPortal(path: string) {
if (isPublicPath(path)) return true;
if (!path.startsWith('/pages/')) return true;
if (appEnv.portal === 'tenant-admin') return path.startsWith('/pages/tenant-admin/');
if (appEnv.portal === 'platform-admin') return path.startsWith('/pages/platform-admin/');
return path.startsWith('/pages/student/');
}
export function shouldGuardPath(path: string) {
if (isPublicPath(path)) return false;
if (!isPathAllowedForPortal(path)) return true;
if (appEnv.portal === 'tenant-admin') return path.startsWith('/pages/tenant-admin/');
if (appEnv.portal === 'platform-admin') return path.startsWith('/pages/platform-admin/');
return path.startsWith('/pages/student/') && path !== '/pages/student/region/index';
@@ -107,6 +116,10 @@ export async function guardCurrentRoute() {
await ensureRuntimeConfigLoaded();
const path = currentPagePath();
if (!path || !shouldGuardPath(path)) return true;
if (!isPathAllowedForPortal(path)) {
redirectToAuthUrl(`/pages/bootstrap/index?redirect=${encodeURIComponent(landingPath())}`);
return false;
}
if (pendingGuardPath === path) return false;
pendingGuardPath = path;
try {