This commit is contained in:
2026-04-17 16:31:32 +08:00
parent adadb3bf1d
commit 2476655b28
116 changed files with 3875 additions and 583 deletions

View File

@@ -1,9 +0,0 @@
import { AppCard } from "../components/AppCard";
export function DashboardPage() {
return (
<AppCard title="项目骨架已就绪">
<p></p>
</AppCard>
);
}

View File

@@ -0,0 +1,32 @@
.dashboard-grid {
display: grid;
gap: 16px;
}
.dashboard-list {
margin: 0;
padding-left: 16px;
display: grid;
gap: 8px;
}
.dashboard-message-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.dashboard-message-item button {
border: 1px solid #cbd5e1;
border-radius: 8px;
background: #fff;
padding: 4px 10px;
cursor: pointer;
}
.dashboard-error {
margin: 0;
color: #dc2626;
font-size: 14px;
}

View File

@@ -0,0 +1,113 @@
import { useEffect, useMemo, useState } from "react";
import { fetchClasses, fetchCurrentUser, fetchInboxMessages, markInboxMessageRead } from "@/api";
import { AppCard } from "@/components";
import type { CurrentRouteUser, UpmsClass, UpmsInboxMessage } from "@/types";
import "./index.scss";
export function DashboardPage() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [currentUser, setCurrentUser] = useState<CurrentRouteUser | null>(null);
const [classes, setClasses] = useState<UpmsClass[]>([]);
const [messages, setMessages] = useState<UpmsInboxMessage[]>([]);
useEffect(() => {
let active = true;
Promise.all([fetchCurrentUser(), fetchClasses(), fetchInboxMessages()])
.then(([user, classList, inbox]) => {
if (!active) {
return;
}
setCurrentUser(user);
setClasses(classList);
setMessages(inbox);
})
.catch((exception) => {
if (!active) {
return;
}
setError(exception instanceof Error ? exception.message : "加载控制台数据失败");
})
.finally(() => {
if (active) {
setLoading(false);
}
});
return () => {
active = false;
};
}, []);
const unreadCount = useMemo(
() => messages.filter((message) => message.readStatus === "UNREAD").length,
[messages]
);
async function handleMarkRead(messageId: string) {
try {
const result = await markInboxMessageRead(messageId);
setMessages((previous) =>
previous.map((message) =>
message.messageId === messageId
? { ...message, readStatus: result.readStatus, readAt: result.readAt }
: message
)
);
} catch (exception) {
setError(exception instanceof Error ? exception.message : "站内信已读操作失败");
}
}
return (
<div className="dashboard-grid">
<AppCard title="当前登录用户">
{loading ? <p>...</p> : null}
{currentUser ? (
<ul className="dashboard-list">
<li>{currentUser.displayName}{currentUser.username}</li>
<li>{currentUser.tenantId}</li>
<li>{currentUser.deptId}</li>
<li>{currentUser.permissionCodes.length}</li>
</ul>
) : null}
</AppCard>
<AppCard title={`班级概览(${classes.length}`}>
{loading ? <p>...</p> : null}
{classes.length > 0 ? (
<ul className="dashboard-list">
{classes.slice(0, 5).map((item) => (
<li key={item.classId}>
{item.className}{item.classCode || "未编码"}
</li>
))}
</ul>
) : !loading ? (
<p></p>
) : null}
</AppCard>
<AppCard title={`站内信(未读 ${unreadCount}`}>
{loading ? <p>...</p> : null}
{messages.length > 0 ? (
<ul className="dashboard-list">
{messages.slice(0, 5).map((message) => (
<li key={message.messageId} className="dashboard-message-item">
<span>
[{message.readStatus}] {message.title}
</span>
{message.readStatus === "UNREAD" ? (
<button type="button" onClick={() => handleMarkRead(message.messageId)}>
</button>
) : null}
</li>
))}
</ul>
) : !loading ? (
<p></p>
) : null}
{error ? <p className="dashboard-error">{error}</p> : null}
</AppCard>
</div>
);
}

View File

@@ -1,42 +0,0 @@
import { type FormEvent, useState } from "react";
import { useNavigate } from "react-router-dom";
import { login } from "../api/auth";
import { setAccessToken } from "../utils/storage";
export function LoginPage() {
const navigate = useNavigate();
const [form, setForm] = useState({
username: "admin",
password: "admin123",
provinceCode: "330000",
areaCode: "330100",
tenantId: "SCH-HQ"
});
async function handleSubmit(event: FormEvent) {
event.preventDefault();
const response = await login(form);
setAccessToken(response.data.accessToken);
navigate("/");
}
return (
<div className="login-page">
<form className="login-form" onSubmit={handleSubmit}>
<h1>K12Study Admin</h1>
<input
value={form.username}
onChange={(event) => setForm({ ...form, username: event.target.value })}
placeholder="用户名"
/>
<input
type="password"
value={form.password}
onChange={(event) => setForm({ ...form, password: event.target.value })}
placeholder="密码"
/>
<button type="submit"></button>
</form>
</div>
);
}

View File

@@ -0,0 +1,41 @@
.login-page {
display: grid;
place-items: center;
min-height: 100vh;
}
.login-form {
display: grid;
gap: 12px;
width: min(360px, calc(100vw - 32px));
padding: 32px;
border-radius: 24px;
background: #ffffff;
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.12);
}
.login-form input,
.login-form button {
height: 44px;
padding: 0 14px;
border-radius: 12px;
border: 1px solid #cbd5e1;
}
.login-form button {
border: none;
background: #2563eb;
color: #ffffff;
cursor: pointer;
}
.login-form button:disabled {
opacity: 0.75;
cursor: not-allowed;
}
.login-form__error {
margin: 0;
color: #dc2626;
font-size: 14px;
}

View File

@@ -0,0 +1,62 @@
/**
* @description Web 端登录页;提交表单调用 /auth/tokens,成功后 window.location.replace 触发路由重建以加载动态路由
* @filename index.tsx
* @author wangys
* @copyright xyzh
* @since 2026-04-17
*/
import { type FormEvent, useState } from "react";
import { login } from "@/api";
import { setTokens } from "@/utils";
import "./index.scss";
export function LoginPage() {
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [form, setForm] = useState({
username: "admin",
password: "admin123",
provinceCode: "330000",
areaCode: "330100",
tenantId: "SCH-HQ",
clientType: "WEB" as const
});
async function handleSubmit(event: FormEvent) {
event.preventDefault();
setSubmitting(true);
setError(null);
try {
const response = await login(form);
setTokens(response.data.accessToken, response.data.refreshToken);
window.location.replace("/");
} catch (exception) {
setError(exception instanceof Error ? exception.message : "登录失败,请稍后重试");
} finally {
setSubmitting(false);
}
}
return (
<div className="login-page">
<form className="login-form" onSubmit={handleSubmit}>
<h1>K12Study Admin</h1>
<input
value={form.username}
onChange={(event) => setForm({ ...form, username: event.target.value })}
placeholder="用户名"
/>
<input
type="password"
value={form.password}
onChange={(event) => setForm({ ...form, password: event.target.value })}
placeholder="密码"
/>
{error ? <p className="login-form__error">{error}</p> : null}
<button type="submit" disabled={submitting}>
{submitting ? "登录中..." : "登录"}
</button>
</form>
</div>
);
}

View File

@@ -0,0 +1,6 @@
.not-found {
display: grid;
place-items: center;
min-height: 100vh;
color: #5b6475;
}

View File

@@ -1,4 +1,5 @@
import { Link } from "react-router-dom";
import "./index.scss";
export function NotFoundPage() {
return (

View File

@@ -1,13 +0,0 @@
import { useLocation } from "react-router-dom";
import { AppCard } from "../components/AppCard";
export function RoutePlaceholderPage() {
const location = useLocation();
return (
<AppCard title="动态路由占位页">
<p>{location.pathname}</p>
<p></p>
</AppCard>
);
}

View File

@@ -0,0 +1,7 @@
.route-placeholder p {
margin: 0 0 8px;
}
.route-placeholder p:last-child {
margin-bottom: 0;
}

View File

@@ -0,0 +1,16 @@
import { AppCard } from "@/components";
import { useLocation } from "react-router-dom";
import "./index.scss";
export function RoutePlaceholderPage() {
const location = useLocation();
return (
<div className="route-placeholder">
<AppCard title="动态路由占位页">
<p>{location.pathname}</p>
<p></p>
</AppCard>
</div>
);
}

View File

@@ -0,0 +1,4 @@
export * from "./DashboardPage";
export * from "./LoginPage";
export * from "./NotFoundPage";
export * from "./RoutePlaceholderPage";