forked from wangziqi/gongxue-base
style: align Taro H5 with legacy question bank UI
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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() });
|
||||
})
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 || []);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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 || {};
|
||||
|
||||
94
apps/taro/src/services/routeGuard.ts
Normal file
94
apps/taro/src/services/routeGuard.ts
Normal 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) });
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user