From 7d629843b7d3b8c6871784d66bf2be3c33817acb Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 2 Jul 2026 01:22:48 +0800 Subject: [PATCH] style: align Taro H5 with legacy question bank UI --- apps/taro/src/app.css | 68 +++- apps/taro/src/app.tsx | 5 + apps/taro/src/components/rich-content.css | 60 ++-- apps/taro/src/pages/bootstrap/index.css | 48 ++- apps/taro/src/pages/bootstrap/index.tsx | 15 +- .../src/pages/platform-admin/platform.css | 197 ++++++------ .../pages/platform-admin/workbench/index.tsx | 10 +- apps/taro/src/pages/student/home/index.css | 283 +++++++++++++++-- apps/taro/src/pages/student/home/index.tsx | 93 ++++-- apps/taro/src/pages/student/login/index.tsx | 8 +- apps/taro/src/pages/student/student.css | 290 ++++++++++-------- apps/taro/src/pages/tenant-admin/admin.css | 208 +++++++------ .../pages/tenant-admin/workbench/index.css | 73 +---- .../pages/tenant-admin/workbench/index.tsx | 12 +- apps/taro/src/services/routeGuard.ts | 94 ++++++ apps/taro/src/types.ts | 4 + 16 files changed, 974 insertions(+), 494 deletions(-) create mode 100644 apps/taro/src/services/routeGuard.ts diff --git a/apps/taro/src/app.css b/apps/taro/src/app.css index 89f82f48..ec7d590f 100644 --- a/apps/taro/src/app.css +++ b/apps/taro/src/app.css @@ -1,8 +1,27 @@ page { min-height: 100%; - background: #f6f8fb; + background: #f4f6f9; color: #172033; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", "Segoe UI", sans-serif; + --tiku-primary: #1152d4; + --tiku-primary-2: #2563eb; + --tiku-primary-soft: #eef5ff; + --tiku-violet: #7c3aed; + --tiku-pink: #ec4899; + --tiku-orange: #f59e0b; + --tiku-green: #10b981; + --tiku-red: #ef4444; + --tiku-page: #f4f6f9; + --tiku-card: #ffffff; + --tiku-card-soft: #f8fafc; + --tiku-text: #111827; + --tiku-muted: #64748b; + --tiku-border: #e2e8f0; + --tiku-border-strong: #cbd5e1; + --tiku-radius: 24px; + --tiku-radius-sm: 16px; + --tiku-shadow: 0 8px 24px rgba(15, 23, 42, 0.06); + --tiku-shadow-blue: 0 14px 28px rgba(17, 82, 212, 0.18); } view, @@ -14,4 +33,49 @@ button { button { margin: 0; + border-radius: var(--tiku-radius-sm); + transition: transform 160ms ease, box-shadow 160ms ease, background 160ms ease, border-color 160ms ease; +} + +button::after { + border: 0; +} + +button:active, +.list-row:active, +.admin-row:active, +.platform-row:active, +.entry-tile:active { + transform: scale(0.985); +} + +input, +textarea { + font-family: inherit; +} + +.legacy-card { + border: 1px solid var(--tiku-border); + border-radius: var(--tiku-radius); + background: var(--tiku-card); + box-shadow: var(--tiku-shadow); +} + +.legacy-blue-surface { + position: relative; + overflow: hidden; + border-radius: var(--tiku-radius); + background: linear-gradient(135deg, #1152d4 0%, #3b82f6 58%, #60a5fa 100%); + box-shadow: var(--tiku-shadow-blue); +} + +.legacy-blue-surface::after { + content: ""; + position: absolute; + right: -58px; + bottom: -72px; + width: 210px; + height: 210px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.13); } diff --git a/apps/taro/src/app.tsx b/apps/taro/src/app.tsx index 11d2e0c1..a12045a5 100644 --- a/apps/taro/src/app.tsx +++ b/apps/taro/src/app.tsx @@ -1,7 +1,12 @@ import { PropsWithChildren } from 'react'; +import { useDidShow } from '@tarojs/taro'; import 'katex/dist/katex.min.css'; +import { guardCurrentRoute } from '@/services/routeGuard'; import './app.css'; export default function App({ children }: PropsWithChildren) { + useDidShow(() => { + guardCurrentRoute().catch(() => undefined); + }); return children; } diff --git a/apps/taro/src/components/rich-content.css b/apps/taro/src/components/rich-content.css index 75471d38..ca5b6d8f 100644 --- a/apps/taro/src/components/rich-content.css +++ b/apps/taro/src/components/rich-content.css @@ -3,7 +3,7 @@ flex-direction: column; gap: 16px; min-width: 0; - color: #111827; + color: var(--tiku-text); } .rich-content.compact { @@ -11,7 +11,7 @@ } .rich-content.muted { - color: #475569; + color: var(--tiku-muted); } .rich-paragraph, @@ -33,11 +33,11 @@ } .rich-content.muted .rich-paragraph { - color: #64748b; + color: var(--tiku-muted); } .rich-heading { - color: #0f172a; + color: var(--tiku-text); font-size: 32px; font-weight: 820; line-height: 1.35; @@ -53,7 +53,7 @@ } .rich-bold { - color: #0f172a; + color: var(--tiku-text); font-weight: 820; } @@ -61,9 +61,9 @@ display: inline-flex; max-width: 100%; padding: 0 8px; - border: 1px solid #dbe3ee; - border-radius: 6px; - background: #f8fafc; + border: 1px solid var(--tiku-border); + border-radius: 10px; + background: var(--tiku-card-soft); color: #334155; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 23px; @@ -76,9 +76,9 @@ max-width: 100%; margin: 0 4px; padding: 0 8px; - border-radius: 6px; - background: #f8fafc; - color: #0f172a; + border-radius: 10px; + background: var(--tiku-card-soft); + color: var(--tiku-text); font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 24px; line-height: 1.45; @@ -106,10 +106,10 @@ .rich-code-block { min-width: 0; padding: 18px; - border: 1px solid #dbe3ee; - border-radius: 8px; - background: #f8fafc; - color: #0f172a; + border: 1px solid var(--tiku-border); + border-radius: 18px; + background: var(--tiku-card-soft); + color: var(--tiku-text); font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 24px; line-height: 1.55; @@ -138,7 +138,7 @@ .rich-code-language { display: block; margin-bottom: 10px; - color: #64748b; + color: var(--tiku-muted); font-size: 20px; font-weight: 800; text-transform: uppercase; @@ -155,8 +155,8 @@ width: 100%; min-width: 0; overflow: hidden; - border: 1px solid #e2e8f0; - border-radius: 8px; + border: 1px solid var(--tiku-border); + border-radius: 18px; background: #fff; } @@ -164,13 +164,13 @@ display: block; width: 100%; max-height: 640px; - background: #f8fafc; + background: var(--tiku-card-soft); } .rich-image-caption { display: block; padding: 10px 14px 14px; - color: #64748b; + color: var(--tiku-muted); font-size: 20px; line-height: 1.4; } @@ -178,7 +178,7 @@ .rich-blocked-image { padding: 16px; border: 1px dashed #fca5a5; - border-radius: 8px; + border-radius: 18px; background: #fff1f2; color: #be123c; font-size: 22px; @@ -189,9 +189,9 @@ .rich-image-pending { padding: 18px; border: 1px dashed #bfdbfe; - border-radius: 8px; - background: #eff6ff; - color: #1d4ed8; + border-radius: 18px; + background: var(--tiku-primary-soft); + color: var(--tiku-primary); font-size: 22px; line-height: 1.45; } @@ -200,15 +200,15 @@ display: block; width: 100%; overflow-x: auto; - border: 1px solid #dbe3ee; - border-radius: 8px; + border: 1px solid var(--tiku-border); + border-radius: 18px; background: #fff; } .rich-table-row { display: flex; min-width: 680px; - border-top: 1px solid #e2e8f0; + border-top: 1px solid var(--tiku-border); } .rich-table-row:first-child { @@ -216,14 +216,14 @@ } .rich-table-row.header { - background: #f8fafc; + background: var(--tiku-card-soft); } .rich-table-cell { flex: 1; min-width: 160px; padding: 14px 16px; - border-left: 1px solid #e2e8f0; + border-left: 1px solid var(--tiku-border); color: #334155; font-size: 22px; line-height: 1.45; @@ -235,7 +235,7 @@ } .rich-table-row.header .rich-table-cell { - color: #0f172a; + color: var(--tiku-text); font-weight: 820; } diff --git a/apps/taro/src/pages/bootstrap/index.css b/apps/taro/src/pages/bootstrap/index.css index 35f84cef..2bc861d2 100644 --- a/apps/taro/src/pages/bootstrap/index.css +++ b/apps/taro/src/pages/bootstrap/index.css @@ -1,33 +1,61 @@ .bootstrap-page { min-height: 100vh; padding: 56px 28px; - background: #101827; - color: #f8fafc; + background: var(--tiku-page); + color: var(--tiku-text); } .bootstrap-shell { + position: relative; display: flex; min-height: calc(100vh - 112px); flex-direction: column; justify-content: center; gap: 18px; + overflow: hidden; + padding: 40px; + border-radius: 32px; + background: linear-gradient(135deg, #1152d4, #3b82f6 62%, #60a5fa); + box-shadow: var(--tiku-shadow-blue); +} + +.bootstrap-shell::after { + content: ""; + position: absolute; + right: -72px; + bottom: -94px; + width: 280px; + height: 280px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.14); +} + +.eyebrow, +.title, +.status, +.error, +.primary-button { + position: relative; + z-index: 1; } .eyebrow { - color: #80b7ff; + color: rgba(255, 255, 255, 0.82); font-size: 24px; - font-weight: 700; + font-weight: 850; } .title { + display: block; max-width: 620px; + color: #fff; font-size: 48px; - font-weight: 800; + font-weight: 950; line-height: 1.18; } .status { - color: #cbd5e1; + color: rgba(255, 255, 255, 0.8); font-size: 28px; } @@ -40,9 +68,11 @@ .primary-button { width: 240px; height: 76px; - border-radius: 8px; - background: #2f7cf6; - color: #fff; + border: 0; + border-radius: 20px; + background: #fff; + color: var(--tiku-primary); font-size: 28px; + font-weight: 900; line-height: 76px; } diff --git a/apps/taro/src/pages/bootstrap/index.tsx b/apps/taro/src/pages/bootstrap/index.tsx index 8e049350..47cb8a7a 100644 --- a/apps/taro/src/pages/bootstrap/index.tsx +++ b/apps/taro/src/pages/bootstrap/index.tsx @@ -3,6 +3,7 @@ import Taro from '@tarojs/taro'; import { Button, Text, View } from '@tarojs/components'; import { appEnv, assertFrontendSecretsAreAbsent, ensureRuntimeConfigLoaded, isH5Runtime } from '@/env'; import { resolveTenant } from '@/services/api'; +import { landingPath, requireSignedIn } from '@/services/routeGuard'; import './index.css'; function hostFromRuntime() { @@ -10,12 +11,6 @@ function hostFromRuntime() { return ''; } -function landingPath() { - if (appEnv.portal === 'tenant-admin') return '/pages/tenant-admin/workbench/index'; - if (appEnv.portal === 'platform-admin') return '/pages/platform-admin/workbench/index'; - return '/pages/student/home/index'; -} - export default function BootstrapPage() { const [status, setStatus] = useState('正在解析租户'); const [error, setError] = useState(''); @@ -24,7 +19,13 @@ export default function BootstrapPage() { assertFrontendSecretsAreAbsent(); ensureRuntimeConfigLoaded() .then(() => resolveTenant({ host: hostFromRuntime() })) - .then(() => { + .then(async () => { + if (appEnv.portal === 'student') return null; + setStatus('正在校验登录状态'); + return requireSignedIn(landingPath()); + }) + .then(authPayload => { + if (appEnv.portal !== 'student' && !authPayload) return; setStatus('租户解析完成'); Taro.redirectTo({ url: landingPath() }); }) diff --git a/apps/taro/src/pages/platform-admin/platform.css b/apps/taro/src/pages/platform-admin/platform.css index 96d5e81c..755b1a4c 100644 --- a/apps/taro/src/pages/platform-admin/platform.css +++ b/apps/taro/src/pages/platform-admin/platform.css @@ -1,8 +1,8 @@ .platform-page { min-height: 100vh; padding: 28px; - background: #f7f9fc; - color: #172033; + background: var(--tiku-page); + color: var(--tiku-text); } .platform-shell { @@ -12,30 +12,50 @@ } .platform-header { - padding-bottom: 24px; - border-bottom: 1px solid #d8e0ec; + position: relative; + overflow: hidden; + padding: 30px; + border-radius: 30px; + background: linear-gradient(135deg, #0f172a, #1152d4 62%, #38bdf8); + box-shadow: var(--tiku-shadow-blue); +} + +.platform-header::after { + content: ""; + position: absolute; + right: -64px; + bottom: -84px; + width: 240px; + height: 240px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.14); +} + +.platform-kicker, +.platform-title, +.platform-subtitle { + position: relative; + z-index: 1; + display: block; } .platform-kicker { - display: block; - color: #5b6b82; + color: rgba(255, 255, 255, 0.78); font-size: 22px; - font-weight: 760; + font-weight: 850; } .platform-title { - display: block; margin-top: 8px; - color: #101827; + color: #fff; font-size: 38px; - font-weight: 820; + font-weight: 920; line-height: 1.2; } .platform-subtitle { - display: block; margin-top: 10px; - color: #64748b; + color: rgba(255, 255, 255, 0.82); font-size: 24px; line-height: 1.45; } @@ -48,28 +68,48 @@ padding-bottom: 6px; } +.platform-button, +.platform-mini-button { + border: 1px solid var(--tiku-border); + background: #fff; + color: var(--tiku-primary); + font-weight: 820; +} + .platform-button { min-width: 132px; height: 62px; - border: 1px solid #cbd5e1; - border-radius: 8px; - background: #fff; - color: #1e3a8a; + border-radius: 16px; font-size: 23px; - font-weight: 720; line-height: 62px; } -.platform-button.primary { - border-color: #1d4ed8; - background: #1d4ed8; - color: #fff; +.platform-mini-button { + min-width: 116px; + height: 54px; + border-radius: 14px; + font-size: 21px; + line-height: 54px; } -.platform-button.active { - border-color: #1d4ed8; - background: #eff6ff; - color: #1d4ed8; +.platform-button.primary { + border-color: var(--tiku-primary); + background: linear-gradient(135deg, var(--tiku-primary), #2563eb); + color: #fff; + box-shadow: 0 10px 20px rgba(17, 82, 212, 0.16); +} + +.platform-button.active, +.platform-mini-button.active { + border-color: var(--tiku-primary); + background: var(--tiku-primary-soft); + color: var(--tiku-primary); +} + +.platform-mini-button.danger { + border-color: #fecdd3; + background: #fff1f2; + color: #be123c; } .platform-grid { @@ -84,24 +124,25 @@ .platform-metric { min-height: 112px; - padding: 18px; - border: 1px solid #e2e8f0; - border-radius: 8px; + padding: 20px; + border: 1px solid var(--tiku-border); + border-radius: 24px; background: #fff; + box-shadow: var(--tiku-shadow); } .platform-metric-label { display: block; - color: #64748b; + color: var(--tiku-muted); font-size: 21px; } .platform-metric-value { display: block; margin-top: 12px; - color: #0f172a; + color: var(--tiku-text); font-size: 31px; - font-weight: 820; + font-weight: 900; line-height: 1.2; } @@ -112,9 +153,9 @@ .platform-section-title { display: block; margin-bottom: 14px; - color: #0f172a; + color: var(--tiku-text); font-size: 29px; - font-weight: 820; + font-weight: 900; } .platform-list { @@ -125,33 +166,34 @@ .platform-row { padding: 20px; - border: 1px solid #e2e8f0; - border-radius: 8px; + border: 1px solid var(--tiku-border); + border-radius: 22px; background: #fff; + box-shadow: 0 5px 16px rgba(15, 23, 42, 0.04); } .platform-row-main { display: block; - color: #111827; + color: var(--tiku-text); font-size: 26px; - font-weight: 760; + font-weight: 820; line-height: 1.35; } .platform-row-meta { display: block; margin-top: 8px; - color: #64748b; + color: var(--tiku-muted); font-size: 21px; line-height: 1.45; } .platform-empty { padding: 32px 22px; - border: 1px dashed #cbd5e1; - border-radius: 8px; + border: 1px dashed var(--tiku-border-strong); + border-radius: 22px; background: #fff; - color: #64748b; + color: var(--tiku-muted); font-size: 23px; line-height: 1.45; } @@ -160,10 +202,10 @@ width: 100%; height: 66px; padding: 0 18px; - border: 1px solid #cbd5e1; - border-radius: 8px; + border: 1px solid var(--tiku-border); + border-radius: 16px; background: #fff; - color: #111827; + color: var(--tiku-text); font-size: 24px; } @@ -189,9 +231,9 @@ .platform-field-label { display: block; margin-bottom: 8px; - color: #475569; + color: var(--tiku-muted); font-size: 20px; - font-weight: 720; + font-weight: 760; } .platform-row-actions { @@ -201,38 +243,14 @@ overflow-x: auto; } -.platform-mini-button { - min-width: 116px; - height: 54px; - border: 1px solid #cbd5e1; - border-radius: 8px; - background: #fff; - color: #1e3a8a; - font-size: 21px; - font-weight: 720; - line-height: 54px; -} - -.platform-mini-button.danger { - border-color: #fecdd3; - background: #fff1f2; - color: #be123c; -} - -.platform-mini-button.active { - border-color: #1d4ed8; - background: #eff6ff; - color: #1d4ed8; -} - .platform-permission-panel { display: flex; flex-direction: column; gap: 16px; margin: 8px 0 16px; padding: 18px; - border: 1px solid #dbe4f0; - border-radius: 8px; + border: 1px solid var(--tiku-border); + border-radius: 22px; background: #fff; } @@ -260,33 +278,26 @@ } .platform-chip { - min-width: 0; - max-width: 100%; - min-height: 48px; - padding: 0 14px; - border: 1px solid #cbd5e1; - border-radius: 8px; - background: #f8fafc; - color: #334155; - font-size: 20px; - font-weight: 680; - line-height: 48px; -} - -.platform-chip.active { - border-color: #1d4ed8; - background: #eff6ff; - color: #1d4ed8; -} - -.platform-chip.readonly { display: inline-flex; align-items: center; max-width: 100%; - color: #475569; + min-height: 48px; + padding: 0 14px; + border: 1px solid var(--tiku-border); + border-radius: 999px; + background: #f8fafc; + color: #334155; + font-size: 20px; + font-weight: 720; line-height: 1.25; } +.platform-chip.active { + border-color: var(--tiku-primary); + background: var(--tiku-primary-soft); + color: var(--tiku-primary); +} + .platform-error { display: block; margin-top: 12px; diff --git a/apps/taro/src/pages/platform-admin/workbench/index.tsx b/apps/taro/src/pages/platform-admin/workbench/index.tsx index 90919ab9..a9db7c7e 100644 --- a/apps/taro/src/pages/platform-admin/workbench/index.tsx +++ b/apps/taro/src/pages/platform-admin/workbench/index.tsx @@ -27,6 +27,7 @@ import { type PlatformQuestionBankItem, type PlatformTenantItem, } from '@/services/platformAdmin'; +import { requirePlatformAdmin } from '@/services/routeGuard'; import '../platform.css'; function money(cents: unknown) { @@ -60,7 +61,9 @@ export default function PlatformWorkbenchPage() { const [busy, setBusy] = useState(''); useEffect(() => { - Promise.all([ + requirePlatformAdmin('/pages/platform-admin/workbench/index').then(payload => { + if (!payload) return; + return Promise.all([ loadPlatformOverview().catch(() => ({ item: null })), loadPlatformTenants({ limit: 6 }).catch(() => ({ items: [] })), loadPlatformInvoices({ limit: 6 }).catch(() => ({ items: [] })), @@ -72,7 +75,10 @@ export default function PlatformWorkbenchPage() { loadPlatformAuditNotificationEvents({ limit: 6 }).catch(() => ({ items: [] })), loadPlatformDunningNotificationChannels({ enabled: true, limit: 6 }).catch(() => ({ items: [] })), loadPlatformDunningNotificationEvents({ limit: 6 }).catch(() => ({ items: [] })), - ]).then(([overviewPayload, tenantPayload, invoicePayload, bankPayload, grantPayload, auditPayload, alertPayload, channelPayload, eventPayload, dunningChannelPayload, dunningEventPayload]) => { + ]); + }).then(result => { + if (!result) return; + const [overviewPayload, tenantPayload, invoicePayload, bankPayload, grantPayload, auditPayload, alertPayload, channelPayload, eventPayload, dunningChannelPayload, dunningEventPayload] = result; setOverview(overviewPayload.item || null); setTenants(tenantPayload.items || []); setInvoices(invoicePayload.items || []); diff --git a/apps/taro/src/pages/student/home/index.css b/apps/taro/src/pages/student/home/index.css index 0c06481a..2a6a2c91 100644 --- a/apps/taro/src/pages/student/home/index.css +++ b/apps/taro/src/pages/student/home/index.css @@ -10,53 +10,200 @@ gap: 20px; } +.home-user { + display: flex; + align-items: center; + min-width: 0; + gap: 16px; +} + +.home-avatar { + position: relative; + display: flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + width: 78px; + height: 78px; + border: 4px solid #fff; + border-radius: 999px; + background: linear-gradient(135deg, var(--tiku-primary), #60a5fa); + box-shadow: 0 10px 24px rgba(17, 82, 212, 0.18); +} + +.home-avatar::after { + content: ""; + position: absolute; + right: 2px; + bottom: 2px; + width: 18px; + height: 18px; + border: 4px solid var(--tiku-page); + border-radius: 999px; + background: var(--tiku-green); +} + +.home-avatar-mark { + color: #fff; + font-size: 34px; + font-weight: 900; + line-height: 1; +} + .brand { display: block; - color: #14213d; + color: var(--tiku-text); font-size: 36px; - font-weight: 800; + font-weight: 900; + line-height: 1.15; } .subline { display: block; margin-top: 8px; - color: #64748b; - font-size: 24px; + color: var(--tiku-muted); + font-size: 23px; + line-height: 1.35; } .ghost-button { + flex: 0 0 auto; width: 132px; height: 64px; - border: 1px solid #cbd5e1; - border-radius: 8px; + border: 1px solid var(--tiku-border); + border-radius: 999px; background: #fff; - color: #1e3a8a; + color: var(--tiku-primary); font-size: 24px; + font-weight: 800; line-height: 64px; + box-shadow: 0 5px 18px rgba(15, 23, 42, 0.05); } .hero-band { - margin-top: 36px; - padding: 34px 30px; - border-left: 8px solid #2f7cf6; - background: #fff; + position: relative; + overflow: hidden; + min-height: 280px; + margin-top: 34px; + padding: 36px 32px; + border-radius: 30px; + background: linear-gradient(135deg, #1152d4 0%, #2563eb 56%, #60a5fa 100%); + box-shadow: 0 18px 34px rgba(17, 82, 212, 0.2); +} + +.hero-band::before, +.hero-band::after { + content: ""; + position: absolute; + border-radius: 999px; + background: rgba(255, 255, 255, 0.12); +} + +.hero-band::before { + right: -54px; + top: -62px; + width: 230px; + height: 230px; +} + +.hero-band::after { + right: 32px; + bottom: -86px; + width: 280px; + height: 280px; + opacity: 0.7; +} + +.hero-content { + position: relative; + z-index: 1; +} + +.hero-kicker { + display: block; + color: rgba(255, 255, 255, 0.82); + font-size: 22px; + font-weight: 800; } .hero-title { display: block; - color: #0f172a; - font-size: 40px; - font-weight: 800; + margin-top: 12px; + color: #fff; + font-size: 44px; + font-weight: 950; + line-height: 1.15; } .hero-copy { display: block; - margin-top: 14px; - color: #475569; + max-width: 560px; + margin-top: 16px; + color: rgba(255, 255, 255, 0.84); font-size: 25px; line-height: 1.55; } +.hero-actions { + display: flex; + gap: 14px; + margin-top: 26px; +} + +.hero-primary, +.hero-secondary { + min-width: 156px; + height: 64px; + border: 0; + border-radius: 18px; + font-size: 24px; + font-weight: 900; + line-height: 64px; +} + +.hero-primary { + background: #fff; + color: var(--tiku-primary); +} + +.hero-secondary { + border: 1px solid rgba(255, 255, 255, 0.3); + background: rgba(255, 255, 255, 0.16); + color: #fff; +} + +.home-stat-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 14px; + margin-top: 20px; +} + +.home-stat { + min-height: 126px; + padding: 20px 18px; + border: 1px solid var(--tiku-border); + border-radius: 24px; + background: #fff; + box-shadow: var(--tiku-shadow); +} + +.home-stat-value { + display: block; + color: var(--tiku-text); + font-size: 34px; + font-weight: 950; + line-height: 1.1; +} + +.home-stat-label { + display: block; + margin-top: 12px; + color: var(--tiku-muted); + font-size: 21px; + font-weight: 700; +} + .section { margin-top: 36px; } @@ -64,9 +211,9 @@ .section-title { display: block; margin-bottom: 18px; - color: #172033; + color: var(--tiku-text); font-size: 30px; - font-weight: 800; + font-weight: 900; } .entry-grid { @@ -76,27 +223,103 @@ } .entry-tile { - min-height: 126px; - padding: 22px; - border: 1px solid #e2e8f0; - border-radius: 8px; - background: #fff; + position: relative; + overflow: hidden; + min-height: 176px; + padding: 24px; + border: 0; + border-radius: 28px; + color: #fff; + box-shadow: 0 12px 24px rgba(15, 23, 42, 0.12); } -.entry-tile.compact { - min-height: 104px; +.entry-tile::after { + content: ""; + position: absolute; + right: -26px; + bottom: -38px; + width: 118px; + height: 118px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.14); +} + +.entry-label { + display: block; + position: relative; + z-index: 1; + color: rgba(255, 255, 255, 0.86); + font-size: 21px; + font-weight: 800; } .entry-name { display: block; - color: #111827; - font-size: 28px; - font-weight: 700; + position: relative; + z-index: 1; + margin-top: 36px; + color: #fff; + font-size: 34px; + font-weight: 950; + line-height: 1.15; } .entry-type { display: block; + position: relative; + z-index: 1; margin-top: 12px; - color: #64748b; - font-size: 22px; + color: rgba(255, 255, 255, 0.78); + font-size: 21px; + font-weight: 700; +} + +.tone-red { background: linear-gradient(135deg, #ef4444, #fb7185); } +.tone-blue { background: linear-gradient(135deg, #1152d4, #38bdf8); } +.tone-orange { background: linear-gradient(135deg, #f97316, #fbbf24); } +.tone-green { background: linear-gradient(135deg, #10b981, #2dd4bf); } +.tone-violet { background: linear-gradient(135deg, #7c3aed, #a78bfa); } +.tone-pink { background: linear-gradient(135deg, #ec4899, #f472b6); } +.tone-slate { background: linear-gradient(135deg, #334155, #64748b); } + +.quick-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.quick-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + min-height: 104px; + padding: 20px 22px; + border: 1px solid var(--tiku-border); + border-radius: 24px; + background: #fff; + box-shadow: 0 5px 16px rgba(15, 23, 42, 0.04); +} + +.quick-name { + display: block; + color: var(--tiku-text); + font-size: 27px; + font-weight: 850; + line-height: 1.25; +} + +.quick-meta { + display: block; + margin-top: 8px; + color: var(--tiku-muted); + font-size: 21px; + line-height: 1.35; +} + +.quick-arrow { + flex: 0 0 auto; + color: #94a3b8; + font-size: 42px; + line-height: 1; } diff --git a/apps/taro/src/pages/student/home/index.tsx b/apps/taro/src/pages/student/home/index.tsx index e6a36346..31e89129 100644 --- a/apps/taro/src/pages/student/home/index.tsx +++ b/apps/taro/src/pages/student/home/index.tsx @@ -3,7 +3,7 @@ import Taro from '@tarojs/taro'; import { Button, Text, View } from '@tarojs/components'; import { getTenantContext } from '@/services/api'; import { loadStudentDashboard, type DashboardSnapshot } from '@/services/catalog'; -import { loadCurrentUser } from '@/services/auth'; +import { requireSignedIn } from '@/services/routeGuard'; import './index.css'; export default function StudentHomePage() { @@ -12,65 +12,106 @@ export default function StudentHomePage() { const [userName, setUserName] = useState(''); useEffect(() => { + requireSignedIn('/pages/student/home/index').then(payload => { + if (payload) setUserName(payload.user?.name || payload.item?.name || ''); + }).catch(() => undefined); loadStudentDashboard().then(setSnapshot).catch(() => setSnapshot({ entries: [], banners: [], announcements: [], profile: null })); - loadCurrentUser().then(payload => setUserName(payload.user?.name || payload.item?.name || '')).catch(() => setUserName('')); }, []); const entryNames = useMemo(() => (snapshot?.entries || []).slice(0, 6), [snapshot]); const brandName = tenant?.branding.brandName || tenant?.branding.shortName || '工学题库'; + const profile = snapshot?.profile && typeof snapshot.profile === 'object' ? snapshot.profile as Record : {}; + const stats = profile.stats && typeof profile.stats === 'object' ? profile.stats as Record : {}; + const answerStats = stats.answers && typeof stats.answers === 'object' ? stats.answers as Record : {}; + const totalAnswered = Number(answerStats.totalAnswered || 0); + const accuracy = Math.round(Number(answerStats.accuracy || 0) * 100); + const wrongCount = Number((stats.wrongBook as Record | undefined)?.unresolvedWrong || 0); const quickActions = [ - { name: '选地区', path: '/pages/student/region/index', meta: '目标地区 / 权益' }, - { name: '刷题', path: '/pages/student/catalog/index', meta: '顺序 / 随机 / 模考' }, - { name: '错题收藏', path: '/pages/student/review/index', meta: '错题 / 收藏复习' }, - { name: '练习报告', path: '/pages/student/reports/index', meta: '模考 / 历史' }, - { name: '背单词', path: '/pages/student/vocabulary/index', meta: '复习计划' }, - { name: '知识手册', path: '/pages/student/handbook/index', meta: '章节阅读' }, - { name: '分数线', path: '/pages/student/scoreline/index', meta: '院校趋势' }, - { name: 'AI择校', path: '/pages/student/ai-school/index', meta: 'SVIP报告' }, - { name: '资料', path: '/pages/student/assets/index', meta: 'PDF 预览' }, - { name: '消息', path: '/pages/student/notifications/index', meta: '通知 / 待办' }, - { name: '个人中心', path: '/pages/student/profile/index', meta: '会员 / 订单' }, + { name: '文化课', path: '/pages/student/catalog/index?entryType=subjects', meta: '必刷好题', tone: 'red' }, + { name: '专业课', path: '/pages/student/catalog/index?entryType=exam_bank', meta: '专项练习', tone: 'blue' }, + { name: '升本单词', path: '/pages/student/vocabulary/index', meta: '今日待背', tone: 'orange' }, + { name: '知识手册', path: '/pages/student/handbook/index', meta: '考前速查', tone: 'green' }, + { name: '错题本', path: '/pages/student/review/index?type=wrong', meta: '查漏补缺', tone: 'violet' }, + { name: '收藏夹', path: '/pages/student/review/index?type=favorite', meta: '重点复习', tone: 'pink' }, + { name: '练习报告', path: '/pages/student/reports/index', meta: '模考 / 历史', tone: 'slate' }, + { name: '分数线', path: '/pages/student/scoreline/index', meta: '院校趋势', tone: 'blue' }, + { name: 'AI择校', path: '/pages/student/ai-school/index', meta: 'SVIP报告', tone: 'violet' }, + { name: '备考资料', path: '/pages/student/assets/index', meta: 'PDF 预览', tone: 'green' }, + { name: '消息通知', path: '/pages/student/notifications/index', meta: '通知 / 待办', tone: 'orange' }, + { name: '个人中心', path: '/pages/student/profile/index', meta: '会员 / 订单', tone: 'slate' }, ]; return ( - - {brandName} - {userName ? `${userName},继续学习` : '题库、手册、资料和会员统一入口'} + + + {(userName || brandName || 'U').slice(0, 1).toUpperCase()} + + + Hi, {userName || '同学'} + {brandName} · 今天也要加油刷题哦 + - 今日学习 - 题库、背单词、知识手册、分数线、资料和会员都已接入新 SaaS 后端。 + + 今天也是元气满满的一天 + 继续你的备考之旅 + 刷题、错题、背单词、资料和会员权益已接入新 SaaS 后端。 + + + + + + + + + + {String(totalAnswered)} + 累计刷题 + + + {accuracy ? `${accuracy}%` : '--'} + 正确率 + + Taro.navigateTo({ url: '/pages/student/review/index?type=wrong' })}> + {String(wrongCount)} + 错题本 + 学习入口 {entryNames.length ? entryNames.map(item => ( - Taro.navigateTo({ url: `/pages/student/catalog/index?entryId=${item.id}` })}> + Taro.navigateTo({ url: `/pages/student/catalog/index?entryId=${item.id}` })}> + 已配置入口 {item.name} {item.entryType || 'content'} )) : quickActions.map(item => ( - Taro.navigateTo({ url: item.path })}> + Taro.navigateTo({ url: item.path })}> + {item.meta} {item.name} - {item.meta} + 点击进入 ))} - 功能导航 - + 全部功能 + {quickActions.map(item => ( - Taro.navigateTo({ url: item.path })}> - {item.name} - {item.meta} + Taro.navigateTo({ url: item.path })}> + + {item.name} + {item.meta} + + ))} diff --git a/apps/taro/src/pages/student/login/index.tsx b/apps/taro/src/pages/student/login/index.tsx index 5459d4c3..fba836b2 100644 --- a/apps/taro/src/pages/student/login/index.tsx +++ b/apps/taro/src/pages/student/login/index.tsx @@ -1,11 +1,13 @@ import { useEffect, useState } from 'react'; -import Taro from '@tarojs/taro'; +import { useRouter } from '@tarojs/taro'; import { Button, Input, Text, View } from '@tarojs/components'; import { getTenantContext } from '@/services/api'; import { loadCurrentUser, sendSmsCode, verifySmsCode } from '@/services/auth'; +import { landingPath, redirectAfterLogin } from '@/services/routeGuard'; import '../student.css'; export default function StudentLoginPage() { + const router = useRouter(); const tenant = getTenantContext(); const [phone, setPhone] = useState(''); const [code, setCode] = useState(''); @@ -17,7 +19,7 @@ export default function StudentLoginPage() { useEffect(() => { loadCurrentUser() - .then(() => Taro.redirectTo({ url: '/pages/student/home/index' })) + .then(() => redirectAfterLogin(router.params?.redirect || landingPath())) .catch(() => undefined); }, []); @@ -42,7 +44,7 @@ export default function StudentLoginPage() { setError(''); try { await verifySmsCode(phone, code || debugCode); - Taro.redirectTo({ url: '/pages/student/home/index' }); + redirectAfterLogin(router.params?.redirect || landingPath()); } catch (nextError) { setError(nextError instanceof Error ? nextError.message : '登录失败'); } finally { diff --git a/apps/taro/src/pages/student/student.css b/apps/taro/src/pages/student/student.css index 366620ad..62bdd42c 100644 --- a/apps/taro/src/pages/student/student.css +++ b/apps/taro/src/pages/student/student.css @@ -1,7 +1,7 @@ .student-page { min-height: 100vh; padding: 28px; - background: #f6f8fb; + background: var(--tiku-page); } .student-topbar { @@ -9,7 +9,7 @@ align-items: center; justify-content: space-between; gap: 18px; - margin-bottom: 28px; + margin-bottom: 24px; } .student-title-block { @@ -18,24 +18,24 @@ .student-kicker { display: block; - color: #64748b; + color: var(--tiku-primary); font-size: 22px; - font-weight: 600; + font-weight: 800; } .student-title { display: block; margin-top: 6px; - color: #111827; + color: var(--tiku-text); font-size: 38px; - font-weight: 800; + font-weight: 900; line-height: 1.2; } .student-subtitle { display: block; margin-top: 8px; - color: #64748b; + color: var(--tiku-muted); font-size: 24px; line-height: 1.45; } @@ -58,29 +58,30 @@ .danger-button { min-width: 120px; height: 64px; - border-radius: 8px; + border-radius: 16px; font-size: 24px; - font-weight: 700; + font-weight: 820; line-height: 64px; } .primary-button { border: 0; - background: #2563eb; + background: linear-gradient(135deg, var(--tiku-primary), #2563eb); color: #fff; + box-shadow: 0 10px 20px rgba(17, 82, 212, 0.18); } .secondary-button, .pill-button { - border: 1px solid #cbd5e1; + border: 1px solid var(--tiku-border); background: #fff; - color: #1e3a8a; + color: var(--tiku-primary); } .secondary-button.active { - border-color: #2563eb; - background: #eff6ff; - color: #1d4ed8; + border-color: var(--tiku-primary); + background: var(--tiku-primary-soft); + color: var(--tiku-primary); } .danger-button { @@ -90,9 +91,9 @@ } .pill-button.active { - border-color: #2563eb; - background: #eff6ff; - color: #1d4ed8; + border-color: var(--tiku-primary); + background: var(--tiku-primary-soft); + color: var(--tiku-primary); } .pill-button.warn { @@ -108,23 +109,40 @@ .section-heading { display: block; margin-bottom: 16px; - color: #172033; + color: var(--tiku-text); font-size: 30px; font-weight: 800; } .quiet-panel { - padding: 26px; - border: 1px solid #e2e8f0; - border-radius: 8px; + padding: 28px; + border: 1px solid var(--tiku-border); + border-radius: var(--tiku-radius); background: #fff; + box-shadow: var(--tiku-shadow); +} + +.list-stack > .quiet-panel:first-child { + border-radius: 28px; +} + +.list-stack > .quiet-panel:first-child .row-meta:first-child { + display: inline-flex; + align-items: center; + min-height: 38px; + padding: 0 14px; + border-radius: 999px; + background: var(--tiku-primary-soft); + color: var(--tiku-primary); + font-weight: 800; } .report-panel { padding: 28px; border: 1px solid #bfdbfe; - border-radius: 8px; - background: #eff6ff; + border-radius: var(--tiku-radius); + background: linear-gradient(135deg, #eff6ff, #fff); + box-shadow: var(--tiku-shadow); } .learning-report-panel { @@ -136,9 +154,10 @@ align-items: center; gap: 22px; padding: 24px; - border: 1px solid #dbe3ee; - border-radius: 8px; + border: 1px solid var(--tiku-border); + border-radius: var(--tiku-radius); background: #fff; + box-shadow: var(--tiku-shadow); } .profile-avatar { @@ -146,16 +165,16 @@ width: 116px; height: 116px; border: 1px solid #bfdbfe; - border-radius: 8px; - background: linear-gradient(135deg, #dbeafe, #eff6ff); - color: #1d4ed8; + border-radius: 24px; + background: linear-gradient(135deg, #1152d4, #60a5fa); + color: #fff; text-align: center; } .profile-avatar.female { border-color: #fbcfe8; - background: linear-gradient(135deg, #fce7f3, #fff1f2); - color: #be185d; + background: linear-gradient(135deg, #ec4899, #f472b6); + color: #fff; } .profile-avatar-mark { @@ -184,14 +203,14 @@ min-width: 0; min-height: 88px; padding: 14px; - border: 1px solid #e2e8f0; - border-radius: 8px; + border: 1px solid var(--tiku-border); + border-radius: 16px; background: #f8fafc; } .avatar-choice.active { - border-color: #2563eb; - background: #eff6ff; + border-color: var(--tiku-primary); + background: var(--tiku-primary-soft); } .avatar-choice.busy { @@ -202,9 +221,9 @@ flex: 0 0 54px; width: 54px; height: 54px; - border-radius: 8px; + border-radius: 16px; background: #dbeafe; - color: #1d4ed8; + color: var(--tiku-primary); text-align: center; } @@ -226,7 +245,7 @@ .avatar-choice-title { display: block; - color: #0f172a; + color: var(--tiku-text); font-size: 24px; font-weight: 780; line-height: 1.25; @@ -235,7 +254,7 @@ .avatar-choice-meta { display: block; margin-top: 4px; - color: #64748b; + color: var(--tiku-muted); font-size: 20px; line-height: 1.25; } @@ -248,14 +267,16 @@ .list-row { padding: 22px; - border: 1px solid #e2e8f0; - border-radius: 8px; + border: 1px solid var(--tiku-border); + border-radius: 22px; background: #fff; + box-shadow: 0 4px 14px rgba(15, 23, 42, 0.04); } .list-row.active { - border-color: #2563eb; - background: #eff6ff; + border-color: var(--tiku-primary); + background: var(--tiku-primary-soft); + box-shadow: 0 8px 20px rgba(17, 82, 212, 0.12); } .notification-row.active { @@ -288,22 +309,34 @@ .sub-explanation { margin-top: 16px; padding-top: 14px; - border-top: 1px solid #e2e8f0; + border-top: 1px solid var(--tiku-border); } .option-line { display: flex; align-items: flex-start; - gap: 10px; + gap: 14px; min-width: 0; } .option-prefix { - flex: 0 0 auto; - color: #0f172a; + display: flex; + align-items: center; + justify-content: center; + flex: 0 0 44px; + width: 44px; + height: 44px; + border-radius: 14px; + background: #f1f5f9; + color: #475569; font-size: 26px; font-weight: 850; - line-height: 1.55; + line-height: 44px; +} + +.list-row.active .option-prefix { + background: #dbeafe; + color: var(--tiku-primary); } .option-rich { @@ -313,21 +346,23 @@ .answer-detail { margin-top: 16px; - padding-top: 14px; - border-top: 1px solid #e2e8f0; + padding: 18px; + border: 1px solid var(--tiku-border); + border-radius: 18px; + background: #f8fafc; } .answer-detail-label { display: block; margin-bottom: 8px; - color: #334155; + color: var(--tiku-primary); font-size: 22px; - font-weight: 820; + font-weight: 850; } .row-main { display: block; - color: #111827; + color: var(--tiku-text); font-size: 28px; font-weight: 750; line-height: 1.35; @@ -336,7 +371,7 @@ .row-meta { display: block; margin-top: 8px; - color: #64748b; + color: var(--tiku-muted); font-size: 22px; line-height: 1.45; } @@ -347,7 +382,7 @@ .notification-action-text { align-self: center; - color: #1d4ed8; + color: var(--tiku-primary); font-weight: 760; } @@ -359,9 +394,10 @@ .metric { padding: 22px; - border: 1px solid #e2e8f0; - border-radius: 8px; + border: 1px solid var(--tiku-border); + border-radius: 22px; background: #fff; + box-shadow: 0 4px 14px rgba(15, 23, 42, 0.04); } .compact-metric { @@ -370,7 +406,7 @@ .metric-value { display: block; - color: #0f172a; + color: var(--tiku-text); font-size: 34px; font-weight: 800; } @@ -378,7 +414,7 @@ .metric-label { display: block; margin-top: 8px; - color: #64748b; + color: var(--tiku-muted); font-size: 22px; } @@ -388,10 +424,10 @@ .empty-state { padding: 38px 26px; - border: 1px dashed #cbd5e1; - border-radius: 8px; + border: 1px dashed var(--tiku-border-strong); + border-radius: 22px; background: #fff; - color: #64748b; + color: var(--tiku-muted); font-size: 24px; line-height: 1.5; } @@ -400,8 +436,8 @@ width: 100%; height: 72px; padding: 0 20px; - border: 1px solid #cbd5e1; - border-radius: 8px; + border: 1px solid var(--tiku-border); + border-radius: 16px; background: #fff; color: #111827; font-size: 26px; @@ -411,8 +447,8 @@ width: 100%; min-height: 140px; padding: 18px 20px; - border: 1px solid #cbd5e1; - border-radius: 8px; + border: 1px solid var(--tiku-border); + border-radius: 16px; background: #fff; color: #111827; font-size: 26px; @@ -436,14 +472,14 @@ } .muted-text { - color: #64748b; + color: var(--tiku-muted); } .video-frame { width: 100%; min-height: 360px; border: 0; - border-radius: 8px; + border-radius: 24px; background: #0f172a; } @@ -522,7 +558,7 @@ align-items: center; min-height: 40px; padding: 0 14px; - border-radius: 8px; + border-radius: 999px; background: #ecfdf5; color: #047857; font-size: 22px; @@ -545,9 +581,10 @@ gap: 10px; align-items: end; padding: 22px; - border: 1px solid #e2e8f0; - border-radius: 8px; + border: 1px solid var(--tiku-border); + border-radius: var(--tiku-radius); background: #fff; + box-shadow: var(--tiku-shadow); } .trend-column { @@ -570,15 +607,15 @@ justify-content: center; height: 136px; margin-top: 8px; - border-radius: 8px; + border-radius: 999px; background: #f1f5f9; } .trend-bar-fill { width: 100%; max-width: 28px; - border-radius: 8px 8px 0 0; - background: #2563eb; + border-radius: 999px 999px 0 0; + background: var(--tiku-primary); } .trend-bar-fill.h-bucket-0 { height: 0; } @@ -598,7 +635,7 @@ .trend-label { display: block; margin-top: 8px; - color: #64748b; + color: var(--tiku-muted); font-size: 18px; line-height: 1.2; } @@ -609,13 +646,13 @@ margin-top: 16px; overflow: hidden; border-radius: 999px; - background: #e2e8f0; + background: var(--tiku-border); } .progress-fill { height: 100%; border-radius: 999px; - background: #10b981; + background: var(--tiku-green); } .progress-fill.w-bucket-0 { width: 0; } @@ -667,14 +704,14 @@ max-width: 360px; min-height: 58px; padding: 0 16px; - border: 1px solid #dbe3ee; - border-radius: 8px; + border: 1px solid var(--tiku-border); + border-radius: 999px; background: #fff; } .handbook-outline-chip.active { - border-color: #2563eb; - background: #eff6ff; + border-color: var(--tiku-primary); + background: var(--tiku-primary-soft); } .handbook-outline-index { @@ -706,9 +743,9 @@ .handbook-search-snippet { margin-top: 10px; padding: 14px 16px; - border: 1px solid #e2e8f0; - border-radius: 8px; - background: #f8fafc; + border: 1px solid var(--tiku-border); + border-radius: 18px; + background: var(--tiku-card-soft); color: #475569; font-size: 22px; line-height: 1.55; @@ -725,7 +762,7 @@ .report-sub-result { margin-top: 14px; padding-top: 12px; - border-top: 1px dashed #cbd5e1; + border-top: 1px dashed var(--tiku-border-strong); } .answer-sheet { @@ -737,8 +774,8 @@ .answer-card { min-width: 0; height: 58px; - border: 1px solid #cbd5e1; - border-radius: 8px; + border: 1px solid var(--tiku-border); + border-radius: 16px; background: #fff; color: #334155; font-size: 22px; @@ -747,9 +784,9 @@ } .answer-card.current { - border-color: #2563eb; - background: #eff6ff; - color: #1d4ed8; + border-color: var(--tiku-primary); + background: var(--tiku-primary-soft); + color: var(--tiku-primary); } .answer-card.answered { @@ -779,7 +816,7 @@ .vocabulary-overview-panel { border-color: #bbf7d0; - background: #f7fdf9; + background: linear-gradient(135deg, #ecfdf5, #fff); } .vocabulary-stats-grid { @@ -811,27 +848,28 @@ height: 12px; overflow: hidden; border-radius: 999px; - background: #e2e8f0; + background: var(--tiku-border); } .vocabulary-progress-fill { height: 100%; border-radius: 999px; - background: #2563eb; + background: var(--tiku-primary); transition: width 180ms ease; } .vocabulary-card { min-height: 560px; padding: 32px; - border: 1px solid #dbe3ee; - border-radius: 8px; + border: 1px solid var(--tiku-border); + border-radius: 30px; background: #fff; + box-shadow: var(--tiku-shadow); } .vocabulary-card.revealed { border-color: #bfdbfe; - background: #f8fbff; + background: linear-gradient(135deg, #eff6ff, #fff); } .vocabulary-word { @@ -848,7 +886,7 @@ .vocabulary-phonetic { display: block; margin-top: 16px; - color: #64748b; + color: var(--tiku-muted); font-size: 28px; line-height: 1.3; text-align: center; @@ -862,8 +900,8 @@ .vocabulary-answer { margin-top: 34px; padding: 24px; - border: 1px solid #e2e8f0; - border-radius: 8px; + border: 1px solid var(--tiku-border); + border-radius: 22px; background: #fff; } @@ -880,14 +918,14 @@ .vocabulary-word-chip { min-width: 0; padding: 18px; - border: 1px solid #e2e8f0; - border-radius: 8px; + border: 1px solid var(--tiku-border); + border-radius: 20px; background: #fff; } .vocabulary-word-chip.active { - border-color: #2563eb; - background: #eff6ff; + border-color: var(--tiku-primary); + background: var(--tiku-primary-soft); } .vocabulary-word-chip.favorite { @@ -917,17 +955,18 @@ .asset-preview-panel { padding: 24px; border: 1px solid #bfdbfe; - border-radius: 8px; + border-radius: 24px; background: #fff; + box-shadow: var(--tiku-shadow); } .asset-preview-frame { position: relative; min-height: 720px; overflow: hidden; - border: 1px solid #dbe3ee; - border-radius: 8px; - background: #f8fafc; + border: 1px solid var(--tiku-border); + border-radius: 22px; + background: var(--tiku-card-soft); } .asset-preview-iframe { @@ -996,16 +1035,17 @@ .asset-security-strip { margin-top: 16px; padding: 18px; - border: 1px solid #e2e8f0; - border-radius: 8px; - background: #f8fafc; + border: 1px solid var(--tiku-border); + border-radius: 18px; + background: var(--tiku-card-soft); } .scoreline-filter-panel { padding: 24px; - border: 1px solid #dbe3ee; - border-radius: 8px; + border: 1px solid var(--tiku-border); + border-radius: 24px; background: #fff; + box-shadow: var(--tiku-shadow); } .scoreline-filter-group { @@ -1043,9 +1083,9 @@ min-width: 112px; height: 58px; padding: 0 16px; - border: 1px solid #dbe3ee; - border-radius: 8px; - background: #f8fafc; + border: 1px solid var(--tiku-border); + border-radius: 999px; + background: var(--tiku-card-soft); } .scoreline-chip.small { @@ -1055,8 +1095,8 @@ } .scoreline-chip.active { - border-color: #2563eb; - background: #eff6ff; + border-color: var(--tiku-primary); + background: var(--tiku-primary-soft); } .scoreline-chip.warn { @@ -1076,7 +1116,7 @@ } .scoreline-chip.active .scoreline-chip-text { - color: #1d4ed8; + color: var(--tiku-primary); } .scoreline-field-grid { @@ -1112,13 +1152,13 @@ min-height: 116px; padding: 18px; border: 1px solid #bfdbfe; - border-radius: 8px; - background: #f8fbff; + border-radius: 22px; + background: linear-gradient(135deg, #eff6ff, #fff); } .scoreline-trend-year { display: block; - color: #1d4ed8; + color: var(--tiku-primary); font-size: 24px; font-weight: 850; line-height: 1.25; @@ -1144,14 +1184,14 @@ min-width: 0; min-height: 74px; padding: 12px 14px; - border: 1px solid #e2e8f0; - border-radius: 8px; - background: #f8fafc; + border: 1px solid var(--tiku-border); + border-radius: 18px; + background: var(--tiku-card-soft); } .scoreline-field-name { display: block; - color: #64748b; + color: var(--tiku-muted); font-size: 19px; line-height: 1.25; overflow: hidden; diff --git a/apps/taro/src/pages/tenant-admin/admin.css b/apps/taro/src/pages/tenant-admin/admin.css index 750256f7..c4267e8e 100644 --- a/apps/taro/src/pages/tenant-admin/admin.css +++ b/apps/taro/src/pages/tenant-admin/admin.css @@ -1,8 +1,8 @@ .admin-page { min-height: 100vh; padding: 28px; - background: #f8fafc; - color: #172033; + background: var(--tiku-page); + color: var(--tiku-text); } .admin-shell { @@ -12,30 +12,50 @@ } .admin-header { - padding-bottom: 24px; - border-bottom: 1px solid #dbe3ef; + position: relative; + overflow: hidden; + padding: 30px; + border-radius: 30px; + background: linear-gradient(135deg, #1152d4, #3b82f6); + box-shadow: var(--tiku-shadow-blue); +} + +.admin-header::after { + content: ""; + position: absolute; + right: -64px; + bottom: -84px; + width: 240px; + height: 240px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.14); +} + +.admin-kicker, +.admin-title, +.admin-subtitle { + position: relative; + z-index: 1; + display: block; } .admin-kicker { - display: block; - color: #64748b; + color: rgba(255, 255, 255, 0.8); font-size: 22px; - font-weight: 700; + font-weight: 850; } .admin-title { - display: block; margin-top: 8px; - color: #111827; + color: #fff; font-size: 38px; - font-weight: 800; + font-weight: 920; line-height: 1.2; } .admin-subtitle { - display: block; margin-top: 10px; - color: #64748b; + color: rgba(255, 255, 255, 0.82); font-size: 24px; line-height: 1.45; } @@ -53,28 +73,48 @@ overflow-x: visible; } +.admin-button, +.admin-mini-button { + border: 1px solid var(--tiku-border); + background: #fff; + color: var(--tiku-primary); + font-weight: 820; +} + .admin-button { min-width: 132px; height: 62px; - border: 1px solid #cbd5e1; - border-radius: 8px; - background: #fff; - color: #1e3a8a; + border-radius: 16px; font-size: 23px; - font-weight: 700; line-height: 62px; } -.admin-button.primary { - border-color: #2563eb; - background: #2563eb; +.admin-mini-button { + min-width: 112px; + height: 54px; + border-radius: 14px; + font-size: 21px; + line-height: 54px; +} + +.admin-button.primary, +.admin-mini-button.primary { + border-color: var(--tiku-primary); + background: linear-gradient(135deg, var(--tiku-primary), #2563eb); color: #fff; + box-shadow: 0 10px 20px rgba(17, 82, 212, 0.16); } .admin-button.active { - border-color: #2563eb; - background: #eff6ff; - color: #1d4ed8; + border-color: var(--tiku-primary); + background: var(--tiku-primary-soft); + color: var(--tiku-primary); +} + +.admin-mini-button.danger { + border-color: #fecdd3; + background: #fff1f2; + color: #be123c; } .admin-section { @@ -84,24 +124,27 @@ .admin-section-title { display: block; margin-bottom: 14px; - color: #0f172a; + color: var(--tiku-text); font-size: 29px; - font-weight: 800; + font-weight: 900; } -.admin-grid { +.admin-grid, +.admin-metric-grid { display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; } -.admin-grid.three { +.admin-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.admin-grid.three, +.admin-metric-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } .admin-metric-grid { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin-bottom: 14px; } @@ -113,10 +156,11 @@ .admin-metric { min-height: 112px; - padding: 18px; - border: 1px solid #e2e8f0; - border-radius: 8px; + padding: 20px; + border: 1px solid var(--tiku-border); + border-radius: 24px; background: #fff; + box-shadow: var(--tiku-shadow); } .admin-metric.theme-template { @@ -124,22 +168,27 @@ } .admin-metric.theme-template.selected { - border-color: #2563eb; + border-color: var(--tiku-primary); box-shadow: 0 0 0 2px #bfdbfe; } -.admin-metric-label { +.admin-metric-label, +.admin-row-meta, +.admin-field-label { display: block; - color: #64748b; + color: var(--tiku-muted); +} + +.admin-metric-label { font-size: 21px; } .admin-metric-value { display: block; margin-top: 12px; - color: #0f172a; + color: var(--tiku-text); font-size: 31px; - font-weight: 800; + font-weight: 900; line-height: 1.2; } @@ -151,50 +200,53 @@ .admin-row { padding: 20px; - border: 1px solid #e2e8f0; - border-radius: 8px; + border: 1px solid var(--tiku-border); + border-radius: 22px; background: #fff; + box-shadow: 0 5px 16px rgba(15, 23, 42, 0.04); } .admin-row.active { - border-color: #93c5fd; - background: #eff6ff; + border-color: var(--tiku-primary); + background: var(--tiku-primary-soft); } .admin-row-main { display: block; - color: #111827; + color: var(--tiku-text); font-size: 26px; - font-weight: 760; + font-weight: 820; line-height: 1.35; } .admin-row-meta { - display: block; margin-top: 8px; - color: #64748b; font-size: 21px; line-height: 1.45; } .admin-empty { padding: 32px 22px; - border: 1px dashed #cbd5e1; - border-radius: 8px; + border: 1px dashed var(--tiku-border-strong); + border-radius: 22px; background: #fff; - color: #64748b; + color: var(--tiku-muted); font-size: 23px; line-height: 1.45; } -.admin-input { +.admin-input, +.admin-textarea { width: 100%; + border: 1px solid var(--tiku-border); + background: #fff; + color: var(--tiku-text); +} + +.admin-input { height: 66px; padding: 0 18px; - border: 1px solid #cbd5e1; - border-radius: 8px; - background: #fff; - color: #111827; + border-radius: 16px; font-size: 24px; } @@ -212,14 +264,10 @@ } .admin-textarea { - width: 100%; min-height: 220px; margin-top: 12px; padding: 18px; - border: 1px solid #cbd5e1; - border-radius: 8px; - background: #fff; - color: #111827; + border-radius: 18px; font-size: 23px; line-height: 1.45; } @@ -231,44 +279,20 @@ overflow-x: auto; } -.admin-mini-button { - min-width: 112px; - height: 54px; - border: 1px solid #cbd5e1; - border-radius: 8px; - background: #fff; - color: #1e3a8a; - font-size: 21px; - font-weight: 700; - line-height: 54px; -} - -.admin-mini-button.primary { - border-color: #2563eb; - background: #2563eb; - color: #fff; -} - -.admin-mini-button.danger { - border-color: #dc2626; - background: #fef2f2; - color: #b91c1c; -} - .admin-sub-list { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; padding: 12px; - border-radius: 8px; + border-radius: 18px; background: #f8fafc; } .admin-sub-row { padding: 10px 12px; - border: 1px solid #e2e8f0; - border-radius: 8px; + border: 1px solid var(--tiku-border); + border-radius: 16px; background: #fff; } @@ -288,15 +312,15 @@ .theme-swatch { width: 54px; height: 54px; - border: 1px solid #e2e8f0; - border-radius: 8px; + border: 1px solid var(--tiku-border); + border-radius: 14px; } .theme-preview-panel { margin-top: 14px; overflow: hidden; - border: 1px solid #dbe3ef; - border-radius: 8px; + border: 1px solid var(--tiku-border); + border-radius: 22px; background: #fff; } @@ -309,7 +333,7 @@ display: block; color: #fff; font-size: 30px; - font-weight: 800; + font-weight: 850; line-height: 1.25; } @@ -330,7 +354,7 @@ .theme-preview-pill { width: 88px; height: 34px; - border-radius: 8px; + border-radius: 999px; } .break-line { diff --git a/apps/taro/src/pages/tenant-admin/workbench/index.css b/apps/taro/src/pages/tenant-admin/workbench/index.css index dfae5662..f0ede60d 100644 --- a/apps/taro/src/pages/tenant-admin/workbench/index.css +++ b/apps/taro/src/pages/tenant-admin/workbench/index.css @@ -1,72 +1 @@ -.admin-page { - min-height: 100vh; - padding: 28px; - background: #f8fafc; -} - -.admin-header { - padding-bottom: 26px; - border-bottom: 1px solid #dbe3ef; -} - -.admin-title { - display: block; - color: #111827; - font-size: 36px; - font-weight: 800; -} - -.admin-subtitle { - display: block; - margin-top: 10px; - color: #64748b; - font-size: 24px; -} - -.metric-row { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 14px; - margin-top: 26px; -} - -.metric { - min-height: 116px; - padding: 18px; - border: 1px solid #e2e8f0; - border-radius: 8px; - background: #fff; -} - -.metric-label { - display: block; - color: #64748b; - font-size: 22px; -} - -.metric-value { - display: block; - margin-top: 16px; - color: #0f172a; - font-size: 30px; - font-weight: 800; -} - -.module-list { - margin-top: 28px; - border-top: 1px solid #dbe3ef; -} - -.module-row { - display: flex; - justify-content: space-between; - padding: 24px 0; - border-bottom: 1px solid #dbe3ef; - color: #172033; - font-size: 26px; -} - -.module-status { - color: #64748b; - font-size: 24px; -} +/* Workbench uses ../admin.css. This file is kept for route-level style isolation. */ diff --git a/apps/taro/src/pages/tenant-admin/workbench/index.tsx b/apps/taro/src/pages/tenant-admin/workbench/index.tsx index 9e557a82..b9156b7e 100644 --- a/apps/taro/src/pages/tenant-admin/workbench/index.tsx +++ b/apps/taro/src/pages/tenant-admin/workbench/index.tsx @@ -10,6 +10,7 @@ import { type TenantOverview, type TenantPermissionsPayload, } from '@/services/tenantAdmin'; +import { requireSignedIn } from '@/services/routeGuard'; import '../admin.css'; interface AdminModule { @@ -73,9 +74,14 @@ export default function TenantWorkbenchPage() { const [permissions, setPermissions] = useState({}); useEffect(() => { - loadTenantDashboard('30d').then(payload => setDashboard(payload.item || null)).catch(() => setDashboard(null)); - loadTenantOverview().then(payload => setOverview(payload.item || null)).catch(() => setOverview(null)); - loadTenantPermissions().then(payload => setPermissions(payload)).catch(() => setPermissions({})); + requireSignedIn('/pages/tenant-admin/workbench/index') + .then(payload => { + if (!payload) return; + loadTenantDashboard('30d').then(next => setDashboard(next.item || null)).catch(() => setDashboard(null)); + loadTenantOverview().then(next => setOverview(next.item || null)).catch(() => setOverview(null)); + loadTenantPermissions().then(next => setPermissions(next)).catch(() => setPermissions({})); + }) + .catch(() => setPermissions({})); }, []); const cards = dashboard?.cards || {}; diff --git a/apps/taro/src/services/routeGuard.ts b/apps/taro/src/services/routeGuard.ts new file mode 100644 index 00000000..4e50b00f --- /dev/null +++ b/apps/taro/src/services/routeGuard.ts @@ -0,0 +1,94 @@ +import Taro from '@tarojs/taro'; +import { appEnv, ensureRuntimeConfigLoaded, isH5Runtime } from '@/env'; +import type { ApiEnvelope, CurrentUser } from '@/types'; +import { getTenantContext, resolveTenant } from './api'; +import { loadCurrentUser } from './auth'; + +let pendingGuardPath = ''; + +function hostFromRuntime() { + if (isH5Runtime() && typeof window !== 'undefined') return window.location.host; + return ''; +} + +function safeRedirectPath(path: string) { + return path.startsWith('/pages/') && !path.startsWith('/pages/student/login/') ? path : '/pages/student/home/index'; +} + +function loginUrl(redirectPath: string) { + return `/pages/student/login/index?redirect=${encodeURIComponent(safeRedirectPath(redirectPath))}`; +} + +function currentPagePath() { + const instance = Taro.getCurrentInstance(); + const path = instance.router?.path || ''; + return path.startsWith('/') ? path : `/${path}`; +} + +function isPublicPath(path: string) { + return path === '/pages/bootstrap/index' || path === '/pages/student/login/index'; +} + +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/'); + return path.startsWith('/pages/student/') && ![ + '/pages/student/region/index', + '/pages/student/catalog/index', + '/pages/student/scoreline/index', + ].includes(path); +} + +export function landingPath() { + if (appEnv.portal === 'tenant-admin') return '/pages/tenant-admin/workbench/index'; + if (appEnv.portal === 'platform-admin') return '/pages/platform-admin/workbench/index'; + return '/pages/student/home/index'; +} + +export async function ensureTenantResolved() { + await ensureRuntimeConfigLoaded(); + const current = getTenantContext(); + if (current?.tenantId) return current; + return resolveTenant({ host: hostFromRuntime() }); +} + +export async function requireSignedIn(redirectPath: string): Promise | null> { + await ensureTenantResolved(); + try { + return await loadCurrentUser(); + } catch { + Taro.redirectTo({ url: loginUrl(redirectPath) }); + return null; + } +} + +export async function requirePlatformAdmin(redirectPath: string) { + const payload = await requireSignedIn(redirectPath); + if (!payload) return null; + const user = payload.user || payload.item; + const roles = user?.roles || []; + if (user?.primaryRole === 'platform_admin' || roles.includes('platform_admin')) return payload; + throw new Error('当前账号不是平台管理员。'); +} + +export async function guardCurrentRoute() { + await ensureRuntimeConfigLoaded(); + const path = currentPagePath(); + if (!path || !shouldGuardPath(path) || pendingGuardPath === path) return; + pendingGuardPath = path; + try { + if (appEnv.portal === 'platform-admin') { + await requirePlatformAdmin(path); + return; + } + await requireSignedIn(path); + } finally { + pendingGuardPath = ''; + } +} + +export function redirectAfterLogin(rawRedirect?: string) { + const redirectPath = rawRedirect ? decodeURIComponent(rawRedirect) : landingPath(); + Taro.redirectTo({ url: safeRedirectPath(redirectPath) }); +} diff --git a/apps/taro/src/types.ts b/apps/taro/src/types.ts index 3cfc620f..fae88a9c 100644 --- a/apps/taro/src/types.ts +++ b/apps/taro/src/types.ts @@ -24,8 +24,12 @@ export interface ApiSession { export interface CurrentUser { id: string; + username?: string; name?: string; phone?: string; + avatarUrl?: string; + primaryRole?: string; + createdAt?: string; roles?: string[]; permissions?: Record; }