更新
This commit is contained in:
@@ -1,9 +0,0 @@
|
||||
import { AppCard } from "../components/AppCard";
|
||||
|
||||
export function DashboardPage() {
|
||||
return (
|
||||
<AppCard title="项目骨架已就绪">
|
||||
<p>当前页面用于承接首版后台管理端骨架,后续可继续补充机构端、教师端等业务模块。</p>
|
||||
</AppCard>
|
||||
);
|
||||
}
|
||||
32
frontend/src/pages/DashboardPage/index.scss
Normal file
32
frontend/src/pages/DashboardPage/index.scss
Normal 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;
|
||||
}
|
||||
113
frontend/src/pages/DashboardPage/index.tsx
Normal file
113
frontend/src/pages/DashboardPage/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
41
frontend/src/pages/LoginPage/index.scss
Normal file
41
frontend/src/pages/LoginPage/index.scss
Normal 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;
|
||||
}
|
||||
62
frontend/src/pages/LoginPage/index.tsx
Normal file
62
frontend/src/pages/LoginPage/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
6
frontend/src/pages/NotFoundPage/index.scss
Normal file
6
frontend/src/pages/NotFoundPage/index.scss
Normal file
@@ -0,0 +1,6 @@
|
||||
.not-found {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 100vh;
|
||||
color: #5b6475;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import "./index.scss";
|
||||
|
||||
export function NotFoundPage() {
|
||||
return (
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
7
frontend/src/pages/RoutePlaceholderPage/index.scss
Normal file
7
frontend/src/pages/RoutePlaceholderPage/index.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
.route-placeholder p {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.route-placeholder p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
16
frontend/src/pages/RoutePlaceholderPage/index.tsx
Normal file
16
frontend/src/pages/RoutePlaceholderPage/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
4
frontend/src/pages/index.ts
Normal file
4
frontend/src/pages/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./DashboardPage";
|
||||
export * from "./LoginPage";
|
||||
export * from "./NotFoundPage";
|
||||
export * from "./RoutePlaceholderPage";
|
||||
Reference in New Issue
Block a user