style: align Taro H5 with legacy question bank UI

This commit is contained in:
Codex
2026-07-02 01:22:48 +08:00
parent 25338be748
commit 7d629843b7
16 changed files with 974 additions and 494 deletions

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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() });
})

View File

@@ -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;

View File

@@ -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 || []);

View File

@@ -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;
}

View File

@@ -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<string, unknown> : {};
const stats = profile.stats && typeof profile.stats === 'object' ? profile.stats as Record<string, unknown> : {};
const answerStats = stats.answers && typeof stats.answers === 'object' ? stats.answers as Record<string, unknown> : {};
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 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 (
<View className='student-page'>
<View className='topbar'>
<View>
<Text className='brand'>{brandName}</Text>
<Text className='subline'>{userName ? `${userName},继续学习` : '题库、手册、资料和会员统一入口'}</Text>
<View className='home-user'>
<View className='home-avatar'>
<Text className='home-avatar-mark'>{(userName || brandName || 'U').slice(0, 1).toUpperCase()}</Text>
</View>
<View>
<Text className='brand'>Hi, {userName || '同学'}</Text>
<Text className='subline'>{brandName} · </Text>
</View>
</View>
<Button className='ghost-button' onClick={() => Taro.navigateTo({ url: '/pages/student/profile/index' })}></Button>
</View>
<View className='hero-band'>
<Text className='hero-title'></Text>
<Text className='hero-copy'>线 SaaS </Text>
<View className='hero-content'>
<Text className='hero-kicker'></Text>
<Text className='hero-title'></Text>
<Text className='hero-copy'> SaaS </Text>
<View className='hero-actions'>
<Button className='hero-primary' onClick={() => Taro.navigateTo({ url: '/pages/student/catalog/index' })}></Button>
<Button className='hero-secondary' onClick={() => Taro.navigateTo({ url: '/pages/student/profile/index' })}></Button>
</View>
</View>
</View>
<View className='home-stat-grid'>
<View className='home-stat'>
<Text className='home-stat-value'>{String(totalAnswered)}</Text>
<Text className='home-stat-label'></Text>
</View>
<View className='home-stat'>
<Text className='home-stat-value'>{accuracy ? `${accuracy}%` : '--'}</Text>
<Text className='home-stat-label'></Text>
</View>
<View className='home-stat' onClick={() => Taro.navigateTo({ url: '/pages/student/review/index?type=wrong' })}>
<Text className='home-stat-value'>{String(wrongCount)}</Text>
<Text className='home-stat-label'></Text>
</View>
</View>
<View className='section'>
<Text className='section-title'></Text>
<View className='entry-grid'>
{entryNames.length ? entryNames.map(item => (
<View className='entry-tile' key={item.id} onClick={() => Taro.navigateTo({ url: `/pages/student/catalog/index?entryId=${item.id}` })}>
<View className='entry-tile tone-blue' key={item.id} onClick={() => Taro.navigateTo({ url: `/pages/student/catalog/index?entryId=${item.id}` })}>
<Text className='entry-label'></Text>
<Text className='entry-name'>{item.name}</Text>
<Text className='entry-type'>{item.entryType || 'content'}</Text>
</View>
)) : quickActions.map(item => (
<View className='entry-tile' key={item.name} onClick={() => Taro.navigateTo({ url: item.path })}>
<View className={`entry-tile tone-${item.tone}`} key={item.name} onClick={() => Taro.navigateTo({ url: item.path })}>
<Text className='entry-label'>{item.meta}</Text>
<Text className='entry-name'>{item.name}</Text>
<Text className='entry-type'>{item.meta}</Text>
<Text className='entry-type'></Text>
</View>
))}
</View>
</View>
<View className='section'>
<Text className='section-title'></Text>
<View className='entry-grid'>
<Text className='section-title'></Text>
<View className='quick-list'>
{quickActions.map(item => (
<View className='entry-tile compact' key={item.path} onClick={() => Taro.navigateTo({ url: item.path })}>
<Text className='entry-name'>{item.name}</Text>
<Text className='entry-type'>{item.meta}</Text>
<View className='quick-row' key={item.path} onClick={() => Taro.navigateTo({ url: item.path })}>
<View>
<Text className='quick-name'>{item.name}</Text>
<Text className='quick-meta'>{item.meta}</Text>
</View>
<Text className='quick-arrow'></Text>
</View>
))}
</View>

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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. */

View File

@@ -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<TenantPermissionsPayload>({});
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 || {};

View File

@@ -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<ApiEnvelope<CurrentUser> | 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) });
}

View File

@@ -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<string, boolean>;
}