diff --git a/apps/taro/src/app.css b/apps/taro/src/app.css index cbfc75c2..9b4bcf61 100644 --- a/apps/taro/src/app.css +++ b/apps/taro/src/app.css @@ -24,6 +24,14 @@ page { --tiku-shadow-blue: 0 14px 28px rgba(17, 82, 212, 0.18); } +body { + background: + linear-gradient(to right, rgba(15, 23, 42, 0.035) 1px, transparent 1px), + linear-gradient(to bottom, rgba(15, 23, 42, 0.035) 1px, transparent 1px), + #f4f6f9; + background-size: 40px 40px; +} + view, text, input, diff --git a/apps/taro/src/app.tsx b/apps/taro/src/app.tsx index e07eeb29..526b8ea0 100644 --- a/apps/taro/src/app.tsx +++ b/apps/taro/src/app.tsx @@ -1,11 +1,48 @@ import { PropsWithChildren, useEffect, useState } from 'react'; import { Text, View } from '@tarojs/components'; -import { useDidShow } from '@tarojs/taro'; import 'katex/dist/katex.min.css'; -import { appEnv } from '@/env'; +import { appEnv, isH5Runtime } from '@/env'; import { currentPagePath, guardCurrentRoute, shouldGuardPath } from '@/services/routeGuard'; import './app.css'; +const routeChangeEvent = 'tiku-route-change'; + +type GuardedHistory = History & { + __tikuRouteGuardPatched?: boolean; +}; + +function installH5RouteListener(callback: () => void) { + if (!isH5Runtime() || typeof window === 'undefined') return () => undefined; + + const guardedHistory = window.history as GuardedHistory; + if (!guardedHistory.__tikuRouteGuardPatched) { + const rawPushState = guardedHistory.pushState.bind(guardedHistory); + const rawReplaceState = guardedHistory.replaceState.bind(guardedHistory); + guardedHistory.pushState = (...args) => { + const result = rawPushState(...args); + window.dispatchEvent(new Event(routeChangeEvent)); + return result; + }; + guardedHistory.replaceState = (...args) => { + const result = rawReplaceState(...args); + window.dispatchEvent(new Event(routeChangeEvent)); + return result; + }; + guardedHistory.__tikuRouteGuardPatched = true; + } + + const onRouteChange = () => window.setTimeout(callback, 0); + window.addEventListener(routeChangeEvent, onRouteChange); + window.addEventListener('popstate', onRouteChange); + window.addEventListener('hashchange', onRouteChange); + + return () => { + window.removeEventListener(routeChangeEvent, onRouteChange); + window.removeEventListener('popstate', onRouteChange); + window.removeEventListener('hashchange', onRouteChange); + }; +} + export default function App({ children }: PropsWithChildren) { const [routeReady, setRouteReady] = useState(() => !shouldGuardPath(currentPagePath())); @@ -20,12 +57,9 @@ export default function App({ children }: PropsWithChildren) { useEffect(() => { verifyCurrentRoute(); + return installH5RouteListener(verifyCurrentRoute); }, []); - useDidShow(() => { - verifyCurrentRoute(); - }); - if (!routeReady && appEnv.portal === 'tenant-admin') { return ( diff --git a/apps/taro/src/index.html b/apps/taro/src/index.html index ee84ec92..aeb06d7b 100644 --- a/apps/taro/src/index.html +++ b/apps/taro/src/index.html @@ -11,6 +11,21 @@ 工学题库 + diff --git a/apps/taro/src/pages/bootstrap/index.tsx b/apps/taro/src/pages/bootstrap/index.tsx index 27051504..f03c3663 100644 --- a/apps/taro/src/pages/bootstrap/index.tsx +++ b/apps/taro/src/pages/bootstrap/index.tsx @@ -1,9 +1,9 @@ import { useEffect, useState } from 'react'; -import Taro from '@tarojs/taro'; +import Taro, { useRouter } from '@tarojs/taro'; import { Button, Text, View } from '@tarojs/components'; import { appEnv, assertFrontendSecretsAreAbsent, ensureRuntimeConfigLoaded, isH5Runtime } from '@/env'; import { resolveTenant } from '@/services/api'; -import { landingPath, requirePlatformAdmin, requireTenantAdmin } from '@/services/routeGuard'; +import { landingPath, requirePlatformAdmin, requireSignedIn, requireTenantAdmin, safeRedirectPath } from '@/services/routeGuard'; import './index.css'; function hostFromRuntime() { @@ -12,23 +12,25 @@ function hostFromRuntime() { } export default function BootstrapPage() { + const router = useRouter(); const [status, setStatus] = useState('正在解析租户'); const [error, setError] = useState(''); useEffect(() => { + const redirectPath = safeRedirectPath(router.params?.redirect ? decodeURIComponent(router.params.redirect) : landingPath()); assertFrontendSecretsAreAbsent(); ensureRuntimeConfigLoaded() .then(() => resolveTenant({ host: hostFromRuntime() })) .then(async () => { - if (appEnv.portal === 'student') return null; setStatus('正在校验登录状态'); - if (appEnv.portal === 'platform-admin') return requirePlatformAdmin(landingPath()); - return requireTenantAdmin(landingPath()); + if (appEnv.portal === 'student') return requireSignedIn(redirectPath); + if (appEnv.portal === 'platform-admin') return requirePlatformAdmin(redirectPath); + return requireTenantAdmin(redirectPath); }) .then(authPayload => { - if (appEnv.portal !== 'student' && !authPayload) return; + if (!authPayload) return; setStatus('租户解析完成'); - Taro.redirectTo({ url: landingPath() }); + Taro.redirectTo({ url: redirectPath }); }) .catch((nextError: Error) => { setError(nextError.message); diff --git a/apps/taro/src/pages/student/home/index.css b/apps/taro/src/pages/student/home/index.css index 014eb50e..8a71774a 100644 --- a/apps/taro/src/pages/student/home/index.css +++ b/apps/taro/src/pages/student/home/index.css @@ -1,6 +1,72 @@ +.student-home-layout { + position: relative; + min-height: 100vh; +} + .student-page { min-height: 100vh; padding: 28px; + padding-bottom: 132px; +} + +.legacy-student-sidebar { + display: none; +} + +.legacy-mobile-dock { + position: fixed; + right: 18px; + bottom: 18px; + left: 18px; + z-index: 30; + display: flex; + justify-content: space-around; + gap: 4px; + padding: 14px 12px; + border: 1px solid rgba(255, 255, 255, 0.6); + border-radius: 28px; + background: rgba(255, 255, 255, 0.94); + box-shadow: 0 14px 34px rgba(15, 23, 42, 0.18); +} + +.legacy-dock-item { + display: flex; + flex: 1; + min-width: 0; + flex-direction: column; + align-items: center; + gap: 5px; + color: #9ca3af; +} + +.legacy-dock-item.active { + color: var(--tiku-primary); +} + +.legacy-dock-dot { + display: flex; + align-items: center; + justify-content: center; + width: 42px; + height: 42px; + border-radius: 16px; + background: #f1f5f9; + font-size: 21px; + font-weight: 900; + line-height: 42px; +} + +.legacy-dock-item.active .legacy-dock-dot { + background: var(--tiku-primary); + color: #fff; + box-shadow: 0 8px 18px rgba(17, 82, 212, 0.24); +} + +.legacy-dock-text { + display: block; + font-size: 18px; + font-weight: 760; + line-height: 1.2; } .topbar { @@ -325,10 +391,183 @@ } @media (min-width: 900px) { + .student-home-layout { + display: grid; + grid-template-columns: 260px minmax(0, 1fr); + gap: 28px; + min-height: 100vh; + padding: 24px; + } + + .legacy-student-sidebar { + position: sticky; + top: 24px; + display: flex; + height: calc(100vh - 48px); + flex-direction: column; + padding: 22px; + border: 1px solid rgba(226, 232, 240, 0.9); + border-radius: 30px; + background: rgba(255, 255, 255, 0.92); + box-shadow: 0 16px 34px rgba(15, 23, 42, 0.08); + } + + .legacy-brand-block { + display: flex; + align-items: center; + gap: 14px; + min-width: 0; + padding: 6px 4px 24px; + } + + .legacy-brand-icon { + display: flex; + align-items: center; + justify-content: center; + flex: 0 0 48px; + width: 48px; + height: 48px; + border-radius: 16px; + background: var(--tiku-primary); + box-shadow: 0 10px 20px rgba(17, 82, 212, 0.26); + } + + .legacy-brand-mark { + color: #fff; + font-size: 26px; + font-weight: 950; + } + + .legacy-brand-copy { + min-width: 0; + } + + .legacy-brand-name { + display: block; + color: #111827; + font-size: 19px; + font-weight: 950; + line-height: 1.25; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .legacy-brand-sub { + display: block; + margin-top: 3px; + color: var(--tiku-primary); + font-size: 14px; + font-weight: 850; + } + + .legacy-nav-list { + display: flex; + flex: 1; + flex-direction: column; + gap: 8px; + padding-top: 8px; + border-top: 1px solid #f1f5f9; + } + + .legacy-nav-item { + display: flex; + align-items: center; + gap: 12px; + min-height: 52px; + padding: 0 14px; + border-radius: 16px; + color: #64748b; + } + + .legacy-nav-item.active { + background: #111827; + color: #fff; + box-shadow: 0 8px 20px rgba(15, 23, 42, 0.16); + } + + .legacy-nav-dot { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 10px; + background: #f1f5f9; + color: inherit; + font-size: 16px; + font-weight: 900; + } + + .legacy-nav-item.active .legacy-nav-dot { + background: rgba(255, 255, 255, 0.16); + } + + .legacy-nav-text { + font-size: 18px; + font-weight: 850; + } + + .legacy-sidebar-user { + display: flex; + align-items: center; + gap: 12px; + padding-top: 18px; + border-top: 1px solid #f1f5f9; + } + + .legacy-mini-avatar { + display: flex; + align-items: center; + justify-content: center; + flex: 0 0 44px; + width: 44px; + height: 44px; + border-radius: 999px; + background: linear-gradient(135deg, #1152d4, #60a5fa); + color: #fff; + } + + .legacy-mini-avatar-mark { + color: #fff; + font-size: 20px; + font-weight: 900; + } + + .legacy-sidebar-user-copy { + min-width: 0; + } + + .legacy-sidebar-user-name { + display: block; + color: #334155; + font-size: 16px; + font-weight: 850; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .legacy-sidebar-user-tag { + display: inline-flex; + margin-top: 4px; + padding: 2px 8px; + border-radius: 999px; + background: linear-gradient(90deg, #fef3c7, #fde68a); + color: #92400e; + font-size: 13px; + font-weight: 850; + } + + .legacy-mobile-dock { + display: none; + } + .student-page { max-width: 980px; margin: 0 auto; padding: 36px; + padding-bottom: 48px; } .hero-band { diff --git a/apps/taro/src/pages/student/home/index.tsx b/apps/taro/src/pages/student/home/index.tsx index 31e89129..2bdbd829 100644 --- a/apps/taro/src/pages/student/home/index.tsx +++ b/apps/taro/src/pages/student/home/index.tsx @@ -40,9 +40,42 @@ export default function StudentHomePage() { { name: '消息通知', path: '/pages/student/notifications/index', meta: '通知 / 待办', tone: 'orange' }, { name: '个人中心', path: '/pages/student/profile/index', meta: '会员 / 订单', tone: 'slate' }, ]; + const navItems = [ + { name: '首页', path: '/pages/student/home/index', active: true }, + { name: '背单词', path: '/pages/student/vocabulary/index' }, + { name: '知识手册', path: '/pages/student/handbook/index' }, + { name: '购买', path: '/pages/student/checkout/index' }, + { name: '分数线', path: '/pages/student/scoreline/index' }, + { name: '我的', path: '/pages/student/profile/index' }, + ]; return ( - + + + + + + {brandName} + 专升本刷题宝 + + + + {navItems.map(item => ( + Taro.navigateTo({ url: item.path })}> + {item.name.slice(0, 1)} + {item.name} + + ))} + + + {(userName || '同').slice(0, 1)} + + {userName || '同学'} + SVIP + + + + @@ -116,6 +149,15 @@ export default function StudentHomePage() { ))} + + + {navItems.slice(0, 5).map(item => ( + Taro.navigateTo({ url: item.path })}> + {item.name.slice(0, 1)} + {item.name} + + ))} + ); } diff --git a/apps/taro/src/services/routeGuard.ts b/apps/taro/src/services/routeGuard.ts index 863aa168..82ee0b62 100644 --- a/apps/taro/src/services/routeGuard.ts +++ b/apps/taro/src/services/routeGuard.ts @@ -11,8 +11,11 @@ function hostFromRuntime() { return ''; } -function safeRedirectPath(path: string) { - return path.startsWith('/pages/') && !path.startsWith('/pages/student/login/') ? path : '/pages/student/home/index'; +export function safeRedirectPath(path: string) { + if (!path.startsWith('/pages/') || path.startsWith('/pages/student/login/') || path.startsWith('/pages/bootstrap/')) return landingPath(); + if (appEnv.portal === 'tenant-admin') return path.startsWith('/pages/tenant-admin/') ? path : landingPath(); + if (appEnv.portal === 'platform-admin') return path.startsWith('/pages/platform-admin/') ? path : landingPath(); + return path.startsWith('/pages/student/') ? path : landingPath(); } function loginUrl(redirectPath: string) { @@ -23,10 +26,19 @@ function forbiddenUrl(reason: string, redirectPath: string) { return `/pages/student/login/index?reason=${encodeURIComponent(reason)}&redirect=${encodeURIComponent(safeRedirectPath(redirectPath))}`; } +function redirectToAuthUrl(url: string) { + if (isH5Runtime() && typeof window !== 'undefined') { + window.location.replace(url); + return; + } + Taro.redirectTo({ url }); +} + export function currentPagePath() { if (isH5Runtime() && typeof window !== 'undefined') { const pathname = window.location.pathname || ''; if (pathname.startsWith('/pages/')) return pathname; + if (!pathname || pathname === '/' || pathname.endsWith('/index.html')) return landingPath(); } const instance = Taro.getCurrentInstance(); const path = instance.router?.path || ''; @@ -67,7 +79,7 @@ export async function requireSignedIn(redirectPath: string): Promise allowedRoles.has(role) || role === 'platform_admin')) return payload; - Taro.redirectTo({ url: forbiddenUrl('当前账号没有租户后台权限', redirectPath) }); + redirectToAuthUrl(forbiddenUrl('当前账号没有租户后台权限', redirectPath)); return null; }